mirror of
https://github.com/Telecominfraproject/oopt-gnpy.git
synced 2025-10-30 17:47:50 +00:00
Compare commits
280 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a0e73e332 | ||
|
|
fa6b8c87e4 | ||
|
|
801c66aae2 | ||
|
|
f60c347a48 | ||
|
|
649bb3bd0f | ||
|
|
6051ad54bc | ||
|
|
95d24d8e20 | ||
|
|
6c449edece | ||
|
|
d051f93d55 | ||
|
|
ee92011b21 | ||
|
|
9861a22ef9 | ||
|
|
59f9f35817 | ||
|
|
1bb475671d | ||
|
|
566dedbdbb | ||
|
|
b44c4cec16 | ||
|
|
93d11ba408 | ||
|
|
637670fcfa | ||
|
|
7836297708 | ||
|
|
1f8b4ab9a2 | ||
|
|
05eb312f4a | ||
|
|
a98e244abd | ||
|
|
c945bc40fe | ||
|
|
749b9287a9 | ||
|
|
202c76bd6e | ||
|
|
eec0943ca2 | ||
|
|
06d59a5834 | ||
|
|
94949d955b | ||
|
|
b74d0a4919 | ||
|
|
c8fa7635e0 | ||
|
|
4c6cfbda5d | ||
|
|
80e9423590 | ||
|
|
78010aaaef | ||
|
|
3857ab1dbb | ||
|
|
dbb09e4108 | ||
|
|
94a8f3568a | ||
|
|
ae7c9321d0 | ||
|
|
648cc3a8e5 | ||
|
|
f9e0d18a9d | ||
|
|
2d57fd9f85 | ||
|
|
f053f32301 | ||
|
|
8e9d715e9f | ||
|
|
9fd55a5289 | ||
|
|
914d0dbecd | ||
|
|
c38fe72ff7 | ||
|
|
80f63d32ed | ||
|
|
9030f8f84f | ||
|
|
0d5f1c7d80 | ||
|
|
5bc42332cd | ||
|
|
093b85d4a3 | ||
|
|
0efa0d310d | ||
|
|
8eb5980ca9 | ||
|
|
754be7ca08 | ||
|
|
c5c5b693f2 | ||
|
|
7f816eb6e7 | ||
|
|
1009b44d2a | ||
|
|
3b61c6ca4c | ||
|
|
cc11bd186c | ||
|
|
0d1225824e | ||
|
|
a2ecfd924c | ||
|
|
95c3c9c488 | ||
|
|
11033a284f | ||
|
|
357e6cbf18 | ||
|
|
bfa6d29908 | ||
|
|
21e0589e7f | ||
|
|
4d836246ec | ||
|
|
566943a099 | ||
|
|
a4c1ea1f55 | ||
|
|
6b10ed15f8 | ||
|
|
c8daa5ed8c | ||
|
|
0b03725295 | ||
|
|
ee5e64408d | ||
|
|
b96ffe6c7b | ||
|
|
3b981853d4 | ||
|
|
6fa3ef8df1 | ||
|
|
1b2b048b47 | ||
|
|
94c5281260 | ||
|
|
7da4ec08d8 | ||
|
|
2b473d26d3 | ||
|
|
9e74e8b0a0 | ||
|
|
648039521e | ||
|
|
24bc023a07 | ||
|
|
19c2ae7f7a | ||
|
|
9f6894a176 | ||
|
|
a094568d6e | ||
|
|
f728d96d07 | ||
|
|
6b1fb7061f | ||
|
|
7ef505f259 | ||
|
|
c8d394348d | ||
|
|
0daa1c3e8c | ||
|
|
60bafd114d | ||
|
|
11509f5686 | ||
|
|
785c823fa2 | ||
|
|
9faf6430a5 | ||
|
|
01c566a325 | ||
|
|
baa9171315 | ||
|
|
2e50337f38 | ||
|
|
8daa298699 | ||
|
|
76cdd5dc71 | ||
|
|
07eb2dd13a | ||
|
|
04e764d024 | ||
|
|
05ccb14e5d | ||
|
|
f60d035e66 | ||
|
|
0823f8de46 | ||
|
|
a453c57996 | ||
|
|
2f84bb5286 | ||
|
|
7b8e68aea9 | ||
|
|
8d553a255f | ||
|
|
2766e37438 | ||
|
|
f56e64410b | ||
|
|
15ea7218e9 | ||
|
|
db28011c61 | ||
|
|
faccc23018 | ||
|
|
49514c0c70 | ||
|
|
0b1557fdf1 | ||
|
|
46f89aa770 | ||
|
|
3548ed74e2 | ||
|
|
145653df6e | ||
|
|
c7589e0bca | ||
|
|
531810cc85 | ||
|
|
63a6256b5e | ||
|
|
c3febb6db4 | ||
|
|
8b1d8b3479 | ||
|
|
3168603908 | ||
|
|
a2128227bd | ||
|
|
376826b3ae | ||
|
|
4b258cdf2e | ||
|
|
fbdd132a3d | ||
|
|
eebcebb33d | ||
|
|
20152036ff | ||
|
|
2a477071a0 | ||
|
|
f02d11e8bc | ||
|
|
0d542f22a7 | ||
|
|
5af195bd2b | ||
|
|
7ab93e7cd9 | ||
|
|
0aec47ddeb | ||
|
|
9a54dbab43 | ||
|
|
fc03be8bbe | ||
|
|
32f10a4507 | ||
|
|
04544d41f6 | ||
|
|
c87be89e07 | ||
|
|
efa05f0653 | ||
|
|
4cf4db9bc2 | ||
|
|
16434c5737 | ||
|
|
14ee9c9a91 | ||
|
|
7cfc4bd1ec | ||
|
|
9d55cde50d | ||
|
|
72183c24da | ||
|
|
4d1a628488 | ||
|
|
fdeaf75361 | ||
|
|
5695fafac6 | ||
|
|
0fe1d195a3 | ||
|
|
ed1aa0aa03 | ||
|
|
3fc024f5ba | ||
|
|
4f8177908f | ||
|
|
ad64595d75 | ||
|
|
80133e9521 | ||
|
|
de58d7d7c2 | ||
|
|
491a05c5a7 | ||
|
|
1d791aa295 | ||
|
|
58921bc346 | ||
|
|
ab6a91692b | ||
|
|
3b45968799 | ||
|
|
c7d69b9a99 | ||
|
|
1eeed78430 | ||
|
|
1cdafbae0f | ||
|
|
47b4f87bc5 | ||
|
|
e24b766cdc | ||
|
|
51fb6bd68e | ||
|
|
7e405a0514 | ||
|
|
194a13e607 | ||
|
|
856f07e707 | ||
|
|
4582aed895 | ||
|
|
734b6dfef4 | ||
|
|
da6e4f33a4 | ||
|
|
5a1e3f30b3 | ||
|
|
094af16792 | ||
|
|
9474ff17de | ||
|
|
c675f5bd38 | ||
|
|
2556658e68 | ||
|
|
4d5d10935a | ||
|
|
eb87e36781 | ||
|
|
14d8d793f3 | ||
|
|
bc4b6642a0 | ||
|
|
fe2b39ee3b | ||
|
|
d56c91ca7f | ||
|
|
25819fadf5 | ||
|
|
0cacb8851d | ||
|
|
14f98836ed | ||
|
|
82ce3384ba | ||
|
|
1f1877f7a9 | ||
|
|
db26ce07db | ||
|
|
e08ae9c959 | ||
|
|
2de1b5567a | ||
|
|
639b379a5b | ||
|
|
0465397b1d | ||
|
|
d3ec39d506 | ||
|
|
bfe68a5948 | ||
|
|
2ea3363613 | ||
|
|
89cce6e6a3 | ||
|
|
0f10ac706c | ||
|
|
66d26f0ffa | ||
|
|
3c96914482 | ||
|
|
61b1e73362 | ||
|
|
03435079cc | ||
|
|
6661907c1d | ||
|
|
fe811f725c | ||
|
|
80eced85ec | ||
|
|
2960d307fa | ||
|
|
c41cddfff5 | ||
|
|
8598e6591f | ||
|
|
5e2259062c | ||
|
|
d483802a86 | ||
|
|
9a0eece69c | ||
|
|
1657bfd05f | ||
|
|
49bf558916 | ||
|
|
c577a75725 | ||
|
|
8827e0cf6f | ||
|
|
b0012fe399 | ||
|
|
31e634615b | ||
|
|
8300a55e39 | ||
|
|
5b939bc57a | ||
|
|
2f1ab9cc50 | ||
|
|
42ba3eb98d | ||
|
|
9eb87fc8e1 | ||
|
|
8fab9bb945 | ||
|
|
1ead232a78 | ||
|
|
b15c8c60ab | ||
|
|
66bdeb0e4d | ||
|
|
1a2e090104 | ||
|
|
a8e280e29b | ||
|
|
edb54b02ac | ||
|
|
83d3f32fe0 | ||
|
|
085a379592 | ||
|
|
37bd5d0404 | ||
|
|
f788b81d21 | ||
|
|
2ff1ce6b34 | ||
|
|
41a1e40d14 | ||
|
|
921e8d2d3c | ||
|
|
c009d28f7d | ||
|
|
898eada097 | ||
|
|
bdfc55e801 | ||
|
|
57f264bedb | ||
|
|
fbe4fa3cf0 | ||
|
|
b2ef345f35 | ||
|
|
471ea7dfba | ||
|
|
1b52f638ff | ||
|
|
84ab38a75f | ||
|
|
916e5377f8 | ||
|
|
534bfd881e | ||
|
|
7c4015324d | ||
|
|
8499ee52f4 | ||
|
|
cc1123863c | ||
|
|
ca382806f6 | ||
|
|
3559fc61c2 | ||
|
|
33581cdcc9 | ||
|
|
991eb02964 | ||
|
|
286e321a2d | ||
|
|
56f158113d | ||
|
|
024f6ff963 | ||
|
|
8118a0f4f4 | ||
|
|
eb89d8fd86 | ||
|
|
a938c1738b | ||
|
|
4f88882513 | ||
|
|
4e8d8b7ddd | ||
|
|
afb7d75749 | ||
|
|
488d0e1fe8 | ||
|
|
708442e4cd | ||
|
|
a2d905dfb1 | ||
|
|
d564fe3e2a | ||
|
|
ea21cce1c0 | ||
|
|
aa9b4aefbe | ||
|
|
8655030e59 | ||
|
|
8107ddeb79 | ||
|
|
76c8e55f06 | ||
|
|
a7b1ab47d8 | ||
|
|
879f587ab9 | ||
|
|
8af2d80219 | ||
|
|
315eea1f55 | ||
|
|
b61e541e15 | ||
|
|
81f88e78c7 |
@@ -1,3 +1,3 @@
|
||||
#!/bin/bash
|
||||
cp -nr /oopt-gnpy/examples /shared
|
||||
cp -nr /opt/application/oopt-gnpy/gnpy/example-data /shared
|
||||
exec "$@"
|
||||
|
||||
@@ -15,7 +15,7 @@ if [[ $ALREADY_FOUND == 0 ]]; then
|
||||
# shared directory setup: do not clobber the real data
|
||||
mkdir trash
|
||||
cd trash
|
||||
docker run -it --rm --volume $(pwd):/shared ${IMAGE_NAME} ./transmission_main_example.py
|
||||
docker run -it --rm --volume $(pwd):/shared ${IMAGE_NAME} gnpy-transmission-example
|
||||
else
|
||||
echo "Image ${IMAGE_NAME}:${IMAGE_TAG} already available, will just update the other tags"
|
||||
fi
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,6 +3,7 @@ __pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
.ipynb_checkpoints
|
||||
.idea
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
5
.gitreview
Normal file
5
.gitreview
Normal file
@@ -0,0 +1,5 @@
|
||||
[gerrit]
|
||||
host=review.gerrithub.io
|
||||
project=Telecominfraproject/oopt-gnpy
|
||||
defaultrebase=0
|
||||
defaultbranch=develop
|
||||
11
.travis.yml
11
.travis.yml
@@ -7,14 +7,11 @@ python:
|
||||
- "3.7"
|
||||
install: skip
|
||||
script:
|
||||
- python setup.py install
|
||||
- python setup.py develop
|
||||
- pip install pytest-cov rstcheck
|
||||
- pytest --cov-report=xml --cov=gnpy
|
||||
- rstcheck --ignore-roles cite --ignore-directives automodule --recursive --ignore-messages '(Duplicate explicit target name.*)' .
|
||||
- ./examples/transmission_main_example.py
|
||||
- ./examples/path_requests_run.py
|
||||
- ./examples/transmission_main_example.py examples/raman_edfa_example_network.json --sim examples/sim_params.json --show-channels
|
||||
- sphinx-build docs/ x-throwaway-location
|
||||
- pytest --cov-report=xml --cov=gnpy -v
|
||||
- rstcheck --ignore-roles cite *.rst
|
||||
- sphinx-build -W --keep-going docs/ x-throwaway-location
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
jobs:
|
||||
|
||||
44
.zuul.yaml
Normal file
44
.zuul.yaml
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
- project:
|
||||
check:
|
||||
jobs:
|
||||
- tox-py36-cover
|
||||
- coverage-diff:
|
||||
voting: false
|
||||
dependencies:
|
||||
- tox-py36-cover-previous
|
||||
- tox-py36-cover
|
||||
vars:
|
||||
coverage_job_name_previous: tox-py36-cover-previous
|
||||
coverage_job_name_current: tox-py36-cover
|
||||
- tox-linters-diff:
|
||||
voting: false
|
||||
- tox-docs-el8
|
||||
- tox-py36-cover-previous
|
||||
gate:
|
||||
jobs:
|
||||
- tox-py36-el8
|
||||
- tox-docs-el8
|
||||
tag:
|
||||
jobs:
|
||||
- oopt-release-python:
|
||||
secrets:
|
||||
- secret: pypi-oopt-gnpy
|
||||
name: pypi_info
|
||||
pass-to-parent: true
|
||||
|
||||
- secret:
|
||||
name: pypi-oopt-gnpy
|
||||
data:
|
||||
username: __token__
|
||||
password: !encrypted/pkcs1-oaep
|
||||
- Taod9JmSMtVAvC5ShSbB3UWuccktQvutdySrj0G7a1Nk4tKFQIdwDXEnBuLpHsZVvsU9Q
|
||||
6uk4wRVQABDSdNNI/+M/1FwmZfoxuOXa02U5S1deuxW/rBHTxzYcuB8xriwhArBvTiDMk
|
||||
zyWHVysgDsjlR+85h/DkEhvsaMRDLYWqFwYgXizMoGNKVkwDVIH+qkhBmbggQfDpcYPKT
|
||||
1gq0d6fw0eKVJtO8+vonMEcE0sWZvHmZvSSu0H++gxoe1W/JtzbCteH3Ak0zktwBHI8Qt
|
||||
WBqFvY3laad335tpkFJN5b949N+DP8svCWwRwXmkZlHplPYZWF6QpYbEEXL/6Q0H6VwL+
|
||||
om4f7ybYpKe9Gl939uv2INnXaKe5EU6CMsSw40r2XZCjnSTjWOTgh9pUn2PsoHnqUlALW
|
||||
VR4Z+ipnCrEbu8aTmX3ROcnwYNS7OXkq4uhwDU1u9QjzyMHet6NQQhwhGtimsTo9KhL4E
|
||||
TEUNiRlbAgow9WOwM5r3vRzddO8T2HZZSGaWj75qNRX46XPQWRWgB7ItAwyXgwLZ8UzWl
|
||||
HdztjS3D7Hlsqno3zxNOVlhA5/vl9uVnhFbJnMtUOJAB07YoTJOeR+LjQ0avx/VzopxXc
|
||||
RA/WvJXVZSBrlAHY0+ip4wPZvdi4Ph90gpmvHJvoH82KVfp2j5jxzUhsage94I=
|
||||
@@ -7,7 +7,7 @@ To learn how to contribute, please see CONTRIBUTING.md
|
||||
|
||||
- Alessio Ferrari (Politecnico di Torino) <alessio.ferrari@polito.it>
|
||||
- Anders Lindgren (Telia Company) <Anders.X.Lindgren@teliacompany.com>
|
||||
- Andrea d'Amico (Politecnico di Torino) <andrea.damico@polito.it>
|
||||
- Andrea D'Amico (Politecnico di Torino) <andrea.damico@polito.it>
|
||||
- Brian Taylor (Facebook) <briantaylor@fb.com>
|
||||
- David Boertjes (Ciena) <dboertje@ciena.com>
|
||||
- Diego Landa (Facebook) <dlanda@fb.com>
|
||||
|
||||
18
Dockerfile
18
Dockerfile
@@ -1,7 +1,15 @@
|
||||
FROM python:3.7-slim
|
||||
COPY . /oopt-gnpy
|
||||
WORKDIR /oopt-gnpy
|
||||
RUN python setup.py install
|
||||
WORKDIR /shared/examples
|
||||
ENTRYPOINT ["/oopt-gnpy/.docker-entry.sh"]
|
||||
WORKDIR /opt/application/oopt-gnpy
|
||||
RUN mkdir -p /shared/example-data \
|
||||
&& groupadd gnpy \
|
||||
&& useradd -g gnpy -m gnpy \
|
||||
&& apt-get update \
|
||||
&& apt-get install git -y \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
COPY . /opt/application/oopt-gnpy
|
||||
WORKDIR /opt/application/oopt-gnpy
|
||||
RUN pip install . \
|
||||
&& chown -Rc gnpy:gnpy /opt/application/oopt-gnpy /shared/example-data
|
||||
USER gnpy
|
||||
ENTRYPOINT ["/opt/application/oopt-gnpy/.docker-entry.sh"]
|
||||
CMD ["/bin/bash"]
|
||||
|
||||
509
README.rst
509
README.rst
@@ -7,7 +7,7 @@
|
||||
`gnpy`: mesh optical network route planning and optimization library
|
||||
====================================================================
|
||||
|
||||
|docs| |build| |doi|
|
||||
|docs| |travis| |doi| |contributors| |codacy-quality| |codecov|
|
||||
|
||||
**`gnpy` is an open-source, community-developed library for building route
|
||||
planning and optimization tools in real-world mesh optical networks.**
|
||||
@@ -27,112 +27,14 @@ Documentation: https://gnpy.readthedocs.io
|
||||
Get In Touch
|
||||
~~~~~~~~~~~~
|
||||
|
||||
There are `weekly calls <https://telecominfraproject.workplace.com/events/458339931322799/>`__ about our progress.
|
||||
There are `weekly calls <https://telecominfraproject.workplace.com/events/702894886867547/>`__ about our progress.
|
||||
Newcomers, users and telecom operators are especially welcome there.
|
||||
We encourage all interested people outside the TIP to `join the project <https://telecominfraproject.com/apply-for-membership/>`__.
|
||||
|
||||
Branches and Tagged Releases
|
||||
----------------------------
|
||||
|
||||
- all releases are `available via GitHub <https://github.com/Telecominfraproject/oopt-gnpy/releases>`_
|
||||
- the `master <https://github.com/Telecominfraproject/oopt-gnpy/tree/master>`_ branch contains stable, `validated code <https://github.com/Telecominfraproject/oopt-gnpy/wiki/Testing-for-Quality>`_. It is updated from develop on a release schedule determined by the OOPT-PSE Working Group.
|
||||
- the `develop <https://github.com/Telecominfraproject/oopt-gnpy/tree/develop>`_ branch contains the latest code under active development, which may not be fully validated and tested.
|
||||
|
||||
How to Install
|
||||
--------------
|
||||
|
||||
Using prebuilt Docker images
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Our `Docker images <https://hub.docker.com/r/telecominfraproject/oopt-gnpy>`_ contain everything needed to run all examples from this guide.
|
||||
Docker transparently fetches the image over the network upon first use.
|
||||
On Linux and Mac, run:
|
||||
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ docker run -it --rm --volume $(pwd):/shared telecominfraproject/oopt-gnpy
|
||||
root@bea050f186f7:/shared/examples#
|
||||
|
||||
On Windows, launch from Powershell as:
|
||||
|
||||
.. code-block:: powershell
|
||||
|
||||
PS C:\> docker run -it --rm --volume ${PWD}:/shared telecominfraproject/oopt-gnpy
|
||||
root@89784e577d44:/shared/examples#
|
||||
|
||||
In both cases, a directory named ``examples/`` will appear in your current working directory.
|
||||
GNPy automaticallly populates it with example files from the current release.
|
||||
Remove that directory if you want to start from scratch.
|
||||
|
||||
Using Python on your computer
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Note**: `gnpy` supports Python 3 only. Python 2 is not supported.
|
||||
`gnpy` requires Python ≥3.6
|
||||
|
||||
**Note**: the `gnpy` maintainers strongly recommend the use of Anaconda for
|
||||
managing dependencies.
|
||||
|
||||
It is recommended that you use a "virtual environment" when installing `gnpy`.
|
||||
Do not install `gnpy` on your system Python.
|
||||
|
||||
We recommend the use of the `Anaconda Python distribution <https://www.anaconda.com/download>`_ which comes with many scientific computing
|
||||
dependencies pre-installed. Anaconda creates a base "virtual environment" for
|
||||
you automatically. You can also create and manage your ``conda`` "virtual
|
||||
environments" yourself (see:
|
||||
https://conda.io/docs/user-guide/tasks/manage-environments.html)
|
||||
|
||||
To activate your Anaconda virtual environment, you may need to do the
|
||||
following:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
$ source /path/to/anaconda/bin/activate # activate Anaconda base environment
|
||||
(base) $ # note the change to the prompt
|
||||
|
||||
You can check which Anaconda environment you are using with:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
(base) $ conda env list # list all environments
|
||||
# conda environments:
|
||||
#
|
||||
base * /src/install/anaconda3
|
||||
|
||||
(base) $ echo $CONDA_DEFAULT_ENV # show default environment
|
||||
base
|
||||
|
||||
You can check your version of Python with the following. If you are using
|
||||
Anaconda's Python 3, you should see similar output as below. Your results may
|
||||
be slightly different depending on your Anaconda installation path and the
|
||||
exact version of Python you are using.
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
$ which python # check which Python executable is used
|
||||
/path/to/anaconda/bin/python
|
||||
$ python -V # check your Python version
|
||||
Python 3.6.5 :: Anaconda, Inc.
|
||||
|
||||
From within your Anaconda Python 3 environment, you can clone the master branch
|
||||
of the `gnpy` repo and install it with:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
$ git clone https://github.com/Telecominfraproject/oopt-gnpy # clone the repo
|
||||
$ cd oopt-gnpy
|
||||
$ python setup.py install # install
|
||||
|
||||
To test that `gnpy` was successfully installed, you can run this command. If it
|
||||
executes without a ``ModuleNotFoundError``, you have successfully installed
|
||||
`gnpy`.
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
$ python -c 'import gnpy' # attempt to import gnpy
|
||||
|
||||
$ pytest # run tests
|
||||
Install either via `Docker <docs/install.rst#install-docker>`__, or as a `Python package <docs/install.rst#install-pip>`__.
|
||||
|
||||
Instructions for First Use
|
||||
--------------------------
|
||||
@@ -157,25 +59,24 @@ This example demonstrates how GNPy can be used to check the expected SNR at the
|
||||
:target: https://asciinema.org/a/252295
|
||||
|
||||
By default, this script operates on a single span network defined in
|
||||
`examples/edfa_example_network.json <examples/edfa_example_network.json>`_
|
||||
`gnpy/example-data/edfa_example_network.json <gnpy/example-data/edfa_example_network.json>`_
|
||||
|
||||
You can specify a different network at the command line as follows. For
|
||||
example, to use the CORONET Global network defined in
|
||||
`examples/CORONET_Global_Topology.json <examples/CORONET_Global_Topology.json>`_:
|
||||
`gnpy/example-data/CORONET_Global_Topology.json <gnpy/example-data/CORONET_Global_Topology.json>`_:
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ ./examples/transmission_main_example.py examples/CORONET_Global_Topology.json
|
||||
$ gnpy-transmission-example $(gnpy-example-data)/CORONET_Global_Topology.json
|
||||
|
||||
It is also possible to use an Excel file input (for example
|
||||
`examples/CORONET_Global_Topology.xls <examples/CORONET_Global_Topology.xls>`_).
|
||||
The Excel file will be processed into a JSON file with the same prefix. For
|
||||
further instructions on how to prepare the Excel input file, see
|
||||
`Excel_userguide.rst <Excel_userguide.rst>`_.
|
||||
`gnpy/example-data/CORONET_Global_Topology.xlsx <gnpy/example-data/CORONET_Global_Topology.xlsx>`_).
|
||||
The Excel file will be processed into a JSON file with the same prefix.
|
||||
Further details about the Excel data structure are available `in the documentation <docs/excel.rst>`__.
|
||||
|
||||
The main transmission example will calculate the average signal OSNR and SNR
|
||||
across network elements (transceiver, ROADMs, fibers, and amplifiers)
|
||||
between two transceivers selected by the user. Additional details are provided by doing ``transmission_main_example.py -h``. (By default, for the CORONET Global
|
||||
between two transceivers selected by the user. Additional details are provided by doing ``gnpy-transmission-example -h``. (By default, for the CORONET Global
|
||||
network, it will show the transmission of spectral information between Abilene and Albany)
|
||||
|
||||
This script calculates the average signal OSNR = |OSNR| and SNR = |SNR|.
|
||||
@@ -189,352 +90,63 @@ interference noise.
|
||||
.. |Pase| replace:: P\ :sub:`ase`
|
||||
.. |Pnli| replace:: P\ :sub:`nli`
|
||||
|
||||
Further Instructions for Use (`transmission_main_example.py`, `path_requests_run.py`)
|
||||
-------------------------------------------------------------------------------------
|
||||
Further Instructions for Use
|
||||
----------------------------
|
||||
|
||||
Design and transmission parameters are defined in a dedicated json file. By
|
||||
default, this information is read from `examples/eqpt_config.json
|
||||
<examples/eqpt_config.json>`_. This file defines the equipment libraries that
|
||||
can be customized (EDFAs, fibers, and transceivers).
|
||||
Simulations are driven by a set of `JSON <docs/json.rst>`__ or `XLS <docs/excel.rst>`__ files.
|
||||
|
||||
It also defines the simulation parameters (spans, ROADMs, and the spectral
|
||||
information to transmit.)
|
||||
|
||||
The EDFA equipment library is a list of supported amplifiers. New amplifiers
|
||||
can be added and existing ones removed. Three different noise models are available:
|
||||
|
||||
1. ``'type_def': 'variable_gain'`` is a simplified model simulating a 2-coil EDFA with internal, input and output VOAs. The NF vs gain response is calculated accordingly based on the input parameters: ``nf_min``, ``nf_max``, and ``gain_flatmax``. It is not a simple interpolation but a 2-stage NF calculation.
|
||||
2. ``'type_def': 'fixed_gain'`` is a fixed gain model. `NF == Cte == nf0` if `gain_min < gain < gain_flatmax`
|
||||
3. ``'type_def': None`` is an advanced model. A detailed JSON configuration file is required (by default `examples/std_medium_gain_advanced_config.json <examples/std_medium_gain_advanced_config.json>`_). It uses a 3rd order polynomial where NF = f(gain), NF_ripple = f(frequency), gain_ripple = f(frequency), N-array dgt = f(frequency). Compared to the previous models, NF ripple and gain ripple are modelled.
|
||||
|
||||
For all amplifier models:
|
||||
|
||||
+------------------------+-----------+-----------------------------------------+
|
||||
| field | type | description |
|
||||
+========================+===========+=========================================+
|
||||
| ``type_variety`` | (string) | a unique name to ID the amplifier in the|
|
||||
| | | JSON/Excel template topology input file |
|
||||
+------------------------+-----------+-----------------------------------------+
|
||||
| ``out_voa_auto`` | (boolean) | auto_design feature to optimize the |
|
||||
| | | amplifier output VOA. If true, output |
|
||||
| | | VOA is present and will be used to push |
|
||||
| | | amplifier gain to its maximum, within |
|
||||
| | | EOL power margins. |
|
||||
+------------------------+-----------+-----------------------------------------+
|
||||
| ``allowed_for_design`` | (boolean) | If false, the amplifier will not be |
|
||||
| | | picked by auto-design but it can still |
|
||||
| | | be used as a manual input (from JSON or |
|
||||
| | | Excel template topology files.) |
|
||||
+------------------------+-----------+-----------------------------------------+
|
||||
|
||||
The fiber library currently describes SSMF and NZDF but additional fiber types can be entered by the user following the same model:
|
||||
|
||||
+----------------------+-----------+-----------------------------------------+
|
||||
| field | type | description |
|
||||
+======================+===========+=========================================+
|
||||
| ``type_variety`` | (string) | a unique name to ID the fiber in the |
|
||||
| | | JSON or Excel template topology input |
|
||||
| | | file |
|
||||
+----------------------+-----------+-----------------------------------------+
|
||||
| ``dispersion`` | (number) | (s.m-1.m-1) |
|
||||
+----------------------+-----------+-----------------------------------------+
|
||||
| ``gamma`` | (number) | 2pi.n2/(lambda*Aeff) (w-2.m-1) |
|
||||
+----------------------+-----------+-----------------------------------------+
|
||||
|
||||
The transceiver equipment library is a list of supported transceivers. New
|
||||
transceivers can be added and existing ones removed at will by the user. It is
|
||||
used to determine the service list path feasibility when running the
|
||||
`path_request_run.py routine <examples/path_request_run.py>`_.
|
||||
|
||||
+----------------------+-----------+-----------------------------------------+
|
||||
| field | type | description |
|
||||
+======================+===========+=========================================+
|
||||
| ``type_variety`` | (string) | A unique name to ID the transceiver in |
|
||||
| | | the JSON or Excel template topology |
|
||||
| | | input file |
|
||||
+----------------------+-----------+-----------------------------------------+
|
||||
| ``frequency`` | (number) | Min/max as below. |
|
||||
+----------------------+-----------+-----------------------------------------+
|
||||
| ``mode`` | (number) | A list of modes supported by the |
|
||||
| | | transponder. New modes can be added at |
|
||||
| | | will by the user. The modes are specific|
|
||||
| | | to each transponder type_variety. |
|
||||
| | | Each mode is described as below. |
|
||||
+----------------------+-----------+-----------------------------------------+
|
||||
|
||||
The modes are defined as follows:
|
||||
|
||||
+----------------------+-----------+-----------------------------------------+
|
||||
| field | type | description |
|
||||
+======================+===========+=========================================+
|
||||
| ``format`` | (string) | a unique name to ID the mode |
|
||||
+----------------------+-----------+-----------------------------------------+
|
||||
| ``baud_rate`` | (number) | in Hz |
|
||||
+----------------------+-----------+-----------------------------------------+
|
||||
| ``OSNR`` | (number) | min required OSNR in 0.1nm (dB) |
|
||||
+----------------------+-----------+-----------------------------------------+
|
||||
| ``bit_rate`` | (number) | in bit/s |
|
||||
+----------------------+-----------+-----------------------------------------+
|
||||
| ``roll_off`` | (number) | Not used. |
|
||||
+----------------------+-----------+-----------------------------------------+
|
||||
| ``tx_osnr`` | (number) | In dB. OSNR out from transponder. |
|
||||
+----------------------+-----------+-----------------------------------------+
|
||||
| ``cost`` | (number) | Arbitrary unit |
|
||||
+----------------------+-----------+-----------------------------------------+
|
||||
|
||||
Simulation parameters are defined as follows.
|
||||
|
||||
Auto-design automatically creates EDFA amplifier network elements when they are
|
||||
missing, after a fiber, or between a ROADM and a fiber. This auto-design
|
||||
functionality can be manually and locally deactivated by introducing a ``Fused``
|
||||
network element after a ``Fiber`` or a ``Roadm`` that doesn't need amplification.
|
||||
The amplifier is chosen in the EDFA list of the equipment library based on
|
||||
gain, power, and NF criteria. Only the EDFA that are marked
|
||||
``'allowed_for_design': true`` are considered.
|
||||
|
||||
For amplifiers defined in the topology JSON input but whose ``gain = 0``
|
||||
(placeholder), auto-design will set its gain automatically: see ``power_mode`` in
|
||||
the ``Spans`` library to find out how the gain is calculated.
|
||||
|
||||
Span configuration is performed as follows. It is not a list (which may change
|
||||
in later releases) and the user can only modify the value of existing
|
||||
parameters:
|
||||
|
||||
+-------------------------------------+-----------+---------------------------------------------+
|
||||
| field | type | description |
|
||||
+=====================================+===========+=============================================+
|
||||
| ``power_mode`` | (boolean) | If false, gain mode. Auto-design sets |
|
||||
| | | amplifier gain = preceding span loss, |
|
||||
| | | unless the amplifier exists and its |
|
||||
| | | gain > 0 in the topology input JSON. |
|
||||
| | | If true, power mode (recommended for |
|
||||
| | | auto-design and power sweep.) |
|
||||
| | | Auto-design sets amplifier power |
|
||||
| | | according to delta_power_range. If the |
|
||||
| | | amplifier exists with gain > 0 in the |
|
||||
| | | topology JSON input, then its gain is |
|
||||
| | | translated into a power target/channel. |
|
||||
| | | Moreover, when performing a power sweep |
|
||||
| | | (see ``power_range_db`` in the SI |
|
||||
| | | configuration library) the power sweep |
|
||||
| | | is performed w/r/t this power target, |
|
||||
| | | regardless of preceding amplifiers |
|
||||
| | | power saturation/limitations. |
|
||||
+-------------------------------------+-----------+---------------------------------------------+
|
||||
| ``delta_power_range_db`` | (number) | Auto-design only, power-mode |
|
||||
| | | only. Specifies the [min, max, step] |
|
||||
| | | power excursion/span. It is a relative |
|
||||
| | | power excursion w/r/t the |
|
||||
| | | power_dbm + power_range_db |
|
||||
| | | (power sweep if applicable) defined in |
|
||||
| | | the SI configuration library. This |
|
||||
| | | relative power excursion is = 1/3 of |
|
||||
| | | the span loss difference with the |
|
||||
| | | reference 20 dB span. The 1/3 slope is |
|
||||
| | | derived from the GN model equations. |
|
||||
| | | For example, a 23 dB span loss will be |
|
||||
| | | set to 1 dB more power than a 20 dB |
|
||||
| | | span loss. The 20 dB reference spans |
|
||||
| | | will *always* be set to |
|
||||
| | | power = power_dbm + power_range_db. |
|
||||
| | | To configure the same power in all |
|
||||
| | | spans, use `[0, 0, 0]`. All spans will |
|
||||
| | | be set to |
|
||||
| | | power = power_dbm + power_range_db. |
|
||||
| | | To configure the same power in all spans |
|
||||
| | | and 3 dB more power just for the longest |
|
||||
| | | spans: `[0, 3, 3]`. The longest spans are |
|
||||
| | | set to |
|
||||
| | | power = power_dbm + power_range_db + 3. |
|
||||
| | | To configure a 4 dB power range across |
|
||||
| | | all spans in 0.5 dB steps: `[-2, 2, 0.5]`. |
|
||||
| | | A 17 dB span is set to |
|
||||
| | | power = power_dbm + power_range_db - 1, |
|
||||
| | | a 20 dB span to |
|
||||
| | | power = power_dbm + power_range_db and |
|
||||
| | | a 23 dB span to |
|
||||
| | | power = power_dbm + power_range_db + 1 |
|
||||
+-------------------------------------+-----------+---------------------------------------------+
|
||||
| ``max_fiber_lineic_loss_for_raman`` | (number) | Maximum linear fiber loss for Raman |
|
||||
| | | amplification use. |
|
||||
+-------------------------------------+-----------+---------------------------------------------+
|
||||
| ``max_length`` | (number) | Split fiber lengths > max_length. |
|
||||
| | | Interest to support high level |
|
||||
| | | topologies that do not specify in line |
|
||||
| | | amplification sites. For example the |
|
||||
| | | CORONET_Global_Topology.xls defines |
|
||||
| | | links > 1000km between 2 sites: it |
|
||||
| | | couldn't be simulated if these links |
|
||||
| | | were not split in shorter span lengths. |
|
||||
+-------------------------------------+-----------+---------------------------------------------+
|
||||
| ``length_unit`` | "m"/"km" | Unit for ``max_length``. |
|
||||
+-------------------------------------+-----------+---------------------------------------------+
|
||||
| ``max_loss`` | (number) | Not used in the current code |
|
||||
| | | implementation. |
|
||||
+-------------------------------------+-----------+---------------------------------------------+
|
||||
| ``padding`` | (number) | In dB. Min span loss before putting an |
|
||||
| | | attenuator before fiber. Attenuator |
|
||||
| | | value |
|
||||
| | | Fiber.att_in = max(0, padding - span_loss). |
|
||||
| | | Padding can be set manually to reach a |
|
||||
| | | higher padding value for a given fiber |
|
||||
| | | by filling in the Fiber/params/att_in |
|
||||
| | | field in the topology json input [1] |
|
||||
| | | but if span_loss = length * loss_coef |
|
||||
| | | + att_in + con_in + con_out < padding, |
|
||||
| | | the specified att_in value will be |
|
||||
| | | completed to have span_loss = padding. |
|
||||
| | | Therefore it is not possible to set |
|
||||
| | | span_loss < padding. |
|
||||
+-------------------------------------+-----------+---------------------------------------------+
|
||||
| ``EOL`` | (number) | All fiber span loss ageing. The value |
|
||||
| | | is added to the con_out (fiber output |
|
||||
| | | connector). So the design and the path |
|
||||
| | | feasibility are performed with |
|
||||
| | | span_loss + EOL. EOL cannot be set |
|
||||
| | | manually for a given fiber span |
|
||||
| | | (workaround is to specify higher |
|
||||
| | | ``con_out`` loss for this fiber). |
|
||||
+-------------------------------------+-----------+---------------------------------------------+
|
||||
| ``con_in``, | (number) | Default values if Fiber/params/con_in/out |
|
||||
| ``con_out`` | | is None in the topology input |
|
||||
| | | description. This default value is |
|
||||
| | | ignored if a Fiber/params/con_in/out |
|
||||
| | | value is input in the topology for a |
|
||||
| | | given Fiber. |
|
||||
+-------------------------------------+-----------+---------------------------------------------+
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"uid": "fiber (A1->A2)",
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params":
|
||||
{
|
||||
"type_variety": "SSMF",
|
||||
"length": 120.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
"att_in": 0,
|
||||
"con_in": 0,
|
||||
"con_out": 0
|
||||
}
|
||||
}
|
||||
|
||||
ROADMs can be configured as follows. The user can only modify the value of
|
||||
existing parameters:
|
||||
|
||||
+--------------------------+-----------+---------------------------------------------+
|
||||
| field | type | description |
|
||||
+==========================+===========+=============================================+
|
||||
| ``target_pch_out_db`` | (number) | Auto-design sets the ROADM egress channel |
|
||||
| | | power. This reflects typical control loop |
|
||||
| | | algorithms that adjust ROADM losses to |
|
||||
| | | equalize channels (eg coming from different |
|
||||
| | | ingress direction or add ports) |
|
||||
| | | This is the default value |
|
||||
| | | Roadm/params/target_pch_out_db if no value |
|
||||
| | | is given in the ``Roadm`` element in the |
|
||||
| | | topology input description. |
|
||||
| | | This default value is ignored if a |
|
||||
| | | params/target_pch_out_db value is input in |
|
||||
| | | the topology for a given ROADM. |
|
||||
+--------------------------+-----------+---------------------------------------------+
|
||||
| ``add_drop_osnr`` | (number) | OSNR contribution from the add/drop ports |
|
||||
+--------------------------+-----------+---------------------------------------------+
|
||||
| ``restrictions`` | (dict of | If non-empty, keys ``preamp_variety_list`` |
|
||||
| | strings) | and ``booster_variety_list`` represent |
|
||||
| | | list of ``type_variety`` amplifiers which |
|
||||
| | | are allowed for auto-design within ROADM's |
|
||||
| | | line degrees. |
|
||||
| | | |
|
||||
| | | If no booster should be placed on a degree, |
|
||||
| | | insert a ``Fused`` node on the degree |
|
||||
| | | output. |
|
||||
+--------------------------+-----------+---------------------------------------------+
|
||||
|
||||
The ``SpectralInformation`` object can be configured as follows. The user can
|
||||
only modify the value of existing parameters. It defines a spectrum of N
|
||||
identical carriers. While the code libraries allow for different carriers and
|
||||
power levels, the current user parametrization only allows one carrier type and
|
||||
one power/channel definition.
|
||||
|
||||
+----------------------+-----------+-------------------------------------------+
|
||||
| field | type | description |
|
||||
+======================+===========+===========================================+
|
||||
| ``f_min``, | (number) | In Hz. Carrier min max excursion. |
|
||||
| ``f_max`` | | |
|
||||
+----------------------+-----------+-------------------------------------------+
|
||||
| ``baud_rate`` | (number) | In Hz. Simulated baud rate. |
|
||||
+----------------------+-----------+-------------------------------------------+
|
||||
| ``spacing`` | (number) | In Hz. Carrier spacing. |
|
||||
+----------------------+-----------+-------------------------------------------+
|
||||
| ``roll_off`` | (number) | Not used. |
|
||||
+----------------------+-----------+-------------------------------------------+
|
||||
| ``tx_osnr`` | (number) | In dB. OSNR out from transponder. |
|
||||
+----------------------+-----------+-------------------------------------------+
|
||||
| ``power_dbm`` | (number) | Reference channel power. In gain mode |
|
||||
| | | (see spans/power_mode = false), all gain |
|
||||
| | | settings are offset w/r/t this reference |
|
||||
| | | power. In power mode, it is the |
|
||||
| | | reference power for |
|
||||
| | | Spans/delta_power_range_db. For example, |
|
||||
| | | if delta_power_range_db = `[0,0,0]`, the |
|
||||
| | | same power=power_dbm is launched in every |
|
||||
| | | spans. The network design is performed |
|
||||
| | | with the power_dbm value: even if a |
|
||||
| | | power sweep is defined (see after) the |
|
||||
| | | design is not repeated. |
|
||||
+----------------------+-----------+-------------------------------------------+
|
||||
| ``power_range_db`` | (number) | Power sweep excursion around power_dbm. |
|
||||
| | | It is not the min and max channel power |
|
||||
| | | values! The reference power becomes: |
|
||||
| | | power_range_db + power_dbm. |
|
||||
+----------------------+-----------+-------------------------------------------+
|
||||
| ``sys_margins`` | (number) | In dB. Added margin on min required |
|
||||
| | | transceiver OSNR. |
|
||||
+----------------------+-----------+-------------------------------------------+
|
||||
|
||||
The `transmission_main_example.py <examples/transmission_main_example.py>`_ script propagates a spectrum of channels at 32 Gbaud, 50 GHz spacing and 0 dBm/channel.
|
||||
The ``gnpy-transmission-example`` script propagates a spectrum of channels at 32 Gbaud, 50 GHz spacing and 0 dBm/channel.
|
||||
Launch power can be overridden by using the ``--power`` argument.
|
||||
Spectrum information is not yet parametrized but can be modified directly in the ``eqpt_config.json`` (via the ``SpectralInformation`` -SI- structure) to accommodate any baud rate or spacing.
|
||||
The number of channel is computed based on ``spacing`` and ``f_min``, ``f_max`` values.
|
||||
|
||||
An experimental support for Raman amplification is available:
|
||||
|
||||
.. code-block:: shell
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ ./examples/transmission_main_example.py \
|
||||
examples/raman_edfa_example_network.json \
|
||||
--sim examples/sim_params.json --show-channels
|
||||
$ gnpy-transmission-example \
|
||||
$(gnpy-example-data)/raman_edfa_example_network.json \
|
||||
--sim $(gnpy-example-data)/sim_params.json --show-channels
|
||||
|
||||
Configuration of Raman pumps (their frequencies, power and pumping direction) is done via the `RamanFiber element in the network topology <examples/raman_edfa_example_network.json>`_.
|
||||
General numeric parameters for simulaiton control are provided in the `examples/sim_params.json <examples/sim_params.json>`_.
|
||||
Configuration of Raman pumps (their frequencies, power and pumping direction) is done via the `RamanFiber element in the network topology <gnpy/example-data/raman_edfa_example_network.json>`_.
|
||||
General numeric parameters for simulaiton control are provided in the `gnpy/example-data/sim_params.json <gnpy/example-data/sim_params.json>`_.
|
||||
|
||||
Use `examples/path_requests_run.py <examples/path_requests_run.py>`_ to run multiple optimizations as follows:
|
||||
Use ``gnpy-path-request`` to request several paths at once:
|
||||
|
||||
.. code-block:: shell
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ python path_requests_run.py -h
|
||||
Usage: path_requests_run.py [-h] [-v] [-o OUTPUT] [network_filename] [service_filename] [eqpt_filename]
|
||||
$ cd $(gnpy-example-data)
|
||||
$ gnpy-path-request -o output_file.json \
|
||||
meshTopologyExampleV2.xls meshTopologyExampleV2_services.json
|
||||
|
||||
The ``network_filename`` and ``service_filename`` can be an XLS or JSON file. The ``eqpt_filename`` must be a JSON file.
|
||||
This program operates on a network topology (`JSON <docs/json.rst>`__ or `Excel <docs/excel.rst>`__ format), processing the list of service requests (JSON or XLS again).
|
||||
The service requests and reply formats are based on the `draft-ietf-teas-yang-path-computation-01 <https://tools.ietf.org/html/draft-ietf-teas-yang-path-computation-01>`__ with custom extensions (e.g., for transponder modes).
|
||||
An example of the JSON input is provided in file `service-template.json`, while results are shown in `path_result_template.json`.
|
||||
|
||||
To see an example of it, run:
|
||||
Important note: ``gnpy-path-request`` is not a network dimensionning tool: each service does not reserve spectrum, or occupy ressources such as transponders. It only computes path feasibility assuming the spectrum (between defined frequencies) is loaded with "nb of channels" spaced by "spacing" values as specified in the system parameters input in the service file, each cannel having the same characteristics in terms of baudrate, format,... as the service transponder. The transceiver element acts as a "logical starting/stopping point" for the spectral information propagation. At that point it is not meant to represent the capacity of add drop ports.
|
||||
As a result transponder type is not part of the network info. it is related to the list of services requests.
|
||||
|
||||
.. code-block:: shell
|
||||
The current version includes a spectrum assigment features that enables to compute a candidate spectrum assignment for each service based on a first fit policy. Spectrum is assigned based on service specified spacing value, path_bandwidth value and selected mode for the transceiver. This spectrum assignment includes a basic capacity planning capability so that the spectrum resource is limited by the frequency min and max values defined for the links. If the requested services reach the link spectrum capacity, additional services feasibility are computed but marked as blocked due to spectrum reason.
|
||||
|
||||
$ cd examples
|
||||
$ python path_requests_run.py meshTopologyExampleV2.xls meshTopologyExampleV2_services.json eqpt_config.json -o output_file.json
|
||||
REST API (experimental)
|
||||
-----------------------
|
||||
``gnpy`` provides an experimental api for requesting several paths at once. It is based on Flask server.
|
||||
You can run it through command line or Docker.
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ gnpy-rest
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ docker run -p 8080:8080 -dit xxxx gnpy-rest
|
||||
|
||||
After starting the api server, you can lauch a request
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ curl -v -X POST -H "Content-Type: application/json" -d @<PATH_TO_JSON_REQUEST_FILE> http://localhost:8080/api/v1/path-computation
|
||||
|
||||
This program requires a list of connections to be estimated and the equipment
|
||||
library. The program computes performances for the list of services (accepts
|
||||
JSON or Excel format) using the same spectrum propagation modules as
|
||||
``transmission_main_example.py``. Explanation on the Excel template is provided in
|
||||
the `Excel_userguide.rst <Excel_userguide.rst#service-sheet>`_. Template for
|
||||
the JSON format can be found here: `service-template.json
|
||||
<service-template.json>`_.
|
||||
|
||||
Contributing
|
||||
------------
|
||||
@@ -591,14 +203,14 @@ working group set out to disrupt the planning landscape by providing an open
|
||||
source simulation model which can be used freely across multiple vendor
|
||||
implementations.
|
||||
|
||||
.. |docs| image:: https://readthedocs.org/projects/gnpy/badge/?version=develop
|
||||
:target: http://gnpy.readthedocs.io/en/develop/?badge=develop
|
||||
.. |docs| image:: https://readthedocs.org/projects/gnpy/badge/?version=master
|
||||
:target: http://gnpy.readthedocs.io/en/master/?badge=master
|
||||
:alt: Documentation Status
|
||||
:scale: 100%
|
||||
|
||||
.. |build| image:: https://travis-ci.com/Telecominfraproject/oopt-gnpy.svg?branch=develop
|
||||
.. |travis| image:: https://travis-ci.com/Telecominfraproject/oopt-gnpy.svg?branch=master
|
||||
:target: https://travis-ci.com/Telecominfraproject/oopt-gnpy
|
||||
:alt: Build Status
|
||||
:alt: Build Status via Travis CI
|
||||
:scale: 100%
|
||||
|
||||
.. |doi| image:: https://zenodo.org/badge/96894149.svg
|
||||
@@ -606,6 +218,21 @@ implementations.
|
||||
:alt: DOI
|
||||
:scale: 100%
|
||||
|
||||
.. |contributors| image:: https://img.shields.io/github/contributors-anon/Telecominfraproject/oopt-gnpy
|
||||
:target: https://github.com/Telecominfraproject/oopt-gnpy/graphs/contributors
|
||||
:alt: Code Contributors via GitHub
|
||||
:scale: 100%
|
||||
|
||||
.. |codacy-quality| image:: https://img.shields.io/lgtm/grade/python/github/Telecominfraproject/oopt-gnpy
|
||||
:target: https://lgtm.com/projects/g/Telecominfraproject/oopt-gnpy/
|
||||
:alt: Code Quality via LGTM.com
|
||||
:scale: 100%
|
||||
|
||||
.. |codecov| image:: https://img.shields.io/codecov/c/github/Telecominfraproject/oopt-gnpy
|
||||
:target: https://codecov.io/gh/Telecominfraproject/oopt-gnpy
|
||||
:alt: Code Coverage via codecov
|
||||
:scale: 100%
|
||||
|
||||
TIP OOPT/PSE & PSE WG Charter
|
||||
-----------------------------
|
||||
|
||||
|
||||
31
docs/conf.py
31
docs/conf.py
@@ -32,7 +32,9 @@ sys.path.insert(0, os.path.abspath('../'))
|
||||
# ones.
|
||||
extensions = ['sphinx.ext.autodoc',
|
||||
'sphinx.ext.mathjax',
|
||||
'sphinx.ext.githubpages','sphinxcontrib.bibtex']
|
||||
'sphinx.ext.githubpages',
|
||||
'sphinxcontrib.bibtex',
|
||||
'pbr.sphinxext',]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
@@ -51,15 +53,6 @@ project = 'gnpy'
|
||||
copyright = '2018, Telecom InfraProject - OOPT PSE Group'
|
||||
author = 'Telecom InfraProject - OOPT PSE Group'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.1'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '0.1'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
@@ -87,8 +80,17 @@ todo_include_todos = False
|
||||
on_rtd = os.environ.get('READTHEDOCS') == 'True'
|
||||
if on_rtd:
|
||||
html_theme = 'default'
|
||||
html_theme_options = {
|
||||
'logo_only': True,
|
||||
}
|
||||
else:
|
||||
html_theme = 'alabaster'
|
||||
html_theme_options = {
|
||||
'logo': 'images/GNPy-logo.png',
|
||||
'logo_name': False,
|
||||
}
|
||||
|
||||
html_logo = 'images/GNPy-logo.png'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
@@ -99,7 +101,7 @@ else:
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
html_static_path = []
|
||||
|
||||
# Custom sidebar templates, must be a dictionary that maps document names
|
||||
# to template names.
|
||||
@@ -173,4 +175,9 @@ texinfo_documents = [
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
autodoc_default_flags = ['members', 'undoc-members', 'private-members', 'show-inheritance']
|
||||
autodoc_default_options = {
|
||||
'members': True,
|
||||
'undoc-members': True,
|
||||
'private-members': True,
|
||||
'show-inheritance': True,
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
Excel (XLS, XLSX) input files
|
||||
=============================
|
||||
|
||||
How to prepare the Excel input file
|
||||
-----------------------------------
|
||||
|
||||
`examples/transmission_main_example.py <examples/transmission_main_example.py>`_ gives the possibility to use an excel input file instead of a json file. The program then will generate the corresponding json file for you.
|
||||
``gnpy-transmission-example`` gives the possibility to use an excel input file instead of a json file. The program then will generate the corresponding json file for you.
|
||||
|
||||
The file named 'meshTopologyExampleV2.xls' is an example.
|
||||
|
||||
@@ -16,6 +15,8 @@ In order to work the excel file MUST contain at least 2 sheets:
|
||||
- Eqt
|
||||
- Service
|
||||
|
||||
.. _excel-nodes-sheet:
|
||||
|
||||
Nodes sheet
|
||||
-----------
|
||||
|
||||
@@ -34,7 +35,7 @@ Each line represents a 'node' (ROADM site or an in line amplifier site ILA or a
|
||||
- If filled, it can take "ROADM", "FUSED" or "ILA" values. If another string is used, it will be considered as not filled. FUSED means that ingress and egress spans will be fused together.
|
||||
|
||||
- *State*, *Country*, *Region* are not mandatory.
|
||||
"Region" is a holdover from the CORONET topology reference file `CORONET_Global_Topology.xls <examples/CORONET_Global_Topology.xls>`_. CORONET separates its network into geographical regions (Europe, Asia, Continental US.) This information is not used by gnpy.
|
||||
"Region" is a holdover from the CORONET topology reference file `CORONET_Global_Topology.xlsx <gnpy/example-data/CORONET_Global_Topology.xlsx>`_. CORONET separates its network into geographical regions (Europe, Asia, Continental US.) This information is not used by gnpy.
|
||||
|
||||
- *Longitude*, *Latitude* are not mandatory. If filled they should contain numbers.
|
||||
|
||||
@@ -44,6 +45,8 @@ Each line represents a 'node' (ROADM site or an in line amplifier site ILA or a
|
||||
**There MUST NOT be empty line(s) between two nodes lines**
|
||||
|
||||
|
||||
.. _excel-links-sheet:
|
||||
|
||||
Links sheet
|
||||
-----------
|
||||
|
||||
@@ -80,11 +83,11 @@ and a fiber span from node3 to node6::
|
||||
|
||||
- If filled it MUST contain numbers. If empty it is replaced by a default "80" km value.
|
||||
- If value is below 150 km, it is considered as a single (bidirectional) fiber span.
|
||||
- If value is over 150 km the `transmission_main_example.py <examples/transmission_main_example.py>`_ program will automatically suppose that intermediate span description are required and will generate fiber spans elements with "_1","_2", ... trailing strings which are not visible in the json output. The reason for the splitting is that current edfa usually do not support large span loss. The current assumption is that links larger than 150km will require intermediate amplification. This value will be revisited when Raman amplification is added”
|
||||
- If value is over 150 km the `gnpy-transmission-example`` program will automatically suppose that intermediate span description are required and will generate fiber spans elements with "_1","_2", ... trailing strings which are not visible in the json output. The reason for the splitting is that current edfa usually do not support large span loss. The current assumption is that links larger than 150km will require intermediate amplification. This value will be revisited when Raman amplification is added”
|
||||
|
||||
- **Fiber type** is not mandatory.
|
||||
|
||||
If filled it must contain types listed in `eqpt_config.json <examples/eqpt_config.json>`_ in "Fiber" list "type_variety".
|
||||
If filled it must contain types listed in `eqpt_config.json <gnpy/example-data/eqpt_config.json>`_ in "Fiber" list "type_variety".
|
||||
If not filled it takes "SSMF" as default value.
|
||||
|
||||
- **Lineic att** is not mandatory.
|
||||
@@ -113,14 +116,16 @@ and a fiber span from node3 to node6::
|
||||
|
||||
(in progress)
|
||||
|
||||
.. _excel-equipment-sheet:
|
||||
|
||||
Eqpt sheet
|
||||
----------
|
||||
|
||||
Eqt sheet is optional. It lists the amplifiers types and characteristics on each degree of the *Node A* line.
|
||||
Eqpt sheet must contain twelve columns::
|
||||
|
||||
<-- east cable from a to z --> <-- west from z to a -->
|
||||
Node A ; Node Z ; amp type ; att_in ; amp gain ; tilt ; att_out ; amp type ; att_in ; amp gain ; tilt ; att_out
|
||||
<-- east cable from a to z --> <-- west from z to a -->
|
||||
Node A ; Node Z ; amp type ; att_in ; amp gain ; tilt ; att_out ; delta_p ; amp type ; att_in ; amp gain ; tilt ; att_out ; delta_p
|
||||
|
||||
If the sheet is present, it MUST have as many lines as egress directions of ROADMs defined in Links Sheet.
|
||||
|
||||
@@ -150,11 +155,11 @@ then Eqpt sheet should contain:
|
||||
C - amp3
|
||||
|
||||
|
||||
In case you already have filled Nodes and Links sheets `create_eqpt_sheet.py <examples/create_eqpt_sheet.py>`_ can be used to automatically create a template for the mandatory entries of the list.
|
||||
In case you already have filled Nodes and Links sheets `create_eqpt_sheet.py <gnpy/example-data/create_eqpt_sheet.py>`_ can be used to automatically create a template for the mandatory entries of the list.
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
$ cd examples
|
||||
$ cd $(gnpy-example-data)
|
||||
$ python create_eqpt_sheet.py meshTopologyExampleV2.xls
|
||||
|
||||
This generates a text file meshTopologyExampleV2_eqt_sheet.txt whose content can be directly copied into the Eqt sheet of the excel file. The user then can fill the values in the rest of the columns.
|
||||
@@ -167,7 +172,7 @@ This generates a text file meshTopologyExampleV2_eqt_sheet.txt whose content ca
|
||||
- **Node Z** is mandatory. It is the egress direction from the *Node A* site. Multiple Links between the same Node A and NodeZ is not supported.
|
||||
|
||||
- **amp type** is not mandatory.
|
||||
If filled it must contain types listed in `eqpt_config.json <examples/eqpt_config.json>`_ in "Edfa" list "type_variety".
|
||||
If filled it must contain types listed in `eqpt_config.json <gnpy/example-data/eqpt_config.json>`_ in "Edfa" list "type_variety".
|
||||
If not filled it takes "std_medium_gain" as default value.
|
||||
If filled with fused, a fused element with 0.0 dB loss will be placed instead of an amplifier. This might be used to avoid booster amplifier on a ROADM direction.
|
||||
|
||||
@@ -175,19 +180,23 @@ This generates a text file meshTopologyExampleV2_eqt_sheet.txt whose content ca
|
||||
If not filled, it will be determined with design rules in the convert.py file.
|
||||
If filled, it must contain positive numbers.
|
||||
|
||||
- *att_in* and *att_out* are not mandatory and are not used yet. They are the value of the attenautor at input and output of amplifier (in dB).
|
||||
- *att_in* and *att_out* are not mandatory and are not used yet. They are the value of the attenuator at input and output of amplifier (in dB).
|
||||
If filled they must contain positive numbers.
|
||||
|
||||
- *tilt* --TODO--
|
||||
|
||||
- **delta_p**, in dBm, is not mandatory. If filled it is used to set the output target power per channel at the output of the amplifier, if power_mode is True. The output power is then set to power_dbm + delta_power.
|
||||
|
||||
# to be completed #
|
||||
|
||||
(in progress)
|
||||
|
||||
.. _excel-service-sheet:
|
||||
|
||||
Service sheet
|
||||
-------------
|
||||
|
||||
Service sheet is optional. It lists the services for which path and feasibility must be computed with path_requests_run.py.
|
||||
Service sheet is optional. It lists the services for which path and feasibility must be computed with ``gnpy-path_request``.
|
||||
|
||||
Service sheet must contain 11 columns::
|
||||
|
||||
@@ -216,36 +225,4 @@ Service sheet must contain 11 columns::
|
||||
- path: is the set of ROADM nodes that must be used by the path. It must contain the list of ROADM names that the path must cross. TODO : only ROADM nodes are accepted in this release. Relax this with any type of nodes. If filled it must contain ROADM ids separated by ' | '. Exact names are required.
|
||||
- is loose? 'no' value means that the list of nodes should be strictly followed, while any other value means that the constraint may be relaxed if the node is not reachable.
|
||||
|
||||
- ** path bandwidth** is optional. It is the amount of capacity required between source and destination in Gbit/s. Default value is 0.0 Gbit/s.
|
||||
|
||||
path_requests_run.py
|
||||
------------------------
|
||||
|
||||
**Usage**: path_requests_run.py [-h] [-v] [-o OUTPUT]
|
||||
[network_filename xls or json] [service_filename xls or json] [eqpt_filename json]
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
$ cd examples
|
||||
$ python path_requests_run.py meshTopologyExampleV2.xls service_file.json eqpt_file -o output_file.json
|
||||
|
||||
A function that computes performances for a list of services provided in the service file (accepts json or excel format.
|
||||
|
||||
if the service <file.xls> is in xls format, path_requests_run.py converts it to a json file <file_services.json> following the Yang model for requesting Path Computation defined in `draft-ietf-teas-yang-path-computation-01.txt <https://www.ietf.org/id/draft-ietf-teas-yang-path-computation-01.pdf>`_. For PSE use, additional fields with trx type and mode have been added to the te-bandwidth field.
|
||||
|
||||
A template for the json file can be found here: `service_template.json <service_template.json>`_
|
||||
|
||||
|
||||
If no output file is given, the computation is shown on standard output for demo.
|
||||
If a file is specified with the optional -o argument, the result of the computation is converted into a json format following the Yang model for requesting Path Computation defined in `draft-ietf-teas-yang-path-computation-01.txt <https://www.ietf.org/id/draft-ietf-teas-yang-path-computation-01.pdf>`_. TODO: verify that this implementation is correct + give feedback to ietf on what is missing for our specific application.
|
||||
|
||||
A template for the result of computation json file can be found here: `path_result_template.json <path_result_template.json>`_
|
||||
|
||||
Important note: path_requests_run.py is not a network dimensionning tool : each service does not reserve spectrum, or occupy ressources such as transponders. It only computes path feasibility assuming the spectrum (between defined frequencies) is loaded with "nb of channels" spaced by "spacing" values as specified in the system parameters input in the service file, each cannel having the same characteristics in terms of baudrate, format, ... as the service transponder. The transceiver element acts as a "logical starting/stopping point" for the spectral information propagation. At that point it is not meant to represent the capacity of add drop ports
|
||||
As a result transponder type is not part of the network info. it is related to the list of services requests.
|
||||
|
||||
In a next step we plan to provide required features to enable dimensionning : alocation of ressources, counting channels, limitation of the number of channels, ...
|
||||
|
||||
(in progress)
|
||||
|
||||
|
||||
- **path bandwidth** is mandatory. It is the amount of capacity required between source and destination in Gbit/s. Value should be positive (non zero). It is used to compute the amount of required spectrum for the service.
|
||||
13
docs/gnpy-api-core.rst
Normal file
13
docs/gnpy-api-core.rst
Normal file
@@ -0,0 +1,13 @@
|
||||
``gnpy.core``
|
||||
-------------
|
||||
|
||||
.. automodule:: gnpy.core
|
||||
.. automodule:: gnpy.core.ansi_escapes
|
||||
.. automodule:: gnpy.core.elements
|
||||
.. automodule:: gnpy.core.equipment
|
||||
.. automodule:: gnpy.core.exceptions
|
||||
.. automodule:: gnpy.core.info
|
||||
.. automodule:: gnpy.core.network
|
||||
.. automodule:: gnpy.core.parameters
|
||||
.. automodule:: gnpy.core.science_utils
|
||||
.. automodule:: gnpy.core.utils
|
||||
9
docs/gnpy-api-tools.rst
Normal file
9
docs/gnpy-api-tools.rst
Normal file
@@ -0,0 +1,9 @@
|
||||
``gnpy.tools``
|
||||
--------------
|
||||
|
||||
.. automodule:: gnpy.tools
|
||||
.. automodule:: gnpy.tools.cli_examples
|
||||
.. automodule:: gnpy.tools.convert
|
||||
.. automodule:: gnpy.tools.json_io
|
||||
.. automodule:: gnpy.tools.plots
|
||||
.. automodule:: gnpy.tools.service_sheet
|
||||
6
docs/gnpy-api-topology.rst
Normal file
6
docs/gnpy-api-topology.rst
Normal file
@@ -0,0 +1,6 @@
|
||||
``gnpy.topology``
|
||||
-----------------
|
||||
|
||||
.. automodule:: gnpy.topology
|
||||
.. automodule:: gnpy.topology.request
|
||||
.. automodule:: gnpy.topology.spectrum_assignment
|
||||
14
docs/gnpy-api.rst
Normal file
14
docs/gnpy-api.rst
Normal file
@@ -0,0 +1,14 @@
|
||||
***************************
|
||||
API Reference Documentation
|
||||
***************************
|
||||
|
||||
``gnpy`` package
|
||||
================
|
||||
|
||||
.. automodule:: gnpy
|
||||
|
||||
.. toctree::
|
||||
|
||||
gnpy-api-core
|
||||
gnpy-api-topology
|
||||
gnpy-api-tools
|
||||
BIN
docs/images/GNPy-logo.png
Normal file
BIN
docs/images/GNPy-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -1,33 +1,18 @@
|
||||
.. gnpy documentation master file, created by
|
||||
sphinx-quickstart on Mon Dec 18 14:41:01 2017.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
GNPy: Optical Route Planning Library
|
||||
=====================================================================
|
||||
|
||||
Welcome to gnpy's documentation!
|
||||
================================
|
||||
|
||||
**gnpy is an open-source, community-developed library for building route planning
|
||||
and optimization tools in real-world mesh optical networks.**
|
||||
|
||||
`gnpy <http://github.com/telecominfraproject/gnpy>`_ is:
|
||||
|
||||
- a sponsored project of the `OOPT/PSE <http://telecominfraproject.com/project-groups-2/backhaul-projects/open-optical-packet-transport/>`_ working group of the `Telecom Infra Project <http://telecominfraproject.com>`_.
|
||||
- fully community-driven, fully open source library
|
||||
- driven by a consortium of operators, vendors, and academic researchers
|
||||
- intended for rapid development of production-grade route planning tools
|
||||
- easily extensible to include custom network elements
|
||||
- performant to the scale of real-world mesh optical networks
|
||||
|
||||
Documentation
|
||||
=============
|
||||
|
||||
The following pages are meant to describe specific implementation details and
|
||||
modeling assumptions behind gnpy.
|
||||
`GNPy <http://github.com/telecominfraproject/gnpy>`_ is an open-source,
|
||||
community-developed library for building route planning and optimization tools
|
||||
in real-world mesh optical networks. It is based on the Gaussian Noise Model.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:maxdepth: 4
|
||||
|
||||
install
|
||||
json
|
||||
excel
|
||||
model
|
||||
gnpy-api
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
@@ -36,67 +21,3 @@ Indices and tables
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
||||
Contributors in alphabetical order
|
||||
==================================
|
||||
+----------+------------+-----------------------+--------------------------------------+
|
||||
| Name | Surname | Affiliation | Contact |
|
||||
+==========+============+=======================+======================================+
|
||||
| Alessio | Ferrari | Politecnico di Torino | alessio.ferrari@polito.it |
|
||||
+----------+------------+-----------------------+--------------------------------------+
|
||||
| Anders | Lindgren | Telia Company | Anders.X.Lindgren@teliacompany.com |
|
||||
+----------+------------+-----------------------+--------------------------------------+
|
||||
| Andrea | d'Amico | Politecnico di Torino | andrea.damico@polito.it |
|
||||
+----------+------------+-----------------------+--------------------------------------+
|
||||
| Brian | Taylor | Facebook | briantaylor@fb.com |
|
||||
+----------+------------+-----------------------+--------------------------------------+
|
||||
| David | Boertjes | Ciena | dboertje@ciena.com |
|
||||
+----------+------------+-----------------------+--------------------------------------+
|
||||
| Diego | Landa | Facebook | dlanda@fb.com |
|
||||
+----------+------------+-----------------------+--------------------------------------+
|
||||
| Esther | Le Rouzic | Orange | esther.lerouzic@orange.com |
|
||||
+----------+------------+-----------------------+--------------------------------------+
|
||||
| Gabriele | Galimberti | Cisco | ggalimbe@cisco.com |
|
||||
+----------+------------+-----------------------+--------------------------------------+
|
||||
| Gert | Grammel | Juniper Networks | ggrammel@juniper.net |
|
||||
+----------+------------+-----------------------+--------------------------------------+
|
||||
| Gilad | Goldfarb | Facebook | giladg@fb.com |
|
||||
+----------+------------+-----------------------+--------------------------------------+
|
||||
| James | Powell | Telecom Infra Project | james.powell@telecominfraproject.com |
|
||||
+----------+------------+-----------------------+--------------------------------------+
|
||||
| Jan | Kundrát | Telecom Infra Project | jan.kundrat@telecominfraproject.com |
|
||||
+----------+------------+-----------------------+--------------------------------------+
|
||||
| Jeanluc | Augé | Orange | jeanluc.auge@orange.com |
|
||||
+----------+------------+-----------------------+--------------------------------------+
|
||||
| Jonas | Mårtensson | RISE Research Sweden | jonas.martensson@ri.se |
|
||||
+----------+------------+-----------------------+--------------------------------------+
|
||||
| Mattia | Cantono | Politecnico di Torino | mattia.cantono@polito.it |
|
||||
+----------+------------+-----------------------+--------------------------------------+
|
||||
| Miguel | Garrich | University Catalunya | miquel.garrich@upct.es |
|
||||
+----------+------------+-----------------------+--------------------------------------+
|
||||
| Raj | Nagarajan | Lumentum | raj.nagarajan@lumentum.com |
|
||||
+----------+------------+-----------------------+--------------------------------------+
|
||||
| Roberts | Miculens | Lattelecom | roberts.miculens@lattelecom.lv |
|
||||
+----------+------------+-----------------------+--------------------------------------+
|
||||
| Shengxiang | Zhu | University of Arizona | szhu@email.arizona.edu |
|
||||
+----------+------------+-----------------------+--------------------------------------+
|
||||
| Stefan | Melin | Telia Company | Stefan.Melin@teliacompany.com |
|
||||
+----------+------------+-----------------------+--------------------------------------+
|
||||
| Vittorio | Curri | Politecnico di Torino | vittorio.curri@polito.it |
|
||||
+----------+------------+-----------------------+--------------------------------------+
|
||||
| Xufeng | Liu | Jabil | xufeng_liu@jabil.com |
|
||||
+----------+------------+-----------------------+--------------------------------------+
|
||||
|
||||
--------------
|
||||
|
||||
- Goal is to build an end-to-end simulation environment which defines the
|
||||
network models of the optical device transfer functions and their parameters.
|
||||
This environment will provide validation of the optical performance
|
||||
requirements for the TIP OLS building blocks.
|
||||
- The model may be approximate or complete depending on the network complexity.
|
||||
Each model shall be validated against the proposed network scenario.
|
||||
- The environment must be able to process network models from multiple vendors,
|
||||
and also allow users to pick any implementation in an open source framework.
|
||||
- The PSE will influence and benefit from the innovation of the DTC, API, and
|
||||
OLS working groups.
|
||||
- The PSE represents a step along the journey towards multi-layer optimization.
|
||||
|
||||
|
||||
111
docs/install.rst
Normal file
111
docs/install.rst
Normal file
@@ -0,0 +1,111 @@
|
||||
Installing GNPy
|
||||
---------------
|
||||
|
||||
There are several methods on how to obtain GNPy.
|
||||
The easiest option for a non-developer is probably going via our :ref:`Docker images<install-docker>`.
|
||||
Developers are encouraged to install the :ref:`Python package in the same way as any other Python package<install-pip>`.
|
||||
Note that this needs a :ref:`working installation of Python<install-python>`, for example :ref:`via Anaconda<install-anaconda>`.
|
||||
|
||||
.. _install-docker:
|
||||
|
||||
Using prebuilt Docker images
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Our `Docker images <https://hub.docker.com/r/telecominfraproject/oopt-gnpy>`_ contain everything needed to run all examples from this guide.
|
||||
Docker transparently fetches the image over the network upon first use.
|
||||
On Linux and Mac, run:
|
||||
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ docker run -it --rm --volume $(pwd):/shared telecominfraproject/oopt-gnpy
|
||||
root@bea050f186f7:/shared/example-data#
|
||||
|
||||
On Windows, launch from Powershell as:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
PS C:\> docker run -it --rm --volume ${PWD}:/shared telecominfraproject/oopt-gnpy
|
||||
root@89784e577d44:/shared/example-data#
|
||||
|
||||
In both cases, a directory named ``example-data/`` will appear in your current working directory.
|
||||
GNPy automaticallly populates it with example files from the current release.
|
||||
Remove that directory if you want to start from scratch.
|
||||
|
||||
.. _install-python:
|
||||
|
||||
Using Python on your computer
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Note**: `gnpy` supports Python 3 only. Python 2 is not supported.
|
||||
`gnpy` requires Python ≥3.6
|
||||
|
||||
**Note**: the `gnpy` maintainers strongly recommend the use of Anaconda for
|
||||
managing dependencies.
|
||||
|
||||
It is recommended that you use a "virtual environment" when installing `gnpy`.
|
||||
Do not install `gnpy` on your system Python.
|
||||
|
||||
.. _install-anaconda:
|
||||
|
||||
We recommend the use of the `Anaconda Python distribution <https://www.anaconda.com/download>`_ which comes with many scientific computing
|
||||
dependencies pre-installed. Anaconda creates a base "virtual environment" for
|
||||
you automatically. You can also create and manage your ``conda`` "virtual
|
||||
environments" yourself (see:
|
||||
https://conda.io/docs/user-guide/tasks/manage-environments.html)
|
||||
|
||||
To activate your Anaconda virtual environment, you may need to do the
|
||||
following:
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ source /path/to/anaconda/bin/activate # activate Anaconda base environment
|
||||
(base) $ # note the change to the prompt
|
||||
|
||||
You can check which Anaconda environment you are using with:
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
(base) $ conda env list # list all environments
|
||||
# conda environments:
|
||||
#
|
||||
base * /src/install/anaconda3
|
||||
|
||||
(base) $ echo $CONDA_DEFAULT_ENV # show default environment
|
||||
base
|
||||
|
||||
You can check your version of Python with the following. If you are using
|
||||
Anaconda's Python 3, you should see similar output as below. Your results may
|
||||
be slightly different depending on your Anaconda installation path and the
|
||||
exact version of Python you are using.
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ which python # check which Python executable is used
|
||||
/path/to/anaconda/bin/python
|
||||
$ python -V # check your Python version
|
||||
Python 3.6.5 :: Anaconda, Inc.
|
||||
|
||||
.. _install-pip:
|
||||
|
||||
Installing the Python package
|
||||
*****************************
|
||||
|
||||
From within your Anaconda Python 3 environment, you can clone the master branch
|
||||
of the `gnpy` repo and install it with:
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ git clone https://github.com/Telecominfraproject/oopt-gnpy # clone the repo
|
||||
$ cd oopt-gnpy
|
||||
$ python setup.py develop
|
||||
|
||||
To test that `gnpy` was successfully installed, you can run this command. If it
|
||||
executes without a ``ModuleNotFoundError``, you have successfully installed
|
||||
`gnpy`.
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
$ python -c 'import gnpy' # attempt to import gnpy
|
||||
|
||||
$ pytest # run tests
|
||||
339
docs/json.rst
Normal file
339
docs/json.rst
Normal file
@@ -0,0 +1,339 @@
|
||||
JSON Input Files
|
||||
================
|
||||
|
||||
GNPy uses a set of JSON files for modeling the network.
|
||||
Some data (such as network topology or the service requests) can be also passed via :ref:`XLS files<excel-service-sheet>`.
|
||||
|
||||
Equipment Library
|
||||
-----------------
|
||||
|
||||
Design and transmission parameters are defined in a dedicated json file. By
|
||||
default, this information is read from `gnpy/example-data/eqpt_config.json
|
||||
<gnpy/example-data/eqpt_config.json>`_. This file defines the equipment libraries that
|
||||
can be customized (EDFAs, fibers, and transceivers).
|
||||
|
||||
It also defines the simulation parameters (spans, ROADMs, and the spectral
|
||||
information to transmit.)
|
||||
|
||||
EDFA
|
||||
~~~~
|
||||
|
||||
The EDFA equipment library is a list of supported amplifiers. New amplifiers
|
||||
can be added and existing ones removed. Three different noise models are available:
|
||||
|
||||
1. ``'type_def': 'variable_gain'`` is a simplified model simulating a 2-coil EDFA with internal, input and output VOAs. The NF vs gain response is calculated accordingly based on the input parameters: ``nf_min``, ``nf_max``, and ``gain_flatmax``. It is not a simple interpolation but a 2-stage NF calculation.
|
||||
2. ``'type_def': 'fixed_gain'`` is a fixed gain model. `NF == Cte == nf0` if `gain_min < gain < gain_flatmax`
|
||||
3. ``'type_def': None`` is an advanced model. A detailed JSON configuration file is required (by default `gnpy/example-data/std_medium_gain_advanced_config.json <gnpy/example-data/std_medium_gain_advanced_config.json>`_). It uses a 3rd order polynomial where NF = f(gain), NF_ripple = f(frequency), gain_ripple = f(frequency), N-array dgt = f(frequency). Compared to the previous models, NF ripple and gain ripple are modelled.
|
||||
|
||||
For all amplifier models:
|
||||
|
||||
+------------------------+-----------+-----------------------------------------+
|
||||
| field | type | description |
|
||||
+========================+===========+=========================================+
|
||||
| ``type_variety`` | (string) | a unique name to ID the amplifier in the|
|
||||
| | | JSON/Excel template topology input file |
|
||||
+------------------------+-----------+-----------------------------------------+
|
||||
| ``out_voa_auto`` | (boolean) | auto_design feature to optimize the |
|
||||
| | | amplifier output VOA. If true, output |
|
||||
| | | VOA is present and will be used to push |
|
||||
| | | amplifier gain to its maximum, within |
|
||||
| | | EOL power margins. |
|
||||
+------------------------+-----------+-----------------------------------------+
|
||||
| ``allowed_for_design`` | (boolean) | If false, the amplifier will not be |
|
||||
| | | picked by auto-design but it can still |
|
||||
| | | be used as a manual input (from JSON or |
|
||||
| | | Excel template topology files.) |
|
||||
+------------------------+-----------+-----------------------------------------+
|
||||
|
||||
Fiber
|
||||
~~~~~
|
||||
|
||||
The fiber library currently describes SSMF and NZDF but additional fiber types can be entered by the user following the same model:
|
||||
|
||||
+----------------------+-----------+-----------------------------------------+
|
||||
| field | type | description |
|
||||
+======================+===========+=========================================+
|
||||
| ``type_variety`` | (string) | a unique name to ID the fiber in the |
|
||||
| | | JSON or Excel template topology input |
|
||||
| | | file |
|
||||
+----------------------+-----------+-----------------------------------------+
|
||||
| ``dispersion`` | (number) | (s.m-1.m-1) |
|
||||
+----------------------+-----------+-----------------------------------------+
|
||||
| ``dispersion_slope`` | (number) | (s.m-1.m-1.m-1) |
|
||||
+----------------------+-----------+-----------------------------------------+
|
||||
| ``gamma`` | (number) | 2pi.n2/(lambda*Aeff) (w-1.m-1) |
|
||||
+----------------------+-----------+-----------------------------------------+
|
||||
| ``pmd_coef`` | (number) | Polarization mode dispersion (PMD) |
|
||||
| | | coefficient. (s.sqrt(m)-1) |
|
||||
+----------------------+-----------+-----------------------------------------+
|
||||
|
||||
Transceiver
|
||||
~~~~~~~~~~~
|
||||
|
||||
The transceiver equipment library is a list of supported transceivers. New
|
||||
transceivers can be added and existing ones removed at will by the user. It is
|
||||
used to determine the service list path feasibility when running the
|
||||
`path_request_run.py routine <gnpy/example-data/path_request_run.py>`_.
|
||||
|
||||
+----------------------+-----------+-----------------------------------------+
|
||||
| field | type | description |
|
||||
+======================+===========+=========================================+
|
||||
| ``type_variety`` | (string) | A unique name to ID the transceiver in |
|
||||
| | | the JSON or Excel template topology |
|
||||
| | | input file |
|
||||
+----------------------+-----------+-----------------------------------------+
|
||||
| ``frequency`` | (number) | Min/max as below. |
|
||||
+----------------------+-----------+-----------------------------------------+
|
||||
| ``mode`` | (number) | A list of modes supported by the |
|
||||
| | | transponder. New modes can be added at |
|
||||
| | | will by the user. The modes are specific|
|
||||
| | | to each transponder type_variety. |
|
||||
| | | Each mode is described as below. |
|
||||
+----------------------+-----------+-----------------------------------------+
|
||||
|
||||
The modes are defined as follows:
|
||||
|
||||
+----------------------+-----------+-----------------------------------------+
|
||||
| field | type | description |
|
||||
+======================+===========+=========================================+
|
||||
| ``format`` | (string) | a unique name to ID the mode |
|
||||
+----------------------+-----------+-----------------------------------------+
|
||||
| ``baud_rate`` | (number) | in Hz |
|
||||
+----------------------+-----------+-----------------------------------------+
|
||||
| ``OSNR`` | (number) | min required OSNR in 0.1nm (dB) |
|
||||
+----------------------+-----------+-----------------------------------------+
|
||||
| ``bit_rate`` | (number) | in bit/s |
|
||||
+----------------------+-----------+-----------------------------------------+
|
||||
| ``roll_off`` | (number) | Pure number between 0 and 1. TX signal |
|
||||
| | | roll-off shape. Used by Raman-aware |
|
||||
| | | simulation code. |
|
||||
+----------------------+-----------+-----------------------------------------+
|
||||
| ``tx_osnr`` | (number) | In dB. OSNR out from transponder. |
|
||||
+----------------------+-----------+-----------------------------------------+
|
||||
| ``cost`` | (number) | Arbitrary unit |
|
||||
+----------------------+-----------+-----------------------------------------+
|
||||
|
||||
Simulation parameters
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Auto-design automatically creates EDFA amplifier network elements when they are
|
||||
missing, after a fiber, or between a ROADM and a fiber. This auto-design
|
||||
functionality can be manually and locally deactivated by introducing a ``Fused``
|
||||
network element after a ``Fiber`` or a ``Roadm`` that doesn't need amplification.
|
||||
The amplifier is chosen in the EDFA list of the equipment library based on
|
||||
gain, power, and NF criteria. Only the EDFA that are marked
|
||||
``'allowed_for_design': true`` are considered.
|
||||
|
||||
For amplifiers defined in the topology JSON input but whose ``gain = 0``
|
||||
(placeholder), auto-design will set its gain automatically: see ``power_mode`` in
|
||||
the ``Spans`` library to find out how the gain is calculated.
|
||||
|
||||
Span
|
||||
~~~~
|
||||
|
||||
Span configuration is not a list (which may change
|
||||
in later releases) and the user can only modify the value of existing
|
||||
parameters:
|
||||
|
||||
+-------------------------------------+-----------+---------------------------------------------+
|
||||
| field | type | description |
|
||||
+=====================================+===========+=============================================+
|
||||
| ``power_mode`` | (boolean) | If false, gain mode. Auto-design sets |
|
||||
| | | amplifier gain = preceding span loss, |
|
||||
| | | unless the amplifier exists and its |
|
||||
| | | gain > 0 in the topology input JSON. |
|
||||
| | | If true, power mode (recommended for |
|
||||
| | | auto-design and power sweep.) |
|
||||
| | | Auto-design sets amplifier power |
|
||||
| | | according to delta_power_range. If the |
|
||||
| | | amplifier exists with gain > 0 in the |
|
||||
| | | topology JSON input, then its gain is |
|
||||
| | | translated into a power target/channel. |
|
||||
| | | Moreover, when performing a power sweep |
|
||||
| | | (see ``power_range_db`` in the SI |
|
||||
| | | configuration library) the power sweep |
|
||||
| | | is performed w/r/t this power target, |
|
||||
| | | regardless of preceding amplifiers |
|
||||
| | | power saturation/limitations. |
|
||||
+-------------------------------------+-----------+---------------------------------------------+
|
||||
| ``delta_power_range_db`` | (number) | Auto-design only, power-mode |
|
||||
| | | only. Specifies the [min, max, step] |
|
||||
| | | power excursion/span. It is a relative |
|
||||
| | | power excursion w/r/t the |
|
||||
| | | power_dbm + power_range_db |
|
||||
| | | (power sweep if applicable) defined in |
|
||||
| | | the SI configuration library. This |
|
||||
| | | relative power excursion is = 1/3 of |
|
||||
| | | the span loss difference with the |
|
||||
| | | reference 20 dB span. The 1/3 slope is |
|
||||
| | | derived from the GN model equations. |
|
||||
| | | For example, a 23 dB span loss will be |
|
||||
| | | set to 1 dB more power than a 20 dB |
|
||||
| | | span loss. The 20 dB reference spans |
|
||||
| | | will *always* be set to |
|
||||
| | | power = power_dbm + power_range_db. |
|
||||
| | | To configure the same power in all |
|
||||
| | | spans, use `[0, 0, 0]`. All spans will |
|
||||
| | | be set to |
|
||||
| | | power = power_dbm + power_range_db. |
|
||||
| | | To configure the same power in all spans |
|
||||
| | | and 3 dB more power just for the longest |
|
||||
| | | spans: `[0, 3, 3]`. The longest spans are |
|
||||
| | | set to |
|
||||
| | | power = power_dbm + power_range_db + 3. |
|
||||
| | | To configure a 4 dB power range across |
|
||||
| | | all spans in 0.5 dB steps: `[-2, 2, 0.5]`. |
|
||||
| | | A 17 dB span is set to |
|
||||
| | | power = power_dbm + power_range_db - 1, |
|
||||
| | | a 20 dB span to |
|
||||
| | | power = power_dbm + power_range_db and |
|
||||
| | | a 23 dB span to |
|
||||
| | | power = power_dbm + power_range_db + 1 |
|
||||
+-------------------------------------+-----------+---------------------------------------------+
|
||||
| ``max_fiber_lineic_loss_for_raman`` | (number) | Maximum linear fiber loss for Raman |
|
||||
| | | amplification use. |
|
||||
+-------------------------------------+-----------+---------------------------------------------+
|
||||
| ``max_length`` | (number) | Split fiber lengths > max_length. |
|
||||
| | | Interest to support high level |
|
||||
| | | topologies that do not specify in line |
|
||||
| | | amplification sites. For example the |
|
||||
| | | CORONET_Global_Topology.xlsx defines |
|
||||
| | | links > 1000km between 2 sites: it |
|
||||
| | | couldn't be simulated if these links |
|
||||
| | | were not split in shorter span lengths. |
|
||||
+-------------------------------------+-----------+---------------------------------------------+
|
||||
| ``length_unit`` | "m"/"km" | Unit for ``max_length``. |
|
||||
+-------------------------------------+-----------+---------------------------------------------+
|
||||
| ``max_loss`` | (number) | Not used in the current code |
|
||||
| | | implementation. |
|
||||
+-------------------------------------+-----------+---------------------------------------------+
|
||||
| ``padding`` | (number) | In dB. Min span loss before putting an |
|
||||
| | | attenuator before fiber. Attenuator |
|
||||
| | | value |
|
||||
| | | Fiber.att_in = max(0, padding - span_loss). |
|
||||
| | | Padding can be set manually to reach a |
|
||||
| | | higher padding value for a given fiber |
|
||||
| | | by filling in the Fiber/params/att_in |
|
||||
| | | field in the topology json input [1] |
|
||||
| | | but if span_loss = length * loss_coef |
|
||||
| | | + att_in + con_in + con_out < padding, |
|
||||
| | | the specified att_in value will be |
|
||||
| | | completed to have span_loss = padding. |
|
||||
| | | Therefore it is not possible to set |
|
||||
| | | span_loss < padding. |
|
||||
+-------------------------------------+-----------+---------------------------------------------+
|
||||
| ``EOL`` | (number) | All fiber span loss ageing. The value |
|
||||
| | | is added to the con_out (fiber output |
|
||||
| | | connector). So the design and the path |
|
||||
| | | feasibility are performed with |
|
||||
| | | span_loss + EOL. EOL cannot be set |
|
||||
| | | manually for a given fiber span |
|
||||
| | | (workaround is to specify higher |
|
||||
| | | ``con_out`` loss for this fiber). |
|
||||
+-------------------------------------+-----------+---------------------------------------------+
|
||||
| ``con_in``, | (number) | Default values if Fiber/params/con_in/out |
|
||||
| ``con_out`` | | is None in the topology input |
|
||||
| | | description. This default value is |
|
||||
| | | ignored if a Fiber/params/con_in/out |
|
||||
| | | value is input in the topology for a |
|
||||
| | | given Fiber. |
|
||||
+-------------------------------------+-----------+---------------------------------------------+
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"uid": "fiber (A1->A2)",
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params":
|
||||
{
|
||||
"length": 120.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
"att_in": 0,
|
||||
"con_in": 0,
|
||||
"con_out": 0
|
||||
}
|
||||
}
|
||||
|
||||
ROADM
|
||||
~~~~~
|
||||
|
||||
The user can only modify the value of existing parameters:
|
||||
|
||||
+--------------------------+-----------+---------------------------------------------+
|
||||
| field | type | description |
|
||||
+==========================+===========+=============================================+
|
||||
| ``target_pch_out_db`` | (number) | Auto-design sets the ROADM egress channel |
|
||||
| | | power. This reflects typical control loop |
|
||||
| | | algorithms that adjust ROADM losses to |
|
||||
| | | equalize channels (eg coming from different |
|
||||
| | | ingress direction or add ports) |
|
||||
| | | This is the default value |
|
||||
| | | Roadm/params/target_pch_out_db if no value |
|
||||
| | | is given in the ``Roadm`` element in the |
|
||||
| | | topology input description. |
|
||||
| | | This default value is ignored if a |
|
||||
| | | params/target_pch_out_db value is input in |
|
||||
| | | the topology for a given ROADM. |
|
||||
+--------------------------+-----------+---------------------------------------------+
|
||||
| ``add_drop_osnr`` | (number) | OSNR contribution from the add/drop ports |
|
||||
+--------------------------+-----------+---------------------------------------------+
|
||||
| ``pmd`` | (number) | Polarization mode dispersion (PMD). (s) |
|
||||
+--------------------------+-----------+---------------------------------------------+
|
||||
| ``restrictions`` | (dict of | If non-empty, keys ``preamp_variety_list`` |
|
||||
| | strings) | and ``booster_variety_list`` represent |
|
||||
| | | list of ``type_variety`` amplifiers which |
|
||||
| | | are allowed for auto-design within ROADM's |
|
||||
| | | line degrees. |
|
||||
| | | |
|
||||
| | | If no booster should be placed on a degree, |
|
||||
| | | insert a ``Fused`` node on the degree |
|
||||
| | | output. |
|
||||
+--------------------------+-----------+---------------------------------------------+
|
||||
|
||||
SpectralInformation
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The user can only modify the value of existing parameters. It defines a spectrum of N
|
||||
identical carriers. While the code libraries allow for different carriers and
|
||||
power levels, the current user parametrization only allows one carrier type and
|
||||
one power/channel definition.
|
||||
|
||||
+----------------------+-----------+-------------------------------------------+
|
||||
| field | type | description |
|
||||
+======================+===========+===========================================+
|
||||
| ``f_min``, | (number) | In Hz. Carrier min max excursion. |
|
||||
| ``f_max`` | | |
|
||||
+----------------------+-----------+-------------------------------------------+
|
||||
| ``baud_rate`` | (number) | In Hz. Simulated baud rate. |
|
||||
+----------------------+-----------+-------------------------------------------+
|
||||
| ``spacing`` | (number) | In Hz. Carrier spacing. |
|
||||
+----------------------+-----------+-------------------------------------------+
|
||||
| ``roll_off`` | (number) | Pure number between 0 and 1. TX signal |
|
||||
| | | roll-off shape. Used by Raman-aware |
|
||||
| | | simulation code. |
|
||||
+----------------------+-----------+-------------------------------------------+
|
||||
| ``tx_osnr`` | (number) | In dB. OSNR out from transponder. |
|
||||
+----------------------+-----------+-------------------------------------------+
|
||||
| ``power_dbm`` | (number) | Reference channel power. In gain mode |
|
||||
| | | (see spans/power_mode = false), all gain |
|
||||
| | | settings are offset w/r/t this reference |
|
||||
| | | power. In power mode, it is the |
|
||||
| | | reference power for |
|
||||
| | | Spans/delta_power_range_db. For example, |
|
||||
| | | if delta_power_range_db = `[0,0,0]`, the |
|
||||
| | | same power=power_dbm is launched in every |
|
||||
| | | spans. The network design is performed |
|
||||
| | | with the power_dbm value: even if a |
|
||||
| | | power sweep is defined (see after) the |
|
||||
| | | design is not repeated. |
|
||||
+----------------------+-----------+-------------------------------------------+
|
||||
| ``power_range_db`` | (number) | Power sweep excursion around power_dbm. |
|
||||
| | | It is not the min and max channel power |
|
||||
| | | values! The reference power becomes: |
|
||||
| | | power_range_db + power_dbm. |
|
||||
+----------------------+-----------+-------------------------------------------+
|
||||
| ``sys_margins`` | (number) | In dB. Added margin on min required |
|
||||
| | | transceiver OSNR. |
|
||||
+----------------------+-----------+-------------------------------------------+
|
||||
@@ -1,5 +1,5 @@
|
||||
The QoT estimation in the PSE framework of TIP-OOPT
|
||||
=======================================================
|
||||
Physical Model used in GNPy
|
||||
===========================
|
||||
|
||||
QoT-E including ASE noise and NLI accumulation
|
||||
----------------------------------------------
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
gnpy\.core package
|
||||
==================
|
||||
|
||||
Submodules
|
||||
----------
|
||||
|
||||
gnpy\.core\.ansi_escapes module
|
||||
-------------------------------
|
||||
|
||||
.. automodule:: gnpy.core.ansi_escapes
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
gnpy\.core\.convert module
|
||||
--------------------------
|
||||
|
||||
.. automodule:: gnpy.core.convert
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
gnpy\.core\.elements module
|
||||
---------------------------
|
||||
|
||||
.. automodule:: gnpy.core.elements
|
||||
|
||||
gnpy\.core\.equipment module
|
||||
----------------------------
|
||||
|
||||
.. automodule:: gnpy.core.equipment
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
gnpy\.core\.exceptions module
|
||||
-----------------------------
|
||||
|
||||
.. automodule:: gnpy.core.exceptions
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
gnpy\.core\.execute module
|
||||
--------------------------
|
||||
|
||||
.. automodule:: gnpy.core.execute
|
||||
|
||||
gnpy\.core\.info module
|
||||
-----------------------
|
||||
|
||||
.. automodule:: gnpy.core.info
|
||||
|
||||
gnpy\.core\.network module
|
||||
--------------------------
|
||||
|
||||
.. automodule:: gnpy.core.network
|
||||
|
||||
gnpy\.core\.node module
|
||||
-----------------------
|
||||
|
||||
.. automodule:: gnpy.core.node
|
||||
|
||||
gnpy\.core\.request module
|
||||
--------------------------
|
||||
|
||||
.. automodule:: gnpy.core.request
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
gnpy\.core\.service_sheet module
|
||||
--------------------------------
|
||||
|
||||
.. automodule:: gnpy.core.service_sheet
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
gnpy\.core\.units module
|
||||
------------------------
|
||||
|
||||
.. automodule:: gnpy.core.units
|
||||
|
||||
gnpy\.core\.utils module
|
||||
------------------------
|
||||
|
||||
.. automodule:: gnpy.core.utils
|
||||
|
||||
|
||||
Module contents
|
||||
---------------
|
||||
|
||||
.. automodule:: gnpy.core
|
||||
@@ -1,14 +0,0 @@
|
||||
gnpy package
|
||||
============
|
||||
|
||||
Subpackages
|
||||
-----------
|
||||
|
||||
.. toctree::
|
||||
|
||||
gnpy.core
|
||||
|
||||
Module contents
|
||||
---------------
|
||||
|
||||
.. automodule:: gnpy
|
||||
@@ -1,7 +0,0 @@
|
||||
gnpy
|
||||
====
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 4
|
||||
|
||||
gnpy
|
||||
@@ -1,382 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
path_requests_run.py
|
||||
====================
|
||||
|
||||
Reads a JSON request file in accordance with the Yang model
|
||||
for requesting path computation and returns path results in terms
|
||||
of path and feasibilty.
|
||||
|
||||
See: draft-ietf-teas-yang-path-computation-01.txt
|
||||
"""
|
||||
|
||||
from sys import exit
|
||||
from argparse import ArgumentParser
|
||||
from pathlib import Path
|
||||
from collections import namedtuple
|
||||
from logging import getLogger, basicConfig, CRITICAL, DEBUG, INFO
|
||||
from json import dumps, loads
|
||||
from networkx import (draw_networkx_nodes, draw_networkx_edges,
|
||||
draw_networkx_labels)
|
||||
from numpy import mean
|
||||
from gnpy.core.service_sheet import convert_service_sheet, Request_element, Element
|
||||
from gnpy.core.utils import load_json
|
||||
from gnpy.core.network import load_network, build_network, save_network
|
||||
from gnpy.core.equipment import load_equipment, trx_mode_params, automatic_nch, automatic_spacing
|
||||
from gnpy.core.elements import Transceiver, Roadm, Edfa, Fused, Fiber
|
||||
from gnpy.core.utils import db2lin, lin2db
|
||||
from gnpy.core.request import (Path_request, Result_element, compute_constrained_path,
|
||||
propagate, jsontocsv, Disjunction, compute_path_dsjctn, requests_aggregation,
|
||||
propagate_and_optimize_mode)
|
||||
from gnpy.core.exceptions import ConfigurationError, EquipmentConfigError, NetworkTopologyError
|
||||
import gnpy.core.ansi_escapes as ansi_escapes
|
||||
from copy import copy, deepcopy
|
||||
from textwrap import dedent
|
||||
from math import ceil
|
||||
|
||||
#EQPT_LIBRARY_FILENAME = Path(__file__).parent / 'eqpt_config.json'
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
parser = ArgumentParser(description = 'A function that computes performances for a list of services provided in a json file or an excel sheet.')
|
||||
parser.add_argument('network_filename', nargs='?', type = Path, default= Path(__file__).parent / 'meshTopologyExampleV2.xls')
|
||||
parser.add_argument('service_filename', nargs='?', type = Path, default= Path(__file__).parent / 'meshTopologyExampleV2.xls')
|
||||
parser.add_argument('eqpt_filename', nargs='?', type = Path, default=Path(__file__).parent / 'eqpt_config.json')
|
||||
parser.add_argument('-v', '--verbose', action='count', default=0, help='increases verbosity for each occurence')
|
||||
parser.add_argument('-o', '--output', type = Path)
|
||||
|
||||
|
||||
def requests_from_json(json_data,equipment):
|
||||
requests_list = []
|
||||
|
||||
for req in json_data['path-request']:
|
||||
# init all params from request
|
||||
params = {}
|
||||
params['request_id'] = req['request-id']
|
||||
params['source'] = req['source']
|
||||
params['destination'] = req['destination']
|
||||
params['trx_type'] = req['path-constraints']['te-bandwidth']['trx_type']
|
||||
params['trx_mode'] = req['path-constraints']['te-bandwidth']['trx_mode']
|
||||
params['format'] = params['trx_mode']
|
||||
params['spacing'] = req['path-constraints']['te-bandwidth']['spacing']
|
||||
try :
|
||||
nd_list = req['explicit-route-objects']['route-object-include-exclude']
|
||||
except KeyError:
|
||||
nd_list = []
|
||||
params['nodes_list'] = [n['num-unnum-hop']['node-id'] for n in nd_list]
|
||||
params['loose_list'] = [n['num-unnum-hop']['hop-type'] for n in nd_list]
|
||||
# recover trx physical param (baudrate, ...) from type and mode
|
||||
# in trx_mode_params optical power is read from equipment['SI']['default'] and
|
||||
# nb_channel is computed based on min max frequency and spacing
|
||||
trx_params = trx_mode_params(equipment,params['trx_type'],params['trx_mode'],True)
|
||||
params.update(trx_params)
|
||||
# print(trx_params['min_spacing'])
|
||||
# optical power might be set differently in the request. if it is indicated then the
|
||||
# params['power'] is updated
|
||||
try:
|
||||
if req['path-constraints']['te-bandwidth']['output-power']:
|
||||
params['power'] = req['path-constraints']['te-bandwidth']['output-power']
|
||||
except KeyError:
|
||||
pass
|
||||
# same process for nb-channel
|
||||
f_min = params['f_min']
|
||||
f_max_from_si = params['f_max']
|
||||
try:
|
||||
if req['path-constraints']['te-bandwidth']['max-nb-of-channel'] is not None:
|
||||
nch = req['path-constraints']['te-bandwidth']['max-nb-of-channel']
|
||||
params['nb_channel'] = nch
|
||||
spacing = params['spacing']
|
||||
params['f_max'] = f_min + nch*spacing
|
||||
else :
|
||||
params['nb_channel'] = automatic_nch(f_min,f_max_from_si,params['spacing'])
|
||||
except KeyError:
|
||||
params['nb_channel'] = automatic_nch(f_min,f_max_from_si,params['spacing'])
|
||||
consistency_check(params, f_max_from_si)
|
||||
|
||||
try :
|
||||
params['path_bandwidth'] = req['path-constraints']['te-bandwidth']['path_bandwidth']
|
||||
except KeyError:
|
||||
pass
|
||||
requests_list.append(Path_request(**params))
|
||||
return requests_list
|
||||
|
||||
def consistency_check(params, f_max_from_si):
|
||||
f_min = params['f_min']
|
||||
f_max = params['f_max']
|
||||
max_recommanded_nb_channels = automatic_nch(f_min,f_max,
|
||||
params['spacing'])
|
||||
if params['baud_rate'] is not None:
|
||||
#implicitely means that a mode is defined with min_spacing
|
||||
if params['min_spacing']>params['spacing'] :
|
||||
msg = f'Request {params["request_id"]} has spacing below transponder {params["trx_type"]}'+\
|
||||
f' {params["trx_mode"]} min spacing value {params["min_spacing"]*1e-9}GHz.\n'+\
|
||||
'Computation stopped'
|
||||
print(msg)
|
||||
logger.critical(msg)
|
||||
exit()
|
||||
if f_max>f_max_from_si:
|
||||
msg = dedent(f'''
|
||||
Requested channel number {params["nb_channel"]}, baud rate {params["baud_rate"]} GHz and requested spacing {params["spacing"]*1e-9}GHz
|
||||
is not consistent with frequency range {f_min*1e-12} THz, {f_max*1e-12} THz, min recommanded spacing {params["min_spacing"]*1e-9}GHz.
|
||||
max recommanded nb of channels is {max_recommanded_nb_channels}
|
||||
Computation stopped.''')
|
||||
logger.critical(msg)
|
||||
exit()
|
||||
|
||||
|
||||
def disjunctions_from_json(json_data):
|
||||
disjunctions_list = []
|
||||
try:
|
||||
temp_test = json_data['synchronization']
|
||||
except KeyError:
|
||||
temp_test = []
|
||||
if temp_test:
|
||||
for snc in json_data['synchronization']:
|
||||
params = {}
|
||||
params['disjunction_id'] = snc['synchronization-id']
|
||||
params['relaxable'] = snc['svec']['relaxable']
|
||||
params['link_diverse'] = 'link' in snc['svec']['disjointness']
|
||||
params['node_diverse'] = 'node' in snc['svec']['disjointness']
|
||||
params['disjunctions_req'] = snc['svec']['request-id-number']
|
||||
disjunctions_list.append(Disjunction(**params))
|
||||
|
||||
return disjunctions_list
|
||||
|
||||
|
||||
def load_requests(filename,eqpt_filename):
|
||||
if filename.suffix.lower() == '.xls':
|
||||
logger.info('Automatically converting requests from XLS to JSON')
|
||||
json_data = convert_service_sheet(filename,eqpt_filename)
|
||||
else:
|
||||
with open(filename, encoding='utf-8') as f:
|
||||
json_data = loads(f.read())
|
||||
return json_data
|
||||
|
||||
def compute_path_with_disjunction(network, equipment, pathreqlist, pathlist):
|
||||
|
||||
# use a list but a dictionnary might be helpful to find path bathsed on request_id
|
||||
# TODO change all these req, dsjct, res lists into dict !
|
||||
path_res_list = []
|
||||
|
||||
for i,pathreq in enumerate(pathreqlist):
|
||||
|
||||
# use the power specified in requests but might be different from the one specified for design
|
||||
# the power is an optional parameter for requests definition
|
||||
# if optional, use the one defines in eqt_config.json
|
||||
p_db = lin2db(pathreq.power*1e3)
|
||||
p_total_db = p_db + lin2db(pathreq.nb_channel)
|
||||
print(f'request {pathreq.request_id}')
|
||||
print(f'Computing path from {pathreq.source} to {pathreq.destination}')
|
||||
print(f'with path constraint: {[pathreq.source]+pathreq.nodes_list}') #adding first node to be clearer on the output
|
||||
|
||||
total_path = pathlist[i]
|
||||
print(f'Computed path (roadms):{[e.uid for e in total_path if isinstance(e, Roadm)]}\n')
|
||||
# for debug
|
||||
# print(f'{pathreq.baud_rate} {pathreq.power} {pathreq.spacing} {pathreq.nb_channel}')
|
||||
if total_path :
|
||||
if pathreq.baud_rate is not None:
|
||||
total_path = propagate(total_path,pathreq,equipment)
|
||||
# for el in total_path: print(el)
|
||||
temp_snr01nm = round(mean(total_path[-1].snr+lin2db(pathreq.baud_rate/(12.5e9))),2)
|
||||
if temp_snr01nm < pathreq.OSNR :
|
||||
msg = f'\tWarning! Request {pathreq.request_id} computed path from {pathreq.source} to {pathreq.destination} does not pass with {pathreq.tsp_mode}\n' +\
|
||||
f'\tcomputedSNR in 0.1nm = {temp_snr01nm} - required osnr {pathreq.OSNR}\n'
|
||||
print(msg)
|
||||
logger.warning(msg)
|
||||
total_path = []
|
||||
else:
|
||||
total_path,mode = propagate_and_optimize_mode(total_path,pathreq,equipment)
|
||||
# if no baudrate satisfies spacing, no mode is returned and an empty path is returned
|
||||
# a warning is shown in the propagate_and_optimize_mode
|
||||
if mode is not None :
|
||||
# propagate_and_optimize_mode function returns the mode with the highest bitrate
|
||||
# that passes. if no mode passes, then it returns an empty path
|
||||
pathreq.baud_rate = mode['baud_rate']
|
||||
pathreq.tsp_mode = mode['format']
|
||||
pathreq.format = mode['format']
|
||||
pathreq.OSNR = mode['OSNR']
|
||||
pathreq.tx_osnr = mode['tx_osnr']
|
||||
pathreq.bit_rate = mode['bit_rate']
|
||||
else :
|
||||
total_path = []
|
||||
# we record the last tranceiver object in order to have th whole
|
||||
# information about spectrum. Important Note: since transceivers
|
||||
# attached to roadms are actually logical elements to simulate
|
||||
# performance, several demands having the same destination may use
|
||||
# the same transponder for the performance simaulation. This is why
|
||||
# we use deepcopy: to ensure each propagation is recorded and not
|
||||
# overwritten
|
||||
|
||||
path_res_list.append(deepcopy(total_path))
|
||||
return path_res_list
|
||||
|
||||
def correct_route_list(network, pathreqlist):
|
||||
# prepares the format of route list of nodes to be consistant
|
||||
# remove wrong names, remove endpoints
|
||||
# also correct source and destination
|
||||
anytype = [n.uid for n in network.nodes()]
|
||||
# TODO there is a problem of identification of fibers in case of parallel fibers bitween two adjacent roadms
|
||||
# so fiber constraint is not supported
|
||||
transponders = [n.uid for n in network.nodes() if isinstance(n, Transceiver)]
|
||||
for pathreq in pathreqlist:
|
||||
for i,n_id in enumerate(pathreq.nodes_list):
|
||||
# replace possibly wrong name with a formated roadm name
|
||||
# print(n_id)
|
||||
if n_id not in anytype :
|
||||
# find nodes name that include constraint among all possible names except
|
||||
# transponders (not yet supported as constraints).
|
||||
nodes_suggestion = [uid for uid in anytype \
|
||||
if n_id.lower() in uid.lower() and uid not in transponders]
|
||||
if pathreq.loose_list[i] == 'LOOSE':
|
||||
if len(nodes_suggestion)>0 :
|
||||
new_n = nodes_suggestion[0]
|
||||
print(f'invalid route node specified:\
|
||||
\n\'{n_id}\', replaced with \'{new_n}\'')
|
||||
pathreq.nodes_list[i] = new_n
|
||||
else:
|
||||
print(f'\x1b[1;33;40m'+f'invalid route node specified \'{n_id}\', could not use it as constraint, skipped!'+'\x1b[0m')
|
||||
pathreq.nodes_list.remove(n_id)
|
||||
pathreq.loose_list.pop(i)
|
||||
else:
|
||||
msg = f'\x1b[1;33;40m'+f'could not find node : {n_id} in network topology. Strict constraint can not be applied.'+'\x1b[0m'
|
||||
logger.critical(msg)
|
||||
raise ValueError(msg)
|
||||
if pathreq.source not in transponders:
|
||||
msg = f'\x1b[1;31;40m'+f'Request: {pathreq.request_id}: could not find transponder source : {pathreq.source}.'+'\x1b[0m'
|
||||
logger.critical(msg)
|
||||
print(f'{msg}\nComputation stopped.')
|
||||
exit()
|
||||
|
||||
if pathreq.destination not in transponders:
|
||||
msg = f'\x1b[1;31;40m'+f'Request: {pathreq.request_id}: could not find transponder destination : {pathreq.destination}.'+'\x1b[0m'
|
||||
logger.critical(msg)
|
||||
print(f'{msg}\nComputation stopped.')
|
||||
exit()
|
||||
|
||||
# TODO remove endpoints from this list in case they were added by the user in the xls or json files
|
||||
return pathreqlist
|
||||
|
||||
def correct_disjn(disjn):
|
||||
local_disjn = disjn.copy()
|
||||
for el in local_disjn:
|
||||
for d in local_disjn:
|
||||
if set(el.disjunctions_req) == set(d.disjunctions_req) and\
|
||||
el.disjunction_id != d.disjunction_id:
|
||||
local_disjn.remove(d)
|
||||
return local_disjn
|
||||
|
||||
|
||||
def path_result_json(pathresult):
|
||||
data = {
|
||||
'response': [n.json for n in pathresult]
|
||||
}
|
||||
return data
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = parser.parse_args()
|
||||
basicConfig(level={2: DEBUG, 1: INFO, 0: CRITICAL}.get(args.verbose, DEBUG))
|
||||
logger.info(f'Computing path requests {args.service_filename} into JSON format')
|
||||
print('\x1b[1;34;40m'+f'Computing path requests {args.service_filename} into JSON format'+ '\x1b[0m')
|
||||
# for debug
|
||||
# print( args.eqpt_filename)
|
||||
try:
|
||||
data = load_requests(args.service_filename,args.eqpt_filename)
|
||||
equipment = load_equipment(args.eqpt_filename)
|
||||
network = load_network(args.network_filename,equipment)
|
||||
except EquipmentConfigError as e:
|
||||
print(f'{ansi_escapes.red}Configuration error in the equipment library:{ansi_escapes.reset} {e}')
|
||||
exit(1)
|
||||
except NetworkTopologyError as e:
|
||||
print(f'{ansi_escapes.red}Invalid network definition:{ansi_escapes.reset} {e}')
|
||||
exit(1)
|
||||
except ConfigurationError as e:
|
||||
print(f'{ansi_escapes.red}Configuration error:{ansi_escapes.reset} {e}')
|
||||
exit(1)
|
||||
|
||||
# Build the network once using the default power defined in SI in eqpt config
|
||||
# TODO power density : db2linp(ower_dbm": 0)/power_dbm": 0 * nb channels as defined by
|
||||
# spacing, f_min and f_max
|
||||
p_db = equipment['SI']['default'].power_dbm
|
||||
|
||||
p_total_db = p_db + lin2db(automatic_nch(equipment['SI']['default'].f_min,\
|
||||
equipment['SI']['default'].f_max, equipment['SI']['default'].spacing))
|
||||
build_network(network, equipment, p_db, p_total_db)
|
||||
save_network(args.network_filename, network)
|
||||
|
||||
rqs = requests_from_json(data, equipment)
|
||||
|
||||
# check that request ids are unique. Non unique ids, may
|
||||
# mess the computation : better to stop the computation
|
||||
all_ids = [r.request_id for r in rqs]
|
||||
if len(all_ids) != len(set(all_ids)):
|
||||
for a in list(set(all_ids)):
|
||||
all_ids.remove(a)
|
||||
msg = f'Requests id {all_ids} are not unique'
|
||||
logger.critical(msg)
|
||||
exit()
|
||||
rqs = correct_route_list(network, rqs)
|
||||
|
||||
# pths = compute_path(network, equipment, rqs)
|
||||
dsjn = disjunctions_from_json(data)
|
||||
|
||||
print('\x1b[1;34;40m'+f'List of disjunctions'+ '\x1b[0m')
|
||||
print(dsjn)
|
||||
# need to warn or correct in case of wrong disjunction form
|
||||
# disjunction must not be repeated with same or different ids
|
||||
dsjn = correct_disjn(dsjn)
|
||||
|
||||
# Aggregate demands with same exact constraints
|
||||
print('\x1b[1;34;40m'+f'Aggregating similar requests'+ '\x1b[0m')
|
||||
|
||||
rqs,dsjn = requests_aggregation(rqs,dsjn)
|
||||
# TODO export novel set of aggregated demands in a json file
|
||||
|
||||
print('\x1b[1;34;40m'+'The following services have been requested:'+ '\x1b[0m')
|
||||
print(rqs)
|
||||
|
||||
print('\x1b[1;34;40m'+f'Computing all paths with constraints'+ '\x1b[0m')
|
||||
pths = compute_path_dsjctn(network, equipment, rqs, dsjn)
|
||||
|
||||
print('\x1b[1;34;40m'+f'Propagating on selected path'+ '\x1b[0m')
|
||||
propagatedpths = compute_path_with_disjunction(network, equipment, rqs, pths)
|
||||
|
||||
print('\x1b[1;34;40m'+f'Result summary'+ '\x1b[0m')
|
||||
|
||||
header = ['req id', ' demand',' snr@bandwidth',' snr@0.1nm',' Receiver minOSNR', ' mode', ' Gbit/s' , ' nb of tsp pairs']
|
||||
data = []
|
||||
data.append(header)
|
||||
for i, p in enumerate(propagatedpths):
|
||||
if p:
|
||||
line = [f'{rqs[i].request_id}', f' {rqs[i].source} to {rqs[i].destination} : ', f'{round(mean(p[-1].snr),2)}',\
|
||||
f'{round(mean(p[-1].snr+lin2db(rqs[i].baud_rate/(12.5e9))),2)}',\
|
||||
f'{rqs[i].OSNR}', f'{rqs[i].tsp_mode}' , f'{round(rqs[i].path_bandwidth * 1e-9,2)}' , f'{ceil(rqs[i].path_bandwidth / rqs[i].bit_rate) }']
|
||||
else:
|
||||
line = [f'{rqs[i].request_id}',f' {rqs[i].source} to {rqs[i].destination} : not feasible ']
|
||||
data.append(line)
|
||||
|
||||
col_width = max(len(word) for row in data for word in row[2:]) # padding
|
||||
firstcol_width = max(len(row[0]) for row in data ) # padding
|
||||
secondcol_width = max(len(row[1]) for row in data ) # padding
|
||||
for row in data:
|
||||
firstcol = ''.join(row[0].ljust(firstcol_width))
|
||||
secondcol = ''.join(row[1].ljust(secondcol_width))
|
||||
remainingcols = ''.join(word.center(col_width,' ') for word in row[2:])
|
||||
print(f'{firstcol} {secondcol} {remainingcols}')
|
||||
|
||||
|
||||
if args.output :
|
||||
result = []
|
||||
# assumes that list of rqs and list of propgatedpths have same order
|
||||
for i,p in enumerate(propagatedpths):
|
||||
result.append(Result_element(rqs[i],p))
|
||||
temp = path_result_json(result)
|
||||
fnamecsv = f'{str(args.output)[0:len(str(args.output))-len(str(args.output.suffix))]}.csv'
|
||||
fnamejson = f'{str(args.output)[0:len(str(args.output))-len(str(args.output.suffix))]}.json'
|
||||
with open(fnamejson, 'w', encoding='utf-8') as f:
|
||||
f.write(dumps(path_result_json(result), indent=2, ensure_ascii=False))
|
||||
with open(fnamecsv,"w", encoding='utf-8') as fcsv :
|
||||
jsontocsv(temp,equipment,fcsv)
|
||||
print('\x1b[1;34;40m'+f'saving in {args.output} and {fnamecsv}'+ '\x1b[0m')
|
||||
@@ -1,318 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
'''
|
||||
transmission_main_example.py
|
||||
============================
|
||||
|
||||
Main example for transmission simulation.
|
||||
|
||||
Reads from network JSON (by default, `edfa_example_network.json`)
|
||||
'''
|
||||
|
||||
from gnpy.core.equipment import load_equipment, trx_mode_params
|
||||
from gnpy.core.utils import db2lin, lin2db, write_csv
|
||||
from argparse import ArgumentParser
|
||||
from sys import exit
|
||||
from pathlib import Path
|
||||
from json import loads
|
||||
from collections import Counter
|
||||
from logging import getLogger, basicConfig, INFO, ERROR, DEBUG
|
||||
from numpy import linspace, mean, log10
|
||||
from matplotlib.pyplot import show, axis, figure, title, text
|
||||
from networkx import (draw_networkx_nodes, draw_networkx_edges,
|
||||
draw_networkx_labels, dijkstra_path)
|
||||
from gnpy.core.network import load_network, build_network, save_network, load_sim_params, configure_network
|
||||
from gnpy.core.elements import Transceiver, Fiber, RamanFiber, Edfa, Roadm
|
||||
from gnpy.core.info import create_input_spectral_information, SpectralInformation, Channel, Power, Pref
|
||||
from gnpy.core.request import Path_request, RequestParams, compute_constrained_path, propagate2
|
||||
from gnpy.core.exceptions import ConfigurationError, EquipmentConfigError, NetworkTopologyError
|
||||
import gnpy.core.ansi_escapes as ansi_escapes
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
def plot_baseline(network):
|
||||
edges = set(network.edges())
|
||||
pos = {n: (n.lng, n.lat) for n in network.nodes()}
|
||||
labels = {n: n.location.city for n in network.nodes() if isinstance(n, Transceiver)}
|
||||
city_labels = set(labels.values())
|
||||
for n in network.nodes():
|
||||
if n.location.city and n.location.city not in city_labels:
|
||||
labels[n] = n.location.city
|
||||
city_labels.add(n.location.city)
|
||||
label_pos = pos
|
||||
|
||||
fig = figure()
|
||||
kwargs = {'figure': fig, 'pos': pos}
|
||||
plot = draw_networkx_nodes(network, nodelist=network.nodes(), node_color='#ababab', **kwargs)
|
||||
draw_networkx_edges(network, edgelist=edges, edge_color='#ababab', **kwargs)
|
||||
draw_networkx_labels(network, labels=labels, font_size=14, **{**kwargs, 'pos': label_pos})
|
||||
axis('off')
|
||||
show()
|
||||
|
||||
def plot_results(network, path, source, destination, infos):
|
||||
path_edges = set(zip(path[:-1], path[1:]))
|
||||
edges = set(network.edges()) - path_edges
|
||||
pos = {n: (n.lng, n.lat) for n in network.nodes()}
|
||||
nodes = {}
|
||||
for k, (x, y) in pos.items():
|
||||
nodes.setdefault((round(x, 1), round(y, 1)), []).append(k)
|
||||
labels = {n: n.location.city for n in network.nodes() if isinstance(n, Transceiver)}
|
||||
city_labels = set(labels.values())
|
||||
for n in network.nodes():
|
||||
if n.location.city and n.location.city not in city_labels:
|
||||
labels[n] = n.location.city
|
||||
city_labels.add(n.location.city)
|
||||
label_pos = pos
|
||||
|
||||
fig = figure()
|
||||
kwargs = {'figure': fig, 'pos': pos}
|
||||
all_nodes = [n for n in network.nodes() if n not in path]
|
||||
plot = draw_networkx_nodes(network, nodelist=all_nodes, node_color='#ababab', node_size=50, **kwargs)
|
||||
draw_networkx_nodes(network, nodelist=path, node_color='#ff0000', node_size=55, **kwargs)
|
||||
draw_networkx_edges(network, edgelist=edges, edge_color='#ababab', **kwargs)
|
||||
draw_networkx_edges(network, edgelist=path_edges, edge_color='#ff0000', **kwargs)
|
||||
draw_networkx_labels(network, labels=labels, font_size=14, **{**kwargs, 'pos': label_pos})
|
||||
title(f'Propagating from {source.loc.city} to {destination.loc.city}')
|
||||
axis('off')
|
||||
|
||||
heading = 'Spectral Information\n\n'
|
||||
textbox = text(0.85, 0.20, heading, fontsize=14, fontname='Ubuntu Mono',
|
||||
verticalalignment='top', transform=fig.axes[0].transAxes,
|
||||
bbox={'boxstyle': 'round', 'facecolor': 'wheat', 'alpha': 0.5})
|
||||
|
||||
msgs = {(x, y): heading + '\n\n'.join(str(n) for n in ns if n in path)
|
||||
for (x, y), ns in nodes.items()}
|
||||
|
||||
def hover(event):
|
||||
if event.xdata is None or event.ydata is None:
|
||||
return
|
||||
if fig.contains(event):
|
||||
x, y = round(event.xdata, 1), round(event.ydata, 1)
|
||||
if (x, y) in msgs:
|
||||
textbox.set_text(msgs[x, y])
|
||||
else:
|
||||
textbox.set_text(heading)
|
||||
fig.canvas.draw_idle()
|
||||
|
||||
fig.canvas.mpl_connect('motion_notify_event', hover)
|
||||
show()
|
||||
|
||||
|
||||
def main(network, equipment, source, destination, sim_params, req=None):
|
||||
result_dicts = {}
|
||||
network_data = [{
|
||||
'network_name' : str(args.filename),
|
||||
'source' : source.uid,
|
||||
'destination' : destination.uid
|
||||
}]
|
||||
result_dicts.update({'network': network_data})
|
||||
design_data = [{
|
||||
'power_mode' : equipment['Span']['default'].power_mode,
|
||||
'span_power_range' : equipment['Span']['default'].delta_power_range_db,
|
||||
'design_pch' : equipment['SI']['default'].power_dbm,
|
||||
'baud_rate' : equipment['SI']['default'].baud_rate
|
||||
}]
|
||||
result_dicts.update({'design': design_data})
|
||||
simulation_data = []
|
||||
result_dicts.update({'simulation results': simulation_data})
|
||||
|
||||
power_mode = equipment['Span']['default'].power_mode
|
||||
print('\n'.join([f'Power mode is set to {power_mode}',
|
||||
f'=> it can be modified in eqpt_config.json - Span']))
|
||||
|
||||
pref_ch_db = lin2db(req.power*1e3) #reference channel power / span (SL=20dB)
|
||||
pref_total_db = pref_ch_db + lin2db(req.nb_channel) #reference total power / span (SL=20dB)
|
||||
build_network(network, equipment, pref_ch_db, pref_total_db)
|
||||
path = compute_constrained_path(network, req)
|
||||
|
||||
if len([s.length for s in path if isinstance(s, RamanFiber)]):
|
||||
if sim_params is None:
|
||||
print(f'{ansi_escapes.red}Invocation error:{ansi_escapes.reset} RamanFiber requires passing simulation params via --sim-params')
|
||||
exit(1)
|
||||
configure_network(network, sim_params)
|
||||
|
||||
spans = [s.length for s in path if isinstance(s, RamanFiber) or isinstance(s, Fiber)]
|
||||
print(f'\nThere are {len(spans)} fiber spans over {sum(spans)/1000:.0f} km between {source.uid} and {destination.uid}')
|
||||
print(f'\nNow propagating between {source.uid} and {destination.uid}:')
|
||||
|
||||
try:
|
||||
p_start, p_stop, p_step = equipment['SI']['default'].power_range_db
|
||||
p_num = abs(int(round((p_stop - p_start)/p_step))) + 1 if p_step != 0 else 1
|
||||
power_range = list(linspace(p_start, p_stop, p_num))
|
||||
except TypeError:
|
||||
print('invalid power range definition in eqpt_config, should be power_range_db: [lower, upper, step]')
|
||||
power_range = [0]
|
||||
|
||||
if not power_mode:
|
||||
#power cannot be changed in gain mode
|
||||
power_range = [0]
|
||||
for dp_db in power_range:
|
||||
req.power = db2lin(pref_ch_db + dp_db)*1e-3
|
||||
if power_mode:
|
||||
print(f'\nPropagating with input power = {ansi_escapes.cyan}{lin2db(req.power*1e3):.2f} dBm{ansi_escapes.reset}:')
|
||||
else:
|
||||
print(f'\nPropagating in {ansi_escapes.cyan}gain mode{ansi_escapes.reset}: power cannot be set manually')
|
||||
infos = propagate2(path, req, equipment)
|
||||
if len(power_range) == 1:
|
||||
for elem in path:
|
||||
print(elem)
|
||||
if power_mode:
|
||||
print(f'\nTransmission result for input power = {lin2db(req.power*1e3):.2f} dBm:')
|
||||
else:
|
||||
print(f'\nTransmission results:')
|
||||
print(f' Final SNR total (0.1 nm): {ansi_escapes.cyan}{mean(destination.snr_01nm):.02f} dB{ansi_escapes.reset}')
|
||||
else:
|
||||
print(path[-1])
|
||||
|
||||
#print(f'\n !!!!!!!!!!!!!!!!! TEST POINT !!!!!!!!!!!!!!!!!!!!!')
|
||||
#print(f'carriers ase output of {path[1]} =\n {list(path[1].carriers("out", "nli"))}')
|
||||
# => use "in" or "out" parameter
|
||||
# => use "nli" or "ase" or "signal" or "total" parameter
|
||||
if power_mode:
|
||||
simulation_data.append({
|
||||
'Pch_dBm' : pref_ch_db + dp_db,
|
||||
'OSNR_ASE_0.1nm' : round(mean(destination.osnr_ase_01nm),2),
|
||||
'OSNR_ASE_signal_bw' : round(mean(destination.osnr_ase),2),
|
||||
'SNR_nli_signal_bw' : round(mean(destination.osnr_nli),2),
|
||||
'SNR_total_signal_bw' : round(mean(destination.snr),2)
|
||||
})
|
||||
else:
|
||||
simulation_data.append({
|
||||
'gain_mode' : 'power canot be set',
|
||||
'OSNR_ASE_0.1nm' : round(mean(destination.osnr_ase_01nm),2),
|
||||
'OSNR_ASE_signal_bw' : round(mean(destination.osnr_ase),2),
|
||||
'SNR_nli_signal_bw' : round(mean(destination.osnr_nli),2),
|
||||
'SNR_total_signal_bw' : round(mean(destination.snr),2)
|
||||
})
|
||||
write_csv(result_dicts, 'simulation_result.csv')
|
||||
return path, infos
|
||||
|
||||
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument('-e', '--equipment', type=Path,
|
||||
default=Path(__file__).parent / 'eqpt_config.json')
|
||||
parser.add_argument('--sim-params', type=Path,
|
||||
default=None, help='Path to the JSON containing simulation parameters (required for Raman)')
|
||||
parser.add_argument('--show-channels', action='store_true', help='Show final per-channel OSNR summary')
|
||||
parser.add_argument('-pl', '--plot', action='store_true')
|
||||
parser.add_argument('-v', '--verbose', action='count', default=0, help='increases verbosity for each occurence')
|
||||
parser.add_argument('-l', '--list-nodes', action='store_true', help='list all transceiver nodes')
|
||||
parser.add_argument('-po', '--power', default=0, help='channel ref power in dBm')
|
||||
parser.add_argument('-names', '--names-matching', action='store_true', help='display network names that are closed matches')
|
||||
parser.add_argument('filename', nargs='?', type=Path,
|
||||
default=Path(__file__).parent / 'edfa_example_network.json')
|
||||
parser.add_argument('source', nargs='?', help='source node')
|
||||
parser.add_argument('destination', nargs='?', help='destination node')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = parser.parse_args()
|
||||
basicConfig(level={0: ERROR, 1: INFO, 2: DEBUG}.get(args.verbose, DEBUG))
|
||||
|
||||
try:
|
||||
equipment = load_equipment(args.equipment)
|
||||
network = load_network(args.filename, equipment, args.names_matching)
|
||||
sim_params = load_sim_params(args.sim_params) if args.sim_params is not None else None
|
||||
except EquipmentConfigError as e:
|
||||
print(f'{ansi_escapes.red}Configuration error in the equipment library:{ansi_escapes.reset} {e}')
|
||||
exit(1)
|
||||
except NetworkTopologyError as e:
|
||||
print(f'{ansi_escapes.red}Invalid network definition:{ansi_escapes.reset} {e}')
|
||||
exit(1)
|
||||
except ConfigurationError as e:
|
||||
print(f'{ansi_escapes.red}Configuration error:{ansi_escapes.reset} {e}')
|
||||
exit(1)
|
||||
|
||||
if args.plot:
|
||||
plot_baseline(network)
|
||||
|
||||
transceivers = {n.uid: n for n in network.nodes() if isinstance(n, Transceiver)}
|
||||
|
||||
if not transceivers:
|
||||
exit('Network has no transceivers!')
|
||||
if len(transceivers) < 2:
|
||||
exit('Network has only one transceiver!')
|
||||
|
||||
if args.list_nodes:
|
||||
for uid in transceivers:
|
||||
print(uid)
|
||||
exit()
|
||||
|
||||
#First try to find exact match if source/destination provided
|
||||
if args.source:
|
||||
source = transceivers.pop(args.source, None)
|
||||
valid_source = True if source else False
|
||||
else:
|
||||
source = None
|
||||
logger.info('No source node specified: picking random transceiver')
|
||||
|
||||
if args.destination:
|
||||
destination = transceivers.pop(args.destination, None)
|
||||
valid_destination = True if destination else False
|
||||
else:
|
||||
destination = None
|
||||
logger.info('No destination node specified: picking random transceiver')
|
||||
|
||||
#If no exact match try to find partial match
|
||||
if args.source and not source:
|
||||
#TODO code a more advanced regex to find nodes match
|
||||
source = next((transceivers.pop(uid) for uid in transceivers \
|
||||
if args.source.lower() in uid.lower()), None)
|
||||
|
||||
if args.destination and not destination:
|
||||
#TODO code a more advanced regex to find nodes match
|
||||
destination = next((transceivers.pop(uid) for uid in transceivers \
|
||||
if args.destination.lower() in uid.lower()), None)
|
||||
|
||||
#If no partial match or no source/destination provided pick random
|
||||
if not source:
|
||||
source = list(transceivers.values())[0]
|
||||
del transceivers[source.uid]
|
||||
|
||||
if not destination:
|
||||
destination = list(transceivers.values())[0]
|
||||
|
||||
logger.info(f'source = {args.source!r}')
|
||||
logger.info(f'destination = {args.destination!r}')
|
||||
|
||||
params = {}
|
||||
params['request_id'] = 0
|
||||
params['trx_type'] = ''
|
||||
params['trx_mode'] = ''
|
||||
params['source'] = source.uid
|
||||
params['destination'] = destination.uid
|
||||
params['nodes_list'] = [destination.uid]
|
||||
params['loose_list'] = ['strict']
|
||||
params['format'] = ''
|
||||
params['path_bandwidth'] = 0
|
||||
trx_params = trx_mode_params(equipment)
|
||||
if args.power:
|
||||
trx_params['power'] = db2lin(float(args.power))*1e-3
|
||||
params.update(trx_params)
|
||||
req = Path_request(**params)
|
||||
path, infos = main(network, equipment, source, destination, sim_params, req)
|
||||
save_network(args.filename, network)
|
||||
|
||||
if args.show_channels:
|
||||
print('\nThe total SNR per channel at the end of the line is:')
|
||||
print('{:>5}{:>26}{:>26}{:>28}{:>28}{:>28}' \
|
||||
.format('Ch. #', 'Channel frequency (THz)', 'Channel power (dBm)', 'OSNR ASE (signal bw, dB)', 'SNR NLI (signal bw, dB)', 'SNR total (signal bw, dB)'))
|
||||
for final_carrier, ch_osnr, ch_snr_nl, ch_snr in zip(infos[path[-1]][1].carriers, path[-1].osnr_ase, path[-1].osnr_nli, path[-1].snr):
|
||||
ch_freq = final_carrier.frequency * 1e-12
|
||||
ch_power = lin2db(final_carrier.power.signal*1e3)
|
||||
print('{:5}{:26.2f}{:26.2f}{:28.2f}{:28.2f}{:28.2f}' \
|
||||
.format(final_carrier.channel_number, round(ch_freq, 2), round(ch_power, 2), round(ch_osnr, 2), round(ch_snr_nl, 2), round(ch_snr, 2)))
|
||||
|
||||
if not args.source:
|
||||
print(f'\n(No source node specified: picked {source.uid})')
|
||||
elif not valid_source:
|
||||
print(f'\n(Invalid source node {args.source!r} replaced with {source.uid})')
|
||||
|
||||
if not args.destination:
|
||||
print(f'\n(No destination node specified: picked {destination.uid})')
|
||||
elif not valid_destination:
|
||||
print(f'\n(Invalid destination node {args.destination!r} replaced with {destination.uid})')
|
||||
|
||||
if args.plot:
|
||||
plot_results(network, path, source, destination, infos)
|
||||
@@ -0,0 +1,8 @@
|
||||
'''
|
||||
GNPy is an open-source, community-developed library for building route planning and optimization tools in real-world mesh optical networks. It is based on the Gaussian Noise Model.
|
||||
|
||||
Signal propagation is implemented in :py:mod:`.core`.
|
||||
Path finding and spectrum assignment is in :py:mod:`.topology`.
|
||||
Various tools and auxiliary code, including the JSON I/O handling, is in
|
||||
:py:mod:`.tools`.
|
||||
'''
|
||||
|
||||
@@ -1,30 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
########################################################################
|
||||
# _____ ___ ____ ____ ____ _____ #
|
||||
# |_ _|_ _| _ \ | _ \/ ___|| ____| #
|
||||
# | | | || |_) | | |_) \___ \| _| #
|
||||
# | | | || __/ | __/ ___) | |___ #
|
||||
# |_| |___|_| |_| |____/|_____| #
|
||||
# #
|
||||
# == Physical Simulation Environment == #
|
||||
# #
|
||||
########################################################################
|
||||
|
||||
|
||||
'''
|
||||
gnpy route planning and optimization library
|
||||
============================================
|
||||
Simulation of signal propagation in the DWDM network
|
||||
|
||||
gnpy is a route planning and optimization library, written in Python, for
|
||||
operators of large-scale mesh optical networks.
|
||||
|
||||
:copyright: © 2018, Telecom Infra Project
|
||||
:license: BSD 3-Clause, see LICENSE for more details.
|
||||
Optical signals, as defined via :class:`.info.SpectralInformation`, enter
|
||||
:py:mod:`.elements` which compute how these signals are affected as they travel
|
||||
through the :py:mod:`.network`.
|
||||
The simulation is controlled via :py:mod:`.parameters` and implemented mainly
|
||||
via :py:mod:`.science_utils`.
|
||||
'''
|
||||
|
||||
from . import elements
|
||||
from .execute import *
|
||||
from .network import *
|
||||
from .utils import *
|
||||
|
||||
@@ -9,5 +9,7 @@ A random subset of ANSI terminal escape codes for colored messages
|
||||
'''
|
||||
|
||||
red = '\x1b[1;31;40m'
|
||||
blue = '\x1b[1;34;40m'
|
||||
cyan = '\x1b[1;36;40m'
|
||||
yellow = '\x1b[1;33;40m'
|
||||
reset = '\x1b[0m'
|
||||
|
||||
@@ -1,627 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
gnpy.core.convert
|
||||
=================
|
||||
|
||||
This module contains utilities for converting between XLS and JSON.
|
||||
|
||||
The input XLS file must contain sheets named "Nodes" and "Links".
|
||||
It may optionally contain a sheet named "Eqpt".
|
||||
|
||||
In the "Nodes" sheet, only the "City" column is mandatory. The column "Type"
|
||||
can be determined automatically given the topology (e.g., if degree 2, ILA;
|
||||
otherwise, ROADM.) Incorrectly specified types (e.g., ILA for node of
|
||||
degree ≠ 2) will be automatically corrected.
|
||||
|
||||
In the "Links" sheet, only the first three columns ("Node A", "Node Z" and
|
||||
"east Distance (km)") are mandatory. Missing "west" information is copied from
|
||||
the "east" information so that it is possible to input undirected data.
|
||||
"""
|
||||
|
||||
from sys import exit
|
||||
try:
|
||||
from xlrd import open_workbook
|
||||
except ModuleNotFoundError:
|
||||
exit('Required: `pip install xlrd`')
|
||||
from argparse import ArgumentParser
|
||||
from collections import namedtuple, Counter, defaultdict
|
||||
from itertools import chain
|
||||
from json import dumps
|
||||
from pathlib import Path
|
||||
from difflib import get_close_matches
|
||||
from gnpy.core.utils import silent_remove
|
||||
import time
|
||||
|
||||
all_rows = lambda sh, start=0: (sh.row(x) for x in range(start, sh.nrows))
|
||||
|
||||
class Node(object):
|
||||
def __init__(self, **kwargs):
|
||||
super(Node, self).__init__()
|
||||
self.update_attr(kwargs)
|
||||
|
||||
def update_attr(self, kwargs):
|
||||
clean_kwargs = {k:v for k,v in kwargs.items() if v !=''}
|
||||
for k,v in self.default_values.items():
|
||||
v = clean_kwargs.get(k,v)
|
||||
setattr(self, k, v)
|
||||
|
||||
default_values = \
|
||||
{
|
||||
'city': '',
|
||||
'state': '',
|
||||
'country': '',
|
||||
'region': '',
|
||||
'latitude': 0,
|
||||
'longitude': 0,
|
||||
'node_type': 'ILA',
|
||||
'booster_restriction' : '',
|
||||
'preamp_restriction' : ''
|
||||
}
|
||||
|
||||
class Link(object):
|
||||
"""attribtes from west parse_ept_headers dict
|
||||
+node_a, node_z, west_fiber_con_in, east_fiber_con_in
|
||||
"""
|
||||
def __init__(self, **kwargs):
|
||||
super(Link, self).__init__()
|
||||
self.update_attr(kwargs)
|
||||
self.distance_units = 'km'
|
||||
|
||||
def update_attr(self, kwargs):
|
||||
clean_kwargs = {k:v for k,v in kwargs.items() if v !=''}
|
||||
for k,v in self.default_values.items():
|
||||
v = clean_kwargs.get(k,v)
|
||||
setattr(self, k, v)
|
||||
k = 'west' + k.split('east')[-1]
|
||||
v = clean_kwargs.get(k,v)
|
||||
setattr(self, k, v)
|
||||
|
||||
def __eq__(self, link):
|
||||
return (self.from_city == link.from_city and self.to_city == link.to_city) \
|
||||
or (self.from_city == link.to_city and self.to_city == link.from_city)
|
||||
|
||||
default_values = \
|
||||
{
|
||||
'from_city': '',
|
||||
'to_city': '',
|
||||
'east_distance': 80,
|
||||
'east_fiber': 'SSMF',
|
||||
'east_lineic': 0.2,
|
||||
'east_con_in': None,
|
||||
'east_con_out': None,
|
||||
'east_pmd': 0.1,
|
||||
'east_cable': ''
|
||||
}
|
||||
|
||||
|
||||
class Eqpt(object):
|
||||
def __init__(self, **kwargs):
|
||||
super(Eqpt, self).__init__()
|
||||
self.update_attr(kwargs)
|
||||
|
||||
def update_attr(self, kwargs):
|
||||
clean_kwargs = {k:v for k,v in kwargs.items() if v !=''}
|
||||
for k,v in self.default_values.items():
|
||||
v_east = clean_kwargs.get(k,v)
|
||||
setattr(self, k, v_east)
|
||||
k = 'west' + k.split('east')[-1]
|
||||
v_west = clean_kwargs.get(k,v)
|
||||
setattr(self, k, v_west)
|
||||
|
||||
default_values = \
|
||||
{
|
||||
'from_city': '',
|
||||
'to_city': '',
|
||||
'east_amp_type': '',
|
||||
'east_att_in': 0,
|
||||
'east_amp_gain': None,
|
||||
'east_amp_dp': None,
|
||||
'east_tilt': 0,
|
||||
'east_att_out': None
|
||||
}
|
||||
|
||||
|
||||
def read_header(my_sheet, line, slice_):
|
||||
""" return the list of headers !:= ''
|
||||
header_i = [(header, header_column_index), ...]
|
||||
in a {line, slice1_x, slice_y} range
|
||||
"""
|
||||
Param_header = namedtuple('Param_header', 'header colindex')
|
||||
try:
|
||||
header = [x.value.strip() for x in my_sheet.row_slice(line, slice_[0], slice_[1])]
|
||||
header_i = [Param_header(header,i+slice_[0]) for i, header in enumerate(header) if header != '']
|
||||
except Exception:
|
||||
header_i = []
|
||||
if header_i != [] and header_i[-1].colindex != slice_[1]:
|
||||
header_i.append(Param_header('',slice_[1]))
|
||||
return header_i
|
||||
|
||||
def read_slice(my_sheet, line, slice_, header):
|
||||
"""return the slice range of a given header
|
||||
in a defined range {line, slice_x, slice_y}"""
|
||||
header_i = read_header(my_sheet, line, slice_)
|
||||
slice_range = (-1,-1)
|
||||
if header_i != []:
|
||||
try:
|
||||
slice_range = next((h.colindex,header_i[i+1].colindex) \
|
||||
for i,h in enumerate(header_i) if header in h.header)
|
||||
except Exception:
|
||||
pass
|
||||
return slice_range
|
||||
|
||||
|
||||
def parse_headers(my_sheet, input_headers_dict, headers, start_line, slice_in):
|
||||
"""return a dict of header_slice
|
||||
key = column index
|
||||
value = header name"""
|
||||
|
||||
|
||||
for h0 in input_headers_dict:
|
||||
slice_out = read_slice(my_sheet, start_line, slice_in, h0)
|
||||
iteration = 1
|
||||
while slice_out == (-1,-1) and iteration < 10:
|
||||
#try next lines
|
||||
#print(h0, iteration)
|
||||
slice_out = read_slice(my_sheet, start_line+iteration, slice_in, h0)
|
||||
iteration += 1
|
||||
if slice_out == (-1, -1):
|
||||
if h0 in ('east', 'Node A', 'Node Z', 'City') :
|
||||
print(f'\x1b[1;31;40m'+f'CRITICAL: missing _{h0}_ header: EXECUTION ENDS'+ '\x1b[0m')
|
||||
exit()
|
||||
else:
|
||||
print(f'missing header {h0}')
|
||||
elif not isinstance(input_headers_dict[h0], dict):
|
||||
headers[slice_out[0]] = input_headers_dict[h0]
|
||||
else:
|
||||
headers = parse_headers(my_sheet, input_headers_dict[h0], headers, start_line+1, slice_out)
|
||||
if headers == {}:
|
||||
print(f'\x1b[1;31;40m'+f'CRITICAL ERROR: could not find any header to read _ ABORT'+ '\x1b[0m')
|
||||
exit()
|
||||
return headers
|
||||
|
||||
def parse_row(row, headers):
|
||||
#print([label for label in ept.values()])
|
||||
#print([i for i in ept.keys()])
|
||||
#print(row[i for i in ept.keys()])
|
||||
return {f: r.value for f, r in \
|
||||
zip([label for label in headers.values()], [row[i] for i in headers])}
|
||||
#if r.ctype != XL_CELL_EMPTY}
|
||||
|
||||
def parse_sheet(my_sheet, input_headers_dict, header_line, start_line, column):
|
||||
headers = parse_headers(my_sheet, input_headers_dict, {}, header_line, (0,column))
|
||||
for row in all_rows(my_sheet, start=start_line):
|
||||
yield parse_row(row[0: column], headers)
|
||||
|
||||
def sanity_check(nodes, links, nodes_by_city, links_by_city, eqpts_by_city):
|
||||
|
||||
duplicate_links = []
|
||||
for l1 in links:
|
||||
for l2 in links:
|
||||
if l1 is not l2 and l1 == l2 and l2 not in duplicate_links:
|
||||
print(f'\nWARNING\n \
|
||||
link {l1.from_city}-{l1.to_city} is duplicate \
|
||||
\nthe 1st duplicate link will be removed but you should check Links sheet input')
|
||||
duplicate_links.append(l1)
|
||||
#if duplicate_links != []:
|
||||
#time.sleep(3)
|
||||
for l in duplicate_links:
|
||||
links.remove(l)
|
||||
|
||||
try :
|
||||
test_nodes = [n for n in nodes_by_city if not n in links_by_city]
|
||||
test_links = [n for n in links_by_city if not n in nodes_by_city]
|
||||
test_eqpts = [n for n in eqpts_by_city if not n in nodes_by_city]
|
||||
assert (test_nodes == [] or test_nodes == [''])\
|
||||
and (test_links == [] or test_links ==[''])\
|
||||
and (test_eqpts == [] or test_eqpts ==[''])
|
||||
except AssertionError:
|
||||
print(f'CRITICAL error: \nNames in Nodes and Links sheets do no match, check:\
|
||||
\n{test_nodes} in Nodes sheet\
|
||||
\n{test_links} in Links sheet\
|
||||
\n{test_eqpts} in Eqpt sheet')
|
||||
exit(1)
|
||||
|
||||
for city,link in links_by_city.items():
|
||||
if nodes_by_city[city].node_type.lower()=='ila' and len(link) != 2:
|
||||
#wrong input: ILA sites can only be Degree 2
|
||||
# => correct to make it a ROADM and remove entry in links_by_city
|
||||
#TODO : put in log rather than print
|
||||
print(f'invalid node type ({nodes_by_city[city].node_type})\
|
||||
specified in {city}, replaced by ROADM')
|
||||
nodes_by_city[city].node_type = 'ROADM'
|
||||
for n in nodes:
|
||||
if n.city==city:
|
||||
n.node_type='ROADM'
|
||||
return nodes, links
|
||||
|
||||
def convert_file(input_filename, names_matching=False, filter_region=[]):
|
||||
nodes, links, eqpts = parse_excel(input_filename)
|
||||
if filter_region:
|
||||
nodes = [n for n in nodes if n.region.lower() in filter_region]
|
||||
cities = {n.city for n in nodes}
|
||||
links = [lnk for lnk in links if lnk.from_city in cities and
|
||||
lnk.to_city in cities]
|
||||
cities = {lnk.from_city for lnk in links} | {lnk.to_city for lnk in links}
|
||||
nodes = [n for n in nodes if n.city in cities]
|
||||
|
||||
global nodes_by_city
|
||||
nodes_by_city = {n.city: n for n in nodes}
|
||||
#create matching dictionary for node name mismatch analysis
|
||||
|
||||
cities = {''.join(c.strip() for c in n.city.split('C+L')).lower(): n.city for n in nodes}
|
||||
cities_to_match = [k for k in cities]
|
||||
city_match_dic = defaultdict(list)
|
||||
for city in cities:
|
||||
if city in cities_to_match:
|
||||
cities_to_match.remove(city)
|
||||
matches = get_close_matches(city, cities_to_match, 4, 0.85)
|
||||
for m in matches:
|
||||
city_match_dic[cities[city]].append(cities[m])
|
||||
#check lower case/upper case
|
||||
for city in nodes_by_city:
|
||||
for match_city in nodes_by_city:
|
||||
if match_city.lower() == city.lower() and match_city != city:
|
||||
city_match_dic[city].append(match_city)
|
||||
|
||||
if names_matching:
|
||||
print('\ncity match dictionary:',city_match_dic)
|
||||
with open('name_match_dictionary.json', 'w', encoding='utf-8') as city_match_dic_file:
|
||||
city_match_dic_file.write(dumps(city_match_dic, indent=2, ensure_ascii=False))
|
||||
|
||||
global links_by_city
|
||||
links_by_city = defaultdict(list)
|
||||
for link in links:
|
||||
links_by_city[link.from_city].append(link)
|
||||
links_by_city[link.to_city].append(link)
|
||||
|
||||
global eqpts_by_city
|
||||
eqpts_by_city = defaultdict(list)
|
||||
for eqpt in eqpts:
|
||||
eqpts_by_city[eqpt.from_city].append(eqpt)
|
||||
|
||||
nodes, links = sanity_check(nodes, links, nodes_by_city, links_by_city, eqpts_by_city)
|
||||
|
||||
data = {
|
||||
'elements':
|
||||
[{'uid': f'trx {x.city}',
|
||||
'metadata': {'location': {'city': x.city,
|
||||
'region': x.region,
|
||||
'latitude': x.latitude,
|
||||
'longitude': x.longitude}},
|
||||
'type': 'Transceiver'}
|
||||
for x in nodes_by_city.values() if x.node_type.lower() == 'roadm'] +
|
||||
[{'uid': f'roadm {x.city}',
|
||||
'metadata': {'location': {'city': x.city,
|
||||
'region': x.region,
|
||||
'latitude': x.latitude,
|
||||
'longitude': x.longitude}},
|
||||
'type': 'Roadm'}
|
||||
for x in nodes_by_city.values() if x.node_type.lower() == 'roadm' \
|
||||
and x.booster_restriction == '' and x.preamp_restriction == ''] +
|
||||
[{'uid': f'roadm {x.city}',
|
||||
'params' : {
|
||||
'restrictions': {
|
||||
'preamp_variety_list': silent_remove(x.preamp_restriction.split(' | '),''),
|
||||
'booster_variety_list': silent_remove(x.booster_restriction.split(' | '),'')
|
||||
}
|
||||
},
|
||||
'metadata': {'location': {'city': x.city,
|
||||
'region': x.region,
|
||||
'latitude': x.latitude,
|
||||
'longitude': x.longitude}},
|
||||
'type': 'Roadm'}
|
||||
for x in nodes_by_city.values() if x.node_type.lower() == 'roadm' and \
|
||||
(x.booster_restriction != '' or x.preamp_restriction != '')] +
|
||||
[{'uid': f'west fused spans in {x.city}',
|
||||
'metadata': {'location': {'city': x.city,
|
||||
'region': x.region,
|
||||
'latitude': x.latitude,
|
||||
'longitude': x.longitude}},
|
||||
'type': 'Fused'}
|
||||
for x in nodes_by_city.values() if x.node_type.lower() == 'fused'] +
|
||||
[{'uid': f'east fused spans in {x.city}',
|
||||
'metadata': {'location': {'city': x.city,
|
||||
'region': x.region,
|
||||
'latitude': x.latitude,
|
||||
'longitude': x.longitude}},
|
||||
'type': 'Fused'}
|
||||
for x in nodes_by_city.values() if x.node_type.lower() == 'fused'] +
|
||||
[{'uid': f'fiber ({x.from_city} \u2192 {x.to_city})-{x.east_cable}',
|
||||
'metadata': {'location': midpoint(nodes_by_city[x.from_city],
|
||||
nodes_by_city[x.to_city])},
|
||||
'type': 'Fiber',
|
||||
'type_variety': x.east_fiber,
|
||||
'params': {'length': round(x.east_distance, 3),
|
||||
'length_units': x.distance_units,
|
||||
'loss_coef': x.east_lineic,
|
||||
'con_in':x.east_con_in,
|
||||
'con_out':x.east_con_out}
|
||||
}
|
||||
for x in links] +
|
||||
[{'uid': f'fiber ({x.to_city} \u2192 {x.from_city})-{x.west_cable}',
|
||||
'metadata': {'location': midpoint(nodes_by_city[x.from_city],
|
||||
nodes_by_city[x.to_city])},
|
||||
'type': 'Fiber',
|
||||
'type_variety': x.west_fiber,
|
||||
'params': {'length': round(x.west_distance, 3),
|
||||
'length_units': x.distance_units,
|
||||
'loss_coef': x.west_lineic,
|
||||
'con_in':x.west_con_in,
|
||||
'con_out':x.west_con_out}
|
||||
} # missing ILA construction
|
||||
for x in links] +
|
||||
[{'uid': f'east edfa in {e.from_city} to {e.to_city}',
|
||||
'metadata': {'location': {'city': nodes_by_city[e.from_city].city,
|
||||
'region': nodes_by_city[e.from_city].region,
|
||||
'latitude': nodes_by_city[e.from_city].latitude,
|
||||
'longitude': nodes_by_city[e.from_city].longitude}},
|
||||
'type': 'Edfa',
|
||||
'type_variety': e.east_amp_type,
|
||||
'operational': {'gain_target': e.east_amp_gain,
|
||||
'delta_p': e.east_amp_dp,
|
||||
'tilt_target': e.east_tilt,
|
||||
'out_voa' : e.east_att_out}
|
||||
}
|
||||
for e in eqpts if (e.east_amp_type.lower() != '' and \
|
||||
e.east_amp_type.lower() != 'fused')] +
|
||||
[{'uid': f'west edfa in {e.from_city} to {e.to_city}',
|
||||
'metadata': {'location': {'city': nodes_by_city[e.from_city].city,
|
||||
'region': nodes_by_city[e.from_city].region,
|
||||
'latitude': nodes_by_city[e.from_city].latitude,
|
||||
'longitude': nodes_by_city[e.from_city].longitude}},
|
||||
'type': 'Edfa',
|
||||
'type_variety': e.west_amp_type,
|
||||
'operational': {'gain_target': e.west_amp_gain,
|
||||
'delta_p': e.west_amp_dp,
|
||||
'tilt_target': e.west_tilt,
|
||||
'out_voa' : e.west_att_out}
|
||||
}
|
||||
for e in eqpts if (e.west_amp_type.lower() != '' and \
|
||||
e.west_amp_type.lower() != 'fused')] +
|
||||
# fused edfa variety is a hack to indicate that there should not be
|
||||
# booster amplifier out the roadm.
|
||||
# If user specifies ILA in Nodes sheet and fused in Eqpt sheet, then assumes that
|
||||
# this is a fused nodes.
|
||||
[{'uid': f'east edfa in {e.from_city} to {e.to_city}',
|
||||
'metadata': {'location': {'city': nodes_by_city[e.from_city].city,
|
||||
'region': nodes_by_city[e.from_city].region,
|
||||
'latitude': nodes_by_city[e.from_city].latitude,
|
||||
'longitude': nodes_by_city[e.from_city].longitude}},
|
||||
'type': 'Fused',
|
||||
'params': {'loss': 0}
|
||||
}
|
||||
for e in eqpts if e.east_amp_type.lower() == 'fused'] +
|
||||
[{'uid': f'west edfa in {e.from_city} to {e.to_city}',
|
||||
'metadata': {'location': {'city': nodes_by_city[e.from_city].city,
|
||||
'region': nodes_by_city[e.from_city].region,
|
||||
'latitude': nodes_by_city[e.from_city].latitude,
|
||||
'longitude': nodes_by_city[e.from_city].longitude}},
|
||||
'type': 'Fused',
|
||||
'params': {'loss': 0}
|
||||
}
|
||||
for e in eqpts if e.west_amp_type.lower() == 'fused'],
|
||||
'connections':
|
||||
list(chain.from_iterable([eqpt_connection_by_city(n.city)
|
||||
for n in nodes]))
|
||||
+
|
||||
list(chain.from_iterable(zip(
|
||||
[{'from_node': f'trx {x.city}',
|
||||
'to_node': f'roadm {x.city}'}
|
||||
for x in nodes_by_city.values() if x.node_type.lower()=='roadm'],
|
||||
[{'from_node': f'roadm {x.city}',
|
||||
'to_node': f'trx {x.city}'}
|
||||
for x in nodes_by_city.values() if x.node_type.lower()=='roadm'])))
|
||||
}
|
||||
|
||||
suffix_filename = str(input_filename.suffixes[0])
|
||||
full_input_filename = str(input_filename)
|
||||
split_filename = [full_input_filename[0:len(full_input_filename)-len(suffix_filename)] , suffix_filename[1:]]
|
||||
output_json_file_name = split_filename[0]+'.json'
|
||||
with open(output_json_file_name, 'w', encoding='utf-8') as edfa_json_file:
|
||||
edfa_json_file.write(dumps(data, indent=2, ensure_ascii=False))
|
||||
return output_json_file_name
|
||||
|
||||
def parse_excel(input_filename):
|
||||
link_headers = \
|
||||
{ 'Node A': 'from_city',
|
||||
'Node Z': 'to_city',
|
||||
'east':{
|
||||
'Distance (km)': 'east_distance',
|
||||
'Fiber type': 'east_fiber',
|
||||
'lineic att': 'east_lineic',
|
||||
'Con_in': 'east_con_in',
|
||||
'Con_out': 'east_con_out',
|
||||
'PMD': 'east_pmd',
|
||||
'Cable id': 'east_cable'
|
||||
},
|
||||
'west':{
|
||||
'Distance (km)': 'west_distance',
|
||||
'Fiber type': 'west_fiber',
|
||||
'lineic att': 'west_lineic',
|
||||
'Con_in': 'west_con_in',
|
||||
'Con_out': 'west_con_out',
|
||||
'PMD': 'west_pmd',
|
||||
'Cable id': 'west_cable'
|
||||
}
|
||||
}
|
||||
node_headers = \
|
||||
{ 'City': 'city',
|
||||
'State': 'state',
|
||||
'Country': 'country',
|
||||
'Region': 'region',
|
||||
'Latitude': 'latitude',
|
||||
'Longitude': 'longitude',
|
||||
'Type': 'node_type',
|
||||
'Booster_restriction': 'booster_restriction',
|
||||
'Preamp_restriction': 'preamp_restriction'
|
||||
}
|
||||
eqpt_headers = \
|
||||
{ 'Node A': 'from_city',
|
||||
'Node Z': 'to_city',
|
||||
'east':{
|
||||
'amp type': 'east_amp_type',
|
||||
'att_in': 'east_att_in',
|
||||
'amp gain': 'east_amp_gain',
|
||||
'delta p': 'east_amp_dp',
|
||||
'tilt': 'east_tilt',
|
||||
'att_out': 'east_att_out'
|
||||
},
|
||||
'west':{
|
||||
'amp type': 'west_amp_type',
|
||||
'att_in': 'west_att_in',
|
||||
'amp gain': 'west_amp_gain',
|
||||
'delta p': 'west_amp_dp',
|
||||
'tilt': 'west_tilt',
|
||||
'att_out': 'west_att_out'
|
||||
}
|
||||
}
|
||||
|
||||
with open_workbook(input_filename) as wb:
|
||||
nodes_sheet = wb.sheet_by_name('Nodes')
|
||||
links_sheet = wb.sheet_by_name('Links')
|
||||
try:
|
||||
eqpt_sheet = wb.sheet_by_name('Eqpt')
|
||||
except Exception:
|
||||
#eqpt_sheet is optional
|
||||
eqpt_sheet = None
|
||||
|
||||
nodes = []
|
||||
for node in parse_sheet(nodes_sheet, node_headers, NODES_LINE, NODES_LINE+1, NODES_COLUMN):
|
||||
nodes.append(Node(**node))
|
||||
expected_node_types = {'ROADM', 'ILA', 'FUSED'}
|
||||
for n in nodes:
|
||||
if n.node_type not in expected_node_types:
|
||||
n.node_type = 'ILA'
|
||||
|
||||
links = []
|
||||
for link in parse_sheet(links_sheet, link_headers, LINKS_LINE, LINKS_LINE+2, LINKS_COLUMN):
|
||||
links.append(Link(**link))
|
||||
#print('\n', [l.__dict__ for l in links])
|
||||
|
||||
eqpts = []
|
||||
if eqpt_sheet != None:
|
||||
for eqpt in parse_sheet(eqpt_sheet, eqpt_headers, EQPTS_LINE, EQPTS_LINE+2, EQPTS_COLUMN):
|
||||
eqpts.append(Eqpt(**eqpt))
|
||||
|
||||
# sanity check
|
||||
all_cities = Counter(n.city for n in nodes)
|
||||
if len(all_cities) != len(nodes):
|
||||
raise ValueError(f'Duplicate city: {all_cities}')
|
||||
if any(ln.from_city not in all_cities or
|
||||
ln.to_city not in all_cities for ln in links):
|
||||
raise ValueError(f'Bad link.')
|
||||
|
||||
return nodes, links, eqpts
|
||||
|
||||
|
||||
def eqpt_connection_by_city(city_name):
|
||||
other_cities = fiber_dest_from_source(city_name)
|
||||
subdata = []
|
||||
if nodes_by_city[city_name].node_type.lower() in {'ila', 'fused'}:
|
||||
# Then len(other_cities) == 2
|
||||
direction = ['west', 'east']
|
||||
for i in range(2):
|
||||
from_ = fiber_link(other_cities[i], city_name)
|
||||
in_ = eqpt_in_city_to_city(city_name, other_cities[0],direction[i])
|
||||
to_ = fiber_link(city_name, other_cities[1-i])
|
||||
subdata += connect_eqpt(from_, in_, to_)
|
||||
elif nodes_by_city[city_name].node_type.lower() == 'roadm':
|
||||
for other_city in other_cities:
|
||||
from_ = f'roadm {city_name}'
|
||||
in_ = eqpt_in_city_to_city(city_name, other_city)
|
||||
to_ = fiber_link(city_name, other_city)
|
||||
subdata += connect_eqpt(from_, in_, to_)
|
||||
|
||||
from_ = fiber_link(other_city, city_name)
|
||||
in_ = eqpt_in_city_to_city(city_name, other_city, "west")
|
||||
to_ = f'roadm {city_name}'
|
||||
subdata += connect_eqpt(from_, in_, to_)
|
||||
return subdata
|
||||
|
||||
|
||||
def connect_eqpt(from_, in_, to_):
|
||||
connections = []
|
||||
if in_ !='':
|
||||
connections = [{'from_node': from_, 'to_node': in_},
|
||||
{'from_node': in_, 'to_node': to_}]
|
||||
else:
|
||||
connections = [{'from_node': from_, 'to_node': to_}]
|
||||
return connections
|
||||
|
||||
|
||||
def eqpt_in_city_to_city(in_city, to_city, direction='east'):
|
||||
rev_direction = 'west' if direction == 'east' else 'east'
|
||||
amp_direction = f'{direction}_amp_type'
|
||||
amp_rev_direction = f'{rev_direction}_amp_type'
|
||||
return_eqpt = ''
|
||||
if in_city in eqpts_by_city:
|
||||
for e in eqpts_by_city[in_city]:
|
||||
if nodes_by_city[in_city].node_type.lower() == 'roadm':
|
||||
if e.to_city == to_city and getattr(e, amp_direction) != '':
|
||||
return_eqpt = f'{direction} edfa in {e.from_city} to {e.to_city}'
|
||||
elif nodes_by_city[in_city].node_type.lower() == 'ila':
|
||||
if e.to_city != to_city:
|
||||
direction = rev_direction
|
||||
amp_direction = amp_rev_direction
|
||||
if getattr(e, amp_direction) != '':
|
||||
return_eqpt = f'{direction} edfa in {e.from_city} to {e.to_city}'
|
||||
if nodes_by_city[in_city].node_type.lower() == 'fused':
|
||||
return_eqpt = f'{direction} fused spans in {in_city}'
|
||||
return return_eqpt
|
||||
|
||||
|
||||
def fiber_dest_from_source(city_name):
|
||||
destinations = []
|
||||
links_from_city = links_by_city[city_name]
|
||||
for l in links_from_city:
|
||||
if l.from_city == city_name:
|
||||
destinations.append(l.to_city)
|
||||
else:
|
||||
destinations.append(l.from_city)
|
||||
return destinations
|
||||
|
||||
|
||||
def fiber_link(from_city, to_city):
|
||||
source_dest = (from_city, to_city)
|
||||
link = links_by_city[from_city]
|
||||
l = next(l for l in link if l.from_city in source_dest and l.to_city in source_dest)
|
||||
if l.from_city == from_city:
|
||||
fiber = f'fiber ({l.from_city} \u2192 {l.to_city})-{l.east_cable}'
|
||||
else:
|
||||
fiber = f'fiber ({l.to_city} \u2192 {l.from_city})-{l.west_cable}'
|
||||
return fiber
|
||||
|
||||
|
||||
def midpoint(city_a, city_b):
|
||||
lats = city_a.latitude, city_b.latitude
|
||||
longs = city_a.longitude, city_b.longitude
|
||||
try:
|
||||
result = {
|
||||
'latitude': sum(lats) / 2,
|
||||
'longitude': sum(longs) / 2
|
||||
}
|
||||
except :
|
||||
result = {
|
||||
'latitude': 0,
|
||||
'longitude': 0
|
||||
}
|
||||
return result
|
||||
|
||||
#output_json_file_name = 'coronet_conus_example.json'
|
||||
#TODO get column size automatically from tupple size
|
||||
|
||||
NODES_COLUMN = 10
|
||||
NODES_LINE = 4
|
||||
LINKS_COLUMN = 16
|
||||
LINKS_LINE = 3
|
||||
EQPTS_LINE = 3
|
||||
EQPTS_COLUMN = 14
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument('workbook', nargs='?', type=Path , default='meshTopologyExampleV2.xls')
|
||||
parser.add_argument('-f', '--filter-region', action='append', default=[])
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = parser.parse_args()
|
||||
convert_file(args.workbook, args.filter_region)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,261 +8,9 @@ gnpy.core.equipment
|
||||
This module contains functionality for specifying equipment.
|
||||
'''
|
||||
|
||||
from numpy import clip, polyval
|
||||
from operator import itemgetter
|
||||
from math import isclose
|
||||
from pathlib import Path
|
||||
from json import load
|
||||
from gnpy.core.utils import lin2db, db2lin, load_json
|
||||
from collections import namedtuple
|
||||
from gnpy.core.elements import Edfa
|
||||
from gnpy.core.utils import automatic_nch, db2lin
|
||||
from gnpy.core.exceptions import EquipmentConfigError
|
||||
import time
|
||||
|
||||
Model_vg = namedtuple('Model_vg', 'nf1 nf2 delta_p')
|
||||
Model_fg = namedtuple('Model_fg', 'nf0')
|
||||
Model_openroadm = namedtuple('Model_openroadm', 'nf_coef')
|
||||
Model_hybrid = namedtuple('Model_hybrid', 'nf_ram gain_ram edfa_variety')
|
||||
Model_dual_stage = namedtuple('Model_dual_stage', 'preamp_variety booster_variety')
|
||||
|
||||
class common:
|
||||
def update_attr(self, default_values, kwargs, name):
|
||||
clean_kwargs = {k:v for k, v in kwargs.items() if v != ''}
|
||||
for k, v in default_values.items():
|
||||
setattr(self, k, clean_kwargs.get(k, v))
|
||||
if k not in clean_kwargs and name != 'Amp':
|
||||
print(f'\x1b[1;31;40m'+
|
||||
f'\n WARNING missing {k} attribute in eqpt_config.json[{name}]'+
|
||||
f'\n default value is {k} = {v}'+
|
||||
f'\x1b[0m')
|
||||
time.sleep(1)
|
||||
|
||||
class SI(common):
|
||||
default_values =\
|
||||
{
|
||||
"f_min": 191.35e12,
|
||||
"f_max": 196.1e12,
|
||||
"baud_rate": 32e9,
|
||||
"spacing": 50e9,
|
||||
"power_dbm": 0,
|
||||
"power_range_db": [0, 0, 0.5],
|
||||
"roll_off": 0.15,
|
||||
"tx_osnr": 45,
|
||||
"sys_margins": 0
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.update_attr(self.default_values, kwargs, 'SI')
|
||||
|
||||
class Span(common):
|
||||
default_values = \
|
||||
{
|
||||
'power_mode': True,
|
||||
'delta_power_range_db': None,
|
||||
'max_fiber_lineic_loss_for_raman': 0.25,
|
||||
'target_extended_gain': 2.5,
|
||||
'max_length': 150,
|
||||
'length_units': 'km',
|
||||
'max_loss': None,
|
||||
'padding': 10,
|
||||
'EOL': 0,
|
||||
'con_in': 0,
|
||||
'con_out': 0
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.update_attr(self.default_values, kwargs, 'Span')
|
||||
|
||||
class Roadm(common):
|
||||
default_values = \
|
||||
{
|
||||
'target_pch_out_db': -17,
|
||||
'add_drop_osnr': 100,
|
||||
'restrictions': {
|
||||
'preamp_variety_list':[],
|
||||
'booster_variety_list':[]
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.update_attr(self.default_values, kwargs, 'Roadm')
|
||||
|
||||
class Transceiver(common):
|
||||
default_values = \
|
||||
{
|
||||
'type_variety': None,
|
||||
'frequency': None,
|
||||
'mode': {}
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.update_attr(self.default_values, kwargs, 'Transceiver')
|
||||
|
||||
class Fiber(common):
|
||||
default_values = \
|
||||
{
|
||||
'type_variety': '',
|
||||
'dispersion': None,
|
||||
'gamma': 0
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.update_attr(self.default_values, kwargs, 'Fiber')
|
||||
|
||||
class RamanFiber(common):
|
||||
default_values = \
|
||||
{
|
||||
'type_variety': '',
|
||||
'dispersion': None,
|
||||
'gamma': 0,
|
||||
'raman_efficiency': None
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.update_attr(self.default_values, kwargs, 'RamanFiber')
|
||||
for param in ('cr', 'frequency_offset'):
|
||||
if param not in self.raman_efficiency:
|
||||
raise EquipmentConfigError(f'RamanFiber.raman_efficiency: missing "{param}" parameter')
|
||||
if self.raman_efficiency['frequency_offset'] != sorted(self.raman_efficiency['frequency_offset']):
|
||||
raise EquipmentConfigError(f'RamanFiber.raman_efficiency.frequency_offset is not sorted')
|
||||
|
||||
class Amp(common):
|
||||
default_values = \
|
||||
{
|
||||
'f_min': 191.35e12,
|
||||
'f_max': 196.1e12,
|
||||
'type_variety': '',
|
||||
'type_def': '',
|
||||
'gain_flatmax': None,
|
||||
'gain_min': None,
|
||||
'p_max': None,
|
||||
'nf_model': None,
|
||||
'dual_stage_model': None,
|
||||
'nf_fit_coeff': None,
|
||||
'nf_ripple': None,
|
||||
'dgt': None,
|
||||
'gain_ripple': None,
|
||||
'out_voa_auto': False,
|
||||
'allowed_for_design': False,
|
||||
'raman': False
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.update_attr(self.default_values, kwargs, 'Amp')
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, filename, **kwargs):
|
||||
config = Path(filename).parent / 'default_edfa_config.json'
|
||||
|
||||
type_variety = kwargs['type_variety']
|
||||
type_def = kwargs.get('type_def', 'variable_gain') # default compatibility with older json eqpt files
|
||||
nf_def = None
|
||||
dual_stage_def = None
|
||||
|
||||
if type_def == 'fixed_gain':
|
||||
try:
|
||||
nf0 = kwargs.pop('nf0')
|
||||
except KeyError: #nf0 is expected for a fixed gain amp
|
||||
raise EquipmentConfigError(f'missing nf0 value input for amplifier: {type_variety} in equipment config')
|
||||
for k in ('nf_min', 'nf_max'):
|
||||
try:
|
||||
del kwargs[k]
|
||||
except KeyError:
|
||||
pass
|
||||
nf_def = Model_fg(nf0)
|
||||
elif type_def == 'advanced_model':
|
||||
config = Path(filename).parent / kwargs.pop('advanced_config_from_json')
|
||||
elif type_def == 'variable_gain':
|
||||
gain_min, gain_max = kwargs['gain_min'], kwargs['gain_flatmax']
|
||||
try: #nf_min and nf_max are expected for a variable gain amp
|
||||
nf_min = kwargs.pop('nf_min')
|
||||
nf_max = kwargs.pop('nf_max')
|
||||
except KeyError:
|
||||
raise EquipmentConfigError(f'missing nf_min or nf_max value input for amplifier: {type_variety} in equipment config')
|
||||
try: #remove all remaining nf inputs
|
||||
del kwargs['nf0']
|
||||
except KeyError: pass #nf0 is not needed for variable gain amp
|
||||
nf1, nf2, delta_p = nf_model(type_variety, gain_min, gain_max, nf_min, nf_max)
|
||||
nf_def = Model_vg(nf1, nf2, delta_p)
|
||||
elif type_def == 'openroadm':
|
||||
try:
|
||||
nf_coef = kwargs.pop('nf_coef')
|
||||
except KeyError: #nf_coef is expected for openroadm amp
|
||||
raise EquipmentConfigError(f'missing nf_coef input for amplifier: {type_variety} in equipment config')
|
||||
nf_def = Model_openroadm(nf_coef)
|
||||
elif type_def == 'dual_stage':
|
||||
try: #nf_ram and gain_ram are expected for a hybrid amp
|
||||
preamp_variety = kwargs.pop('preamp_variety')
|
||||
booster_variety = kwargs.pop('booster_variety')
|
||||
except KeyError:
|
||||
raise EquipmentConfigError(f'missing preamp/booster variety input for amplifier: {type_variety} in equipment config')
|
||||
dual_stage_def = Model_dual_stage(preamp_variety, booster_variety)
|
||||
|
||||
with open(config, encoding='utf-8') as f:
|
||||
json_data = load(f)
|
||||
|
||||
return cls(**{**kwargs, **json_data,
|
||||
'nf_model': nf_def, 'dual_stage_model': dual_stage_def})
|
||||
|
||||
|
||||
def nf_model(type_variety, gain_min, gain_max, nf_min, nf_max):
|
||||
if nf_min < -10:
|
||||
raise EquipmentConfigError(f'Invalid nf_min value {nf_min!r} for amplifier {type_variety}')
|
||||
if nf_max < -10:
|
||||
raise EquipmentConfigError(f'Invalid nf_max value {nf_max!r} for amplifier {type_variety}')
|
||||
|
||||
# NF estimation model based on nf_min and nf_max
|
||||
# delta_p: max power dB difference between first and second stage coils
|
||||
# dB g1a: first stage gain - internal VOA attenuation
|
||||
# nf1, nf2: first and second stage coils
|
||||
# calculated by solving nf_{min,max} = nf1 + nf2 / g1a{min,max}
|
||||
delta_p = 5
|
||||
g1a_min = gain_min - (gain_max - gain_min) - delta_p
|
||||
g1a_max = gain_max - delta_p
|
||||
nf2 = lin2db((db2lin(nf_min) - db2lin(nf_max)) /
|
||||
(1/db2lin(g1a_max) - 1/db2lin(g1a_min)))
|
||||
nf1 = lin2db(db2lin(nf_min) - db2lin(nf2)/db2lin(g1a_max))
|
||||
|
||||
if nf1 < 4:
|
||||
raise EquipmentConfigError(f'First coil value too low {nf1} for amplifier {type_variety}')
|
||||
|
||||
# Check 1 dB < delta_p < 6 dB to ensure nf_min and nf_max values make sense.
|
||||
# There shouldn't be high nf differences between the two coils:
|
||||
# nf2 should be nf1 + 0.3 < nf2 < nf1 + 2
|
||||
# If not, recompute and check delta_p
|
||||
if not nf1 + 0.3 < nf2 < nf1 + 2:
|
||||
nf2 = clip(nf2, nf1 + 0.3, nf1 + 2)
|
||||
g1a_max = lin2db(db2lin(nf2) / (db2lin(nf_min) - db2lin(nf1)))
|
||||
delta_p = gain_max - g1a_max
|
||||
g1a_min = gain_min - (gain_max-gain_min) - delta_p
|
||||
if not 1 < delta_p < 11:
|
||||
raise EquipmentConfigError(f'Computed \N{greek capital letter delta}P invalid \
|
||||
\n 1st coil vs 2nd coil calculated DeltaP {delta_p:.2f} for \
|
||||
\n amplifier {type_variety} is not valid: revise inputs \
|
||||
\n calculated 1st coil NF = {nf1:.2f}, 2nd coil NF = {nf2:.2f}')
|
||||
# Check calculated values for nf1 and nf2
|
||||
calc_nf_min = lin2db(db2lin(nf1) + db2lin(nf2)/db2lin(g1a_max))
|
||||
if not isclose(nf_min, calc_nf_min, abs_tol=0.01):
|
||||
raise EquipmentConfigError(f'nf_min does not match calc_nf_min, {nf_min} vs {calc_nf_min} for amp {type_variety}')
|
||||
calc_nf_max = lin2db(db2lin(nf1) + db2lin(nf2)/db2lin(g1a_min))
|
||||
if not isclose(nf_max, calc_nf_max, abs_tol=0.01):
|
||||
raise EquipmentConfigError(f'nf_max does not match calc_nf_max, {nf_max} vs {calc_nf_max} for amp {type_variety}')
|
||||
|
||||
return nf1, nf2, delta_p
|
||||
|
||||
def edfa_nf(gain_target, variety_type, equipment):
|
||||
amp_params = equipment['Edfa'][variety_type]
|
||||
amp = Edfa(
|
||||
uid = f'calc_NF',
|
||||
params = amp_params.__dict__,
|
||||
operational = {
|
||||
'gain_target': gain_target,
|
||||
'tilt_target': 0
|
||||
}
|
||||
)
|
||||
amp.pin_db = 0
|
||||
amp.nch = 88
|
||||
return amp._calc_nf(True)
|
||||
|
||||
def trx_mode_params(equipment, trx_type_variety='', trx_mode='', error_message=False):
|
||||
"""return the trx and SI parameters from eqpt_config for a given type_variety and mode (ie format)"""
|
||||
@@ -271,38 +19,38 @@ def trx_mode_params(equipment, trx_type_variety='', trx_mode='', error_message=F
|
||||
|
||||
try:
|
||||
trxs = equipment['Transceiver']
|
||||
#if called from path_requests_run.py, trx_mode is filled with None when not specified by user
|
||||
#if called from transmission_main.py, trx_mode is ''
|
||||
# if called from path_requests_run.py, trx_mode is filled with None when not specified by user
|
||||
# if called from transmission_main.py, trx_mode is ''
|
||||
if trx_mode is not None:
|
||||
mode_params = next(mode for trx in trxs \
|
||||
if trx == trx_type_variety \
|
||||
for mode in trxs[trx].mode \
|
||||
if mode['format'] == trx_mode)
|
||||
mode_params = next(mode for trx in trxs
|
||||
if trx == trx_type_variety
|
||||
for mode in trxs[trx].mode
|
||||
if mode['format'] == trx_mode)
|
||||
trx_params = {**mode_params}
|
||||
# sanity check: spacing baudrate must be smaller than min spacing
|
||||
if trx_params['baud_rate'] > trx_params['min_spacing'] :
|
||||
raise EquipmentConfigError(f'Inconsistency in equipment library:\n Transpoder "{trx_type_variety}" mode "{trx_params["format"]}" '+\
|
||||
f'has baud rate: {trx_params["baud_rate"]*1e-9} GHz greater than min_spacing {trx_params["min_spacing"]*1e-9}.')
|
||||
if trx_params['baud_rate'] > trx_params['min_spacing']:
|
||||
raise EquipmentConfigError(f'Inconsistency in equipment library:\n Transpoder "{trx_type_variety}" mode "{trx_params["format"]}" ' +
|
||||
f'has baud rate {trx_params["baud_rate"]*1e-9} GHz greater than min_spacing {trx_params["min_spacing"]*1e-9}.')
|
||||
else:
|
||||
mode_params = {"format": "undetermined",
|
||||
"baud_rate": None,
|
||||
"OSNR": None,
|
||||
"bit_rate": None,
|
||||
"roll_off": None,
|
||||
"tx_osnr":None,
|
||||
"min_spacing":None,
|
||||
"cost":None}
|
||||
"tx_osnr": None,
|
||||
"min_spacing": None,
|
||||
"cost": None}
|
||||
trx_params = {**mode_params}
|
||||
trx_params['f_min'] = equipment['Transceiver'][trx_type_variety].frequency['min']
|
||||
trx_params['f_max'] = equipment['Transceiver'][trx_type_variety].frequency['max']
|
||||
|
||||
# TODO: novel automatic feature maybe unwanted if spacing is specified
|
||||
# trx_params['spacing'] = automatic_spacing(trx_params['baud_rate'])
|
||||
# trx_params['spacing'] = _automatic_spacing(trx_params['baud_rate'])
|
||||
# temp = trx_params['spacing']
|
||||
# print(f'spacing {temp}')
|
||||
except StopIteration :
|
||||
except StopIteration:
|
||||
if error_message:
|
||||
raise EquipmentConfigError(f'Computation stoped: could not find tsp : {trx_type_variety} with mode: {trx_mode} in eqpt library')
|
||||
raise EquipmentConfigError(f'Could not find transponder "{trx_type_variety}" with mode "{trx_mode}" in equipment library')
|
||||
else:
|
||||
# default transponder charcteristics
|
||||
# mainly used with transmission_main_example.py
|
||||
@@ -320,82 +68,6 @@ def trx_mode_params(equipment, trx_type_variety='', trx_mode='', error_message=F
|
||||
trx_params['nb_channel'] = nch
|
||||
print(f'There are {nch} channels propagating')
|
||||
|
||||
trx_params['power'] = db2lin(default_si_data.power_dbm)*1e-3
|
||||
trx_params['power'] = db2lin(default_si_data.power_dbm) * 1e-3
|
||||
|
||||
return trx_params
|
||||
|
||||
def automatic_spacing(baud_rate):
|
||||
"""return the min possible channel spacing for a given baud rate"""
|
||||
# TODO : this should parametrized in a cfg file
|
||||
# list of possible tuples [(max_baud_rate, spacing_for_this_baud_rate)]
|
||||
spacing_list = [(33e9, 37.5e9), (38e9, 50e9), (50e9, 62.5e9), (67e9, 75e9), (92e9, 100e9)]
|
||||
return min((s[1] for s in spacing_list if s[0] > baud_rate), default=baud_rate*1.2)
|
||||
|
||||
def automatic_nch(f_min, f_max, spacing):
|
||||
return int((f_max - f_min)//spacing)
|
||||
|
||||
def automatic_fmax(f_min, spacing, nch):
|
||||
return f_min + spacing * nch
|
||||
|
||||
def load_equipment(filename):
|
||||
json_data = load_json(filename)
|
||||
return equipment_from_json(json_data, filename)
|
||||
|
||||
def update_trx_osnr(equipment):
|
||||
"""add sys_margins to all Transceivers OSNR values"""
|
||||
for trx in equipment['Transceiver'].values():
|
||||
for m in trx.mode:
|
||||
m['OSNR'] = m['OSNR'] + equipment['SI']['default'].sys_margins
|
||||
return equipment
|
||||
|
||||
def update_dual_stage(equipment):
|
||||
edfa_dict = equipment['Edfa']
|
||||
for edfa in edfa_dict.values():
|
||||
if edfa.type_def == 'dual_stage':
|
||||
edfa_preamp = edfa_dict[edfa.dual_stage_model.preamp_variety]
|
||||
edfa_booster = edfa_dict[edfa.dual_stage_model.booster_variety]
|
||||
for key, value in edfa_preamp.__dict__.items():
|
||||
attr_k = 'preamp_' + key
|
||||
setattr(edfa, attr_k, value)
|
||||
for key, value in edfa_booster.__dict__.items():
|
||||
attr_k = 'booster_' + key
|
||||
setattr(edfa, attr_k, value)
|
||||
edfa.p_max = edfa_booster.p_max
|
||||
edfa.gain_flatmax = edfa_booster.gain_flatmax + edfa_preamp.gain_flatmax
|
||||
if edfa.gain_min < edfa_preamp.gain_min:
|
||||
raise EquipmentConfigError(f'Dual stage {edfa.type_variety} min gain is lower than its preamp min gain')
|
||||
return equipment
|
||||
|
||||
def roadm_restrictions_sanity_check(equipment):
|
||||
""" verifies that booster and preamp restrictions specified in roadm equipment are listed
|
||||
in the edfa.
|
||||
"""
|
||||
restrictions = equipment['Roadm']['default'].restrictions['booster_variety_list'] + \
|
||||
equipment['Roadm']['default'].restrictions['preamp_variety_list']
|
||||
for amp_name in restrictions:
|
||||
if amp_name not in equipment['Edfa']:
|
||||
raise EquipmentConfigError(f'ROADM restriction {amp_name} does not refer to a defined EDFA name')
|
||||
|
||||
def equipment_from_json(json_data, filename):
|
||||
"""build global dictionnary eqpt_library that stores all eqpt characteristics:
|
||||
edfa type type_variety, fiber type_variety
|
||||
from the eqpt_config.json (filename parameter)
|
||||
also read advanced_config_from_json file parameters for edfa if they are available:
|
||||
typically nf_ripple, dfg gain ripple, dgt and nf polynomial nf_fit_coeff
|
||||
if advanced_config_from_json file parameter is not present: use nf_model:
|
||||
requires nf_min and nf_max values boundaries of the edfa gain range
|
||||
"""
|
||||
equipment = {}
|
||||
for key, entries in json_data.items():
|
||||
equipment[key] = {}
|
||||
typ = globals()[key]
|
||||
for entry in entries:
|
||||
subkey = entry.get('type_variety', 'default')
|
||||
if key == 'Edfa':
|
||||
equipment[key][subkey] = Amp.from_json(filename, **entry)
|
||||
else:
|
||||
equipment[key][subkey] = typ(**entry)
|
||||
equipment = update_trx_osnr(equipment)
|
||||
equipment = update_dual_stage(equipment)
|
||||
roadm_restrictions_sanity_check(equipment)
|
||||
return equipment
|
||||
|
||||
@@ -12,8 +12,26 @@ Exceptions thrown by other gnpy modules
|
||||
class ConfigurationError(Exception):
|
||||
'''User-provided configuration contains an error'''
|
||||
|
||||
|
||||
class EquipmentConfigError(ConfigurationError):
|
||||
'''Incomplete or wrong configuration within the equipment library'''
|
||||
|
||||
|
||||
class NetworkTopologyError(ConfigurationError):
|
||||
'''Topology of user-provided network is wrong'''
|
||||
|
||||
|
||||
class ServiceError(Exception):
|
||||
'''Service of user-provided request is wrong'''
|
||||
|
||||
|
||||
class DisjunctionError(ServiceError):
|
||||
'''Disjunction of user-provided request can not be satisfied'''
|
||||
|
||||
|
||||
class SpectrumError(Exception):
|
||||
'''Spectrum errors of the program'''
|
||||
|
||||
|
||||
class ParametersError(ConfigurationError):
|
||||
'''Incomplete or wrong configurations within parameters json'''
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
'''
|
||||
gnpy.core.execute
|
||||
=================
|
||||
|
||||
This module contains functions for executing the propogation of
|
||||
spectral information on a `gnpy` network.
|
||||
'''
|
||||
@@ -10,22 +10,28 @@ This module contains classes for modelling :class:`SpectralInformation`.
|
||||
|
||||
|
||||
from collections import namedtuple
|
||||
from numpy import array
|
||||
from gnpy.core.utils import lin2db, db2lin
|
||||
from json import loads
|
||||
from gnpy.core.utils import load_json
|
||||
from gnpy.core.equipment import automatic_nch, automatic_spacing
|
||||
from gnpy.core.utils import automatic_nch, lin2db
|
||||
|
||||
|
||||
class Power(namedtuple('Power', 'signal nli ase')):
|
||||
"""carriers power in W"""
|
||||
|
||||
|
||||
class Channel(namedtuple('Channel', 'channel_number frequency baud_rate roll_off power')):
|
||||
pass
|
||||
class Channel(namedtuple('Channel', 'channel_number frequency baud_rate roll_off power chromatic_dispersion pmd')):
|
||||
""" Class containing the parameters of a WDM signal.
|
||||
|
||||
:param channel_number: channel number in the WDM grid
|
||||
:param frequency: central frequency of the signal (Hz)
|
||||
:param baud_rate: the symbol rate of the signal (Baud)
|
||||
:param roll_off: the roll off of the signal. It is a pure number between 0 and 1
|
||||
:param power (gnpy.core.info.Power): power of signal, ASE noise and NLI (W)
|
||||
:param chromatic_dispersion: chromatic dispersion (s/m)
|
||||
:param pmd: polarization mode dispersion (s)
|
||||
"""
|
||||
|
||||
|
||||
class Pref(namedtuple('Pref', 'p_span0, p_spani, neq_ch ')):
|
||||
"""noiseless reference power in dBm:
|
||||
"""noiseless reference power in dBm:
|
||||
p_span0: inital target carrier power
|
||||
p_spani: carrier power after element i
|
||||
neq_ch: equivalent channel count in dB"""
|
||||
@@ -44,29 +50,8 @@ def create_input_spectral_information(f_min, f_max, roll_off, baud_rate, power,
|
||||
si = SpectralInformation(
|
||||
pref=Pref(pref, pref, lin2db(nb_channel)),
|
||||
carriers=[
|
||||
Channel(f, (f_min+spacing*f),
|
||||
baud_rate, roll_off, Power(power, 0, 0)) for f in range(1,nb_channel+1)
|
||||
])
|
||||
return si
|
||||
|
||||
if __name__ == '__main__':
|
||||
pref = lin2db(power * 1e3)
|
||||
si = SpectralInformation(
|
||||
Pref(pref, pref),
|
||||
Channel(1, 193.95e12, 32e9, 0.15, # 193.95 THz, 32 Gbaud
|
||||
Power(1e-3, 1e-6, 1e-6)), # 1 mW, 1uW, 1uW
|
||||
Channel(1, 195.95e12, 32e9, 0.15, # 195.95 THz, 32 Gbaud
|
||||
Power(1.2e-3, 1e-6, 1e-6)), # 1.2 mW, 1uW, 1uW
|
||||
Channel(f, (f_min + spacing * f),
|
||||
baud_rate, roll_off, Power(power, 0, 0), 0, 0) for f in range(1, nb_channel + 1)
|
||||
]
|
||||
)
|
||||
|
||||
si = SpectralInformation()
|
||||
spacing = 0.05 # THz
|
||||
|
||||
si = si._replace(carriers=tuple(Channel(f+1, 191.3+spacing*(f+1), 32e9, 0.15, Power(1e-3, f, 1)) for f in range(96)))
|
||||
|
||||
print(f'si = {si}')
|
||||
print(f'si = {si.carriers[0].power.nli}')
|
||||
print(f'si = {si.carriers[20].power.nli}')
|
||||
si2 = si._replace(carriers=tuple(c._replace(power = c.power._replace(nli = c.power.nli * 1e5))
|
||||
for c in si.carriers))
|
||||
print(f'si2 = {si2}')
|
||||
return si
|
||||
|
||||
@@ -5,92 +5,31 @@
|
||||
gnpy.core.network
|
||||
=================
|
||||
|
||||
This module contains functions for constructing networks of network elements.
|
||||
Working with networks which consist of network elements
|
||||
'''
|
||||
|
||||
from gnpy.core.convert import convert_file
|
||||
from networkx import DiGraph
|
||||
from numpy import arange
|
||||
from scipy.interpolate import interp1d
|
||||
from logging import getLogger
|
||||
from os import path
|
||||
from operator import itemgetter, attrgetter
|
||||
from gnpy.core import elements
|
||||
from gnpy.core.elements import Fiber, Edfa, Transceiver, Roadm, Fused, RamanFiber
|
||||
from gnpy.core.equipment import edfa_nf
|
||||
from operator import attrgetter
|
||||
from gnpy.core import ansi_escapes, elements
|
||||
from gnpy.core.exceptions import ConfigurationError, NetworkTopologyError
|
||||
from gnpy.core.units import UNITS
|
||||
from gnpy.core.utils import (load_json, save_json, round2float, db2lin,
|
||||
merge_amplifier_restrictions)
|
||||
from gnpy.core.science_utils import SimParams
|
||||
from gnpy.core.utils import round2float, convert_length
|
||||
from collections import namedtuple
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
def load_network(filename, equipment, name_matching = False):
|
||||
json_filename = ''
|
||||
if filename.suffix.lower() == '.xls':
|
||||
logger.info('Automatically generating topology JSON file')
|
||||
json_filename = convert_file(filename, name_matching)
|
||||
elif filename.suffix.lower() == '.json':
|
||||
json_filename = filename
|
||||
else:
|
||||
raise ValueError(f'unsuported topology filename extension {filename.suffix.lower()}')
|
||||
json_data = load_json(json_filename)
|
||||
return network_from_json(json_data, equipment)
|
||||
|
||||
def save_network(filename, network):
|
||||
filename_output = path.splitext(filename)[0] + '_auto_design.json'
|
||||
json_data = network_to_json(network)
|
||||
save_json(json_data, filename_output)
|
||||
|
||||
def network_from_json(json_data, equipment):
|
||||
# NOTE|dutc: we could use the following, but it would tie our data format
|
||||
# too closely to the graph library
|
||||
# from networkx import node_link_graph
|
||||
g = DiGraph()
|
||||
for el_config in json_data['elements']:
|
||||
typ = el_config.pop('type')
|
||||
variety = el_config.pop('type_variety', 'default')
|
||||
if typ in equipment and variety in equipment[typ]:
|
||||
extra_params = equipment[typ][variety]
|
||||
temp = el_config.setdefault('params', {})
|
||||
temp = merge_amplifier_restrictions(temp, extra_params.__dict__)
|
||||
el_config['params'] = temp
|
||||
elif typ in ['Edfa', 'Fiber']: # catch it now because the code will crash later!
|
||||
raise ConfigurationError(f'The {typ} of variety type {variety} was not recognized:'
|
||||
'\nplease check it is properly defined in the eqpt_config json file')
|
||||
cls = getattr(elements, typ)
|
||||
el = cls(**el_config)
|
||||
g.add_node(el)
|
||||
|
||||
nodes = {k.uid: k for k in g.nodes()}
|
||||
|
||||
for cx in json_data['connections']:
|
||||
from_node, to_node = cx['from_node'], cx['to_node']
|
||||
try:
|
||||
if isinstance(nodes[from_node], Fiber):
|
||||
edge_length = nodes[from_node].params.length
|
||||
else:
|
||||
edge_length = 0.01
|
||||
g.add_edge(nodes[from_node], nodes[to_node], weight = edge_length)
|
||||
except KeyError:
|
||||
raise NetworkTopologyError(f'can not find {from_node} or {to_node} defined in {cx}')
|
||||
|
||||
return g
|
||||
|
||||
def network_to_json(network):
|
||||
data = {
|
||||
'elements': [n.to_json for n in network]
|
||||
def edfa_nf(gain_target, variety_type, equipment):
|
||||
amp_params = equipment['Edfa'][variety_type]
|
||||
amp = elements.Edfa(
|
||||
uid='calc_NF',
|
||||
params=amp_params.__dict__,
|
||||
operational={
|
||||
'gain_target': gain_target,
|
||||
'tilt_target': 0
|
||||
}
|
||||
connections = {
|
||||
'connections': [{"from_node": n.uid,
|
||||
"to_node": next_n.uid}
|
||||
for n in network
|
||||
for next_n in network.successors(n) if next_n is not None]
|
||||
}
|
||||
data.update(connections)
|
||||
return data
|
||||
)
|
||||
amp.pin_db = 0
|
||||
amp.nch = 88
|
||||
return amp._calc_nf(True)
|
||||
|
||||
|
||||
def select_edfa(raman_allowed, gain_target, power_target, equipment, uid, restrictions=None):
|
||||
"""amplifer selection algorithm
|
||||
@@ -103,7 +42,7 @@ def select_edfa(raman_allowed, gain_target, power_target, equipment, uid, restri
|
||||
# because main use case is to have specific radm amp which are not allowed for ILA
|
||||
# with the auto design
|
||||
edfa_dict = {name: amp for (name, amp) in equipment['Edfa'].items()
|
||||
if restrictions is None or name in restrictions}
|
||||
if restrictions is None or name in restrictions}
|
||||
|
||||
pin = power_target - gain_target
|
||||
|
||||
@@ -114,51 +53,49 @@ def select_edfa(raman_allowed, gain_target, power_target, equipment, uid, restri
|
||||
# extended gain max allowance TARGET_EXTENDED_GAIN is coming from eqpt_config.json
|
||||
# power attribut include power AND gain limitations
|
||||
edfa_list = [Edfa_list(
|
||||
variety=edfa_variety,
|
||||
power=min(
|
||||
pin
|
||||
+edfa.gain_flatmax
|
||||
+TARGET_EXTENDED_GAIN,
|
||||
edfa.p_max
|
||||
)
|
||||
-power_target,
|
||||
gain_min=
|
||||
gain_target+3
|
||||
-edfa.gain_min,
|
||||
nf=edfa_nf(gain_target, edfa_variety, equipment)) \
|
||||
for edfa_variety, edfa in edfa_dict.items()
|
||||
if ((edfa.allowed_for_design or restrictions is not None) and not edfa.raman)]
|
||||
variety=edfa_variety,
|
||||
power=min(
|
||||
pin
|
||||
+ edfa.gain_flatmax
|
||||
+ TARGET_EXTENDED_GAIN,
|
||||
edfa.p_max
|
||||
)
|
||||
- power_target,
|
||||
gain_min=gain_target + 3
|
||||
- edfa.gain_min,
|
||||
nf=edfa_nf(gain_target, edfa_variety, equipment))
|
||||
for edfa_variety, edfa in edfa_dict.items()
|
||||
if ((edfa.allowed_for_design or restrictions is not None) and not edfa.raman)]
|
||||
|
||||
#consider a Raman list because of different gain_min requirement:
|
||||
#do not allow extended gain min for Raman
|
||||
# consider a Raman list because of different gain_min requirement:
|
||||
# do not allow extended gain min for Raman
|
||||
raman_list = [Edfa_list(
|
||||
variety=edfa_variety,
|
||||
power=min(
|
||||
pin
|
||||
+edfa.gain_flatmax
|
||||
+TARGET_EXTENDED_GAIN,
|
||||
edfa.p_max
|
||||
)
|
||||
-power_target,
|
||||
gain_min=
|
||||
gain_target
|
||||
-edfa.gain_min,
|
||||
nf=edfa_nf(gain_target, edfa_variety, equipment))
|
||||
for edfa_variety, edfa in edfa_dict.items()
|
||||
if (edfa.allowed_for_design and edfa.raman)] \
|
||||
if raman_allowed else []
|
||||
variety=edfa_variety,
|
||||
power=min(
|
||||
pin
|
||||
+ edfa.gain_flatmax
|
||||
+ TARGET_EXTENDED_GAIN,
|
||||
edfa.p_max
|
||||
)
|
||||
- power_target,
|
||||
gain_min=gain_target
|
||||
- edfa.gain_min,
|
||||
nf=edfa_nf(gain_target, edfa_variety, equipment))
|
||||
for edfa_variety, edfa in edfa_dict.items()
|
||||
if (edfa.allowed_for_design and edfa.raman)] \
|
||||
if raman_allowed else []
|
||||
|
||||
#merge raman and edfa lists
|
||||
# merge raman and edfa lists
|
||||
amp_list = edfa_list + raman_list
|
||||
|
||||
#filter on min gain limitation:
|
||||
acceptable_gain_min_list = [x for x in amp_list if x.gain_min>0]
|
||||
# filter on min gain limitation:
|
||||
acceptable_gain_min_list = [x for x in amp_list if x.gain_min > 0]
|
||||
|
||||
if len(acceptable_gain_min_list) < 1:
|
||||
#do not take this empty list into account for the rest of the code
|
||||
#but issue a warning to the user and do not consider Raman
|
||||
#Raman below min gain should not be allowed because i is meant to be a design requirement
|
||||
#and raman padding at the amplifier input is impossible!
|
||||
# do not take this empty list into account for the rest of the code
|
||||
# but issue a warning to the user and do not consider Raman
|
||||
# Raman below min gain should not be allowed because i is meant to be a design requirement
|
||||
# and raman padding at the amplifier input is impossible!
|
||||
|
||||
if len(edfa_list) < 1:
|
||||
raise ConfigurationError(f'auto_design could not find any amplifier \
|
||||
@@ -167,48 +104,42 @@ def select_edfa(raman_allowed, gain_target, power_target, equipment, uid, restri
|
||||
else:
|
||||
# TODO: convert to logging
|
||||
print(
|
||||
f'\x1b[1;31;40m'\
|
||||
+ f'WARNING: target gain in node {uid} is below all available amplifiers min gain: \
|
||||
amplifier input padding will be assumed, consider increase span fiber padding instead'\
|
||||
+ '\x1b[0m'
|
||||
)
|
||||
f'{ansi_escapes.red}WARNING:{ansi_escapes.reset} target gain in node {uid} is below all available amplifiers min gain: \
|
||||
amplifier input padding will be assumed, consider increase span fiber padding instead'
|
||||
)
|
||||
acceptable_gain_min_list = edfa_list
|
||||
|
||||
#filter on gain+power limitation:
|
||||
#this list checks both the gain and the power requirement
|
||||
#because of the way .power is calculated in the list
|
||||
acceptable_power_list = [x for x in acceptable_gain_min_list if x.power>0]
|
||||
# filter on gain+power limitation:
|
||||
# this list checks both the gain and the power requirement
|
||||
# because of the way .power is calculated in the list
|
||||
acceptable_power_list = [x for x in acceptable_gain_min_list if x.power > 0]
|
||||
if len(acceptable_power_list) < 1:
|
||||
#no amplifier satisfies the required power, so pick the highest power(s):
|
||||
# no amplifier satisfies the required power, so pick the highest power(s):
|
||||
power_max = max(acceptable_gain_min_list, key=attrgetter('power')).power
|
||||
#check and pick if other amplifiers may have a similar gain/power
|
||||
#allow a 0.3dB power range
|
||||
#this allows to chose an amplifier with a better NF subsequentely
|
||||
# check and pick if other amplifiers may have a similar gain/power
|
||||
# allow a 0.3dB power range
|
||||
# this allows to chose an amplifier with a better NF subsequentely
|
||||
acceptable_power_list = [x for x in acceptable_gain_min_list
|
||||
if x.power-power_max>-0.3]
|
||||
if x.power - power_max > -0.3]
|
||||
|
||||
|
||||
# gain and power requirements are resolved,
|
||||
# =>chose the amp with the best NF among the acceptable ones:
|
||||
selected_edfa = min(acceptable_power_list, key=attrgetter('nf')) #filter on NF
|
||||
#check what are the gain and power limitations of this amp
|
||||
power_reduction = round(min(selected_edfa.power, 0),2)
|
||||
selected_edfa = min(acceptable_power_list, key=attrgetter('nf')) # filter on NF
|
||||
# check what are the gain and power limitations of this amp
|
||||
power_reduction = round(min(selected_edfa.power, 0), 2)
|
||||
if power_reduction < -0.5:
|
||||
print(
|
||||
f'\x1b[1;31;40m'\
|
||||
+ f'WARNING: target gain and power in node {uid}\n \
|
||||
f'{ansi_escapes.red}WARNING:{ansi_escapes.reset} target gain and power in node {uid}\n \
|
||||
is beyond all available amplifiers capabilities and/or extended_gain_range:\n\
|
||||
a power reduction of {power_reduction} is applied\n'\
|
||||
+ '\x1b[0m'
|
||||
)
|
||||
|
||||
a power reduction of {power_reduction} is applied\n'
|
||||
)
|
||||
|
||||
return selected_edfa.variety, power_reduction
|
||||
|
||||
def target_power(network, node, equipment): #get_fiber_dp
|
||||
|
||||
def target_power(network, node, equipment): # get_fiber_dp
|
||||
SPAN_LOSS_REF = 20
|
||||
POWER_SLOPE = 0.3
|
||||
power_mode = equipment['Span']['default'].power_mode
|
||||
dp_range = list(equipment['Span']['default'].delta_power_range_db)
|
||||
node_loss = span_loss(network, node)
|
||||
|
||||
@@ -218,13 +149,14 @@ def target_power(network, node, equipment): #get_fiber_dp
|
||||
dp = min(dp_range[1], dp)
|
||||
except KeyError:
|
||||
raise ConfigurationError(f'invalid delta_power_range_db definition in eqpt_config[Span]'
|
||||
f'delta_power_range_db: [lower_bound, upper_bound, step]')
|
||||
f'delta_power_range_db: [lower_bound, upper_bound, step]')
|
||||
|
||||
if isinstance(node, Roadm):
|
||||
if isinstance(node, elements.Roadm):
|
||||
dp = 0
|
||||
|
||||
return dp
|
||||
|
||||
|
||||
def prev_node_generator(network, node):
|
||||
"""fused spans interest:
|
||||
iterate over all predecessors while they are Fused or Fiber type"""
|
||||
@@ -233,12 +165,13 @@ def prev_node_generator(network, node):
|
||||
except StopIteration:
|
||||
raise NetworkTopologyError(f'Node {node.uid} is not properly connected, please check network topology')
|
||||
# yield and re-iterate
|
||||
if isinstance(prev_node, Fused) or isinstance(node, Fused):
|
||||
if isinstance(prev_node, elements.Fused) or isinstance(node, elements.Fused):
|
||||
yield prev_node
|
||||
yield from prev_node_generator(network, prev_node)
|
||||
else:
|
||||
StopIteration
|
||||
|
||||
|
||||
def next_node_generator(network, node):
|
||||
"""fused spans interest:
|
||||
iterate over all successors while they are Fused or Fiber type"""
|
||||
@@ -247,30 +180,32 @@ def next_node_generator(network, node):
|
||||
except StopIteration:
|
||||
raise NetworkTopologyError('Node {node.uid} is not properly connected, please check network topology')
|
||||
# yield and re-iterate
|
||||
if isinstance(next_node, Fused) or isinstance(node, Fused):
|
||||
if isinstance(next_node, elements.Fused) or isinstance(node, elements.Fused):
|
||||
yield next_node
|
||||
yield from next_node_generator(network, next_node)
|
||||
else:
|
||||
StopIteration
|
||||
|
||||
|
||||
def span_loss(network, node):
|
||||
"""Fused span interest:
|
||||
return the total span loss of all the fibers spliced by a Fused node"""
|
||||
loss = node.loss if node.passive else 0
|
||||
try:
|
||||
prev_node = next(n for n in network.predecessors(node))
|
||||
if isinstance(prev_node, Fused):
|
||||
if isinstance(prev_node, elements.Fused):
|
||||
loss += sum(n.loss for n in prev_node_generator(network, node))
|
||||
except StopIteration:
|
||||
pass
|
||||
try:
|
||||
next_node = next(n for n in network.successors(node))
|
||||
if isinstance(next_node, Fused):
|
||||
if isinstance(next_node, elements.Fused):
|
||||
loss += sum(n.loss for n in next_node_generator(network, node))
|
||||
except StopIteration:
|
||||
pass
|
||||
return loss
|
||||
|
||||
|
||||
def find_first_node(network, node):
|
||||
"""Fused node interest:
|
||||
returns the 1st node at the origin of a succession of fused nodes
|
||||
@@ -280,6 +215,7 @@ def find_first_node(network, node):
|
||||
pass
|
||||
return this_node
|
||||
|
||||
|
||||
def find_last_node(network, node):
|
||||
"""Fused node interest:
|
||||
returns the last node in a succession of fused nodes
|
||||
@@ -289,29 +225,30 @@ def find_last_node(network, node):
|
||||
pass
|
||||
return this_node
|
||||
|
||||
|
||||
def set_amplifier_voa(amp, power_target, power_mode):
|
||||
VOA_MARGIN = 1 #do not maximize the VOA optimization
|
||||
VOA_MARGIN = 1 # do not maximize the VOA optimization
|
||||
if amp.out_voa is None:
|
||||
if power_mode:
|
||||
gain_target = amp.effective_gain
|
||||
voa = min(amp.params.p_max-power_target,
|
||||
amp.params.gain_flatmax-amp.effective_gain)
|
||||
voa = min(amp.params.p_max - power_target,
|
||||
amp.params.gain_flatmax - amp.effective_gain)
|
||||
voa = max(round2float(max(voa, 0), 0.5) - VOA_MARGIN, 0) if amp.params.out_voa_auto else 0
|
||||
amp.delta_p = amp.delta_p + voa
|
||||
amp.effective_gain = amp.effective_gain + voa
|
||||
else:
|
||||
voa = 0 # no output voa optimization in gain mode
|
||||
voa = 0 # no output voa optimization in gain mode
|
||||
amp.out_voa = voa
|
||||
|
||||
|
||||
def set_egress_amplifier(network, roadm, equipment, pref_total_db):
|
||||
power_mode = equipment['Span']['default'].power_mode
|
||||
next_oms = (n for n in network.successors(roadm) if not isinstance(n, Transceiver))
|
||||
next_oms = (n for n in network.successors(roadm) if not isinstance(n, elements.Transceiver))
|
||||
for oms in next_oms:
|
||||
#go through all the OMS departing from the Roadm
|
||||
# go through all the OMS departing from the Roadm
|
||||
node = roadm
|
||||
prev_node = roadm
|
||||
next_node = oms
|
||||
# if isinstance(next_node, Fused): #support ROADM wo egress amp for metro applications
|
||||
# if isinstance(next_node, elements.Fused): #support ROADM wo egress amp for metro applications
|
||||
# node = find_last_node(next_node)
|
||||
# next_node = next(n for n in network.successors(node))
|
||||
# next_node = find_last_node(next_node)
|
||||
@@ -320,8 +257,8 @@ def set_egress_amplifier(network, roadm, equipment, pref_total_db):
|
||||
prev_voa = 0
|
||||
voa = 0
|
||||
while True:
|
||||
#go through all nodes in the OMS (loop until next Roadm instance)
|
||||
if isinstance(node, Edfa):
|
||||
# go through all nodes in the OMS (loop until next Roadm instance)
|
||||
if isinstance(node, elements.Edfa):
|
||||
node_loss = span_loss(network, prev_node)
|
||||
voa = node.out_voa if node.out_voa else 0
|
||||
if node.delta_p is None:
|
||||
@@ -331,25 +268,25 @@ def set_egress_amplifier(network, roadm, equipment, pref_total_db):
|
||||
gain_from_dp = node_loss + dp - prev_dp + prev_voa
|
||||
if node.effective_gain is None or power_mode:
|
||||
gain_target = gain_from_dp
|
||||
else: #gain mode with effective_gain
|
||||
else: # gain mode with effective_gain
|
||||
gain_target = node.effective_gain
|
||||
dp = prev_dp - node_loss + gain_target
|
||||
|
||||
power_target = pref_total_db + dp
|
||||
power_target = pref_total_db + dp
|
||||
|
||||
raman_allowed = False
|
||||
if isinstance(prev_node, Fiber):
|
||||
if isinstance(prev_node, elements.Fiber):
|
||||
max_fiber_lineic_loss_for_raman = \
|
||||
equipment['Span']['default'].max_fiber_lineic_loss_for_raman
|
||||
equipment['Span']['default'].max_fiber_lineic_loss_for_raman
|
||||
raman_allowed = prev_node.params.loss_coef < max_fiber_lineic_loss_for_raman
|
||||
|
||||
# implementation of restrictions on roadm boosters
|
||||
if isinstance(prev_node,Roadm):
|
||||
if isinstance(prev_node, elements.Roadm):
|
||||
if prev_node.restrictions['booster_variety_list']:
|
||||
restrictions = prev_node.restrictions['booster_variety_list']
|
||||
else:
|
||||
restrictions = None
|
||||
elif isinstance(next_node,Roadm):
|
||||
elif isinstance(next_node, elements.Roadm):
|
||||
# implementation of restrictions on roadm preamp
|
||||
if next_node.restrictions['preamp_variety_list']:
|
||||
restrictions = next_node.restrictions['preamp_variety_list']
|
||||
@@ -358,25 +295,19 @@ def set_egress_amplifier(network, roadm, equipment, pref_total_db):
|
||||
else:
|
||||
restrictions = None
|
||||
|
||||
if node.params.type_variety == '':
|
||||
edfa_variety, power_reduction = select_edfa(raman_allowed,
|
||||
gain_target, power_target, equipment, node.uid, restrictions)
|
||||
if node.params.type_variety == '':
|
||||
edfa_variety, power_reduction = select_edfa(raman_allowed, gain_target, power_target, equipment, node.uid, restrictions)
|
||||
extra_params = equipment['Edfa'][edfa_variety]
|
||||
node.params.update_params(extra_params.__dict__)
|
||||
dp += power_reduction
|
||||
gain_target += power_reduction
|
||||
elif node.params.raman and not raman_allowed:
|
||||
print(
|
||||
f'\x1b[1;31;40m'\
|
||||
+ f'WARNING: raman is used in node {node.uid}\n \
|
||||
but fiber lineic loss is above threshold\n'\
|
||||
+ '\x1b[0m'
|
||||
)
|
||||
|
||||
print(f'{ansi_escapes.red}WARNING{ansi_escapes.reset}: raman is used in node {node.uid}\n but fiber lineic loss is above threshold\n')
|
||||
|
||||
node.delta_p = dp if power_mode else None
|
||||
node.effective_gain = gain_target
|
||||
set_amplifier_voa(node, power_target, power_mode)
|
||||
if isinstance(next_node, Roadm) or isinstance(next_node, Transceiver):
|
||||
if isinstance(next_node, elements.Roadm) or isinstance(next_node, elements.Transceiver):
|
||||
break
|
||||
prev_dp = dp
|
||||
prev_voa = voa
|
||||
@@ -388,32 +319,32 @@ def set_egress_amplifier(network, roadm, equipment, pref_total_db):
|
||||
|
||||
def add_egress_amplifier(network, node):
|
||||
next_nodes = [n for n in network.successors(node)
|
||||
if not (isinstance(n, Transceiver) or isinstance(n, Fused) or isinstance(n, Edfa))]
|
||||
#no amplification for fused spans or TRX
|
||||
if not (isinstance(n, elements.Transceiver) or isinstance(n, elements.Fused) or isinstance(n, elements.Edfa))]
|
||||
# no amplification for fused spans or TRX
|
||||
for i, next_node in enumerate(next_nodes):
|
||||
network.remove_edge(node, next_node)
|
||||
amp = Edfa(
|
||||
uid = f'Edfa{i}_{node.uid}',
|
||||
params = {},
|
||||
metadata = {
|
||||
'location': {
|
||||
'latitude': (node.lat * 2 + next_node.lat * 2) / 4,
|
||||
'longitude': (node.lng * 2 + next_node.lng * 2) / 4,
|
||||
'city': node.loc.city,
|
||||
'region': node.loc.region,
|
||||
}
|
||||
},
|
||||
operational = {
|
||||
'gain_target': None,
|
||||
'tilt_target': 0,
|
||||
})
|
||||
amp = elements.Edfa(
|
||||
uid=f'Edfa{i}_{node.uid}',
|
||||
params={},
|
||||
metadata={
|
||||
'location': {
|
||||
'latitude': (node.lat * 2 + next_node.lat * 2) / 4,
|
||||
'longitude': (node.lng * 2 + next_node.lng * 2) / 4,
|
||||
'city': node.loc.city,
|
||||
'region': node.loc.region,
|
||||
}
|
||||
},
|
||||
operational={
|
||||
'gain_target': None,
|
||||
'tilt_target': 0,
|
||||
})
|
||||
network.add_node(amp)
|
||||
if isinstance(node,Fiber):
|
||||
if isinstance(node, elements.Fiber):
|
||||
edgeweight = node.params.length
|
||||
else:
|
||||
edgeweight = 0.01
|
||||
network.add_edge(node, amp, weight = edgeweight)
|
||||
network.add_edge(amp, next_node, weight = 0.01)
|
||||
network.add_edge(node, amp, weight=edgeweight)
|
||||
network.add_edge(amp, next_node, weight=0.01)
|
||||
|
||||
|
||||
def calculate_new_length(fiber_length, bounds, target_length):
|
||||
@@ -422,17 +353,17 @@ def calculate_new_length(fiber_length, bounds, target_length):
|
||||
|
||||
n_spans = int(fiber_length // target_length)
|
||||
|
||||
length1 = fiber_length / (n_spans+1)
|
||||
delta1 = target_length-length1
|
||||
result1 = (length1, n_spans+1)
|
||||
length1 = fiber_length / (n_spans + 1)
|
||||
delta1 = target_length - length1
|
||||
result1 = (length1, n_spans + 1)
|
||||
|
||||
length2 = fiber_length / n_spans
|
||||
delta2 = length2-target_length
|
||||
delta2 = length2 - target_length
|
||||
result2 = (length2, n_spans)
|
||||
|
||||
if (bounds.start<=length1<=bounds.stop) and not(bounds.start<=length2<=bounds.stop):
|
||||
if (bounds.start <= length1 <= bounds.stop) and not(bounds.start <= length2 <= bounds.stop):
|
||||
result = result1
|
||||
elif (bounds.start<=length2<=bounds.stop) and not(bounds.start<=length1<=bounds.stop):
|
||||
elif (bounds.start <= length2 <= bounds.stop) and not(bounds.start <= length1 <= bounds.stop):
|
||||
result = result2
|
||||
else:
|
||||
result = result1 if delta1 < delta2 else result2
|
||||
@@ -441,7 +372,7 @@ def calculate_new_length(fiber_length, bounds, target_length):
|
||||
|
||||
|
||||
def split_fiber(network, fiber, bounds, target_length, equipment):
|
||||
new_length, n_spans = calculate_new_length(fiber.length, bounds, target_length)
|
||||
new_length, n_spans = calculate_new_length(fiber.params.length, bounds, target_length)
|
||||
if n_spans == 1:
|
||||
return
|
||||
|
||||
@@ -453,80 +384,83 @@ def split_fiber(network, fiber, bounds, target_length, equipment):
|
||||
|
||||
network.remove_node(fiber)
|
||||
|
||||
fiber_params = fiber.params._asdict()
|
||||
fiber_params['length'] = new_length / UNITS[fiber.params.length_units]
|
||||
fiber_params['con_in'] = fiber.con_in
|
||||
fiber_params['con_out'] = fiber.con_out
|
||||
fiber.params.length = new_length
|
||||
|
||||
f = interp1d([prev_node.lng, next_node.lng], [prev_node.lat, next_node.lat])
|
||||
xpos = [prev_node.lng + (next_node.lng - prev_node.lng) * (n+1)/(n_spans+1) for n in range(n_spans)]
|
||||
xpos = [prev_node.lng + (next_node.lng - prev_node.lng) * (n + 1) / (n_spans + 1) for n in range(n_spans)]
|
||||
ypos = f(xpos)
|
||||
for span, lng, lat in zip(range(n_spans), xpos, ypos):
|
||||
new_span = Fiber(uid = f'{fiber.uid}_({span+1}/{n_spans})',
|
||||
metadata = {
|
||||
'location': {
|
||||
'latitude': lat,
|
||||
'longitude': lng,
|
||||
'city': fiber.loc.city,
|
||||
'region': fiber.loc.region,
|
||||
}
|
||||
},
|
||||
params = fiber_params)
|
||||
if isinstance(prev_node,Fiber):
|
||||
new_span = elements.Fiber(uid=f'{fiber.uid}_({span+1}/{n_spans})',
|
||||
type_variety=fiber.type_variety,
|
||||
metadata={
|
||||
'location': {
|
||||
'latitude': lat,
|
||||
'longitude': lng,
|
||||
'city': fiber.loc.city,
|
||||
'region': fiber.loc.region,
|
||||
}
|
||||
},
|
||||
params=fiber.params.asdict())
|
||||
if isinstance(prev_node, elements.Fiber):
|
||||
edgeweight = prev_node.params.length
|
||||
else:
|
||||
edgeweight = 0.01
|
||||
network.add_edge(prev_node, new_span, weight = edgeweight)
|
||||
network.add_edge(prev_node, new_span, weight=edgeweight)
|
||||
prev_node = new_span
|
||||
if isinstance(prev_node,Fiber):
|
||||
if isinstance(prev_node, elements.Fiber):
|
||||
edgeweight = prev_node.params.length
|
||||
else:
|
||||
edgeweight = 0.01
|
||||
network.add_edge(prev_node, next_node, weight = edgeweight)
|
||||
edgeweight = 0.01
|
||||
network.add_edge(prev_node, next_node, weight=edgeweight)
|
||||
|
||||
|
||||
def add_connector_loss(network, fibers, default_con_in, default_con_out, EOL):
|
||||
for fiber in fibers:
|
||||
if fiber.con_in is None: fiber.con_in = default_con_in
|
||||
if fiber.con_out is None: fiber.con_out = default_con_out
|
||||
if fiber.params.con_in is None:
|
||||
fiber.params.con_in = default_con_in
|
||||
if fiber.params.con_out is None:
|
||||
fiber.params.con_out = default_con_out
|
||||
next_node = next(n for n in network.successors(fiber))
|
||||
if not isinstance(next_node, Fused):
|
||||
fiber.con_out += EOL
|
||||
if not isinstance(next_node, elements.Fused):
|
||||
fiber.params.con_out += EOL
|
||||
|
||||
|
||||
def add_fiber_padding(network, fibers, padding):
|
||||
"""last_fibers = (fiber for n in network.nodes()
|
||||
if not (isinstance(n, Fiber) or isinstance(n, Fused))
|
||||
if not (isinstance(n, elements.Fiber) or isinstance(n, elements.Fused))
|
||||
for fiber in network.predecessors(n)
|
||||
if isinstance(fiber, Fiber))"""
|
||||
if isinstance(fiber, elements.Fiber))"""
|
||||
for fiber in fibers:
|
||||
this_span_loss = span_loss(network, fiber)
|
||||
try:
|
||||
next_node = next(network.successors(fiber))
|
||||
except StopIteration:
|
||||
raise NetworkTopologyError(f'Fiber {fiber.uid} is not properly connected, please check network topology')
|
||||
if this_span_loss < padding and not (isinstance(next_node, Fused)):
|
||||
#add a padding att_in at the input of the 1st fiber:
|
||||
#address the case when several fibers are spliced together
|
||||
if this_span_loss < padding and not (isinstance(next_node, elements.Fused)):
|
||||
# add a padding att_in at the input of the 1st fiber:
|
||||
# address the case when several fibers are spliced together
|
||||
first_fiber = find_first_node(network, fiber)
|
||||
# in order to support no booster , fused might be placed
|
||||
# just after a roadm: need to check that first_fiber is really a fiber
|
||||
if isinstance(first_fiber,Fiber):
|
||||
if first_fiber.att_in is None:
|
||||
first_fiber.att_in = padding - this_span_loss
|
||||
if isinstance(first_fiber, elements.Fiber):
|
||||
if first_fiber.params.att_in is None:
|
||||
first_fiber.params.att_in = padding - this_span_loss
|
||||
else:
|
||||
first_fiber.att_in = first_fiber.att_in + padding - this_span_loss
|
||||
first_fiber.params.att_in = first_fiber.params.att_in + padding - this_span_loss
|
||||
|
||||
|
||||
def build_network(network, equipment, pref_ch_db, pref_total_db):
|
||||
default_span_data = equipment['Span']['default']
|
||||
max_length = int(default_span_data.max_length * UNITS[default_span_data.length_units])
|
||||
min_length = max(int(default_span_data.padding/0.2*1e3),50_000)
|
||||
max_length = int(convert_length(default_span_data.max_length, default_span_data.length_units))
|
||||
min_length = max(int(default_span_data.padding / 0.2 * 1e3), 50_000)
|
||||
bounds = range(min_length, max_length)
|
||||
target_length = max(min_length, 90_000)
|
||||
default_con_in = default_span_data.con_in
|
||||
default_con_out = default_span_data.con_out
|
||||
padding = default_span_data.padding
|
||||
|
||||
#set roadm loss for gain_mode before to build network
|
||||
fibers = [f for f in network.nodes() if isinstance(f, Fiber)]
|
||||
# set roadm loss for gain_mode before to build network
|
||||
fibers = [f for f in network.nodes() if isinstance(f, elements.Fiber)]
|
||||
add_connector_loss(network, fibers, default_con_in, default_con_out, default_span_data.EOL)
|
||||
add_fiber_padding(network, fibers, padding)
|
||||
# don't group split fiber and add amp in the same loop
|
||||
@@ -534,27 +468,17 @@ def build_network(network, equipment, pref_ch_db, pref_total_db):
|
||||
for fiber in fibers:
|
||||
split_fiber(network, fiber, bounds, target_length, equipment)
|
||||
|
||||
amplified_nodes = [n for n in network.nodes()
|
||||
if isinstance(n, Fiber) or isinstance(n, Roadm)]
|
||||
amplified_nodes = [n for n in network.nodes() if isinstance(n, elements.Fiber) or isinstance(n, elements.Roadm)]
|
||||
|
||||
for node in amplified_nodes:
|
||||
add_egress_amplifier(network, node)
|
||||
|
||||
roadms = [r for r in network.nodes() if isinstance(r, Roadm)]
|
||||
roadms = [r for r in network.nodes() if isinstance(r, elements.Roadm)]
|
||||
for roadm in roadms:
|
||||
set_egress_amplifier(network, roadm, equipment, pref_total_db)
|
||||
|
||||
#support older json input topology wo Roadms:
|
||||
# support older json input topology wo Roadms:
|
||||
if len(roadms) == 0:
|
||||
trx = [t for t in network.nodes() if isinstance(t, Transceiver)]
|
||||
trx = [t for t in network.nodes() if isinstance(t, elements.Transceiver)]
|
||||
for t in trx:
|
||||
set_egress_amplifier(network, t, equipment, pref_total_db)
|
||||
|
||||
def load_sim_params(filename):
|
||||
sim_params = load_json(filename)
|
||||
return SimParams(params=sim_params)
|
||||
|
||||
def configure_network(network, sim_params):
|
||||
for node in network.nodes:
|
||||
if isinstance(node, RamanFiber):
|
||||
node.sim_params = sim_params
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
#! /bin/usr/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
'''
|
||||
gnpy.core.node
|
||||
==============
|
||||
|
||||
This module contains the base class for a network element.
|
||||
|
||||
Strictly, a network element is any callable which accepts an immutable
|
||||
:class:`.info.SpectralInformation` object and returns an :class:`.info.SpectralInformation` object
|
||||
(a copy).
|
||||
|
||||
Network elements MUST implement two attributes .uid and .name representing a
|
||||
unique identifier and a printable name.
|
||||
|
||||
This base class provides a more convenient way to define a network element
|
||||
via subclassing.
|
||||
'''
|
||||
|
||||
from uuid import uuid4
|
||||
from collections import namedtuple
|
||||
|
||||
class Location(namedtuple('Location', 'latitude longitude city region')):
|
||||
def __new__(cls, latitude=0, longitude=0, city=None, region=None):
|
||||
return super().__new__(cls, latitude, longitude, city, region)
|
||||
|
||||
class Node:
|
||||
def __init__(self, uid, name=None, params=None, metadata=None, operational=None):
|
||||
if name is None:
|
||||
name = uid
|
||||
self.uid, self.name = uid, name
|
||||
if metadata is None:
|
||||
metadata = {'location': {}}
|
||||
if metadata and not isinstance(metadata.get('location'), Location):
|
||||
metadata['location'] = Location(**metadata.pop('location', {}))
|
||||
self.params, self.metadata, self.operational = params, metadata, operational
|
||||
|
||||
@property
|
||||
def coords(self):
|
||||
return self.lng, self.lat
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
return self.metadata['location']
|
||||
loc = location
|
||||
|
||||
@property
|
||||
def longitude(self):
|
||||
return self.location.longitude
|
||||
lng = longitude
|
||||
|
||||
@property
|
||||
def latitude(self):
|
||||
return self.location.latitude
|
||||
lat = latitude
|
||||
287
gnpy/core/parameters.py
Normal file
287
gnpy/core/parameters.py
Normal file
@@ -0,0 +1,287 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
gnpy.core.parameters
|
||||
====================
|
||||
|
||||
This module contains all parameters to configure standard network elements.
|
||||
|
||||
"""
|
||||
|
||||
from scipy.constants import c, pi
|
||||
from numpy import squeeze, log10, exp
|
||||
|
||||
|
||||
from gnpy.core.utils import db2lin, convert_length
|
||||
from gnpy.core.exceptions import ParametersError
|
||||
|
||||
|
||||
class Parameters:
|
||||
def asdict(self):
|
||||
class_dict = self.__class__.__dict__
|
||||
instance_dict = self.__dict__
|
||||
new_dict = {}
|
||||
for key in class_dict:
|
||||
if isinstance(class_dict[key], property):
|
||||
new_dict[key] = instance_dict['_' + key]
|
||||
return new_dict
|
||||
|
||||
|
||||
class PumpParams(Parameters):
|
||||
def __init__(self, power, frequency, propagation_direction):
|
||||
self._power = power
|
||||
self._frequency = frequency
|
||||
self._propagation_direction = propagation_direction
|
||||
|
||||
@property
|
||||
def power(self):
|
||||
return self._power
|
||||
|
||||
@property
|
||||
def frequency(self):
|
||||
return self._frequency
|
||||
|
||||
@property
|
||||
def propagation_direction(self):
|
||||
return self._propagation_direction
|
||||
|
||||
|
||||
class RamanParams(Parameters):
|
||||
def __init__(self, **kwargs):
|
||||
self._flag_raman = kwargs['flag_raman']
|
||||
self._space_resolution = kwargs['space_resolution'] if 'space_resolution' in kwargs else None
|
||||
self._tolerance = kwargs['tolerance'] if 'tolerance' in kwargs else None
|
||||
|
||||
@property
|
||||
def flag_raman(self):
|
||||
return self._flag_raman
|
||||
|
||||
@property
|
||||
def space_resolution(self):
|
||||
return self._space_resolution
|
||||
|
||||
@property
|
||||
def tolerance(self):
|
||||
return self._tolerance
|
||||
|
||||
|
||||
class NLIParams(Parameters):
|
||||
def __init__(self, **kwargs):
|
||||
self._nli_method_name = kwargs['nli_method_name']
|
||||
self._wdm_grid_size = kwargs['wdm_grid_size']
|
||||
self._dispersion_tolerance = kwargs['dispersion_tolerance']
|
||||
self._phase_shift_tolerance = kwargs['phase_shift_tolerance']
|
||||
self._f_cut_resolution = None
|
||||
self._f_pump_resolution = None
|
||||
self._computed_channels = kwargs['computed_channels'] if 'computed_channels' in kwargs else None
|
||||
|
||||
@property
|
||||
def nli_method_name(self):
|
||||
return self._nli_method_name
|
||||
|
||||
@property
|
||||
def wdm_grid_size(self):
|
||||
return self._wdm_grid_size
|
||||
|
||||
@property
|
||||
def dispersion_tolerance(self):
|
||||
return self._dispersion_tolerance
|
||||
|
||||
@property
|
||||
def phase_shift_tolerance(self):
|
||||
return self._phase_shift_tolerance
|
||||
|
||||
@property
|
||||
def f_cut_resolution(self):
|
||||
return self._f_cut_resolution
|
||||
|
||||
@f_cut_resolution.setter
|
||||
def f_cut_resolution(self, f_cut_resolution):
|
||||
self._f_cut_resolution = f_cut_resolution
|
||||
|
||||
@property
|
||||
def f_pump_resolution(self):
|
||||
return self._f_pump_resolution
|
||||
|
||||
@f_pump_resolution.setter
|
||||
def f_pump_resolution(self, f_pump_resolution):
|
||||
self._f_pump_resolution = f_pump_resolution
|
||||
|
||||
@property
|
||||
def computed_channels(self):
|
||||
return self._computed_channels
|
||||
|
||||
|
||||
class SimParams(Parameters):
|
||||
def __init__(self, **kwargs):
|
||||
try:
|
||||
if 'nli_parameters' in kwargs:
|
||||
self._nli_params = NLIParams(**kwargs['nli_parameters'])
|
||||
else:
|
||||
self._nli_params = None
|
||||
if 'raman_parameters' in kwargs:
|
||||
self._raman_params = RamanParams(**kwargs['raman_parameters'])
|
||||
else:
|
||||
self._raman_params = None
|
||||
except KeyError as e:
|
||||
raise ParametersError(f'Simulation parameters must include {e}. Configuration: {kwargs}')
|
||||
|
||||
@property
|
||||
def nli_params(self):
|
||||
return self._nli_params
|
||||
|
||||
@property
|
||||
def raman_params(self):
|
||||
return self._raman_params
|
||||
|
||||
|
||||
class FiberParams(Parameters):
|
||||
def __init__(self, **kwargs):
|
||||
try:
|
||||
self._length = convert_length(kwargs['length'], kwargs['length_units'])
|
||||
# fixed attenuator for padding
|
||||
self._att_in = kwargs['att_in'] if 'att_in' in kwargs else 0
|
||||
# if not defined in the network json connector loss in/out
|
||||
# the None value will be updated in network.py[build_network]
|
||||
# with default values from eqpt_config.json[Spans]
|
||||
self._con_in = kwargs['con_in'] if 'con_in' in kwargs else None
|
||||
self._con_out = kwargs['con_out'] if 'con_out' in kwargs else None
|
||||
if 'ref_wavelength' in kwargs:
|
||||
self._ref_wavelength = kwargs['ref_wavelength']
|
||||
self._ref_frequency = c / self.ref_wavelength
|
||||
elif 'ref_frequency' in kwargs:
|
||||
self._ref_frequency = kwargs['ref_frequency']
|
||||
self._ref_wavelength = c / self.ref_frequency
|
||||
else:
|
||||
self._ref_wavelength = 1550e-9
|
||||
self._ref_frequency = c / self.ref_wavelength
|
||||
self._dispersion = kwargs['dispersion'] # s/m/m
|
||||
self._dispersion_slope = kwargs['dispersion_slope'] if 'dispersion_slope' in kwargs else \
|
||||
-2 * self._dispersion/self.ref_wavelength # s/m/m/m
|
||||
self._beta2 = -(self.ref_wavelength ** 2) * self.dispersion / (2 * pi * c) # 1/(m * Hz^2)
|
||||
# Eq. (3.23) in Abramczyk, Halina. "Dispersion phenomena in optical fibers." Virtual European University
|
||||
# on Lasers. Available online: http://mitr.p.lodz.pl/evu/lectures/Abramczyk3.pdf
|
||||
# (accessed on 25 March 2018) (2005).
|
||||
self._beta3 = ((self.dispersion_slope - (4*pi*c/self.ref_wavelength**3) * self.beta2) /
|
||||
(2*pi*c/self.ref_wavelength**2)**2)
|
||||
self._gamma = kwargs['gamma'] # 1/W/m
|
||||
self._pmd_coef = kwargs['pmd_coef'] # s/sqrt(m)
|
||||
if type(kwargs['loss_coef']) == dict:
|
||||
self._loss_coef = squeeze(kwargs['loss_coef']['loss_coef_power']) * 1e-3 # lineic loss dB/m
|
||||
self._f_loss_ref = squeeze(kwargs['loss_coef']['frequency']) # Hz
|
||||
else:
|
||||
self._loss_coef = kwargs['loss_coef'] * 1e-3 # lineic loss dB/m
|
||||
self._f_loss_ref = 193.5e12 # Hz
|
||||
self._lin_attenuation = db2lin(self.length * self.loss_coef)
|
||||
self._lin_loss_exp = self.loss_coef / (10 * log10(exp(1))) # linear power exponent loss Neper/m
|
||||
self._effective_length = (1 - exp(- self.lin_loss_exp * self.length)) / self.lin_loss_exp
|
||||
self._asymptotic_length = 1 / self.lin_loss_exp
|
||||
# raman parameters (not compulsory)
|
||||
self._raman_efficiency = kwargs['raman_efficiency'] if 'raman_efficiency' in kwargs else None
|
||||
self._pumps_loss_coef = kwargs['pumps_loss_coef'] if 'pumps_loss_coef' in kwargs else None
|
||||
except KeyError as e:
|
||||
raise ParametersError(f'Fiber configurations json must include {e}. Configuration: {kwargs}')
|
||||
|
||||
@property
|
||||
def length(self):
|
||||
return self._length
|
||||
|
||||
@length.setter
|
||||
def length(self, length):
|
||||
"""length must be in m"""
|
||||
self._length = length
|
||||
|
||||
@property
|
||||
def att_in(self):
|
||||
return self._att_in
|
||||
|
||||
@att_in.setter
|
||||
def att_in(self, att_in):
|
||||
self._att_in = att_in
|
||||
|
||||
@property
|
||||
def con_in(self):
|
||||
return self._con_in
|
||||
|
||||
@con_in.setter
|
||||
def con_in(self, con_in):
|
||||
self._con_in = con_in
|
||||
|
||||
@property
|
||||
def con_out(self):
|
||||
return self._con_out
|
||||
|
||||
@con_out.setter
|
||||
def con_out(self, con_out):
|
||||
self._con_out = con_out
|
||||
|
||||
@property
|
||||
def dispersion(self):
|
||||
return self._dispersion
|
||||
|
||||
@property
|
||||
def dispersion_slope(self):
|
||||
return self._dispersion_slope
|
||||
|
||||
@property
|
||||
def gamma(self):
|
||||
return self._gamma
|
||||
|
||||
@property
|
||||
def pmd_coef(self):
|
||||
return self._pmd_coef
|
||||
|
||||
@property
|
||||
def ref_wavelength(self):
|
||||
return self._ref_wavelength
|
||||
|
||||
@property
|
||||
def ref_frequency(self):
|
||||
return self._ref_frequency
|
||||
|
||||
@property
|
||||
def beta2(self):
|
||||
return self._beta2
|
||||
|
||||
@property
|
||||
def beta3(self):
|
||||
return self._beta3
|
||||
|
||||
@property
|
||||
def loss_coef(self):
|
||||
return self._loss_coef
|
||||
|
||||
@property
|
||||
def f_loss_ref(self):
|
||||
return self._f_loss_ref
|
||||
|
||||
@property
|
||||
def lin_loss_exp(self):
|
||||
return self._lin_loss_exp
|
||||
|
||||
@property
|
||||
def lin_attenuation(self):
|
||||
return self._lin_attenuation
|
||||
|
||||
@property
|
||||
def effective_length(self):
|
||||
return self._effective_length
|
||||
|
||||
@property
|
||||
def asymptotic_length(self):
|
||||
return self._asymptotic_length
|
||||
|
||||
@property
|
||||
def raman_efficiency(self):
|
||||
return self._raman_efficiency
|
||||
|
||||
@property
|
||||
def pumps_loss_coef(self):
|
||||
return self._pumps_loss_coef
|
||||
|
||||
def asdict(self):
|
||||
dictionary = super().asdict()
|
||||
dictionary['loss_coef'] = self.loss_coef * 1e3
|
||||
dictionary['length_units'] = 'm'
|
||||
return dictionary
|
||||
@@ -1,892 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
gnpy.core.request
|
||||
=================
|
||||
|
||||
This module contains path request functionality.
|
||||
|
||||
This functionality allows the user to provide a JSON request
|
||||
file in accordance with a Yang model for requesting path
|
||||
computations and returns path results in terms of path
|
||||
and feasibility
|
||||
|
||||
See: draft-ietf-teas-yang-path-computation-01.txt
|
||||
"""
|
||||
|
||||
from sys import exit
|
||||
from collections import namedtuple
|
||||
from logging import getLogger, basicConfig, CRITICAL, DEBUG, INFO
|
||||
from networkx import (dijkstra_path, NetworkXNoPath, all_simple_paths,shortest_path_length)
|
||||
from networkx.utils import pairwise
|
||||
from numpy import mean
|
||||
from gnpy.core.service_sheet import convert_service_sheet, Request_element, Element
|
||||
from gnpy.core.elements import Transceiver, Roadm, Edfa, Fused
|
||||
from gnpy.core.utils import db2lin, lin2db
|
||||
from gnpy.core.info import create_input_spectral_information, SpectralInformation, Channel, Power
|
||||
from copy import copy, deepcopy
|
||||
from csv import writer
|
||||
from math import ceil
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
RequestParams = namedtuple('RequestParams','request_id source destination trx_type'+
|
||||
' trx_mode nodes_list loose_list spacing power nb_channel f_min f_max format baud_rate OSNR bit_rate roll_off tx_osnr min_spacing cost path_bandwidth')
|
||||
DisjunctionParams = namedtuple('DisjunctionParams','disjunction_id relaxable link_diverse node_diverse disjunctions_req')
|
||||
|
||||
class Path_request:
|
||||
def __init__(self, *args, **params):
|
||||
params = RequestParams(**params)
|
||||
self.request_id = params.request_id
|
||||
self.source = params.source
|
||||
self.destination = params.destination
|
||||
self.tsp = params.trx_type
|
||||
self.tsp_mode = params.trx_mode
|
||||
self.baud_rate = params.baud_rate
|
||||
self.nodes_list = params.nodes_list
|
||||
self.loose_list = params.loose_list
|
||||
self.spacing = params.spacing
|
||||
self.power = params.power
|
||||
self.nb_channel = params.nb_channel
|
||||
self.f_min = params.f_min
|
||||
self.f_max = params.f_max
|
||||
self.format = params.format
|
||||
self.OSNR = params.OSNR
|
||||
self.bit_rate = params.bit_rate
|
||||
self.roll_off = params.roll_off
|
||||
self.tx_osnr = params.tx_osnr
|
||||
self.min_spacing = params.min_spacing
|
||||
self.cost = params.cost
|
||||
self.path_bandwidth = params.path_bandwidth
|
||||
|
||||
def __str__(self):
|
||||
return '\n\t'.join([ f'{type(self).__name__} {self.request_id}',
|
||||
f'source: {self.source}',
|
||||
f'destination: {self.destination}'])
|
||||
def __repr__(self):
|
||||
if self.baud_rate is not None:
|
||||
temp = self.baud_rate * 1e-9
|
||||
temp2 = self.bit_rate * 1e-9
|
||||
else:
|
||||
temp = self.baud_rate
|
||||
temp2 = self.bit_rate
|
||||
|
||||
return '\n\t'.join([ f'{type(self).__name__} {self.request_id}',
|
||||
f'source: \t{self.source}',
|
||||
f'destination:\t{self.destination}',
|
||||
f'trx type:\t{self.tsp}',
|
||||
f'trx mode:\t{self.tsp_mode}',
|
||||
f'baud_rate:\t{temp} Gbaud',
|
||||
f'bit_rate:\t{temp2} Gb/s',
|
||||
f'spacing:\t{self.spacing * 1e-9} GHz',
|
||||
f'power: \t{round(lin2db(self.power)+30, 2)} dBm',
|
||||
f'nb channels: \t{self.nb_channel}',
|
||||
f'path_bandwidth: \t{round(self.path_bandwidth * 1e-9, 2)} Gbit/s',
|
||||
f'nodes-list:\t{self.nodes_list}',
|
||||
f'loose-list:\t{self.loose_list}'
|
||||
'\n'])
|
||||
class Disjunction:
|
||||
def __init__(self, *args, **params):
|
||||
params = DisjunctionParams(**params)
|
||||
self.disjunction_id = params.disjunction_id
|
||||
self.relaxable = params.relaxable
|
||||
self.link_diverse = params.link_diverse
|
||||
self.node_diverse = params.node_diverse
|
||||
self.disjunctions_req = params.disjunctions_req
|
||||
|
||||
def __str__(self):
|
||||
return '\n\t'.join([f'relaxable: {self.relaxable}',
|
||||
f'link-diverse: {self.link_diverse}',
|
||||
f'node-diverse: {self.node_diverse}',
|
||||
f'request-id-numbers: {self.disjunctions_req}']
|
||||
)
|
||||
def __repr__(self):
|
||||
return '\n\t'.join([ f'{type(self).__name__} {self.disjunction_id}',
|
||||
f'relaxable: {self.relaxable}',
|
||||
f'link-diverse: {self.link_diverse}',
|
||||
f'node-diverse: {self.node_diverse}',
|
||||
f'request-id-numbers: {self.disjunctions_req}'
|
||||
'\n'])
|
||||
|
||||
class Result_element(Element):
|
||||
def __init__(self,path_request,computed_path):
|
||||
self.path_id = path_request.request_id
|
||||
self.path_request = path_request
|
||||
self.computed_path = computed_path
|
||||
uid = property(lambda self: repr(self))
|
||||
@property
|
||||
def pathresult(self):
|
||||
if not self.computed_path:
|
||||
return {
|
||||
'response-id': self.path_id,
|
||||
'no-path': "Response without path information, due to failure performing the path computation"
|
||||
}
|
||||
else:
|
||||
index = 0
|
||||
pro_list = []
|
||||
for element in self.computed_path:
|
||||
temp = {
|
||||
'path-route-object': {
|
||||
'index': index,
|
||||
'num-unnum-hop': {
|
||||
'node-id': element.uid,
|
||||
'link-tp-id': element.uid,
|
||||
# TODO change index in order to insert transponder attribute
|
||||
}
|
||||
}
|
||||
}
|
||||
pro_list.append(temp)
|
||||
index += 1
|
||||
if isinstance(element, Transceiver):
|
||||
temp = {
|
||||
'path-route-object': {
|
||||
'index': index,
|
||||
'transponder' : {
|
||||
'transponder-type' : self.path_request.tsp,
|
||||
'transponder-mode' : self.path_request.tsp_mode,
|
||||
}
|
||||
}
|
||||
}
|
||||
pro_list.append(temp)
|
||||
index += 1
|
||||
|
||||
response = {
|
||||
'response-id': self.path_id,
|
||||
'path-properties':{
|
||||
'path-metric': [
|
||||
{
|
||||
'metric-type': 'SNR-bandwidth',
|
||||
'accumulative-value': round(mean(self.computed_path[-1].snr), 2)
|
||||
},
|
||||
{
|
||||
'metric-type': 'SNR-0.1nm',
|
||||
'accumulative-value': round(mean(self.computed_path[-1]. snr + \
|
||||
lin2db(self.path_request.baud_rate/12.5e9)), 2)
|
||||
},
|
||||
{
|
||||
'metric-type': 'OSNR-bandwidth',
|
||||
'accumulative-value': round(mean(self.computed_path[-1].osnr_ase), 2)
|
||||
},
|
||||
{
|
||||
'metric-type': 'OSNR-0.1nm',
|
||||
'accumulative-value': round(mean(self.computed_path[-1].osnr_ase_01nm), 2)
|
||||
},
|
||||
{
|
||||
'metric-type': 'reference_power',
|
||||
'accumulative-value': self.path_request.power
|
||||
},
|
||||
{
|
||||
'metric-type': 'path_bandwidth',
|
||||
'accumulative-value': self.path_request.path_bandwidth
|
||||
}
|
||||
],
|
||||
'path-route-objects': pro_list
|
||||
}
|
||||
}
|
||||
return response
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
return self.pathresult
|
||||
|
||||
def compute_constrained_path(network, req):
|
||||
trx = [n for n in network.nodes() if isinstance(n, Transceiver)]
|
||||
roadm = [n for n in network.nodes() if isinstance(n, Roadm)]
|
||||
edfa = [n for n in network.nodes() if isinstance(n, Edfa)]
|
||||
anytypenode = [n for n in network.nodes()]
|
||||
|
||||
source = next(el for el in trx if el.uid == req.source)
|
||||
|
||||
# This method ensures that the constraint can be satisfied without loops
|
||||
# except when it is not possible : eg if constraints makes a loop
|
||||
# It requires that the source, dest and nodes are correct (no error in the names)
|
||||
destination = next(el for el in trx if el.uid == req.destination)
|
||||
nodes_list = []
|
||||
for n in req.nodes_list :
|
||||
# for debug excel print(n)
|
||||
nodes_list.append(next(el for el in anytypenode if el.uid == n))
|
||||
# nodes_list contains at least the destination
|
||||
if nodes_list is None :
|
||||
msg = f'Request {req.request_id} problem in the constitution of nodes_list: should at least include destination'
|
||||
logger.critical(msg)
|
||||
exit()
|
||||
if req.nodes_list[-1] != req.destination:
|
||||
msg = f'Request {req.request_id} malformed list of nodes: last node should be destination trx'
|
||||
logger.critical(msg)
|
||||
exit()
|
||||
|
||||
if len(nodes_list) == 1 :
|
||||
try :
|
||||
total_path = dijkstra_path(network, source, destination, weight = 'weight')
|
||||
# print('checking edges length is correct')
|
||||
# print(shortest_path_length(network,source,destination))
|
||||
# print(shortest_path_length(network,source,destination,weight ='weight'))
|
||||
# s = total_path[0]
|
||||
# for e in total_path[1:]:
|
||||
# print(s.uid)
|
||||
# print(network.get_edge_data(s,e))
|
||||
# s = e
|
||||
except NetworkXNoPath:
|
||||
msg = f'\x1b[1;33;40m'+f'Request {req.request_id} could not find a path from {source.uid} to node : {destination.uid} in network topology'+ '\x1b[0m'
|
||||
logger.critical(msg)
|
||||
print(msg)
|
||||
total_path = []
|
||||
else :
|
||||
all_simp_pths = list(all_simple_paths(network,source=source,\
|
||||
target=destination, cutoff=120))
|
||||
candidate = []
|
||||
for p in all_simp_pths :
|
||||
if ispart(nodes_list, p) :
|
||||
# print(f'selection{[el.uid for el in p if el in roadm]}')
|
||||
candidate.append(p)
|
||||
# select the shortest path (in nb of hops) -> changed to shortest path in km length
|
||||
if len(candidate)>0 :
|
||||
# candidate.sort(key=lambda x: len(x))
|
||||
candidate.sort(key=lambda x: sum(network.get_edge_data(x[i],x[i+1])['weight'] for i in range(len(x)-2)))
|
||||
total_path = candidate[0]
|
||||
else:
|
||||
# TODO: better account for individual loose and strict node
|
||||
# to ease: suppose that one strict makes the whole liste strict (except for the
|
||||
# last node which is the transceiver)
|
||||
# if all nodes i n node_list are LOOSE constraint, skip the constraints and find
|
||||
# a path w/o constraints, else there is no possible path
|
||||
if nodes_list[:-len("STRICT")]:
|
||||
print(f'\x1b[1;33;40m'+f'Request {req.request_id} could not find a path crossing ' +\
|
||||
f'{[el.uid for el in nodes_list[:-len("STRICT")]]} in network topology'+ '\x1b[0m')
|
||||
else:
|
||||
print(f'\x1b[1;33;40m'+f'User include_node constraints could not be applied ' +\
|
||||
f'(invalid names specified)'+ '\x1b[0m')
|
||||
if 'STRICT' not in req.loose_list[:-len('STRICT')]:
|
||||
msg = f'\x1b[1;33;40m'+f'Request {req.request_id} could not find a path with user_' +\
|
||||
f'include node constraints' + '\x1b[0m'
|
||||
logger.info(msg)
|
||||
print(f'constraint ignored')
|
||||
total_path = dijkstra_path(network, source, destination, weight = 'weight')
|
||||
else:
|
||||
msg = f'\x1b[1;33;40m'+f'Request {req.request_id} could not find a path with user ' +\
|
||||
f'include node constraints.\nNo path computed'+ '\x1b[0m'
|
||||
logger.critical(msg)
|
||||
print(msg)
|
||||
total_path = []
|
||||
|
||||
# obsolete method: this does not guaranty to avoid loops or correct results
|
||||
# Here is the demonstration :
|
||||
# 1 1
|
||||
# eg a----b-----c
|
||||
# |1 |0.5 |1
|
||||
# e----f--h--g
|
||||
# 1 0.5 0.5
|
||||
# if I have to compute a to g with constraint f-c
|
||||
# result will be a concatenation of: a-b-f and f-b-c and c-g
|
||||
# which means a loop.
|
||||
# if to avoid loops I iteratively suppress edges of the segmenst in the topo
|
||||
# segment 1 = a-b-f
|
||||
# 1
|
||||
# eg a b-----c
|
||||
# |1 |1
|
||||
# e----f--h--g
|
||||
# 1 0.5 0.5
|
||||
# then
|
||||
# segment 2 = f-h-g-c
|
||||
# 1
|
||||
# eg a b-----c
|
||||
# |1
|
||||
# e----f h g
|
||||
# 1
|
||||
# then there is no more path to g destination
|
||||
#
|
||||
#
|
||||
# total_path = [source]
|
||||
|
||||
# for n in req.nodes_list:
|
||||
# try :
|
||||
# node = next(el for el in trx if el.uid == n)
|
||||
# except StopIteration:
|
||||
# try:
|
||||
# node = next(el for el in anytypenode if el.uid == n)
|
||||
# except StopIteration:
|
||||
# try:
|
||||
# # TODO this test is not giving good results: full name of the
|
||||
# # amp is required to avoid ambiguity on the direction
|
||||
# node = next(el for el in anytypenode
|
||||
# if n in el.uid)
|
||||
# except StopIteration:
|
||||
# msg = f'could not find node : {n} in network topology: \
|
||||
# not a trx, roadm, edfa, fiber or fused element'
|
||||
# logger.critical(msg)
|
||||
# raise ValueError(msg)
|
||||
# # extend path list without repeating source -> skip first element in the list
|
||||
# try:
|
||||
# # to avoid looping back: use an alternate graph were current path edges and vertex are suppressed
|
||||
|
||||
# total_path.extend(dijkstra_path(network, source, node)[1:])
|
||||
# source = node
|
||||
# except NetworkXNoPath:
|
||||
# if req.loose_list[req.nodes_list.index(n)] == 'loose':
|
||||
# print(f'could not find a path from {source.uid} to loose node : {n} in network topology')
|
||||
# print(f'node {n} is skipped')
|
||||
# else:
|
||||
# msg = f'could not find a path from {source.uid} to node : {n} in network topology'
|
||||
# logger.critical(msg)
|
||||
# print(msg)
|
||||
# total_path = []
|
||||
|
||||
return total_path
|
||||
|
||||
def propagate(path, req, equipment):
|
||||
si = create_input_spectral_information(
|
||||
req.f_min, req.f_max, req.roll_off, req.baud_rate,
|
||||
req.power, req.spacing)
|
||||
for el in path:
|
||||
si = el(si)
|
||||
path[-1].update_snr(req.tx_osnr, equipment['Roadm']['default'].add_drop_osnr)
|
||||
return path
|
||||
|
||||
def propagate2(path, req, equipment):
|
||||
si = create_input_spectral_information(
|
||||
req.f_min, req.f_max, req.roll_off, req.baud_rate,
|
||||
req.power, req.spacing)
|
||||
infos = {}
|
||||
for el in path:
|
||||
before_si = si
|
||||
after_si = si = el(si)
|
||||
infos[el] = before_si, after_si
|
||||
path[-1].update_snr(req.tx_osnr, equipment['Roadm']['default'].add_drop_osnr)
|
||||
return infos
|
||||
|
||||
def propagate_and_optimize_mode(path, req, equipment):
|
||||
# if mode is unknown : loops on the modes starting from the highest baudrate fiting in the
|
||||
# step 1: create an ordered list of modes based on baudrate
|
||||
baudrate_to_explore = list(set([m['baud_rate'] for m in equipment['Transceiver'][req.tsp].mode
|
||||
if float(m['min_spacing'])<= req.spacing]))
|
||||
# TODO be carefull on limits cases if spacing very close to req spacing eg 50.001 50.000
|
||||
baudrate_to_explore = sorted(baudrate_to_explore, reverse=True)
|
||||
if baudrate_to_explore :
|
||||
# at least 1 baudrate can be tested wrt spacing
|
||||
for b in baudrate_to_explore :
|
||||
modes_to_explore = [m for m in equipment['Transceiver'][req.tsp].mode
|
||||
if m['baud_rate'] == b and float(m['min_spacing'])<= req.spacing]
|
||||
modes_to_explore = sorted(modes_to_explore,
|
||||
key = lambda x: x['bit_rate'], reverse=True)
|
||||
# print(modes_to_explore)
|
||||
# step2 : computes propagation for each baudrate: stop and select the first that passes
|
||||
found_a_feasible_mode = False
|
||||
# TODO : the case of roll of is not included: for now use SI one
|
||||
# TODO : if the loop in mode optimization does not have a feasible path, then bugs
|
||||
si = create_input_spectral_information(
|
||||
req.f_min, req.f_max, equipment['SI']['default'].roll_off,
|
||||
b, req.power, req.spacing)
|
||||
for el in path:
|
||||
si = el(si)
|
||||
for m in modes_to_explore :
|
||||
if path[-1].snr is not None:
|
||||
path[-1].update_snr(m['tx_osnr'], equipment['Roadm']['default'].add_drop_osnr)
|
||||
if round(min(path[-1].snr+lin2db(b/(12.5e9))),2) > m['OSNR'] :
|
||||
found_a_feasible_mode = True
|
||||
return path, m
|
||||
else:
|
||||
return [], None
|
||||
# only get to this point if no baudrate/mode satisfies OSNR requirement
|
||||
|
||||
# returns the last propagated path and mode
|
||||
msg = f'\tWarning! Request {req.request_id}: no mode satisfies path SNR requirement.\n'
|
||||
print(msg)
|
||||
logger.info(msg)
|
||||
return [],None
|
||||
else :
|
||||
# no baudrate satisfying spacing
|
||||
msg = f'\tWarning! Request {req.request_id}: no baudrate satisfies spacing requirement.\n'
|
||||
print(msg)
|
||||
logger.info(msg)
|
||||
return [], None
|
||||
|
||||
|
||||
def jsontocsv(json_data,equipment,fileout):
|
||||
# read json path result file in accordance with:
|
||||
# Yang model for requesting Path Computation
|
||||
# draft-ietf-teas-yang-path-computation-01.txt.
|
||||
# and write results in an CSV file
|
||||
|
||||
mywriter = writer(fileout)
|
||||
mywriter.writerow(('response-id','source','destination','path_bandwidth','Pass?',\
|
||||
'nb of tsp pairs','total cost','transponder-type','transponder-mode',\
|
||||
'OSNR-0.1nm','SNR-0.1nm','SNR-bandwidth','baud rate (Gbaud)',\
|
||||
'input power (dBm)','path'))
|
||||
tspjsondata = equipment['Transceiver']
|
||||
#print(tspjsondata)
|
||||
|
||||
for pth_el in json_data['response']:
|
||||
path_id = pth_el['response-id']
|
||||
try:
|
||||
if pth_el['no-path'] :
|
||||
source = ''
|
||||
destination = ''
|
||||
tsp = ''
|
||||
mode = ''
|
||||
isok = False
|
||||
nb_tsp = 0
|
||||
pthbdbw = ''
|
||||
rosnr = ''
|
||||
rsnr = ''
|
||||
rsnrb = ''
|
||||
br = ''
|
||||
pw = ''
|
||||
total_cost = ''
|
||||
pth = ''
|
||||
except KeyError:
|
||||
|
||||
source = pth_el['path-properties']['path-route-objects'][0]\
|
||||
['path-route-object']['num-unnum-hop']['node-id']
|
||||
destination = pth_el['path-properties']['path-route-objects'][-2]\
|
||||
['path-route-object']['num-unnum-hop']['node-id']
|
||||
# selects only roadm nodes
|
||||
temp = []
|
||||
for e in pth_el['path-properties']['path-route-objects']:
|
||||
try :
|
||||
temp.append(e['path-route-object']['num-unnum-hop']['node-id'])
|
||||
except KeyError:
|
||||
pass
|
||||
pth = ' | '.join(temp)
|
||||
|
||||
temp_tsp = pth_el['path-properties']['path-route-objects'][1]\
|
||||
['path-route-object']['transponder']
|
||||
tsp = temp_tsp['transponder-type']
|
||||
mode = temp_tsp['transponder-mode']
|
||||
|
||||
# find the min acceptable OSNR, baud rate from the eqpt library based on tsp (tupe) and mode (format)
|
||||
# loading equipment already tests the existence of tsp type and mode:
|
||||
if mode !='not feasible with this transponder' :
|
||||
[minosnr, baud_rate, bit_rate, cost] = next([m['OSNR'] , m['baud_rate'] , m['bit_rate'], m['cost']]
|
||||
for m in equipment['Transceiver'][tsp].mode if m['format']==mode)
|
||||
# else:
|
||||
# [minosnr, baud_rate, bit_rate] = ['','','','']
|
||||
output_snr = next(e['accumulative-value']
|
||||
for e in pth_el['path-properties']['path-metric'] if e['metric-type'] == 'SNR-0.1nm')
|
||||
output_snrbandwidth = next(e['accumulative-value']
|
||||
for e in pth_el['path-properties']['path-metric'] if e['metric-type'] == 'SNR-bandwidth')
|
||||
output_osnr = next(e['accumulative-value']
|
||||
for e in pth_el['path-properties']['path-metric'] if e['metric-type'] == 'OSNR-0.1nm')
|
||||
output_osnrbandwidth = next(e['accumulative-value']
|
||||
for e in pth_el['path-properties']['path-metric'] if e['metric-type'] == 'OSNR-bandwidth')
|
||||
power = next(e['accumulative-value']
|
||||
for e in pth_el['path-properties']['path-metric'] if e['metric-type'] == 'reference_power')
|
||||
path_bandwidth = next(e['accumulative-value']
|
||||
for e in pth_el['path-properties']['path-metric'] if e['metric-type'] == 'path_bandwidth')
|
||||
if isinstance(output_snr, str):
|
||||
isok = False
|
||||
nb_tsp = 0
|
||||
pthbdbw = round(path_bandwidth*1e-9,2)
|
||||
rosnr = ''
|
||||
rsnr = ''
|
||||
rsnrb = ''
|
||||
br = ''
|
||||
pw = ''
|
||||
total_cost = ''
|
||||
else:
|
||||
isok = output_snr >= minosnr
|
||||
nb_tsp = ceil(path_bandwidth / bit_rate)
|
||||
pthbdbw = round(path_bandwidth*1e-9,2)
|
||||
rosnr = round(output_osnr,2)
|
||||
rsnr = round(output_snr,2)
|
||||
rsnrb = round(output_snrbandwidth,2)
|
||||
br = round(baud_rate*1e-9,2)
|
||||
pw = round(lin2db(power)+30,2)
|
||||
total_cost = nb_tsp * cost
|
||||
|
||||
mywriter.writerow((path_id,
|
||||
source,
|
||||
destination,
|
||||
pthbdbw,
|
||||
isok,
|
||||
nb_tsp,
|
||||
total_cost,
|
||||
tsp,
|
||||
mode,
|
||||
rosnr,
|
||||
rsnr,
|
||||
rsnrb,
|
||||
br,
|
||||
pw,
|
||||
pth
|
||||
))
|
||||
|
||||
|
||||
def compute_path_dsjctn(network, equipment, pathreqlist, disjunctions_list):
|
||||
# pathreqlist is a list of Path_request objects
|
||||
# disjunctions_list a list of Disjunction objects
|
||||
|
||||
# given a network, a list of requests with the set of disjunction features between
|
||||
# request, the function computes the set of path satisfying : first the disjunction
|
||||
# constraint and second the routing constraint if the request include an explicit
|
||||
# set of elements to pass through.
|
||||
# the algorithm used allows to specify disjunction for demands not sharing source or
|
||||
# destination.
|
||||
# a request might be declared as disjoint from several requests
|
||||
# it is a iterative process:
|
||||
# first computes a list of all shortest path (this may add computation time)
|
||||
# second elaborate the set of path solution for each synchronization vector
|
||||
# third select only the candidates that satisfy all synchronization vectors they belong to
|
||||
# fourth apply route constraints : remove candidate path that do not satisfy the constraint
|
||||
# fifth select the first candidate among the set of candidates.
|
||||
# the example network used in comments has been added to the set of data tests files
|
||||
|
||||
# define the list to be returned
|
||||
path_res_list = []
|
||||
|
||||
# all disjctn must be computed at once together to avoid blocking
|
||||
# 1 1
|
||||
# eg a----b-----c
|
||||
# |1 |0.5 |1
|
||||
# e----f--h--g
|
||||
# 1 0.5 0.5
|
||||
# if I have to compute a to g and a to h
|
||||
# I must not compute a-b-f-h-g, otherwise there is no disjoint path remaining for a to h
|
||||
# instead I should list all most disjoint path and select the one that have the less
|
||||
# number of commonalities
|
||||
# \ path abfh aefh abcgh
|
||||
# \___cost 2 2.5 3.5
|
||||
# path| cost
|
||||
# abfhg| 2.5 x x x
|
||||
# abcg | 3 x x
|
||||
# aefhg| 3 x x x
|
||||
# from this table abcg and aefh have no common links and should be preferred
|
||||
# even they are not the shortest paths
|
||||
|
||||
# build the list of pathreqlist elements not concerned by disjunction
|
||||
global_disjunctions_list = [e for d in disjunctions_list for e in d.disjunctions_req ]
|
||||
pathreqlist_simple = [e for e in pathreqlist if e.request_id not in global_disjunctions_list]
|
||||
pathreqlist_disjt = [e for e in pathreqlist if e.request_id in global_disjunctions_list]
|
||||
|
||||
# use a mirror class to record path and the corresponding requests
|
||||
class Pth:
|
||||
def __init__(self, req, pth, simplepth):
|
||||
self.req = req
|
||||
self.pth = pth
|
||||
self.simplepth = simplepth
|
||||
|
||||
# step 1
|
||||
# for each remaining request compute a set of simple path
|
||||
allpaths = {}
|
||||
rqs = {}
|
||||
simple_rqs = {}
|
||||
simple_rqs_reversed = {}
|
||||
for pathreq in pathreqlist_disjt :
|
||||
all_simp_pths = list(all_simple_paths(network,\
|
||||
source=next(el for el in network.nodes() if el.uid == pathreq.source),\
|
||||
target=next(el for el in network.nodes() if el.uid == pathreq.destination),\
|
||||
cutoff=80))
|
||||
# sort them in km length instead of hop
|
||||
# all_simp_pths = sorted(all_simp_pths, key=lambda path: len(path))
|
||||
all_simp_pths = sorted(all_simp_pths, key=lambda \
|
||||
x: sum(network.get_edge_data(x[i],x[i+1])['weight'] for i in range(len(x)-2)))
|
||||
# reversed direction paths required to check disjunction on both direction
|
||||
all_simp_pths_reversed = []
|
||||
for pth in all_simp_pths:
|
||||
all_simp_pths_reversed.append(find_reversed_path(pth,network))
|
||||
rqs[pathreq.request_id] = all_simp_pths
|
||||
temp =[]
|
||||
for p in all_simp_pths :
|
||||
# build a short list representing each roadm+direction with the first item
|
||||
# start enumeration at 1 to avoid Trx in the list
|
||||
s = [e.uid for i,e in enumerate(p[1:-1]) \
|
||||
if (isinstance(e,Roadm) | (isinstance(p[i],Roadm) ))]
|
||||
temp.append(s)
|
||||
# id(s) is unique even if path is the same: two objects with same
|
||||
# path have two different ids
|
||||
allpaths[id(s)] = Pth(pathreq,p,s)
|
||||
simple_rqs[pathreq.request_id] = temp
|
||||
temp =[]
|
||||
for p in all_simp_pths_reversed :
|
||||
# build a short list representing each roadm+direction with the first item
|
||||
# start enumeration at 1 to avoid Trx in the list
|
||||
temp.append([e.uid for i,e in enumerate(p[1:-1]) \
|
||||
if (isinstance(e,Roadm) | (isinstance(p[i],Roadm) ))] )
|
||||
simple_rqs_reversed[pathreq.request_id] = temp
|
||||
# step 2
|
||||
# for each set of requests that need to be disjoint
|
||||
# select the disjoint path combination
|
||||
|
||||
candidates = {}
|
||||
for d in disjunctions_list :
|
||||
dlist = d.disjunctions_req.copy()
|
||||
# each line of dpath is one combination of path that satisfies disjunction
|
||||
dpath = []
|
||||
for i,p in enumerate(simple_rqs[dlist[0]]):
|
||||
dpath.append([p])
|
||||
# allpaths[id(p)].d_id = d.disjunction_id
|
||||
# in each loop, dpath is updated with a path for rq that satisfies
|
||||
# disjunction with each path in dpath
|
||||
# for example, assume set of requests in the vector (disjunction_list) is {rq1,rq2, rq3}
|
||||
# rq1 p1: abfhg
|
||||
# p2: aefhg
|
||||
# p3: abcg
|
||||
# rq2 p8: bf
|
||||
# rq3 p4: abcgh
|
||||
# p6: aefh
|
||||
# p7: abfh
|
||||
# initiate with rq1
|
||||
# dpath = [[p1]
|
||||
# [p2]
|
||||
# [p3]]
|
||||
# after first loop:
|
||||
# dpath = [[p1 p8]
|
||||
# [p3 p8]]
|
||||
# since p2 and p8 are not disjoint
|
||||
# after second loop:
|
||||
# dpath = [ p3 p8 p6 ]
|
||||
# since p1 and p4 are not disjoint
|
||||
# p1 and p7 are not disjoint
|
||||
# p3 and p4 are not disjoint
|
||||
# p3 and p7 are not disjoint
|
||||
|
||||
for e1 in dlist[1:] :
|
||||
temp = []
|
||||
for j,p1 in enumerate(simple_rqs[e1]):
|
||||
# allpaths[id(p1)].d_id = d.disjunction_id
|
||||
# can use index j in simple_rqs_reversed because index
|
||||
# of direct and reversed paths have been kept identical
|
||||
p1_reversed = simple_rqs_reversed[e1][j]
|
||||
# print(p1_reversed)
|
||||
# print('\n\n')
|
||||
for k,c in enumerate(dpath) :
|
||||
# print(f' c: \t{c}')
|
||||
temp2 = c.copy()
|
||||
all_disjoint = 0
|
||||
for p in c :
|
||||
all_disjoint += isdisjoint(p1,p)+ isdisjoint(p1_reversed,p)
|
||||
if all_disjoint ==0:
|
||||
temp2.append(p1)
|
||||
temp.append(temp2)
|
||||
# print(f' coucou {e1}: \t{temp}')
|
||||
dpath = temp
|
||||
# print(dpath)
|
||||
candidates[d.disjunction_id] = dpath
|
||||
|
||||
# for i in disjunctions_list :
|
||||
# print(f'\n{candidates[i.disjunction_id]}')
|
||||
|
||||
# step 3
|
||||
# now for each request, select the path that satisfies all disjunctions
|
||||
# path must be in candidates[id] for all concerned ids
|
||||
# for example, assume set of sync vectors (disjunction groups) is
|
||||
# s1 = {rq1 rq2} s2 = {rq1 rq3}
|
||||
# candidate[s1] = [[p1 p8]
|
||||
# [p3 p8]]
|
||||
# candidate[s2] = [[p3 p6]]
|
||||
# for rq1 p3 should be preferred
|
||||
|
||||
|
||||
for pathreq in pathreqlist_disjt:
|
||||
concerned_d_id = [d.disjunction_id for d in disjunctions_list if pathreq.request_id in d.disjunctions_req]
|
||||
# for each set of solution, verify that the same path is used for the same request
|
||||
candidate_paths = simple_rqs[pathreq.request_id]
|
||||
# print('coucou')
|
||||
# print(pathreq.request_id)
|
||||
for p in candidate_paths :
|
||||
iscandidate = 0
|
||||
for sol in concerned_d_id :
|
||||
test = 1
|
||||
# for each solution test if p is part of the solution
|
||||
# if yes, then p can remain a candidate
|
||||
for i,m in enumerate(candidates[sol]) :
|
||||
if p in m:
|
||||
if allpaths[id(m[m.index(p)])].req.request_id == pathreq.request_id :
|
||||
test = 0
|
||||
break
|
||||
iscandidate += test
|
||||
if iscandidate != 0:
|
||||
for l in concerned_d_id :
|
||||
for m in candidates[l] :
|
||||
if p in m :
|
||||
candidates[l].remove(m)
|
||||
|
||||
# for i in disjunctions_list :
|
||||
# print(i.disjunction_id)
|
||||
# print(f'\n{candidates[i.disjunction_id]}')
|
||||
|
||||
# step 4 apply route constraints : remove candidate path that do not satisfy the constraint
|
||||
# only in the case of disjounction: the simple path is processed in request.compute_constrained_path
|
||||
# TODO : keep a version without the loose constraint
|
||||
for d in disjunctions_list :
|
||||
temp = []
|
||||
for j,sol in enumerate(candidates[d.disjunction_id]) :
|
||||
testispartok = True
|
||||
for i,p in enumerate(sol) :
|
||||
# print(f'test {allpaths[id(p)].req.request_id}')
|
||||
# print(f'length of route {len(allpaths[id(p)].req.nodes_list)}')
|
||||
if allpaths[id(p)].req.nodes_list :
|
||||
# if p does not containt the ordered list node, remove sol from the candidate
|
||||
# except if this was the last solution: then check if the constraint is loose or not
|
||||
if not ispart(allpaths[id(p)].req.nodes_list, p) :
|
||||
# print(f'nb of solutions {len(temp)}')
|
||||
if j < len(candidates[d.disjunction_id])-1 :
|
||||
msg = f'removing {sol}'
|
||||
logger.info(msg)
|
||||
testispartok = False
|
||||
#break
|
||||
else:
|
||||
if 'LOOSE' in allpaths[id(p)].req.loose_list:
|
||||
logger.info(f'Could not apply route constraint'+
|
||||
f'{allpaths[id(p)].req.nodes_list} on request' +\
|
||||
f' {allpaths[id(p)].req.request_id}')
|
||||
else :
|
||||
logger.info(f'removing last solution from candidate paths\n{sol}')
|
||||
testispartok = False
|
||||
if testispartok :
|
||||
temp.append(sol)
|
||||
candidates[d.disjunction_id] = temp
|
||||
|
||||
# step 5 select the first combination that works
|
||||
pathreslist_disjoint = {}
|
||||
for d in disjunctions_list :
|
||||
test_sol = True
|
||||
while test_sol:
|
||||
# print('coucou')
|
||||
if candidates[d.disjunction_id] :
|
||||
for p in candidates[d.disjunction_id][0]:
|
||||
if allpaths[id(p)].req in pathreqlist_disjt:
|
||||
# print(f'selected path :{p} for req {allpaths[id(p)].req.request_id}')
|
||||
pathreslist_disjoint[allpaths[id(p)].req] = allpaths[id(p)].pth
|
||||
pathreqlist_disjt.remove(allpaths[id(p)].req)
|
||||
candidates = remove_candidate(candidates, allpaths, allpaths[id(p)].req, p)
|
||||
test_sol = False
|
||||
else:
|
||||
msg = f'No disjoint path found with added constraint'
|
||||
logger.critical(msg)
|
||||
print(f'{msg}\nComputation stopped.')
|
||||
# TODO in this case: replay step 5 with the candidate without constraints
|
||||
exit()
|
||||
|
||||
# for i in disjunctions_list :
|
||||
# print(i.disjunction_id)
|
||||
# print(f'\n{candidates[i.disjunction_id]}')
|
||||
|
||||
# list the results in the same order as initial pathreqlist
|
||||
for req in pathreqlist :
|
||||
req.nodes_list.append(req.destination)
|
||||
# we assume that the destination is a strict constraint
|
||||
req.loose_list.append('STRICT')
|
||||
if req in pathreqlist_simple:
|
||||
path_res_list.append(compute_constrained_path(network, req))
|
||||
else:
|
||||
path_res_list.append(pathreslist_disjoint[req])
|
||||
return path_res_list
|
||||
|
||||
def isdisjoint(p1,p2) :
|
||||
# returns 0 if disjoint
|
||||
edge1 = list(pairwise(p1))
|
||||
edge2 = list(pairwise(p2))
|
||||
for e in edge1 :
|
||||
if e in edge2 :
|
||||
return 1
|
||||
return 0
|
||||
|
||||
def find_reversed_path(p,network) :
|
||||
# select of intermediate roadms and find the path between them
|
||||
# note that this function may not give an exact result in case of multiple
|
||||
# links between two adjacent nodes.
|
||||
# TODO add some indication on elements to indicate from which other they
|
||||
# are the reversed direction
|
||||
reversed_roadm_path = list(reversed([e for e in p if isinstance (e,Roadm)]))
|
||||
source = p[-1]
|
||||
destination = p[0]
|
||||
total_path = [source]
|
||||
for node in reversed_roadm_path :
|
||||
total_path.extend(dijkstra_path(network, source, node, weight = 'weight')[1:])
|
||||
source = node
|
||||
total_path.append(destination)
|
||||
return total_path
|
||||
|
||||
def ispart(a,b) :
|
||||
# the functions takes two paths a and b and retrns True
|
||||
# if all a elements are part of b and in the same order
|
||||
j = 0
|
||||
for i, el in enumerate(a):
|
||||
if el in b :
|
||||
if b.index(el) >= j :
|
||||
j = b.index(el)
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
def remove_candidate(candidates, allpaths, rq, pth) :
|
||||
# print(f'coucou {rq.request_id}')
|
||||
for key, candidate in candidates.items() :
|
||||
temp = candidate.copy()
|
||||
for i,sol in enumerate(candidate) :
|
||||
for p in sol :
|
||||
if allpaths[id(p)].req.request_id == rq.request_id :
|
||||
if id(p) != id(pth) :
|
||||
temp.remove(sol)
|
||||
break
|
||||
candidates[key] = temp
|
||||
return candidates
|
||||
|
||||
def compare_reqs(req1,req2,disjlist) :
|
||||
dis1 = [d for d in disjlist if req1.request_id in d.disjunctions_req]
|
||||
dis2 = [d for d in disjlist if req2.request_id in d.disjunctions_req]
|
||||
same_disj = False
|
||||
if dis1 and dis2 :
|
||||
temp1 = []
|
||||
for d in dis1:
|
||||
temp1.extend(d.disjunctions_req)
|
||||
temp1.remove(req1.request_id)
|
||||
temp2 = []
|
||||
for d in dis2:
|
||||
temp2.extend(d.disjunctions_req)
|
||||
temp2.remove(req2.request_id)
|
||||
if set(temp1) == set(temp2) :
|
||||
same_disj = True
|
||||
elif not dis2 and not dis1:
|
||||
same_disj = True
|
||||
|
||||
if req1.source == req2.source and \
|
||||
req1.destination == req2.destination and \
|
||||
req1.tsp == req2.tsp and \
|
||||
req1.tsp_mode == req2.tsp_mode and \
|
||||
req1.baud_rate == req2.baud_rate and \
|
||||
req1.nodes_list == req2.nodes_list and \
|
||||
req1.loose_list == req2.loose_list and \
|
||||
req1.spacing == req2.spacing and \
|
||||
req1.power == req2.power and \
|
||||
req1.nb_channel == req2.nb_channel and \
|
||||
req1.f_min == req2.f_min and \
|
||||
req1.f_max == req2.f_max and \
|
||||
req1.format == req2.format and \
|
||||
req1.OSNR == req2.OSNR and \
|
||||
req1.roll_off == req2.roll_off and \
|
||||
same_disj:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def requests_aggregation(pathreqlist,disjlist):
|
||||
# this function aggregates requests so that if several requests
|
||||
# exist between same source and destination and with same transponder type
|
||||
# todo maybe add conditions on mode ??, spacing ...
|
||||
# currently if undefined takes the default values
|
||||
local_list = pathreqlist.copy()
|
||||
for req in pathreqlist:
|
||||
for r in local_list:
|
||||
if req.request_id != r.request_id and compare_reqs(req, r, disjlist):
|
||||
# aggregate
|
||||
r.path_bandwidth += req.path_bandwidth
|
||||
temp_r_id = r.request_id
|
||||
r.request_id = ' | '.join((r.request_id,req.request_id))
|
||||
# remove request from list
|
||||
local_list.remove(req)
|
||||
# todo change also disjunction req with new demand
|
||||
|
||||
for d in disjlist:
|
||||
if req.request_id in d.disjunctions_req:
|
||||
d.disjunctions_req.remove(req.request_id)
|
||||
d.disjunctions_req.append(r.request_id)
|
||||
for d in disjlist:
|
||||
if temp_r_id in d.disjunctions_req:
|
||||
disjlist.remove(d)
|
||||
break
|
||||
return local_list, disjlist
|
||||
@@ -1,162 +1,27 @@
|
||||
import numpy as np
|
||||
from operator import attrgetter
|
||||
from collections import namedtuple
|
||||
from logging import getLogger
|
||||
import scipy.constants as ph
|
||||
from scipy.integrate import solve_bvp
|
||||
from scipy.integrate import cumtrapz
|
||||
from scipy.interpolate import interp1d
|
||||
from scipy.optimize import OptimizeResult
|
||||
from math import isclose
|
||||
|
||||
from gnpy.core.utils import db2lin
|
||||
from gnpy.core.utils import db2lin, lin2db
|
||||
from gnpy.core.exceptions import EquipmentConfigError
|
||||
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class RamanParams():
|
||||
def __init__(self, params):
|
||||
self._flag_raman = params['flag_raman']
|
||||
self._space_resolution = params['space_resolution']
|
||||
self._tolerance = params['tolerance']
|
||||
|
||||
@property
|
||||
def flag_raman(self):
|
||||
return self._flag_raman
|
||||
|
||||
@property
|
||||
def space_resolution(self):
|
||||
return self._space_resolution
|
||||
|
||||
@property
|
||||
def tolerance(self):
|
||||
return self._tolerance
|
||||
|
||||
class NLIParams():
|
||||
def __init__(self, params):
|
||||
self._nli_method_name = params['nli_method_name']
|
||||
self._wdm_grid_size = params['wdm_grid_size']
|
||||
self._dispersion_tolerance = params['dispersion_tolerance']
|
||||
self._phase_shift_tollerance = params['phase_shift_tollerance']
|
||||
self._f_cut_resolution = None
|
||||
self._f_pump_resolution = None
|
||||
|
||||
@property
|
||||
def nli_method_name(self):
|
||||
return self._nli_method_name
|
||||
|
||||
@property
|
||||
def wdm_grid_size(self):
|
||||
return self._wdm_grid_size
|
||||
|
||||
@property
|
||||
def dispersion_tolerance(self):
|
||||
return self._dispersion_tolerance
|
||||
|
||||
@property
|
||||
def phase_shift_tollerance(self):
|
||||
return self._phase_shift_tollerance
|
||||
|
||||
@property
|
||||
def f_cut_resolution(self):
|
||||
return self._f_cut_resolution
|
||||
|
||||
@f_cut_resolution.setter
|
||||
def f_cut_resolution(self, f_cut_resolution):
|
||||
self._f_cut_resolution = f_cut_resolution
|
||||
|
||||
@property
|
||||
def f_pump_resolution(self):
|
||||
return self._f_pump_resolution
|
||||
|
||||
@f_pump_resolution.setter
|
||||
def f_pump_resolution(self, f_pump_resolution):
|
||||
self._f_pump_resolution = f_pump_resolution
|
||||
|
||||
class SimParams():
|
||||
def __init__(self, params):
|
||||
self._raman_computed_channels = params['raman_computed_channels']
|
||||
self._raman_params = RamanParams(params=params['raman_parameters'])
|
||||
self._nli_params = NLIParams(params=params['nli_parameters'])
|
||||
|
||||
@property
|
||||
def raman_computed_channels(self):
|
||||
return self._raman_computed_channels
|
||||
|
||||
@property
|
||||
def raman_params(self):
|
||||
return self._raman_params
|
||||
|
||||
@property
|
||||
def nli_params(self):
|
||||
return self._nli_params
|
||||
|
||||
class FiberParams():
|
||||
def __init__(self, fiber):
|
||||
self._loss_coef = 2 * fiber.dbkm_2_lin()[1]
|
||||
self._length = fiber.length
|
||||
self._gamma = fiber.gamma
|
||||
self._beta2 = fiber.beta2()
|
||||
self._beta3 = fiber.beta3 if hasattr(fiber, 'beta3') else 0
|
||||
self._f_ref_beta = fiber.f_ref_beta if hasattr(fiber, 'f_ref_beta') else 0
|
||||
self._raman_efficiency = fiber.params.raman_efficiency
|
||||
self._temperature = fiber.operational['temperature']
|
||||
|
||||
@property
|
||||
def loss_coef(self):
|
||||
return self._loss_coef
|
||||
|
||||
@property
|
||||
def length(self):
|
||||
return self._length
|
||||
|
||||
@property
|
||||
def gamma(self):
|
||||
return self._gamma
|
||||
|
||||
@property
|
||||
def beta2(self):
|
||||
return self._beta2
|
||||
|
||||
@property
|
||||
def beta3(self):
|
||||
return self._beta3
|
||||
|
||||
@property
|
||||
def f_ref_beta(self):
|
||||
return self._f_ref_beta
|
||||
|
||||
@property
|
||||
def raman_efficiency(self):
|
||||
return self._raman_efficiency
|
||||
|
||||
@property
|
||||
def temperature(self):
|
||||
return self._temperature
|
||||
|
||||
def alpha0(self, f_ref=193.5e12):
|
||||
""" It returns the zero element of the series expansion of attenuation coefficient alpha(f) in the
|
||||
reference frequency f_ref
|
||||
|
||||
:param f_ref: reference frequency of series expansion [Hz]
|
||||
:return: alpha0: power attenuation coefficient in f_ref [Neper/m]
|
||||
"""
|
||||
if not hasattr(self.loss_coef, 'alpha_power'):
|
||||
alpha0 = self.loss_coef
|
||||
else:
|
||||
alpha_interp = interp1d(self.loss_coef['frequency'],
|
||||
self.loss_coef['alpha_power'])
|
||||
alpha0 = alpha_interp(f_ref)
|
||||
return alpha0
|
||||
|
||||
pump = namedtuple('RamanPump', 'power frequency propagation_direction')
|
||||
|
||||
def propagate_raman_fiber(fiber, *carriers):
|
||||
sim_params = fiber.sim_params
|
||||
raman_params = fiber.sim_params.raman_params
|
||||
nli_params = fiber.sim_params.nli_params
|
||||
simulation = Simulation.get_simulation()
|
||||
sim_params = simulation.sim_params
|
||||
raman_params = sim_params.raman_params
|
||||
nli_params = sim_params.nli_params
|
||||
# apply input attenuation to carriers
|
||||
attenuation_in = db2lin(fiber.con_in + fiber.att_in)
|
||||
attenuation_in = db2lin(fiber.params.con_in + fiber.params.att_in)
|
||||
chan = []
|
||||
for carrier in carriers:
|
||||
pwr = carrier.power
|
||||
@@ -166,36 +31,32 @@ def propagate_raman_fiber(fiber, *carriers):
|
||||
carrier = carrier._replace(power=pwr)
|
||||
chan.append(carrier)
|
||||
carriers = tuple(f for f in chan)
|
||||
fiber_params = FiberParams(fiber)
|
||||
|
||||
# evaluate fiber attenuation involving also SRS if required by sim_params
|
||||
if 'raman_pumps' in fiber.operational:
|
||||
raman_pumps = tuple(pump(p['power'], p['frequency'], p['propagation_direction'])
|
||||
for p in fiber.operational['raman_pumps'])
|
||||
else:
|
||||
raman_pumps = None
|
||||
raman_solver = RamanSolver(raman_params=raman_params, fiber_params=fiber_params)
|
||||
stimulated_raman_scattering = raman_solver.stimulated_raman_scattering(carriers=carriers,
|
||||
raman_pumps=raman_pumps)
|
||||
raman_solver = fiber.raman_solver
|
||||
raman_solver.carriers = carriers
|
||||
raman_solver.raman_pumps = fiber.raman_pumps
|
||||
stimulated_raman_scattering = raman_solver.stimulated_raman_scattering
|
||||
|
||||
fiber_attenuation = (stimulated_raman_scattering.rho[:, -1])**-2
|
||||
if not raman_params.flag_raman:
|
||||
fiber_attenuation = tuple(fiber.lin_attenuation for _ in carriers)
|
||||
fiber_attenuation = tuple(fiber.params.lin_attenuation for _ in carriers)
|
||||
|
||||
# evaluate Raman ASE noise if required by sim_params and if raman pumps are present
|
||||
if raman_params.flag_raman and raman_pumps:
|
||||
if raman_params.flag_raman and fiber.raman_pumps:
|
||||
raman_ase = raman_solver.spontaneous_raman_scattering.power[:, -1]
|
||||
else:
|
||||
raman_ase = tuple(0 for _ in carriers)
|
||||
|
||||
# evaluate nli and propagate in fiber
|
||||
attenuation_out = db2lin(fiber.con_out)
|
||||
nli_solver = NliSolver(nli_params=nli_params, fiber_params=fiber_params)
|
||||
attenuation_out = db2lin(fiber.params.con_out)
|
||||
nli_solver = fiber.nli_solver
|
||||
nli_solver.stimulated_raman_scattering = stimulated_raman_scattering
|
||||
|
||||
nli_frequencies = []
|
||||
computed_nli = []
|
||||
for carrier in (c for c in carriers if c.channel_number in sim_params.raman_computed_channels):
|
||||
resolution_param = frequency_resolution(carrier, carriers, sim_params, fiber_params)
|
||||
for carrier in (c for c in carriers if c.channel_number in sim_params.nli_params.computed_channels):
|
||||
resolution_param = frequency_resolution(carrier, carriers, sim_params, fiber)
|
||||
f_cut_resolution, f_pump_resolution, _, _ = resolution_param
|
||||
nli_params.f_cut_resolution = f_cut_resolution
|
||||
nli_params.f_pump_resolution = f_pump_resolution
|
||||
@@ -206,13 +67,14 @@ def propagate_raman_fiber(fiber, *carriers):
|
||||
for carrier, attenuation, rmn_ase in zip(carriers, fiber_attenuation, raman_ase):
|
||||
carrier_nli = np.interp(carrier.frequency, nli_frequencies, computed_nli)
|
||||
pwr = carrier.power
|
||||
pwr = pwr._replace(signal=pwr.signal/attenuation/attenuation_out,
|
||||
nli=(pwr.nli+carrier_nli)/attenuation/attenuation_out,
|
||||
ase=((pwr.ase/attenuation)+rmn_ase)/attenuation_out)
|
||||
pwr = pwr._replace(signal=pwr.signal / attenuation / attenuation_out,
|
||||
nli=(pwr.nli + carrier_nli) / attenuation / attenuation_out,
|
||||
ase=((pwr.ase / attenuation) + rmn_ase) / attenuation_out)
|
||||
new_carriers.append(carrier._replace(power=pwr))
|
||||
return new_carriers
|
||||
|
||||
def frequency_resolution(carrier, carriers, sim_params, fiber_params):
|
||||
|
||||
def frequency_resolution(carrier, carriers, sim_params, fiber):
|
||||
def _get_freq_res_k_phi(delta_count, grid_size, alpha0, delta_z, beta2, k_tol, phi_tol):
|
||||
res_phi = _get_freq_res_phase_rotation(delta_count, grid_size, delta_z, beta2, phi_tol)
|
||||
res_k = _get_freq_res_dispersion_attenuation(delta_count, grid_size, alpha0, beta2, k_tol)
|
||||
@@ -228,10 +90,10 @@ def frequency_resolution(carrier, carriers, sim_params, fiber_params):
|
||||
|
||||
grid_size = sim_params.nli_params.wdm_grid_size
|
||||
delta_z = sim_params.raman_params.space_resolution
|
||||
alpha0 = fiber_params.alpha0()
|
||||
beta2 = fiber_params.beta2
|
||||
alpha0 = fiber.alpha0()
|
||||
beta2 = fiber.params.beta2
|
||||
k_tol = sim_params.nli_params.dispersion_tolerance
|
||||
phi_tol = sim_params.nli_params.phase_shift_tollerance
|
||||
phi_tol = sim_params.nli_params.phase_shift_tolerance
|
||||
f_pump_resolution, method_f_pump, res_dict_pump = \
|
||||
_get_freq_res_k_phi(0, grid_size, alpha0, delta_z, beta2, k_tol, phi_tol)
|
||||
f_cut_resolution = {}
|
||||
@@ -247,6 +109,7 @@ def frequency_resolution(carrier, carriers, sim_params, fiber_params):
|
||||
res_dict_cut[delta_number] = res_dict
|
||||
return [f_cut_resolution, f_pump_resolution, (method_f_cut, method_f_pump), (res_dict_cut, res_dict_pump)]
|
||||
|
||||
|
||||
def raised_cosine_comb(f, *carriers):
|
||||
""" Returns an array storing the PSD of a WDM comb of raised cosine shaped
|
||||
channels at the input frequencies defined in array f
|
||||
@@ -270,30 +133,59 @@ def raised_cosine_comb(f, *carriers):
|
||||
np.where(tf > 0, 1., 0.) * np.where(np.abs(ff) <= stopband, 1., 0.)) + psd
|
||||
return psd
|
||||
|
||||
|
||||
class Simulation:
|
||||
_shared_dict = {}
|
||||
|
||||
def __init__(self):
|
||||
if type(self) == Simulation:
|
||||
raise NotImplementedError('Simulation cannot be instatiated')
|
||||
|
||||
@classmethod
|
||||
def set_params(cls, sim_params):
|
||||
cls._shared_dict['sim_params'] = sim_params
|
||||
|
||||
@classmethod
|
||||
def get_simulation(cls):
|
||||
self = cls.__new__(cls)
|
||||
return self
|
||||
|
||||
@property
|
||||
def sim_params(self):
|
||||
return self._shared_dict['sim_params']
|
||||
|
||||
|
||||
class SpontaneousRamanScattering:
|
||||
def __init__(self, frequency, z, power):
|
||||
self.frequency = frequency
|
||||
self.z = z
|
||||
self.power = power
|
||||
|
||||
|
||||
class StimulatedRamanScattering:
|
||||
def __init__(self, frequency, z, rho, power):
|
||||
self.frequency = frequency
|
||||
self.z = z
|
||||
self.rho = rho
|
||||
self.power = power
|
||||
|
||||
|
||||
class RamanSolver:
|
||||
def __init__(self, raman_params=None, fiber_params=None):
|
||||
""" Initialize the fiber object with its physical parameters
|
||||
:param length: fiber length in m.
|
||||
:param alphap: fiber power attenuation coefficient vs frequency in 1/m. numpy array
|
||||
:param freq_alpha: frequency axis of alphap in Hz. numpy array
|
||||
:param cr_raman: Raman efficiency vs frequency offset in 1/W/m. numpy array
|
||||
:param freq_cr: reference frequency offset axis for cr_raman. numpy array
|
||||
:param raman_params: namedtuple containing the solver parameters (optional).
|
||||
def __init__(self, fiber=None):
|
||||
""" Initialize the Raman solver object.
|
||||
:param fiber: instance of elements.py/Fiber.
|
||||
:param carriers: tuple of carrier objects
|
||||
:param raman_pumps: tuple containing pumps characteristics
|
||||
"""
|
||||
self.fiber_params = fiber_params
|
||||
self.raman_params = raman_params
|
||||
self._fiber = fiber
|
||||
self._carriers = None
|
||||
self._raman_pumps = None
|
||||
self._stimulated_raman_scattering = None
|
||||
self._spontaneous_raman_scattering = None
|
||||
|
||||
@property
|
||||
def fiber_params(self):
|
||||
return self._fiber_params
|
||||
|
||||
@fiber_params.setter
|
||||
def fiber_params(self, fiber_params):
|
||||
self._stimulated_raman_scattering = None
|
||||
self._fiber_params = fiber_params
|
||||
def fiber(self):
|
||||
return self._fiber
|
||||
|
||||
@property
|
||||
def carriers(self):
|
||||
@@ -301,11 +193,8 @@ class RamanSolver:
|
||||
|
||||
@carriers.setter
|
||||
def carriers(self, carriers):
|
||||
"""
|
||||
:param carriers: tuple of namedtuples containing information about carriers
|
||||
:return:
|
||||
"""
|
||||
self._carriers = carriers
|
||||
self._spontaneous_raman_scattering = None
|
||||
self._stimulated_raman_scattering = None
|
||||
|
||||
@property
|
||||
@@ -318,62 +207,43 @@ class RamanSolver:
|
||||
self._stimulated_raman_scattering = None
|
||||
|
||||
@property
|
||||
def raman_params(self):
|
||||
return self._raman_params
|
||||
|
||||
@raman_params.setter
|
||||
def raman_params(self, raman_params):
|
||||
"""
|
||||
:param raman_params: namedtuple containing the solver parameters (optional).
|
||||
:return:
|
||||
"""
|
||||
self._raman_params = raman_params
|
||||
self._stimulated_raman_scattering = None
|
||||
self._spontaneous_raman_scattering = None
|
||||
def stimulated_raman_scattering(self):
|
||||
if self._stimulated_raman_scattering is None:
|
||||
self.calculate_stimulated_raman_scattering(self.carriers, self.raman_pumps)
|
||||
return self._stimulated_raman_scattering
|
||||
|
||||
@property
|
||||
def spontaneous_raman_scattering(self):
|
||||
if self._spontaneous_raman_scattering is None:
|
||||
# SET STUFF
|
||||
loss_coef = self.fiber_params.loss_coef
|
||||
raman_efficiency = self.fiber_params.raman_efficiency
|
||||
temperature = self.fiber_params.temperature
|
||||
carriers = self.carriers
|
||||
raman_pumps = self.raman_pumps
|
||||
|
||||
logger.debug('Start computing fiber Spontaneous Raman Scattering')
|
||||
power_spectrum, freq_array, prop_direct, bn_array = self._compute_power_spectrum(carriers, raman_pumps)
|
||||
|
||||
if not hasattr(loss_coef, 'alpha_power'):
|
||||
alphap_fiber = loss_coef * np.ones(freq_array.shape)
|
||||
else:
|
||||
interp_alphap = interp1d(loss_coef['frequency'], loss_coef['alpha_power'])
|
||||
alphap_fiber = interp_alphap(freq_array)
|
||||
|
||||
freq_diff = abs(freq_array - np.reshape(freq_array, (len(freq_array), 1)))
|
||||
interp_cr = interp1d(raman_efficiency['frequency_offset'], raman_efficiency['cr'])
|
||||
cr = interp_cr(freq_diff)
|
||||
|
||||
# z propagation axis
|
||||
z_array = self._stimulated_raman_scattering.z
|
||||
ase_bc = np.zeros(freq_array.shape)
|
||||
|
||||
# calculate ase power
|
||||
spontaneous_raman_scattering = self._int_spontaneous_raman(z_array, self._stimulated_raman_scattering.power,
|
||||
alphap_fiber, freq_array, cr, freq_diff, ase_bc,
|
||||
bn_array, temperature)
|
||||
|
||||
setattr(spontaneous_raman_scattering, 'frequency', freq_array)
|
||||
setattr(spontaneous_raman_scattering, 'z', z_array)
|
||||
setattr(spontaneous_raman_scattering, 'power', spontaneous_raman_scattering.x)
|
||||
delattr(spontaneous_raman_scattering, 'x')
|
||||
|
||||
logger.debug(spontaneous_raman_scattering.message)
|
||||
|
||||
self._spontaneous_raman_scattering = spontaneous_raman_scattering
|
||||
|
||||
self.calculate_spontaneous_raman_scattering(self.carriers, self.raman_pumps)
|
||||
return self._spontaneous_raman_scattering
|
||||
|
||||
def calculate_spontaneous_raman_scattering(self, carriers, raman_pumps):
|
||||
raman_efficiency = self.fiber.params.raman_efficiency
|
||||
temperature = self.fiber.operational['temperature']
|
||||
|
||||
logger.debug('Start computing fiber Spontaneous Raman Scattering')
|
||||
power_spectrum, freq_array, prop_direct, bn_array = self._compute_power_spectrum(carriers, raman_pumps)
|
||||
|
||||
alphap_fiber = self.fiber.alpha(freq_array)
|
||||
|
||||
freq_diff = abs(freq_array - np.reshape(freq_array, (len(freq_array), 1)))
|
||||
interp_cr = interp1d(raman_efficiency['frequency_offset'], raman_efficiency['cr'])
|
||||
cr = interp_cr(freq_diff)
|
||||
|
||||
# z propagation axis
|
||||
z_array = self.stimulated_raman_scattering.z
|
||||
ase_bc = np.zeros(freq_array.shape)
|
||||
|
||||
# calculate ase power
|
||||
int_spontaneous_raman = self._int_spontaneous_raman(z_array, self._stimulated_raman_scattering.power,
|
||||
alphap_fiber, freq_array, cr, freq_diff, ase_bc,
|
||||
bn_array, temperature)
|
||||
|
||||
spontaneous_raman_scattering = SpontaneousRamanScattering(freq_array, z_array, int_spontaneous_raman.x)
|
||||
logger.debug("Spontaneous Raman Scattering evaluated successfully")
|
||||
self._spontaneous_raman_scattering = spontaneous_raman_scattering
|
||||
|
||||
@staticmethod
|
||||
def _compute_power_spectrum(carriers, raman_pumps=None):
|
||||
"""
|
||||
@@ -412,10 +282,14 @@ class RamanSolver:
|
||||
|
||||
return pow_array, f_array, propagation_direction, noise_bandwidth_array
|
||||
|
||||
def _int_spontaneous_raman(self, z_array, raman_matrix, alphap_fiber, freq_array, cr_raman_matrix, freq_diff, ase_bc, bn_array, temperature):
|
||||
def _int_spontaneous_raman(self, z_array, raman_matrix, alphap_fiber, freq_array,
|
||||
cr_raman_matrix, freq_diff, ase_bc, bn_array, temperature):
|
||||
spontaneous_raman_scattering = OptimizeResult()
|
||||
|
||||
dx = self.raman_params.space_resolution
|
||||
simulation = Simulation.get_simulation()
|
||||
sim_params = simulation.sim_params
|
||||
|
||||
dx = sim_params.raman_params.space_resolution
|
||||
h = ph.value('Planck constant')
|
||||
kb = ph.value('Boltzmann constant')
|
||||
|
||||
@@ -425,85 +299,73 @@ class RamanSolver:
|
||||
for f_ind, f_ase in enumerate(freq_array):
|
||||
cr_raman = cr_raman_matrix[f_ind, :]
|
||||
vibrational_loss = f_ase / freq_array[:f_ind]
|
||||
eta = 1/(np.exp((h*freq_diff[f_ind, f_ind+1:])/(kb*temperature)) - 1)
|
||||
eta = 1 / (np.exp((h * freq_diff[f_ind, f_ind + 1:]) / (kb * temperature)) - 1)
|
||||
|
||||
int_fiber_loss = -alphap_fiber[f_ind] * z_array
|
||||
int_raman_loss = np.sum((cr_raman[:f_ind] * vibrational_loss * int_pump[:f_ind, :].transpose()).transpose(), axis=0)
|
||||
int_raman_loss = np.sum((cr_raman[:f_ind] * vibrational_loss * int_pump[:f_ind, :].transpose()).transpose(),
|
||||
axis=0)
|
||||
int_raman_gain = np.sum((cr_raman[f_ind + 1:] * int_pump[f_ind + 1:, :].transpose()).transpose(), axis=0)
|
||||
|
||||
int_gain_loss = int_fiber_loss + int_raman_gain + int_raman_loss
|
||||
|
||||
new_ase = np.sum((cr_raman[f_ind+1:] * (1 + eta) * raman_matrix[f_ind+1:, :].transpose()).transpose() * h * f_ase * bn_array[f_ind], axis=0)
|
||||
new_ase = np.sum((cr_raman[f_ind + 1:] * (1 + eta) * raman_matrix[f_ind + 1:, :].transpose()).transpose()
|
||||
* h * f_ase * bn_array[f_ind], axis=0)
|
||||
|
||||
bc_evolution = ase_bc[f_ind] * np.exp(int_gain_loss)
|
||||
ase_evolution = np.exp(int_gain_loss) * cumtrapz(new_ase*np.exp(-int_gain_loss), z_array, dx=dx, initial=0)
|
||||
ase_evolution = np.exp(int_gain_loss) * cumtrapz(new_ase *
|
||||
np.exp(-int_gain_loss), z_array, dx=dx, initial=0)
|
||||
|
||||
power_ase[f_ind, :] = bc_evolution + ase_evolution
|
||||
|
||||
spontaneous_raman_scattering.x = 2 * power_ase
|
||||
spontaneous_raman_scattering.success = True
|
||||
spontaneous_raman_scattering.message = "Spontaneous Raman Scattering evaluated successfully"
|
||||
|
||||
return spontaneous_raman_scattering
|
||||
|
||||
def stimulated_raman_scattering(self, carriers, raman_pumps=None):
|
||||
""" Returns stimulated Raman scattering solution including
|
||||
def calculate_stimulated_raman_scattering(self, carriers, raman_pumps):
|
||||
""" Returns stimulated Raman scattering solution including
|
||||
fiber gain/loss profile.
|
||||
:return: self._stimulated_raman_scattering: the SRS problem solution.
|
||||
scipy.interpolate.PPoly instance
|
||||
:return: None
|
||||
"""
|
||||
# fiber parameters
|
||||
fiber_length = self.fiber.params.length
|
||||
raman_efficiency = self.fiber.params.raman_efficiency
|
||||
simulation = Simulation.get_simulation()
|
||||
sim_params = simulation.sim_params
|
||||
|
||||
if self._stimulated_raman_scattering is None:
|
||||
# fiber parameters
|
||||
fiber_length = self.fiber_params.length
|
||||
loss_coef = self.fiber_params.loss_coef
|
||||
if self.raman_params.flag_raman:
|
||||
raman_efficiency = self.fiber_params.raman_efficiency
|
||||
else:
|
||||
raman_efficiency = self.fiber_params.raman_efficiency
|
||||
raman_efficiency['cr'] = np.array(raman_efficiency['cr']) * 0
|
||||
# raman solver parameters
|
||||
z_resolution = self.raman_params.space_resolution
|
||||
tolerance = self.raman_params.tolerance
|
||||
if not sim_params.raman_params.flag_raman:
|
||||
raman_efficiency['cr'] = np.zeros(len(raman_efficiency['cr']))
|
||||
# raman solver parameters
|
||||
z_resolution = sim_params.raman_params.space_resolution
|
||||
tolerance = sim_params.raman_params.tolerance
|
||||
|
||||
logger.debug('Start computing fiber Stimulated Raman Scattering')
|
||||
logger.debug('Start computing fiber Stimulated Raman Scattering')
|
||||
|
||||
power_spectrum, freq_array, prop_direct, _ = self._compute_power_spectrum(carriers, raman_pumps)
|
||||
power_spectrum, freq_array, prop_direct, _ = self._compute_power_spectrum(carriers, raman_pumps)
|
||||
|
||||
if not hasattr(loss_coef, 'alpha_power'):
|
||||
alphap_fiber = loss_coef * np.ones(freq_array.shape)
|
||||
else:
|
||||
interp_alphap = interp1d(loss_coef['frequency'], loss_coef['alpha_power'])
|
||||
alphap_fiber = interp_alphap(freq_array)
|
||||
alphap_fiber = self.fiber.alpha(freq_array)
|
||||
|
||||
freq_diff = abs(freq_array - np.reshape(freq_array, (len(freq_array), 1)))
|
||||
interp_cr = interp1d(raman_efficiency['frequency_offset'], raman_efficiency['cr'])
|
||||
cr = interp_cr(freq_diff)
|
||||
freq_diff = abs(freq_array - np.reshape(freq_array, (len(freq_array), 1)))
|
||||
interp_cr = interp1d(raman_efficiency['frequency_offset'], raman_efficiency['cr'])
|
||||
cr = interp_cr(freq_diff)
|
||||
|
||||
# z propagation axis
|
||||
z = np.arange(0, fiber_length+1, z_resolution)
|
||||
# z propagation axis
|
||||
z = np.arange(0, fiber_length + 1, z_resolution)
|
||||
|
||||
ode_function = lambda z, p: self._ode_stimulated_raman(z, p, alphap_fiber, freq_array, cr, prop_direct)
|
||||
boundary_residual = lambda ya, yb: self._residuals_stimulated_raman(ya, yb, power_spectrum, prop_direct)
|
||||
initial_guess_conditions = self._initial_guess_stimulated_raman(z, power_spectrum, alphap_fiber, prop_direct)
|
||||
def ode_function(z, p):
|
||||
return self._ode_stimulated_raman(z, p, alphap_fiber, freq_array, cr, prop_direct)
|
||||
|
||||
# ODE SOLVER
|
||||
stimulated_raman_scattering = solve_bvp(ode_function, boundary_residual, z, initial_guess_conditions, tol=tolerance)
|
||||
def boundary_residual(ya, yb):
|
||||
return self._residuals_stimulated_raman(ya, yb, power_spectrum, prop_direct)
|
||||
|
||||
rho = (stimulated_raman_scattering.y.transpose() / power_spectrum).transpose()
|
||||
rho = np.sqrt(rho) # From power attenuation to field attenuation
|
||||
setattr(stimulated_raman_scattering, 'frequency', freq_array)
|
||||
setattr(stimulated_raman_scattering, 'z', stimulated_raman_scattering.x)
|
||||
setattr(stimulated_raman_scattering, 'rho', rho)
|
||||
setattr(stimulated_raman_scattering, 'power', stimulated_raman_scattering.y)
|
||||
delattr(stimulated_raman_scattering, 'x')
|
||||
delattr(stimulated_raman_scattering, 'y')
|
||||
initial_guess_conditions = self._initial_guess_stimulated_raman(z, power_spectrum, alphap_fiber, prop_direct)
|
||||
|
||||
self.carriers = carriers
|
||||
self.raman_pumps = raman_pumps
|
||||
self._stimulated_raman_scattering = stimulated_raman_scattering
|
||||
# ODE SOLVER
|
||||
bvp_solution = solve_bvp(ode_function, boundary_residual, z, initial_guess_conditions, tol=tolerance)
|
||||
|
||||
return self._stimulated_raman_scattering
|
||||
rho = (bvp_solution.y.transpose() / power_spectrum).transpose()
|
||||
rho = np.sqrt(rho) # From power attenuation to field attenuation
|
||||
stimulated_raman_scattering = StimulatedRamanScattering(freq_array, bvp_solution.x, rho, bvp_solution.y)
|
||||
|
||||
self._stimulated_raman_scattering = stimulated_raman_scattering
|
||||
|
||||
def _residuals_stimulated_raman(self, ya, yb, power_spectrum, prop_direct):
|
||||
|
||||
@@ -520,11 +382,14 @@ class RamanSolver:
|
||||
def _initial_guess_stimulated_raman(self, z, power_spectrum, alphap_fiber, prop_direct):
|
||||
""" Computes the initial guess knowing the boundary conditions
|
||||
:param z: patial axis [m]. numpy array
|
||||
:param power_spectrum: power in each frequency slice [W]. Frequency axis is defined by freq_array. numpy array
|
||||
:param alphap_fiber: frequency dependent fiber attenuation of signal power [1/m]. Frequency defined by freq_array. numpy array
|
||||
:param power_spectrum: power in each frequency slice [W].
|
||||
Frequency axis is defined by freq_array. numpy array
|
||||
:param alphap_fiber: frequency dependent fiber attenuation of signal power [1/m].
|
||||
Frequency defined by freq_array. numpy array
|
||||
:param prop_direct: indicates the propagation direction of each power slice in power_spectrum:
|
||||
+1 for forward propagation and -1 for backward propagation. Frequency defined by freq_array. numpy array
|
||||
:return: power_guess: guess on the initial conditions [W]. The first ndarray index identifies the frequency slice,
|
||||
:return: power_guess: guess on the initial conditions [W].
|
||||
The first ndarray index identifies the frequency slice,
|
||||
the second ndarray index identifies the step in z. ndarray
|
||||
"""
|
||||
|
||||
@@ -538,14 +403,19 @@ class RamanSolver:
|
||||
return power_guess
|
||||
|
||||
def _ode_stimulated_raman(self, z, power_spectrum, alphap_fiber, freq_array, cr_raman_matrix, prop_direct):
|
||||
""" Aim of ode_raman is to implement the set of ordinary differential equations (ODEs) describing the Raman effect.
|
||||
""" Aim of ode_raman is to implement the set of ordinary differential equations (ODEs)
|
||||
describing the Raman effect.
|
||||
:param z: spatial axis (unused).
|
||||
:param power_spectrum: power in each frequency slice [W]. Frequency axis is defined by freq_array. numpy array. Size n
|
||||
:param alphap_fiber: frequency dependent fiber attenuation of signal power [1/m]. Frequency defined by freq_array. numpy array. Size n
|
||||
:param power_spectrum: power in each frequency slice [W].
|
||||
Frequency axis is defined by freq_array. numpy array. Size n
|
||||
:param alphap_fiber: frequency dependent fiber attenuation of signal power [1/m].
|
||||
Frequency defined by freq_array. numpy array. Size n
|
||||
:param freq_array: reference frequency axis [Hz]. numpy array. Size n
|
||||
:param cr_raman: Cr(f) Raman gain efficiency variation in frequency [1/W/m]. Frequency defined by freq_array. numpy ndarray. Size nxn
|
||||
:param cr_raman: Cr(f) Raman gain efficiency variation in frequency [1/W/m].
|
||||
Frequency defined by freq_array. numpy ndarray. Size nxn
|
||||
:param prop_direct: indicates the propagation direction of each power slice in power_spectrum:
|
||||
+1 for forward propagation and -1 for backward propagation. Frequency defined by freq_array. numpy array. Size n
|
||||
+1 for forward propagation and -1 for backward propagation.
|
||||
Frequency defined by freq_array. numpy array. Size n
|
||||
:return: dP/dz: the power variation in dz [W/m]. numpy array. Size n
|
||||
"""
|
||||
|
||||
@@ -555,7 +425,7 @@ class RamanSolver:
|
||||
vibrational_loss = freq_array[f_ind] / freq_array[:f_ind]
|
||||
|
||||
for z_ind, power_sample in enumerate(power):
|
||||
raman_gain = np.sum(cr_raman[f_ind+1:] * power_spectrum[f_ind+1:, z_ind])
|
||||
raman_gain = np.sum(cr_raman[f_ind + 1:] * power_spectrum[f_ind + 1:, z_ind])
|
||||
raman_loss = np.sum(vibrational_loss * cr_raman[:f_ind] * power_spectrum[:f_ind, z_ind])
|
||||
|
||||
dpdz_element = prop_direct[f_ind] * (-alphap_fiber[f_ind] + raman_gain - raman_loss) * power_sample
|
||||
@@ -563,28 +433,25 @@ class RamanSolver:
|
||||
|
||||
return np.vstack(dpdz)
|
||||
|
||||
|
||||
class NliSolver:
|
||||
""" This class implements the NLI models.
|
||||
Model and method can be specified in `self.nli_params.method`.
|
||||
Model and method can be specified in `sim_params.nli_params.method`.
|
||||
List of implemented methods:
|
||||
'gn_model_analytic': brute force triple integral solution
|
||||
'ggn_spectrally_separated_xpm_spm': XPM plus SPM
|
||||
"""
|
||||
|
||||
def __init__(self, nli_params=None, fiber_params=None):
|
||||
""" Initialize the fiber object with its physical parameters
|
||||
def __init__(self, fiber=None):
|
||||
""" Initialize the Nli solver object.
|
||||
:param fiber: instance of elements.py/Fiber.
|
||||
"""
|
||||
self.fiber_params = fiber_params
|
||||
self.nli_params = nli_params
|
||||
self.stimulated_raman_scattering = None
|
||||
self._fiber = fiber
|
||||
self._stimulated_raman_scattering = None
|
||||
|
||||
@property
|
||||
def fiber_params(self):
|
||||
return self._fiber_params
|
||||
|
||||
@fiber_params.setter
|
||||
def fiber_params(self, fiber_params):
|
||||
self._fiber_params = fiber_params
|
||||
def fiber(self):
|
||||
return self._fiber
|
||||
|
||||
@property
|
||||
def stimulated_raman_scattering(self):
|
||||
@@ -594,28 +461,19 @@ class NliSolver:
|
||||
def stimulated_raman_scattering(self, stimulated_raman_scattering):
|
||||
self._stimulated_raman_scattering = stimulated_raman_scattering
|
||||
|
||||
@property
|
||||
def nli_params(self):
|
||||
return self._nli_params
|
||||
|
||||
@nli_params.setter
|
||||
def nli_params(self, nli_params):
|
||||
"""
|
||||
:param model_params: namedtuple containing the parameters used to compute the NLI.
|
||||
"""
|
||||
self._nli_params = nli_params
|
||||
|
||||
def compute_nli(self, carrier, *carriers):
|
||||
""" Compute NLI power generated by the WDM comb `*carriers` on the channel under test `carrier`
|
||||
at the end of the fiber span.
|
||||
"""
|
||||
if 'gn_model_analytic' == self.nli_params.nli_method_name.lower():
|
||||
simulation = Simulation.get_simulation()
|
||||
sim_params = simulation.sim_params
|
||||
if 'gn_model_analytic' == sim_params.nli_params.nli_method_name.lower():
|
||||
carrier_nli = self._gn_analytic(carrier, *carriers)
|
||||
elif 'ggn_spectrally_separated' in self.nli_params.nli_method_name.lower():
|
||||
elif 'ggn_spectrally_separated' in sim_params.nli_params.nli_method_name.lower():
|
||||
eta_matrix = self._compute_eta_matrix(carrier, *carriers)
|
||||
carrier_nli = self._carrier_nli_from_eta_matrix(eta_matrix, carrier, *carriers)
|
||||
else:
|
||||
raise ValueError(f'Method {self.nli_params.method_nli} not implemented.')
|
||||
raise ValueError(f'Method {sim_params.nli_params.method_nli} not implemented.')
|
||||
|
||||
return carrier_nli
|
||||
|
||||
@@ -624,14 +482,16 @@ class NliSolver:
|
||||
carrier_nli = 0
|
||||
for pump_carrier_1 in carriers:
|
||||
for pump_carrier_2 in carriers:
|
||||
carrier_nli += eta_matrix[pump_carrier_1.channel_number-1, pump_carrier_2.channel_number-1] * \
|
||||
pump_carrier_1.power.signal * pump_carrier_2.power.signal
|
||||
carrier_nli += eta_matrix[pump_carrier_1.channel_number - 1, pump_carrier_2.channel_number - 1] * \
|
||||
pump_carrier_1.power.signal * pump_carrier_2.power.signal
|
||||
carrier_nli *= carrier.power.signal
|
||||
|
||||
return carrier_nli
|
||||
|
||||
def _compute_eta_matrix(self, carrier_cut, *carriers):
|
||||
cut_index = carrier_cut.channel_number - 1
|
||||
simulation = Simulation.get_simulation()
|
||||
sim_params = simulation.sim_params
|
||||
# Matrix initialization
|
||||
matrix_size = max(carriers, key=lambda x: getattr(x, 'channel_number')).channel_number
|
||||
eta_matrix = np.zeros(shape=(matrix_size, matrix_size))
|
||||
@@ -639,10 +499,10 @@ class NliSolver:
|
||||
# SPM
|
||||
logger.debug(f'Start computing SPM on channel #{carrier_cut.channel_number}')
|
||||
# SPM GGN
|
||||
if 'ggn' in self.nli_params.nli_method_name.lower():
|
||||
if 'ggn' in sim_params.nli_params.nli_method_name.lower():
|
||||
partial_nli = self._generalized_spectrally_separated_spm(carrier_cut)
|
||||
# SPM GN
|
||||
elif 'gn' in self.nli_params.nli_method_name.lower():
|
||||
elif 'gn' in sim_params.nli_params.nli_method_name.lower():
|
||||
partial_nli = self._gn_analytic(carrier_cut, *[carrier_cut])
|
||||
eta_matrix[cut_index, cut_index] = partial_nli / (carrier_cut.power.signal**3)
|
||||
|
||||
@@ -653,13 +513,13 @@ class NliSolver:
|
||||
logger.debug(f'Start computing XPM on channel #{carrier_cut.channel_number} '
|
||||
f'from channel #{pump_carrier.channel_number}')
|
||||
# XPM GGN
|
||||
if 'ggn' in self.nli_params.nli_method_name.lower():
|
||||
if 'ggn' in sim_params.nli_params.nli_method_name.lower():
|
||||
partial_nli = self._generalized_spectrally_separated_xpm(carrier_cut, pump_carrier)
|
||||
# XPM GGN
|
||||
elif 'gn' in self.nli_params.nli_method_name.lower():
|
||||
elif 'gn' in sim_params.nli_params.nli_method_name.lower():
|
||||
partial_nli = self._gn_analytic(carrier_cut, *[pump_carrier])
|
||||
eta_matrix[pump_index, pump_index] = partial_nli /\
|
||||
(carrier_cut.power.signal * pump_carrier.power.signal**2)
|
||||
(carrier_cut.power.signal * pump_carrier.power.signal**2)
|
||||
return eta_matrix
|
||||
|
||||
# Methods for computing GN-model
|
||||
@@ -670,48 +530,52 @@ class NliSolver:
|
||||
:param carriers: the full WDM comb
|
||||
:return: carrier_nli: the amount of nonlinear interference in W on the carrier under analysis
|
||||
"""
|
||||
alpha = self.fiber_params.alpha0() / 2
|
||||
beta2 = self.fiber_params.beta2
|
||||
gamma = self.fiber_params.gamma
|
||||
length = self.fiber_params.length
|
||||
effective_length = (1 - np.exp(-2 * alpha * length)) / (2 * alpha)
|
||||
asymptotic_length = 1 / (2 * alpha)
|
||||
beta2 = self.fiber.params.beta2
|
||||
gamma = self.fiber.params.gamma
|
||||
effective_length = self.fiber.params.effective_length
|
||||
asymptotic_length = self.fiber.params.asymptotic_length
|
||||
|
||||
g_nli = 0
|
||||
for interfering_carrier in carriers:
|
||||
g_interfearing = interfering_carrier.power.signal / interfering_carrier.baud_rate
|
||||
g_signal = carrier.power.signal / carrier.baud_rate
|
||||
g_nli += g_interfearing**2 * g_signal \
|
||||
* _psi(carrier, interfering_carrier, beta2=self.fiber_params.beta2, asymptotic_length=1/self.fiber_params.alpha0())
|
||||
g_nli *= (16.0 / 27.0) * (gamma * effective_length)**2 /\
|
||||
* _psi(carrier, interfering_carrier, beta2=beta2, asymptotic_length=asymptotic_length)
|
||||
g_nli *= (16.0 / 27.0) * (gamma * effective_length) ** 2 /\
|
||||
(2 * np.pi * abs(beta2) * asymptotic_length)
|
||||
carrier_nli = carrier.baud_rate * g_nli
|
||||
return carrier_nli
|
||||
|
||||
# Methods for computing the GGN-model
|
||||
def _generalized_spectrally_separated_spm(self, carrier):
|
||||
f_cut_resolution = self.nli_params.f_cut_resolution['delta_0']
|
||||
gamma = self.fiber.params.gamma
|
||||
simulation = Simulation.get_simulation()
|
||||
sim_params = simulation.sim_params
|
||||
f_cut_resolution = sim_params.nli_params.f_cut_resolution['delta_0']
|
||||
f_eval = carrier.frequency
|
||||
g_cut = (carrier.power.signal / carrier.baud_rate)
|
||||
|
||||
spm_nli = carrier.baud_rate * (16.0 / 27.0) * self.fiber_params.gamma**2 * g_cut**3 * \
|
||||
self._generalized_psi(carrier, carrier, f_eval, f_cut_resolution, f_cut_resolution)
|
||||
spm_nli = carrier.baud_rate * (16.0 / 27.0) * gamma ** 2 * g_cut ** 3 * \
|
||||
self._generalized_psi(carrier, carrier, f_eval, f_cut_resolution, f_cut_resolution)
|
||||
return spm_nli
|
||||
|
||||
def _generalized_spectrally_separated_xpm(self, carrier_cut, pump_carrier):
|
||||
gamma = self.fiber.params.gamma
|
||||
simulation = Simulation.get_simulation()
|
||||
sim_params = simulation.sim_params
|
||||
delta_index = pump_carrier.channel_number - carrier_cut.channel_number
|
||||
f_cut_resolution = self.nli_params.f_cut_resolution[f'delta_{delta_index}']
|
||||
f_pump_resolution = self.nli_params.f_pump_resolution
|
||||
f_cut_resolution = sim_params.nli_params.f_cut_resolution[f'delta_{delta_index}']
|
||||
f_pump_resolution = sim_params.nli_params.f_pump_resolution
|
||||
f_eval = carrier_cut.frequency
|
||||
g_pump = (pump_carrier.power.signal / pump_carrier.baud_rate)
|
||||
g_cut = (carrier_cut.power.signal / carrier_cut.baud_rate)
|
||||
frequency_offset_threshold = self._frequency_offset_threshold(pump_carrier.baud_rate)
|
||||
if abs(carrier_cut.frequency - pump_carrier.frequency) <= frequency_offset_threshold:
|
||||
xpm_nli = carrier_cut.baud_rate * (16.0 / 27.0) * self.fiber_params.gamma**2 * g_pump**2 * g_cut * \
|
||||
2 * self._generalized_psi(carrier_cut, pump_carrier, f_eval, f_cut_resolution, f_pump_resolution)
|
||||
xpm_nli = carrier_cut.baud_rate * (16.0 / 27.0) * gamma ** 2 * g_pump**2 * g_cut * \
|
||||
2 * self._generalized_psi(carrier_cut, pump_carrier, f_eval, f_cut_resolution, f_pump_resolution)
|
||||
else:
|
||||
xpm_nli = carrier_cut.baud_rate * (16.0 / 27.0) * self.fiber_params.gamma**2 * g_pump**2 * g_cut * \
|
||||
2 * self._fast_generalized_psi(carrier_cut, pump_carrier, f_eval, f_cut_resolution)
|
||||
xpm_nli = carrier_cut.baud_rate * (16.0 / 27.0) * gamma ** 2 * g_pump**2 * g_cut * \
|
||||
2 * self._fast_generalized_psi(carrier_cut, pump_carrier, f_eval, f_cut_resolution)
|
||||
return xpm_nli
|
||||
|
||||
def _fast_generalized_psi(self, carrier_cut, pump_carrier, f_eval, f_cut_resolution):
|
||||
@@ -719,15 +583,15 @@ class NliSolver:
|
||||
:return: generalized_psi
|
||||
"""
|
||||
# Fiber parameters
|
||||
alpha0 = self.fiber_params.alpha0(f_eval)
|
||||
beta2 = self.fiber_params.beta2
|
||||
beta3 = self.fiber_params.beta3
|
||||
f_ref_beta = self.fiber_params.f_ref_beta
|
||||
alpha0 = self.fiber.alpha0(f_eval)
|
||||
beta2 = self.fiber.params.beta2
|
||||
beta3 = self.fiber.params.beta3
|
||||
f_ref_beta = self.fiber.params.ref_frequency
|
||||
z = self.stimulated_raman_scattering.z
|
||||
frequency_rho = self.stimulated_raman_scattering.frequency
|
||||
rho_norm = self.stimulated_raman_scattering.rho * np.exp(np.abs(alpha0) * z / 2)
|
||||
if len(frequency_rho) == 1:
|
||||
rho_function = lambda f: rho_norm[0, :]
|
||||
def rho_function(f): return rho_norm[0, :]
|
||||
else:
|
||||
rho_function = interp1d(frequency_rho, rho_norm, axis=0, fill_value='extrapolate')
|
||||
rho_norm_pump = rho_function(pump_carrier.frequency)
|
||||
@@ -741,7 +605,7 @@ class NliSolver:
|
||||
integrand_f1 = np.zeros(len(f1_array))
|
||||
for f1_index, f1 in enumerate(f1_array):
|
||||
delta_beta = 4 * np.pi**2 * (f1 - f_eval) * (f2_array - f_eval) * \
|
||||
(beta2 + np.pi * beta3 * (f1 + f2_array - 2 * f_ref_beta))
|
||||
(beta2 + np.pi * beta3 * (f1 + f2_array - 2 * f_ref_beta))
|
||||
integrand_f2 = self._generalized_rho_nli(delta_beta, rho_norm_pump, z, alpha0)
|
||||
integrand_f1[f1_index] = 2 * np.trapz(integrand_f2, f2_array) # 2x since integrand_f2 is symmetric in f2
|
||||
generalized_psi = 0.5 * sum(integrand_f1) * pump_carrier.baud_rate
|
||||
@@ -752,15 +616,15 @@ class NliSolver:
|
||||
:return: generalized_psi
|
||||
"""
|
||||
# Fiber parameters
|
||||
alpha0 = self.fiber_params.alpha0(f_eval)
|
||||
beta2 = self.fiber_params.beta2
|
||||
beta3 = self.fiber_params.beta3
|
||||
f_ref_beta = self.fiber_params.f_ref_beta
|
||||
alpha0 = self.fiber.alpha0(f_eval)
|
||||
beta2 = self.fiber.params.beta2
|
||||
beta3 = self.fiber.params.beta3
|
||||
f_ref_beta = self.fiber.params.ref_frequency
|
||||
z = self.stimulated_raman_scattering.z
|
||||
frequency_rho = self.stimulated_raman_scattering.frequency
|
||||
rho_norm = self.stimulated_raman_scattering.rho * np.exp(np.abs(alpha0) * z / 2)
|
||||
if len(frequency_rho) == 1:
|
||||
rho_function = lambda f: rho_norm[0, :]
|
||||
def rho_function(f): return rho_norm[0, :]
|
||||
else:
|
||||
rho_function = interp1d(frequency_rho, rho_norm, axis=0, fill_value='extrapolate')
|
||||
rho_norm_pump = rho_function(pump_carrier.frequency)
|
||||
@@ -781,7 +645,7 @@ class NliSolver:
|
||||
ggg = psd1_sample * psd2 * psd3
|
||||
|
||||
delta_beta = 4 * np.pi**2 * (f1 - f_eval) * (f2_array - f_eval) * \
|
||||
(beta2 + np.pi * beta3 * (f1 + f2_array - 2 * f_ref_beta))
|
||||
(beta2 + np.pi * beta3 * (f1 + f2_array - 2 * f_ref_beta))
|
||||
|
||||
integrand_f2 = ggg * self._generalized_rho_nli(delta_beta, rho_norm_pump, z, alpha0)
|
||||
integrand_f1[f1_index] = np.trapz(integrand_f2, f2_array)
|
||||
@@ -803,18 +667,66 @@ class NliSolver:
|
||||
beta2_ref = 21.3e-27
|
||||
delta_f_ref = 50e9
|
||||
rs_ref = 32e9
|
||||
freq_offset_th = ((k_ref * delta_f_ref) * rs_ref * beta2_ref) / (self.fiber_params.beta2 * symbol_rate)
|
||||
beta2 = abs(self.fiber.params.beta2)
|
||||
freq_offset_th = ((k_ref * delta_f_ref) * rs_ref * beta2_ref) / (beta2 * symbol_rate)
|
||||
return freq_offset_th
|
||||
|
||||
|
||||
def _psi(carrier, interfering_carrier, beta2, asymptotic_length):
|
||||
"""Calculates eq. 123 from `arXiv:1209.0394 <https://arxiv.org/abs/1209.0394>`__"""
|
||||
|
||||
if carrier.channel_number == interfering_carrier.channel_number: # SCI, SPM
|
||||
if carrier.channel_number == interfering_carrier.channel_number: # SCI, SPM
|
||||
psi = np.arcsinh(0.5 * np.pi**2 * asymptotic_length * abs(beta2) * carrier.baud_rate**2)
|
||||
else: # XCI, XPM
|
||||
else: # XCI, XPM
|
||||
delta_f = carrier.frequency - interfering_carrier.frequency
|
||||
psi = np.arcsinh(np.pi**2 * asymptotic_length * abs(beta2) *
|
||||
carrier.baud_rate * (delta_f + 0.5 * interfering_carrier.baud_rate))
|
||||
psi -= np.arcsinh(np.pi**2 * asymptotic_length * abs(beta2) *
|
||||
carrier.baud_rate * (delta_f - 0.5 * interfering_carrier.baud_rate))
|
||||
return psi
|
||||
|
||||
|
||||
def estimate_nf_model(type_variety, gain_min, gain_max, nf_min, nf_max):
|
||||
if nf_min < -10:
|
||||
raise EquipmentConfigError(f'Invalid nf_min value {nf_min!r} for amplifier {type_variety}')
|
||||
if nf_max < -10:
|
||||
raise EquipmentConfigError(f'Invalid nf_max value {nf_max!r} for amplifier {type_variety}')
|
||||
|
||||
# NF estimation model based on nf_min and nf_max
|
||||
# delta_p: max power dB difference between first and second stage coils
|
||||
# dB g1a: first stage gain - internal VOA attenuation
|
||||
# nf1, nf2: first and second stage coils
|
||||
# calculated by solving nf_{min,max} = nf1 + nf2 / g1a{min,max}
|
||||
delta_p = 5
|
||||
g1a_min = gain_min - (gain_max - gain_min) - delta_p
|
||||
g1a_max = gain_max - delta_p
|
||||
nf2 = lin2db((db2lin(nf_min) - db2lin(nf_max)) /
|
||||
(1 / db2lin(g1a_max) - 1 / db2lin(g1a_min)))
|
||||
nf1 = lin2db(db2lin(nf_min) - db2lin(nf2) / db2lin(g1a_max))
|
||||
|
||||
if nf1 < 4:
|
||||
raise EquipmentConfigError(f'First coil value too low {nf1} for amplifier {type_variety}')
|
||||
|
||||
# Check 1 dB < delta_p < 6 dB to ensure nf_min and nf_max values make sense.
|
||||
# There shouldn't be high nf differences between the two coils:
|
||||
# nf2 should be nf1 + 0.3 < nf2 < nf1 + 2
|
||||
# If not, recompute and check delta_p
|
||||
if not nf1 + 0.3 < nf2 < nf1 + 2:
|
||||
nf2 = np.clip(nf2, nf1 + 0.3, nf1 + 2)
|
||||
g1a_max = lin2db(db2lin(nf2) / (db2lin(nf_min) - db2lin(nf1)))
|
||||
delta_p = gain_max - g1a_max
|
||||
g1a_min = gain_min - (gain_max - gain_min) - delta_p
|
||||
if not 1 < delta_p < 11:
|
||||
raise EquipmentConfigError(f'Computed \N{greek capital letter delta}P invalid \
|
||||
\n 1st coil vs 2nd coil calculated DeltaP {delta_p:.2f} for \
|
||||
\n amplifier {type_variety} is not valid: revise inputs \
|
||||
\n calculated 1st coil NF = {nf1:.2f}, 2nd coil NF = {nf2:.2f}')
|
||||
# Check calculated values for nf1 and nf2
|
||||
calc_nf_min = lin2db(db2lin(nf1) + db2lin(nf2) / db2lin(g1a_max))
|
||||
if not isclose(nf_min, calc_nf_min, abs_tol=0.01):
|
||||
raise EquipmentConfigError(f'nf_min does not match calc_nf_min, {nf_min} vs {calc_nf_min} for amp {type_variety}')
|
||||
calc_nf_max = lin2db(db2lin(nf1) + db2lin(nf2) / db2lin(g1a_min))
|
||||
if not isclose(nf_max, calc_nf_max, abs_tol=0.01):
|
||||
raise EquipmentConfigError(f'nf_max does not match calc_nf_max, {nf_max} vs {calc_nf_max} for amp {type_variety}')
|
||||
|
||||
return nf1, nf2, delta_p
|
||||
|
||||
@@ -1,257 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
gnpy.core.service_sheet
|
||||
========================
|
||||
|
||||
XLS parser that can be called to create a JSON request file in accordance with
|
||||
Yang model for requesting path computation.
|
||||
|
||||
See: draft-ietf-teas-yang-path-computation-01.txt
|
||||
"""
|
||||
|
||||
from sys import exit
|
||||
try:
|
||||
from xlrd import open_workbook, XL_CELL_EMPTY
|
||||
except ModuleNotFoundError:
|
||||
exit('Required: `pip install xlrd`')
|
||||
from collections import namedtuple
|
||||
from logging import getLogger, basicConfig, CRITICAL, DEBUG, INFO
|
||||
from json import dumps
|
||||
from pathlib import Path
|
||||
from gnpy.core.equipment import load_equipment
|
||||
from gnpy.core.utils import db2lin, lin2db
|
||||
|
||||
SERVICES_COLUMN = 12
|
||||
#EQPT_LIBRARY_FILENAME = Path(__file__).parent / 'eqpt_config.json'
|
||||
|
||||
all_rows = lambda sheet, start=0: (sheet.row(x) for x in range(start, sheet.nrows))
|
||||
logger = getLogger(__name__)
|
||||
|
||||
# Type for input data
|
||||
class Request(namedtuple('Request', 'request_id source destination trx_type mode \
|
||||
spacing power nb_channel disjoint_from nodes_list is_loose path_bandwidth')):
|
||||
def __new__(cls, request_id, source, destination, trx_type, mode=None , spacing= None , power = None, nb_channel = None , disjoint_from ='' , nodes_list = None, is_loose = '', path_bandwidth = None):
|
||||
return super().__new__(cls, request_id, source, destination, trx_type, mode, spacing, power, nb_channel, disjoint_from, nodes_list, is_loose, path_bandwidth)
|
||||
|
||||
# Type for output data: // from dutc
|
||||
class Element:
|
||||
def __eq__(self, other):
|
||||
return type(self) == type(other) and self.uid == other.uid
|
||||
def __hash__(self):
|
||||
return hash((type(self), self.uid))
|
||||
|
||||
class Request_element(Element):
|
||||
def __init__(self,Request,eqpt_filename):
|
||||
# request_id is str
|
||||
# excel has automatic number formatting that adds .0 on integer values
|
||||
# the next lines recover the pure int value, assuming this .0 is unwanted
|
||||
self.request_id = correct_xlrd_int_to_str_reading(Request.request_id)
|
||||
self.source = f'trx {Request.source}'
|
||||
self.destination = f'trx {Request.destination}'
|
||||
# TODO: the automatic naming generated by excel parser requires that source and dest name
|
||||
# be a string starting with 'trx' : this is manually added here.
|
||||
self.srctpid = f'trx {Request.source}'
|
||||
self.dsttpid = f'trx {Request.destination}'
|
||||
# test that trx_type belongs to eqpt_config.json
|
||||
# if not replace it with a default
|
||||
equipment = load_equipment(eqpt_filename)
|
||||
try :
|
||||
if equipment['Transceiver'][Request.trx_type]:
|
||||
self.trx_type = correct_xlrd_int_to_str_reading(Request.trx_type)
|
||||
if Request.mode is not None :
|
||||
Requestmode = correct_xlrd_int_to_str_reading(Request.mode)
|
||||
if [mode for mode in equipment['Transceiver'][Request.trx_type].mode if mode['format'] == Requestmode]:
|
||||
self.mode = Requestmode
|
||||
else :
|
||||
msg = f'Request Id: {self.request_id} - could not find tsp : \'{Request.trx_type}\' with mode: \'{Requestmode}\' in eqpt library \nComputation stopped.'
|
||||
#print(msg)
|
||||
logger.critical(msg)
|
||||
exit(1)
|
||||
else:
|
||||
Requestmode = None
|
||||
self.mode = Request.mode
|
||||
except KeyError:
|
||||
msg = f'Request Id: {self.request_id} - could not find tsp : \'{Request.trx_type}\' with mode: \'{Request.mode}\' in eqpt library \nComputation stopped.'
|
||||
#print(msg)
|
||||
logger.critical(msg)
|
||||
exit()
|
||||
# excel input are in GHz and dBm
|
||||
if Request.spacing is not None:
|
||||
self.spacing = Request.spacing * 1e9
|
||||
else:
|
||||
msg = f'Request {self.request_id} missing spacing: spacing is mandatory.\ncomputation stopped'
|
||||
logger.critical(msg)
|
||||
exit()
|
||||
if Request.power is not None:
|
||||
self.power = db2lin(Request.power) * 1e-3
|
||||
else:
|
||||
self.power = None
|
||||
if Request.nb_channel is not None :
|
||||
self.nb_channel = int(Request.nb_channel)
|
||||
else:
|
||||
self.nb_channel = None
|
||||
|
||||
value = correct_xlrd_int_to_str_reading(Request.disjoint_from)
|
||||
self.disjoint_from = [n for n in value.split(' | ') if value]
|
||||
self.nodes_list = []
|
||||
if Request.nodes_list :
|
||||
self.nodes_list = Request.nodes_list.split(' | ')
|
||||
|
||||
# cleaning the list of nodes to remove source and destination
|
||||
# (because the remaining of the program assumes that the nodes list are nodes
|
||||
# on the path and should not include source and destination)
|
||||
try :
|
||||
self.nodes_list.remove(self.source)
|
||||
msg = f'{self.source} removed from explicit path node-list'
|
||||
logger.info(msg)
|
||||
except ValueError:
|
||||
msg = f'{self.source} already removed from explicit path node-list'
|
||||
logger.info(msg)
|
||||
|
||||
try :
|
||||
self.nodes_list.remove(self.destination)
|
||||
msg = f'{self.destination} removed from explicit path node-list'
|
||||
logger.info(msg)
|
||||
except ValueError:
|
||||
msg = f'{self.destination} already removed from explicit path node-list'
|
||||
logger.info(msg)
|
||||
|
||||
# the excel parser applies the same hop-type to all nodes in the route nodes_list.
|
||||
# user can change this per node in the generated json
|
||||
self.loose = 'LOOSE'
|
||||
if Request.is_loose == 'no' :
|
||||
self.loose = 'STRICT'
|
||||
self.path_bandwidth = None
|
||||
if Request.path_bandwidth is not None:
|
||||
self.path_bandwidth = Request.path_bandwidth * 1e9
|
||||
else:
|
||||
self.path_bandwidth = 0
|
||||
|
||||
uid = property(lambda self: repr(self))
|
||||
@property
|
||||
def pathrequest(self):
|
||||
|
||||
req_dictionnary = {
|
||||
'request-id':self.request_id,
|
||||
'source': self.source,
|
||||
'destination': self.destination,
|
||||
'src-tp-id': self.srctpid,
|
||||
'dst-tp-id': self.dsttpid,
|
||||
'path-constraints':{
|
||||
'te-bandwidth': {
|
||||
'technology': 'flexi-grid',
|
||||
'trx_type' : self.trx_type,
|
||||
'trx_mode' : self.mode,
|
||||
'effective-freq-slot':[{'N': 'null', 'M': 'null'}],
|
||||
'spacing' : self.spacing,
|
||||
'max-nb-of-channel' : self.nb_channel,
|
||||
'output-power' : self.power
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.nodes_list:
|
||||
req_dictionnary['explicit-route-objects'] = {}
|
||||
temp = {'route-object-include-exclude' : [
|
||||
{'explicit-route-usage': 'route-include-ero',
|
||||
'index': self.nodes_list.index(node),
|
||||
'num-unnum-hop': {
|
||||
'node-id': f'{node}',
|
||||
'link-tp-id': 'link-tp-id is not used',
|
||||
'hop-type': f'{self.loose}',
|
||||
}
|
||||
}
|
||||
for node in self.nodes_list]
|
||||
}
|
||||
req_dictionnary['explicit-route-objects'] = temp
|
||||
if self.path_bandwidth is not None:
|
||||
req_dictionnary['path-constraints']['te-bandwidth']['path_bandwidth'] = self.path_bandwidth
|
||||
|
||||
return req_dictionnary
|
||||
@property
|
||||
def pathsync(self):
|
||||
if self.disjoint_from :
|
||||
return {'synchronization-id':self.request_id,
|
||||
'svec': {
|
||||
'relaxable' : 'false',
|
||||
'disjointness': 'node link',
|
||||
'request-id-number': [self.request_id]+ [n for n in self.disjoint_from]
|
||||
}
|
||||
}
|
||||
else:
|
||||
return None
|
||||
# TO-DO: avoid multiple entries with same synchronisation vectors
|
||||
@property
|
||||
def json(self):
|
||||
return self.pathrequest , self.pathsync
|
||||
|
||||
def convert_service_sheet(input_filename, eqpt_filename, output_filename='', filter_region=[]):
|
||||
service = parse_excel(input_filename)
|
||||
req = [Request_element(n,eqpt_filename) for n in service]
|
||||
# dumps the output into a json file with name
|
||||
# split_filename = [input_filename[0:len(input_filename)-len(suffix_filename)] , suffix_filename[1:]]
|
||||
if output_filename=='':
|
||||
output_filename = f'{str(input_filename)[0:len(str(input_filename))-len(str(input_filename.suffixes[0]))]}_services.json'
|
||||
# for debug
|
||||
# print(json_filename)
|
||||
# if there is no sync vector , do not write any synchronization
|
||||
synchro = [n.json[1] for n in req if n.json[1] is not None]
|
||||
if synchro:
|
||||
data = {
|
||||
'path-request': [n.json[0] for n in req],
|
||||
'synchronization': synchro
|
||||
}
|
||||
else:
|
||||
data = {
|
||||
'path-request': [n.json[0] for n in req]
|
||||
}
|
||||
with open(output_filename, 'w', encoding='utf-8') as f:
|
||||
f.write(dumps(data, indent=2, ensure_ascii=False))
|
||||
return data
|
||||
|
||||
def correct_xlrd_int_to_str_reading(v) :
|
||||
if not isinstance(v,str):
|
||||
value = str(int(v))
|
||||
if value.endswith('.0'):
|
||||
value = value[:-2]
|
||||
else:
|
||||
value = v
|
||||
return value
|
||||
|
||||
# to be used from dutc
|
||||
def parse_row(row, fieldnames):
|
||||
return {f: r.value for f, r in zip(fieldnames, row[0:SERVICES_COLUMN])
|
||||
if r.ctype != XL_CELL_EMPTY}
|
||||
#
|
||||
|
||||
def parse_excel(input_filename):
|
||||
with open_workbook(input_filename) as wb:
|
||||
service_sheet = wb.sheet_by_name('Service')
|
||||
services = list(parse_service_sheet(service_sheet))
|
||||
return services
|
||||
|
||||
def parse_service_sheet(service_sheet):
|
||||
logger.info(f'Validating headers on {service_sheet.name!r}')
|
||||
# add a test on field to enable the '' field case that arises when columns on the
|
||||
# right hand side are used as comments or drawing in the excel sheet
|
||||
header = [x.value.strip() for x in service_sheet.row(4)[0:SERVICES_COLUMN] if len(x.value.strip())>0]
|
||||
|
||||
# create a service_fieldname independant from the excel column order
|
||||
# to be compatible with any version of the sheet
|
||||
# the following dictionnary records the excel field names and the corresponding parameter's name
|
||||
|
||||
authorized_fieldnames = {'route id':'request_id', 'Source':'source', 'Destination':'destination', \
|
||||
'TRX type':'trx_type', 'Mode' : 'mode', 'System: spacing':'spacing', \
|
||||
'System: input power (dBm)':'power', 'System: nb of channels':'nb_channel',\
|
||||
'routing: disjoint from': 'disjoint_from', 'routing: path':'nodes_list',\
|
||||
'routing: is loose?':'is_loose', 'path bandwidth':'path_bandwidth'}
|
||||
try :
|
||||
service_fieldnames = [authorized_fieldnames[e] for e in header]
|
||||
except KeyError:
|
||||
msg = f'Malformed header on Service sheet: {header} field not in {authorized_fieldnames}'
|
||||
logger.critical(msg)
|
||||
raise ValueError(msg)
|
||||
for row in all_rows(service_sheet, start=5):
|
||||
yield Request(**parse_row(row[0:SERVICES_COLUMN], service_fieldnames))
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
UNITS = {'m': 1,
|
||||
'km': 1E3}
|
||||
@@ -9,30 +9,19 @@ This module contains utility functions that are used with gnpy.
|
||||
'''
|
||||
|
||||
|
||||
import json
|
||||
|
||||
from csv import writer
|
||||
import numpy as np
|
||||
from numpy import pi, cos, sqrt, log10
|
||||
from scipy import constants
|
||||
from gnpy.core.exceptions import ConfigurationError
|
||||
|
||||
|
||||
def load_json(filename):
|
||||
with open(filename, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
return data
|
||||
|
||||
|
||||
def save_json(obj, filename):
|
||||
with open(filename, 'w', encoding='utf-8') as f:
|
||||
json.dump(obj, f, indent=2, ensure_ascii=False)
|
||||
|
||||
def write_csv(obj, filename):
|
||||
"""
|
||||
convert dictionary items to a csv file
|
||||
the dictionary format :
|
||||
Convert dictionary items to a CSV file the dictionary format:
|
||||
::
|
||||
|
||||
{'result category 1':
|
||||
{'result category 1':
|
||||
[
|
||||
# 1st line of results
|
||||
{'header 1' : value_xxx,
|
||||
@@ -41,66 +30,83 @@ def write_csv(obj, filename):
|
||||
{'header 1' : value_www,
|
||||
'header 2' : value_zzz}
|
||||
],
|
||||
'result_category 2':
|
||||
'result_category 2':
|
||||
[
|
||||
{},{}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
the generated csv file will be:
|
||||
result_category 1
|
||||
header 1 header 2
|
||||
value_xxx value_yyy
|
||||
value_www value_zzz
|
||||
result_category 2
|
||||
...
|
||||
The generated csv file will be:
|
||||
::
|
||||
|
||||
result_category 1
|
||||
header 1 header 2
|
||||
value_xxx value_yyy
|
||||
value_www value_zzz
|
||||
result_category 2
|
||||
...
|
||||
"""
|
||||
with open(filename, 'w', encoding='utf-8') as f:
|
||||
w = writer(f)
|
||||
for data_key, data_list in obj.items():
|
||||
#main header
|
||||
# main header
|
||||
w.writerow([data_key])
|
||||
#sub headers:
|
||||
# sub headers:
|
||||
headers = [_ for _ in data_list[0].keys()]
|
||||
w.writerow(headers)
|
||||
for data_dict in data_list:
|
||||
w.writerow([_ for _ in data_dict.values()])
|
||||
|
||||
def c():
|
||||
"""
|
||||
Returns the speed of light in meters per second
|
||||
"""
|
||||
return constants.c
|
||||
|
||||
|
||||
def arrange_frequencies(length, start, stop):
|
||||
"""Create an array of frequencies
|
||||
|
||||
:param length: number of elements
|
||||
:param star: Start frequency in THz
|
||||
:param start: Start frequency in THz
|
||||
:param stop: Stop frequency in THz
|
||||
:type length: integer
|
||||
:type start: float
|
||||
:type stop: float
|
||||
:return an array of frequencies determined by the spacing parameter
|
||||
:return: an array of frequencies determined by the spacing parameter
|
||||
:rtype: numpy.ndarray
|
||||
"""
|
||||
return np.linspace(start, stop, length)
|
||||
|
||||
def h():
|
||||
"""
|
||||
Returns plank's constant in J*s
|
||||
"""
|
||||
return constants.h
|
||||
|
||||
|
||||
def lin2db(value):
|
||||
"""Convert linear unit to logarithmic (dB)
|
||||
|
||||
>>> lin2db(0.001)
|
||||
-30.0
|
||||
>>> round(lin2db(1.0), 2)
|
||||
0.0
|
||||
>>> round(lin2db(1.26), 2)
|
||||
1.0
|
||||
>>> round(lin2db(10.0), 2)
|
||||
10.0
|
||||
>>> round(lin2db(100.0), 2)
|
||||
20.0
|
||||
"""
|
||||
return 10 * log10(value)
|
||||
|
||||
|
||||
def db2lin(value):
|
||||
"""Convert logarithimic units to linear
|
||||
|
||||
>>> round(db2lin(10.0), 2)
|
||||
10.0
|
||||
>>> round(db2lin(20.0), 2)
|
||||
100.0
|
||||
>>> round(db2lin(1.0), 2)
|
||||
1.26
|
||||
>>> round(db2lin(0.0), 2)
|
||||
1.0
|
||||
>>> round(db2lin(-10.0), 2)
|
||||
0.1
|
||||
"""
|
||||
return 10**(value / 10)
|
||||
|
||||
|
||||
def round2float(number, step):
|
||||
step = round(step, 1)
|
||||
if step >= 0.01:
|
||||
@@ -110,19 +116,28 @@ def round2float(number, step):
|
||||
number = round(number, 2)
|
||||
return number
|
||||
|
||||
|
||||
wavelength2freq = constants.lambda2nu
|
||||
freq2wavelength = constants.nu2lambda
|
||||
|
||||
|
||||
def freq2wavelength(value):
|
||||
""" Converts frequency units to wavelength units.
|
||||
|
||||
>>> round(freq2wavelength(191.35e12) * 1e9, 3)
|
||||
1566.723
|
||||
>>> round(freq2wavelength(196.1e12) * 1e9, 3)
|
||||
1528.773
|
||||
"""
|
||||
return c() / value
|
||||
return constants.c / value
|
||||
|
||||
|
||||
def snr_sum(snr, bw, snr_added, bw_added=12.5e9):
|
||||
snr_added = snr_added - lin2db(bw/bw_added)
|
||||
snr = -lin2db(db2lin(-snr)+db2lin(-snr_added))
|
||||
snr_added = snr_added - lin2db(bw / bw_added)
|
||||
snr = -lin2db(db2lin(-snr) + db2lin(-snr_added))
|
||||
return snr
|
||||
|
||||
|
||||
def deltawl2deltaf(delta_wl, wavelength):
|
||||
""" deltawl2deltaf(delta_wl, wavelength):
|
||||
delta_wl is BW in wavelength units
|
||||
@@ -184,6 +199,7 @@ def rrc(ffs, baud_rate, alpha):
|
||||
hf[p_inds] = 1
|
||||
return sqrt(hf)
|
||||
|
||||
|
||||
def merge_amplifier_restrictions(dict1, dict2):
|
||||
"""Updates contents of dicts recursively
|
||||
|
||||
@@ -206,6 +222,7 @@ def merge_amplifier_restrictions(dict1, dict2):
|
||||
copy_dict1[key] = dict2[key]
|
||||
return copy_dict1
|
||||
|
||||
|
||||
def silent_remove(this_list, elem):
|
||||
"""Remove matching elements from a list without raising ValueError
|
||||
|
||||
@@ -223,3 +240,59 @@ def silent_remove(this_list, elem):
|
||||
except ValueError:
|
||||
pass
|
||||
return this_list
|
||||
|
||||
|
||||
def automatic_nch(f_min, f_max, spacing):
|
||||
"""How many channels are available in the spectrum
|
||||
|
||||
:param f_min Lowest frequenecy [Hz]
|
||||
:param f_max Highest frequency [Hz]
|
||||
:param spacing Channel width [Hz]
|
||||
:return Number of uniform channels
|
||||
|
||||
>>> automatic_nch(191.325e12, 196.125e12, 50e9)
|
||||
96
|
||||
>>> automatic_nch(193.475e12, 193.525e12, 50e9)
|
||||
1
|
||||
"""
|
||||
return int((f_max - f_min) // spacing)
|
||||
|
||||
|
||||
def automatic_fmax(f_min, spacing, nch):
|
||||
"""Find the high-frequenecy boundary of a spectrum
|
||||
|
||||
:param f_min Start of the spectrum (lowest frequency edge) [Hz]
|
||||
:param spacing Grid/channel spacing [Hz]
|
||||
:param nch Number of channels
|
||||
:return End of the spectrum (highest frequency) [Hz]
|
||||
|
||||
>>> automatic_fmax(191.325e12, 50e9, 96)
|
||||
196125000000000.0
|
||||
"""
|
||||
return f_min + spacing * nch
|
||||
|
||||
|
||||
def convert_length(value, units):
|
||||
"""Convert length into basic SI units
|
||||
|
||||
>>> convert_length(1, 'km')
|
||||
1000.0
|
||||
>>> convert_length(2.0, 'km')
|
||||
2000.0
|
||||
>>> convert_length(123, 'm')
|
||||
123.0
|
||||
>>> convert_length(123.0, 'm')
|
||||
123.0
|
||||
>>> convert_length(42.1, 'km')
|
||||
42100.0
|
||||
>>> convert_length(666, 'yards')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
gnpy.core.exceptions.ConfigurationError: Cannot convert length in "yards" into meters
|
||||
"""
|
||||
if units == 'm':
|
||||
return value * 1e0
|
||||
elif units == 'km':
|
||||
return value * 1e3
|
||||
else:
|
||||
raise ConfigurationError(f'Cannot convert length in "{units}" into meters')
|
||||
|
||||
@@ -11,20 +11,22 @@ If not present in the "Nodes" sheet, the "Type" column will be implicitly
|
||||
determined based on the topology.
|
||||
"""
|
||||
|
||||
try:
|
||||
from xlrd import open_workbook
|
||||
except ModuleNotFoundError:
|
||||
exit('Required: `pip install xlrd`')
|
||||
from xlrd import open_workbook
|
||||
from argparse import ArgumentParser
|
||||
|
||||
PARSER = ArgumentParser()
|
||||
PARSER.add_argument('workbook', nargs='?', default='meshTopologyExampleV2.xls',
|
||||
help='create the mandatory columns in Eqpt sheet')
|
||||
ALL_ROWS = lambda sh, start=0: (sh.row(x) for x in range(start, sh.nrows))
|
||||
|
||||
|
||||
def ALL_ROWS(sh, start=0):
|
||||
return (sh.row(x) for x in range(start, sh.nrows))
|
||||
|
||||
|
||||
class Node:
|
||||
""" Node element contains uid, list of connected nodes and eqpt type
|
||||
"""
|
||||
|
||||
def __init__(self, uid, to_node):
|
||||
self.uid = uid
|
||||
self.to_node = to_node
|
||||
@@ -36,6 +38,7 @@ class Node:
|
||||
def __str__(self):
|
||||
return f'uid {self.uid} \nto_node {[node for node in self.to_node]}\neqpt {self.eqpt}\n'
|
||||
|
||||
|
||||
def read_excel(input_filename):
|
||||
""" read excel Nodes and Links sheets and create a dict of nodes with
|
||||
their to_nodes and type of eqpt
|
||||
@@ -73,6 +76,7 @@ def read_excel(input_filename):
|
||||
exit()
|
||||
return nodes
|
||||
|
||||
|
||||
def create_eqt_template(nodes, input_filename):
|
||||
""" writes list of node A node Z corresponding to Nodes and Links sheets in order
|
||||
to help user populating Eqpt
|
||||
@@ -85,7 +89,6 @@ def create_eqt_template(nodes, input_filename):
|
||||
\nNode A \tNode Z \tamp type \tatt_in \tamp gain \ttilt \tatt_out\
|
||||
amp type \tatt_in \tamp gain \ttilt \tatt_out\n')
|
||||
|
||||
|
||||
for node in nodes.values():
|
||||
if node.eqpt == 'ILA':
|
||||
my_file.write(f'{node.uid}\t{node.to_node[0]}\n')
|
||||
@@ -93,8 +96,8 @@ def create_eqt_template(nodes, input_filename):
|
||||
for to_node in node.to_node:
|
||||
my_file.write(f'{node.uid}\t{to_node}\n')
|
||||
|
||||
print(f'File {output_filename} successfully created with Node A - Node Z ' +
|
||||
' entries for Eqpt sheet in excel file.')
|
||||
print(f'File {output_filename} successfully created with Node A - Node Z entries for Eqpt sheet in excel file.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
ARGS = PARSER.parse_args()
|
||||
@@ -1,198 +1,8 @@
|
||||
{
|
||||
"nf_ripple": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"gain_ripple": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"dgt": [
|
||||
@@ -293,4 +103,4 @@
|
||||
1.017807767853702,
|
||||
1.0
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ Amplifier models and configuration
|
||||
|
||||
Equipment description defines equipment types and parameters.
|
||||
It takes place in the default **eqpt_config.json** file.
|
||||
By default **transmission_main_example.py** uses **eqpt_config.json** file and that
|
||||
By default **gnpy-transmission-example** uses **eqpt_config.json** file and that
|
||||
can be changed with **-e** or **--equipment** command line parameter.
|
||||
|
||||
2. Amplifier parameters and subtypes
|
||||
@@ -266,7 +266,7 @@ In an opensource and multi-vendor environnement, it is needed to support differe
|
||||
4. advanced_config_from_json
|
||||
#######################################
|
||||
|
||||
The build_oa_json.py library in gnpy/examples/edfa_model can be used to build the json file required for the amplifier advanced_model type_def:
|
||||
The build_oa_json.py library in ``gnpy/example-data/edfa_model/`` can be used to build the json file required for the amplifier advanced_model type_def:
|
||||
|
||||
Update an existing json file with all the 96ch txt files for a given amplifier type
|
||||
amplifier type 'OA_type1' is hard coded but can be modified and other types added
|
||||
@@ -13,7 +13,6 @@ import re
|
||||
import sys
|
||||
import json
|
||||
import numpy as np
|
||||
from gnpy.core.utils import lin2db, db2lin
|
||||
|
||||
"""amplifier file names
|
||||
convert a set of amplifier files + input json definiton file into a valid edfa_json_file:
|
||||
@@ -28,60 +27,63 @@ input json file in argument (defult = 'OA.json')
|
||||
the json input file should have the following fields:
|
||||
{
|
||||
"nf_fit_coeff": "nf_filename.txt",
|
||||
"nf_ripple": "nf_ripple_filename.txt",
|
||||
"nf_ripple": "nf_ripple_filename.txt",
|
||||
"gain_ripple": "DFG_filename.txt",
|
||||
"dgt": "DGT_filename.txt",
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
input_json_file_name = "OA.json" #default path
|
||||
input_json_file_name = "OA.json" # default path
|
||||
output_json_file_name = "default_edfa_config.json"
|
||||
gain_ripple_field = "gain_ripple"
|
||||
nf_ripple_field = "nf_ripple"
|
||||
nf_fit_coeff = "nf_fit_coeff"
|
||||
|
||||
|
||||
def read_file(field, file_name):
|
||||
"""read and format the 96 channels txt files describing the amplifier NF and ripple
|
||||
convert dfg into gain ripple by removing the mean component
|
||||
"""
|
||||
|
||||
#with open(path + file_name,'r') as this_file:
|
||||
# with open(path + file_name,'r') as this_file:
|
||||
# data = this_file.read()
|
||||
#data.strip()
|
||||
# data.strip()
|
||||
#data = re.sub(r"([0-9])([ ]{1,3})([0-9-+])",r"\1,\3",data)
|
||||
#data = list(data.split(","))
|
||||
#data = [float(x) for x in data]
|
||||
data = np.loadtxt(file_name)
|
||||
print(len(data), file_name)
|
||||
if field == gain_ripple_field or field == nf_ripple_field:
|
||||
#consider ripple excursion only to avoid redundant information
|
||||
#because the max flat_gain is already given by the 'gain_flat' field in json
|
||||
#remove the mean component
|
||||
# consider ripple excursion only to avoid redundant information
|
||||
# because the max flat_gain is already given by the 'gain_flat' field in json
|
||||
# remove the mean component
|
||||
print(file_name, ', mean value =', data.mean(), ' is substracted')
|
||||
data = data - data.mean()
|
||||
data = data.tolist()
|
||||
return data
|
||||
|
||||
|
||||
def input_json(path):
|
||||
"""read the json input file and add all the 96 channels txt files
|
||||
create the output json file with output_json_file_name"""
|
||||
with open(path,'r') as edfa_json_file:
|
||||
with open(path, 'r') as edfa_json_file:
|
||||
amp_text = edfa_json_file.read()
|
||||
amp_dict = json.loads(amp_text)
|
||||
|
||||
for k, v in amp_dict.items():
|
||||
if re.search(r'.txt$',str(v)) :
|
||||
if re.search(r'.txt$', str(v)):
|
||||
amp_dict[k] = read_file(k, v)
|
||||
|
||||
amp_text = json.dumps(amp_dict, indent=4)
|
||||
#print(amp_text)
|
||||
with open(output_json_file_name,'w') as edfa_json_file:
|
||||
# print(amp_text)
|
||||
with open(output_json_file_name, 'w') as edfa_json_file:
|
||||
edfa_json_file.write(amp_text)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) == 2:
|
||||
path = sys.argv[1]
|
||||
else:
|
||||
path = input_json_file_name
|
||||
input_json(path)
|
||||
input_json(path)
|
||||
@@ -146,23 +146,27 @@
|
||||
"Fiber":[{
|
||||
"type_variety": "SSMF",
|
||||
"dispersion": 1.67e-05,
|
||||
"gamma": 0.00127
|
||||
"gamma": 0.00127,
|
||||
"pmd_coef": 1.265e-15
|
||||
},
|
||||
{
|
||||
"type_variety": "NZDF",
|
||||
"dispersion": 0.5e-05,
|
||||
"gamma": 0.00146
|
||||
"gamma": 0.00146,
|
||||
"pmd_coef": 1.265e-15
|
||||
},
|
||||
{
|
||||
"type_variety": "LOF",
|
||||
"dispersion": 2.2e-05,
|
||||
"gamma": 0.000843
|
||||
"gamma": 0.000843,
|
||||
"pmd_coef": 1.265e-15
|
||||
}
|
||||
],
|
||||
"RamanFiber":[{
|
||||
"type_variety": "SSMF",
|
||||
"dispersion": 1.67e-05,
|
||||
"gamma": 0.00127,
|
||||
"pmd_coef": 1.265e-15,
|
||||
"raman_efficiency": {
|
||||
"cr":[
|
||||
0, 9.4E-06, 2.92E-05, 4.88E-05, 6.82E-05, 8.31E-05, 9.4E-05, 0.0001014, 0.0001069, 0.0001119,
|
||||
@@ -206,6 +210,7 @@
|
||||
"Roadm":[{
|
||||
"target_pch_out_db": -20,
|
||||
"add_drop_osnr": 38,
|
||||
"pmd": 0,
|
||||
"restrictions": {
|
||||
"preamp_variety_list":[],
|
||||
"booster_variety_list":[]
|
||||
@@ -643,44 +643,6 @@
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"uid": "east edfa in Corlay to Loudeac",
|
||||
"metadata": {
|
||||
"location": {
|
||||
"city": "Corlay",
|
||||
"region": "RLD",
|
||||
"latitude": 2.0,
|
||||
"longitude": 1.0
|
||||
}
|
||||
},
|
||||
"type": "Edfa",
|
||||
"type_variety": "std_low_gain",
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": 1.0,
|
||||
"tilt_target": 0,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"uid": "east edfa in Loudeac to Lorient_KMA",
|
||||
"metadata": {
|
||||
"location": {
|
||||
"city": "Loudeac",
|
||||
"region": "RLD",
|
||||
"latitude": 2.0,
|
||||
"longitude": 2.0
|
||||
}
|
||||
},
|
||||
"type": "Edfa",
|
||||
"type_variety": "std_low_gain",
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": 1.0,
|
||||
"tilt_target": 0,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"uid": "east edfa in Lannion_CAS to Stbrieuc",
|
||||
"metadata": {
|
||||
@@ -833,44 +795,6 @@
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"uid": "west edfa in Corlay to Loudeac",
|
||||
"metadata": {
|
||||
"location": {
|
||||
"city": "Corlay",
|
||||
"region": "RLD",
|
||||
"latitude": 2.0,
|
||||
"longitude": 1.0
|
||||
}
|
||||
},
|
||||
"type": "Edfa",
|
||||
"type_variety": "std_low_gain",
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": 1.0,
|
||||
"tilt_target": 0,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"uid": "west edfa in Loudeac to Lorient_KMA",
|
||||
"metadata": {
|
||||
"location": {
|
||||
"city": "Loudeac",
|
||||
"region": "RLD",
|
||||
"latitude": 2.0,
|
||||
"longitude": 2.0
|
||||
}
|
||||
},
|
||||
"type": "Edfa",
|
||||
"type_variety": "std_low_gain",
|
||||
"operational": {
|
||||
"gain_target": null,
|
||||
"delta_p": 1.0,
|
||||
"tilt_target": 0,
|
||||
"out_voa": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"uid": "west edfa in Lorient_KMA to Vannes_KBE",
|
||||
"metadata": {
|
||||
Binary file not shown.
@@ -6,6 +6,7 @@
|
||||
"destination": "trx Vannes_KBE",
|
||||
"src-tp-id": "trx Lorient_KMA",
|
||||
"dst-tp-id": "trx Vannes_KBE",
|
||||
"bidirectional": false,
|
||||
"path-constraints": {
|
||||
"te-bandwidth": {
|
||||
"technology": "flexi-grid",
|
||||
@@ -13,8 +14,8 @@
|
||||
"trx_mode": null,
|
||||
"effective-freq-slot": [
|
||||
{
|
||||
"N": "null",
|
||||
"M": "null"
|
||||
"N": null,
|
||||
"M": null
|
||||
}
|
||||
],
|
||||
"spacing": 50000000000.0,
|
||||
@@ -30,6 +31,7 @@
|
||||
"destination": "trx Vannes_KBE",
|
||||
"src-tp-id": "trx Brest_KLA",
|
||||
"dst-tp-id": "trx Vannes_KBE",
|
||||
"bidirectional": false,
|
||||
"path-constraints": {
|
||||
"te-bandwidth": {
|
||||
"technology": "flexi-grid",
|
||||
@@ -37,8 +39,8 @@
|
||||
"trx_mode": "mode 1",
|
||||
"effective-freq-slot": [
|
||||
{
|
||||
"N": "null",
|
||||
"M": "null"
|
||||
"N": null,
|
||||
"M": null
|
||||
}
|
||||
],
|
||||
"spacing": 50000000000.0,
|
||||
@@ -94,6 +96,7 @@
|
||||
"destination": "trx Rennes_STA",
|
||||
"src-tp-id": "trx Lannion_CAS",
|
||||
"dst-tp-id": "trx Rennes_STA",
|
||||
"bidirectional": false,
|
||||
"path-constraints": {
|
||||
"te-bandwidth": {
|
||||
"technology": "flexi-grid",
|
||||
@@ -101,8 +104,8 @@
|
||||
"trx_mode": "mode 1",
|
||||
"effective-freq-slot": [
|
||||
{
|
||||
"N": "null",
|
||||
"M": "null"
|
||||
"N": null,
|
||||
"M": null
|
||||
}
|
||||
],
|
||||
"spacing": 50000000000.0,
|
||||
@@ -118,6 +121,7 @@
|
||||
"destination": "trx Lannion_CAS",
|
||||
"src-tp-id": "trx Rennes_STA",
|
||||
"dst-tp-id": "trx Lannion_CAS",
|
||||
"bidirectional": false,
|
||||
"path-constraints": {
|
||||
"te-bandwidth": {
|
||||
"technology": "flexi-grid",
|
||||
@@ -125,8 +129,8 @@
|
||||
"trx_mode": null,
|
||||
"effective-freq-slot": [
|
||||
{
|
||||
"N": "null",
|
||||
"M": "null"
|
||||
"N": null,
|
||||
"M": null
|
||||
}
|
||||
],
|
||||
"spacing": 75000000000.0,
|
||||
@@ -142,6 +146,7 @@
|
||||
"destination": "trx Lannion_CAS",
|
||||
"src-tp-id": "trx Rennes_STA",
|
||||
"dst-tp-id": "trx Lannion_CAS",
|
||||
"bidirectional": false,
|
||||
"path-constraints": {
|
||||
"te-bandwidth": {
|
||||
"technology": "flexi-grid",
|
||||
@@ -149,8 +154,8 @@
|
||||
"trx_mode": "mode 2",
|
||||
"effective-freq-slot": [
|
||||
{
|
||||
"N": "null",
|
||||
"M": "null"
|
||||
"N": null,
|
||||
"M": null
|
||||
}
|
||||
],
|
||||
"spacing": 75000000000.0,
|
||||
@@ -166,6 +171,7 @@
|
||||
"destination": "trx Lorient_KMA",
|
||||
"src-tp-id": "trx Lannion_CAS",
|
||||
"dst-tp-id": "trx Lorient_KMA",
|
||||
"bidirectional": false,
|
||||
"path-constraints": {
|
||||
"te-bandwidth": {
|
||||
"technology": "flexi-grid",
|
||||
@@ -173,8 +179,8 @@
|
||||
"trx_mode": "mode 1",
|
||||
"effective-freq-slot": [
|
||||
{
|
||||
"N": "null",
|
||||
"M": "null"
|
||||
"N": null,
|
||||
"M": null
|
||||
}
|
||||
],
|
||||
"spacing": 50000000000.0,
|
||||
@@ -190,6 +196,7 @@
|
||||
"destination": "trx Lorient_KMA",
|
||||
"src-tp-id": "trx Lannion_CAS",
|
||||
"dst-tp-id": "trx Lorient_KMA",
|
||||
"bidirectional": false,
|
||||
"path-constraints": {
|
||||
"te-bandwidth": {
|
||||
"technology": "flexi-grid",
|
||||
@@ -197,8 +204,8 @@
|
||||
"trx_mode": "mode 1",
|
||||
"effective-freq-slot": [
|
||||
{
|
||||
"N": "null",
|
||||
"M": "null"
|
||||
"N": null,
|
||||
"M": null
|
||||
}
|
||||
],
|
||||
"spacing": 50000000000.0,
|
||||
@@ -214,6 +221,7 @@
|
||||
"destination": "trx Lorient_KMA",
|
||||
"src-tp-id": "trx Lannion_CAS",
|
||||
"dst-tp-id": "trx Lorient_KMA",
|
||||
"bidirectional": false,
|
||||
"path-constraints": {
|
||||
"te-bandwidth": {
|
||||
"technology": "flexi-grid",
|
||||
@@ -221,8 +229,8 @@
|
||||
"trx_mode": "mode 1",
|
||||
"effective-freq-slot": [
|
||||
{
|
||||
"N": "null",
|
||||
"M": "null"
|
||||
"N": null,
|
||||
"M": null
|
||||
}
|
||||
],
|
||||
"spacing": 75000000000.0,
|
||||
1831
gnpy/example-data/path_computation_request_api.json
Normal file
1831
gnpy/example-data/path_computation_request_api.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"raman_computed_channels": [1, 18, 37, 56, 75],
|
||||
"raman_parameters": {
|
||||
"flag_raman": true,
|
||||
"space_resolution": 10e3,
|
||||
@@ -9,6 +8,7 @@
|
||||
"nli_method_name": "ggn_spectrally_separated",
|
||||
"wdm_grid_size": 50e9,
|
||||
"dispersion_tolerance": 1,
|
||||
"phase_shift_tollerance": 0.1
|
||||
"phase_shift_tolerance": 0.1,
|
||||
"computed_channels": [1, 18, 37, 56, 75]
|
||||
}
|
||||
}
|
||||
@@ -14,14 +14,14 @@ See: draft-ietf-teas-yang-path-computation-01.txt
|
||||
from argparse import ArgumentParser
|
||||
from pathlib import Path
|
||||
from json import loads
|
||||
from gnpy.core.equipment import load_equipment
|
||||
from gnpy.core.request import jsontocsv
|
||||
from gnpy.tools.json_io import load_equipment
|
||||
from gnpy.topology.request import jsontocsv
|
||||
|
||||
|
||||
parser = ArgumentParser(description = 'A function that writes json path results in an excel sheet.')
|
||||
parser.add_argument('filename', nargs='?', type = Path)
|
||||
parser.add_argument('output_filename', nargs='?', type = Path)
|
||||
parser.add_argument('eqpt_filename', nargs='?', type = Path, default=Path(__file__).parent / 'eqpt_config.json')
|
||||
parser = ArgumentParser(description='A function that writes json path results in an excel sheet.')
|
||||
parser.add_argument('filename', nargs='?', type=Path)
|
||||
parser.add_argument('output_filename', nargs='?', type=Path)
|
||||
parser.add_argument('eqpt_filename', nargs='?', type=Path, default=Path(__file__).parent / 'eqpt_config.json')
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = parser.parse_args()
|
||||
@@ -32,5 +32,4 @@ if __name__ == '__main__':
|
||||
json_data = loads(f.read())
|
||||
equipment = load_equipment(args.eqpt_filename)
|
||||
print(f'Writing in {args.output_filename}')
|
||||
jsontocsv(json_data,equipment,file)
|
||||
|
||||
jsontocsv(json_data, equipment, file)
|
||||
5
gnpy/tools/__init__.py
Normal file
5
gnpy/tools/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
'''
|
||||
Processing of data via :py:mod:`.json_io`.
|
||||
Utilities for Excel conversion in :py:mod:`.convert` and :py:mod:`.service_sheet`.
|
||||
Example code in :py:mod:`.cli_examples` and :py:mod:`.plots`.
|
||||
'''
|
||||
434
gnpy/tools/cli_examples.py
Normal file
434
gnpy/tools/cli_examples.py
Normal file
@@ -0,0 +1,434 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
'''
|
||||
gnpy.tools.cli_examples
|
||||
=======================
|
||||
|
||||
Common code for CLI examples
|
||||
'''
|
||||
|
||||
import argparse
|
||||
from json import dumps
|
||||
import logging
|
||||
import os.path
|
||||
import sys
|
||||
from math import ceil
|
||||
from numpy import linspace, mean
|
||||
from pathlib import Path
|
||||
import gnpy.core.ansi_escapes as ansi_escapes
|
||||
from gnpy.core.elements import Transceiver, Fiber, RamanFiber
|
||||
from gnpy.core.equipment import trx_mode_params
|
||||
import gnpy.core.exceptions as exceptions
|
||||
from gnpy.core.network import build_network
|
||||
from gnpy.core.parameters import SimParams
|
||||
from gnpy.core.science_utils import Simulation
|
||||
from gnpy.core.utils import db2lin, lin2db, automatic_nch
|
||||
from gnpy.topology.request import (ResultElement, jsontocsv, compute_path_dsjctn, requests_aggregation,
|
||||
BLOCKING_NOPATH, correct_json_route_list,
|
||||
deduplicate_disjunctions, compute_path_with_disjunction,
|
||||
PathRequest, compute_constrained_path, propagate2)
|
||||
from gnpy.topology.spectrum_assignment import build_oms_list, pth_assign_spectrum
|
||||
from gnpy.tools.json_io import load_equipment, load_network, load_json, load_requests, save_network, \
|
||||
requests_from_json, disjunctions_from_json, save_json
|
||||
from gnpy.tools.plots import plot_baseline, plot_results
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
_examples_dir = Path(__file__).parent.parent / 'example-data'
|
||||
_help_footer = '''
|
||||
This program is part of GNPy, https://github.com/TelecomInfraProject/oopt-gnpy
|
||||
|
||||
Learn more at https://gnpy.readthedocs.io/
|
||||
|
||||
'''
|
||||
_help_fname_json = 'FILE.json'
|
||||
_help_fname_json_csv = 'FILE.(json|csv)'
|
||||
|
||||
|
||||
def show_example_data_dir():
|
||||
print(f'{_examples_dir}/')
|
||||
|
||||
|
||||
def load_common_data(equipment_filename, topology_filename, simulation_filename, save_raw_network_filename):
|
||||
'''Load common configuration from JSON files'''
|
||||
|
||||
try:
|
||||
equipment = load_equipment(equipment_filename)
|
||||
network = load_network(topology_filename, equipment)
|
||||
if save_raw_network_filename is not None:
|
||||
save_network(network, save_raw_network_filename)
|
||||
print(f'{ansi_escapes.blue}Raw network (no optimizations) saved to {save_raw_network_filename}{ansi_escapes.reset}')
|
||||
sim_params = SimParams(**load_json(simulation_filename)) if simulation_filename is not None else None
|
||||
if not sim_params:
|
||||
if next((node for node in network if isinstance(node, RamanFiber)), None) is not None:
|
||||
print(f'{ansi_escapes.red}Invocation error:{ansi_escapes.reset} '
|
||||
f'RamanFiber requires passing simulation params via --sim-params')
|
||||
sys.exit(1)
|
||||
else:
|
||||
Simulation.set_params(sim_params)
|
||||
except exceptions.EquipmentConfigError as e:
|
||||
print(f'{ansi_escapes.red}Configuration error in the equipment library:{ansi_escapes.reset} {e}')
|
||||
sys.exit(1)
|
||||
except exceptions.NetworkTopologyError as e:
|
||||
print(f'{ansi_escapes.red}Invalid network definition:{ansi_escapes.reset} {e}')
|
||||
sys.exit(1)
|
||||
except exceptions.ConfigurationError as e:
|
||||
print(f'{ansi_escapes.red}Configuration error:{ansi_escapes.reset} {e}')
|
||||
sys.exit(1)
|
||||
except exceptions.ParametersError as e:
|
||||
print(f'{ansi_escapes.red}Simulation parameters error:{ansi_escapes.reset} {e}')
|
||||
sys.exit(1)
|
||||
except exceptions.ServiceError as e:
|
||||
print(f'{ansi_escapes.red}Service error:{ansi_escapes.reset} {e}')
|
||||
sys.exit(1)
|
||||
|
||||
return (equipment, network)
|
||||
|
||||
|
||||
def _setup_logging(args):
|
||||
logging.basicConfig(level={2: logging.DEBUG, 1: logging.INFO, 0: logging.CRITICAL}.get(args.verbose, logging.DEBUG))
|
||||
|
||||
|
||||
def _add_common_options(parser: argparse.ArgumentParser, network_default: Path):
|
||||
parser.add_argument('topology', nargs='?', type=Path, metavar='NETWORK-TOPOLOGY.(json|xls|xlsx)',
|
||||
default=network_default,
|
||||
help='Input network topology')
|
||||
parser.add_argument('-v', '--verbose', action='count', default=0,
|
||||
help='Increase verbosity (can be specified several times)')
|
||||
parser.add_argument('-e', '--equipment', type=Path, metavar=_help_fname_json,
|
||||
default=_examples_dir / 'eqpt_config.json', help='Equipment library')
|
||||
parser.add_argument('--sim-params', type=Path, metavar=_help_fname_json,
|
||||
default=None, help='Path to the JSON containing simulation parameters (required for Raman). '
|
||||
f'Example: {_examples_dir / "sim_params.json"}')
|
||||
parser.add_argument('--save-network', type=Path, metavar=_help_fname_json,
|
||||
help='Save the final network as a JSON file')
|
||||
parser.add_argument('--save-network-before-autodesign', type=Path, metavar=_help_fname_json,
|
||||
help='Dump the network into a JSON file prior to autodesign')
|
||||
|
||||
|
||||
def transmission_main_example(args=None):
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Send a full spectrum load through the network from point A to point B',
|
||||
epilog=_help_footer,
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
_add_common_options(parser, network_default=_examples_dir / 'edfa_example_network.json')
|
||||
parser.add_argument('--show-channels', action='store_true', help='Show final per-channel OSNR summary')
|
||||
parser.add_argument('-pl', '--plot', action='store_true')
|
||||
parser.add_argument('-l', '--list-nodes', action='store_true', help='list all transceiver nodes')
|
||||
parser.add_argument('-po', '--power', default=0, help='channel ref power in dBm')
|
||||
parser.add_argument('source', nargs='?', help='source node')
|
||||
parser.add_argument('destination', nargs='?', help='destination node')
|
||||
|
||||
args = parser.parse_args(args if args is not None else sys.argv[1:])
|
||||
_setup_logging(args)
|
||||
|
||||
(equipment, network) = load_common_data(args.equipment, args.topology, args.sim_params, args.save_network_before_autodesign)
|
||||
|
||||
if args.plot:
|
||||
plot_baseline(network)
|
||||
|
||||
transceivers = {n.uid: n for n in network.nodes() if isinstance(n, Transceiver)}
|
||||
|
||||
if not transceivers:
|
||||
sys.exit('Network has no transceivers!')
|
||||
if len(transceivers) < 2:
|
||||
sys.exit('Network has only one transceiver!')
|
||||
|
||||
if args.list_nodes:
|
||||
for uid in transceivers:
|
||||
print(uid)
|
||||
sys.exit()
|
||||
|
||||
# First try to find exact match if source/destination provided
|
||||
if args.source:
|
||||
source = transceivers.pop(args.source, None)
|
||||
valid_source = True if source else False
|
||||
else:
|
||||
source = None
|
||||
_logger.info('No source node specified: picking random transceiver')
|
||||
|
||||
if args.destination:
|
||||
destination = transceivers.pop(args.destination, None)
|
||||
valid_destination = True if destination else False
|
||||
else:
|
||||
destination = None
|
||||
_logger.info('No destination node specified: picking random transceiver')
|
||||
|
||||
# If no exact match try to find partial match
|
||||
if args.source and not source:
|
||||
# TODO code a more advanced regex to find nodes match
|
||||
source = next((transceivers.pop(uid) for uid in transceivers
|
||||
if args.source.lower() in uid.lower()), None)
|
||||
|
||||
if args.destination and not destination:
|
||||
# TODO code a more advanced regex to find nodes match
|
||||
destination = next((transceivers.pop(uid) for uid in transceivers
|
||||
if args.destination.lower() in uid.lower()), None)
|
||||
|
||||
# If no partial match or no source/destination provided pick random
|
||||
if not source:
|
||||
source = list(transceivers.values())[0]
|
||||
del transceivers[source.uid]
|
||||
|
||||
if not destination:
|
||||
destination = list(transceivers.values())[0]
|
||||
|
||||
_logger.info(f'source = {args.source!r}')
|
||||
_logger.info(f'destination = {args.destination!r}')
|
||||
|
||||
params = {}
|
||||
params['request_id'] = 0
|
||||
params['trx_type'] = ''
|
||||
params['trx_mode'] = ''
|
||||
params['source'] = source.uid
|
||||
params['destination'] = destination.uid
|
||||
params['bidir'] = False
|
||||
params['nodes_list'] = [destination.uid]
|
||||
params['loose_list'] = ['strict']
|
||||
params['format'] = ''
|
||||
params['path_bandwidth'] = 0
|
||||
params['effective_freq_slot'] = None
|
||||
trx_params = trx_mode_params(equipment)
|
||||
if args.power:
|
||||
trx_params['power'] = db2lin(float(args.power)) * 1e-3
|
||||
params.update(trx_params)
|
||||
req = PathRequest(**params)
|
||||
|
||||
power_mode = equipment['Span']['default'].power_mode
|
||||
print('\n'.join([f'Power mode is set to {power_mode}',
|
||||
f'=> it can be modified in eqpt_config.json - Span']))
|
||||
|
||||
pref_ch_db = lin2db(req.power * 1e3) # reference channel power / span (SL=20dB)
|
||||
pref_total_db = pref_ch_db + lin2db(req.nb_channel) # reference total power / span (SL=20dB)
|
||||
build_network(network, equipment, pref_ch_db, pref_total_db)
|
||||
path = compute_constrained_path(network, req)
|
||||
|
||||
spans = [s.params.length for s in path if isinstance(s, RamanFiber) or isinstance(s, Fiber)]
|
||||
print(f'\nThere are {len(spans)} fiber spans over {sum(spans)/1000:.0f} km between {source.uid} '
|
||||
f'and {destination.uid}')
|
||||
print(f'\nNow propagating between {source.uid} and {destination.uid}:')
|
||||
|
||||
try:
|
||||
p_start, p_stop, p_step = equipment['SI']['default'].power_range_db
|
||||
p_num = abs(int(round((p_stop - p_start) / p_step))) + 1 if p_step != 0 else 1
|
||||
power_range = list(linspace(p_start, p_stop, p_num))
|
||||
except TypeError:
|
||||
print('invalid power range definition in eqpt_config, should be power_range_db: [lower, upper, step]')
|
||||
power_range = [0]
|
||||
|
||||
if not power_mode:
|
||||
# power cannot be changed in gain mode
|
||||
power_range = [0]
|
||||
for dp_db in power_range:
|
||||
req.power = db2lin(pref_ch_db + dp_db) * 1e-3
|
||||
if power_mode:
|
||||
print(f'\nPropagating with input power = {ansi_escapes.cyan}{lin2db(req.power*1e3):.2f} dBm{ansi_escapes.reset}:')
|
||||
else:
|
||||
print(f'\nPropagating in {ansi_escapes.cyan}gain mode{ansi_escapes.reset}: power cannot be set manually')
|
||||
infos = propagate2(path, req, equipment)
|
||||
if len(power_range) == 1:
|
||||
for elem in path:
|
||||
print(elem)
|
||||
if power_mode:
|
||||
print(f'\nTransmission result for input power = {lin2db(req.power*1e3):.2f} dBm:')
|
||||
else:
|
||||
print(f'\nTransmission results:')
|
||||
print(f' Final SNR total (0.1 nm): {ansi_escapes.cyan}{mean(destination.snr_01nm):.02f} dB{ansi_escapes.reset}')
|
||||
else:
|
||||
print(path[-1])
|
||||
|
||||
# print(f'\n !!!!!!!!!!!!!!!!! TEST POINT !!!!!!!!!!!!!!!!!!!!!')
|
||||
# print(f'carriers ase output of {path[1]} =\n {list(path[1].carriers("out", "nli"))}')
|
||||
# => use "in" or "out" parameter
|
||||
# => use "nli" or "ase" or "signal" or "total" parameter
|
||||
|
||||
if args.save_network is not None:
|
||||
save_network(network, args.save_network)
|
||||
print(f'{ansi_escapes.blue}Network (after autodesign) saved to {args.save_network}{ansi_escapes.reset}')
|
||||
|
||||
if args.show_channels:
|
||||
print('\nThe total SNR per channel at the end of the line is:')
|
||||
print(
|
||||
'{:>5}{:>26}{:>26}{:>28}{:>28}{:>28}' .format(
|
||||
'Ch. #',
|
||||
'Channel frequency (THz)',
|
||||
'Channel power (dBm)',
|
||||
'OSNR ASE (signal bw, dB)',
|
||||
'SNR NLI (signal bw, dB)',
|
||||
'SNR total (signal bw, dB)'))
|
||||
for final_carrier, ch_osnr, ch_snr_nl, ch_snr in zip(
|
||||
infos[path[-1]][1].carriers, path[-1].osnr_ase, path[-1].osnr_nli, path[-1].snr):
|
||||
ch_freq = final_carrier.frequency * 1e-12
|
||||
ch_power = lin2db(final_carrier.power.signal * 1e3)
|
||||
print(
|
||||
'{:5}{:26.2f}{:26.2f}{:28.2f}{:28.2f}{:28.2f}' .format(
|
||||
final_carrier.channel_number, round(
|
||||
ch_freq, 2), round(
|
||||
ch_power, 2), round(
|
||||
ch_osnr, 2), round(
|
||||
ch_snr_nl, 2), round(
|
||||
ch_snr, 2)))
|
||||
|
||||
if not args.source:
|
||||
print(f'\n(No source node specified: picked {source.uid})')
|
||||
elif not valid_source:
|
||||
print(f'\n(Invalid source node {args.source!r} replaced with {source.uid})')
|
||||
|
||||
if not args.destination:
|
||||
print(f'\n(No destination node specified: picked {destination.uid})')
|
||||
elif not valid_destination:
|
||||
print(f'\n(Invalid destination node {args.destination!r} replaced with {destination.uid})')
|
||||
|
||||
if args.plot:
|
||||
plot_results(network, path, source, destination, infos)
|
||||
|
||||
|
||||
def _path_result_json(pathresult):
|
||||
return {'response': [n.json for n in pathresult]}
|
||||
|
||||
|
||||
def path_requests_run(args=None):
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Compute performance for a list of services provided in a json file or an excel sheet',
|
||||
epilog=_help_footer,
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
_add_common_options(parser, network_default=_examples_dir / 'meshTopologyExampleV2.xls')
|
||||
parser.add_argument('service_filename', nargs='?', type=Path, metavar='SERVICES-REQUESTS.(json|xls|xlsx)',
|
||||
default=_examples_dir / 'meshTopologyExampleV2.xls',
|
||||
help='Input service file')
|
||||
parser.add_argument('-bi', '--bidir', action='store_true',
|
||||
help='considers that all demands are bidir')
|
||||
parser.add_argument('-o', '--output', type=Path, metavar=_help_fname_json_csv,
|
||||
help='Store satisifed requests into a JSON or CSV file')
|
||||
|
||||
args = parser.parse_args(args if args is not None else sys.argv[1:])
|
||||
_setup_logging(args)
|
||||
|
||||
_logger.info(f'Computing path requests {args.service_filename} into JSON format')
|
||||
print(f'{ansi_escapes.blue}Computing path requests {os.path.relpath(args.service_filename)} into JSON format{ansi_escapes.reset}')
|
||||
|
||||
(equipment, network) = load_common_data(args.equipment, args.topology, args.sim_params, args.save_network_before_autodesign)
|
||||
|
||||
# Build the network once using the default power defined in SI in eqpt config
|
||||
# TODO power density: db2linp(ower_dbm": 0)/power_dbm": 0 * nb channels as defined by
|
||||
# spacing, f_min and f_max
|
||||
p_db = equipment['SI']['default'].power_dbm
|
||||
|
||||
p_total_db = p_db + lin2db(automatic_nch(equipment['SI']['default'].f_min,
|
||||
equipment['SI']['default'].f_max, equipment['SI']['default'].spacing))
|
||||
build_network(network, equipment, p_db, p_total_db)
|
||||
if args.save_network is not None:
|
||||
save_network(network, args.save_network)
|
||||
print(f'{ansi_escapes.blue}Network (after autodesign) saved to {args.save_network}{ansi_escapes.reset}')
|
||||
oms_list = build_oms_list(network, equipment)
|
||||
|
||||
try:
|
||||
data = load_requests(args.service_filename, equipment, bidir=args.bidir,
|
||||
network=network, network_filename=args.topology)
|
||||
rqs = requests_from_json(data, equipment)
|
||||
except exceptions.ServiceError as e:
|
||||
print(f'{ansi_escapes.red}Service error:{ansi_escapes.reset} {e}')
|
||||
sys.exit(1)
|
||||
# check that request ids are unique. Non unique ids, may
|
||||
# mess the computation: better to stop the computation
|
||||
all_ids = [r.request_id for r in rqs]
|
||||
if len(all_ids) != len(set(all_ids)):
|
||||
for item in list(set(all_ids)):
|
||||
all_ids.remove(item)
|
||||
msg = f'Requests id {all_ids} are not unique'
|
||||
_logger.critical(msg)
|
||||
sys.exit()
|
||||
rqs = correct_json_route_list(network, rqs)
|
||||
|
||||
# pths = compute_path(network, equipment, rqs)
|
||||
dsjn = disjunctions_from_json(data)
|
||||
|
||||
print(f'{ansi_escapes.blue}List of disjunctions{ansi_escapes.reset}')
|
||||
print(dsjn)
|
||||
# need to warn or correct in case of wrong disjunction form
|
||||
# disjunction must not be repeated with same or different ids
|
||||
dsjn = deduplicate_disjunctions(dsjn)
|
||||
|
||||
# Aggregate demands with same exact constraints
|
||||
print(f'{ansi_escapes.blue}Aggregating similar requests{ansi_escapes.reset}')
|
||||
|
||||
rqs, dsjn = requests_aggregation(rqs, dsjn)
|
||||
# TODO export novel set of aggregated demands in a json file
|
||||
|
||||
print(f'{ansi_escapes.blue}The following services have been requested:{ansi_escapes.reset}')
|
||||
print(rqs)
|
||||
|
||||
print(f'{ansi_escapes.blue}Computing all paths with constraints{ansi_escapes.reset}')
|
||||
try:
|
||||
pths = compute_path_dsjctn(network, equipment, rqs, dsjn)
|
||||
except exceptions.DisjunctionError as this_e:
|
||||
print(f'{ansi_escapes.red}Disjunction error:{ansi_escapes.reset} {this_e}')
|
||||
sys.exit(1)
|
||||
|
||||
print(f'{ansi_escapes.blue}Propagating on selected path{ansi_escapes.reset}')
|
||||
propagatedpths, reversed_pths, reversed_propagatedpths = compute_path_with_disjunction(network, equipment, rqs, pths)
|
||||
# Note that deepcopy used in compute_path_with_disjunction returns
|
||||
# a list of nodes which are not belonging to network (they are copies of the node objects).
|
||||
# so there can not be propagation on these nodes.
|
||||
|
||||
pth_assign_spectrum(pths, rqs, oms_list, reversed_pths)
|
||||
|
||||
print(f'{ansi_escapes.blue}Result summary{ansi_escapes.reset}')
|
||||
header = ['req id', ' demand', ' snr@bandwidth A-Z (Z-A)', ' snr@0.1nm A-Z (Z-A)',
|
||||
' Receiver minOSNR', ' mode', ' Gbit/s', ' nb of tsp pairs',
|
||||
'N,M or blocking reason']
|
||||
data = []
|
||||
data.append(header)
|
||||
for i, this_p in enumerate(propagatedpths):
|
||||
rev_pth = reversed_propagatedpths[i]
|
||||
if rev_pth and this_p:
|
||||
psnrb = f'{round(mean(this_p[-1].snr),2)} ({round(mean(rev_pth[-1].snr),2)})'
|
||||
psnr = f'{round(mean(this_p[-1].snr_01nm), 2)}' +\
|
||||
f' ({round(mean(rev_pth[-1].snr_01nm),2)})'
|
||||
elif this_p:
|
||||
psnrb = f'{round(mean(this_p[-1].snr),2)}'
|
||||
psnr = f'{round(mean(this_p[-1].snr_01nm),2)}'
|
||||
|
||||
try:
|
||||
if rqs[i].blocking_reason in BLOCKING_NOPATH:
|
||||
line = [f'{rqs[i].request_id}', f' {rqs[i].source} to {rqs[i].destination} :',
|
||||
f'-', f'-', f'-', f'{rqs[i].tsp_mode}', f'{round(rqs[i].path_bandwidth * 1e-9,2)}',
|
||||
f'-', f'{rqs[i].blocking_reason}']
|
||||
else:
|
||||
line = [f'{rqs[i].request_id}', f' {rqs[i].source} to {rqs[i].destination} : ', psnrb,
|
||||
psnr, f'-', f'{rqs[i].tsp_mode}', f'{round(rqs[i].path_bandwidth * 1e-9, 2)}',
|
||||
f'-', f'{rqs[i].blocking_reason}']
|
||||
except AttributeError:
|
||||
line = [f'{rqs[i].request_id}', f' {rqs[i].source} to {rqs[i].destination} : ', psnrb,
|
||||
psnr, f'{rqs[i].OSNR}', f'{rqs[i].tsp_mode}', f'{round(rqs[i].path_bandwidth * 1e-9,2)}',
|
||||
f'{ceil(rqs[i].path_bandwidth / rqs[i].bit_rate) }', f'({rqs[i].N},{rqs[i].M})']
|
||||
data.append(line)
|
||||
|
||||
col_width = max(len(word) for row in data for word in row[2:]) # padding
|
||||
firstcol_width = max(len(row[0]) for row in data) # padding
|
||||
secondcol_width = max(len(row[1]) for row in data) # padding
|
||||
for row in data:
|
||||
firstcol = ''.join(row[0].ljust(firstcol_width))
|
||||
secondcol = ''.join(row[1].ljust(secondcol_width))
|
||||
remainingcols = ''.join(word.center(col_width, ' ') for word in row[2:])
|
||||
print(f'{firstcol} {secondcol} {remainingcols}')
|
||||
print(f'{ansi_escapes.yellow}Result summary shows mean SNR and OSNR (average over all channels){ansi_escapes.reset}')
|
||||
|
||||
if args.output:
|
||||
result = []
|
||||
# assumes that list of rqs and list of propgatedpths have same order
|
||||
for i, pth in enumerate(propagatedpths):
|
||||
result.append(ResultElement(rqs[i], pth, reversed_propagatedpths[i]))
|
||||
temp = _path_result_json(result)
|
||||
if args.output.suffix.lower() == '.json':
|
||||
save_json(temp, args.output)
|
||||
print(f'{ansi_escapes.blue}Saved JSON to {args.output}{ansi_escapes.reset}')
|
||||
elif args.output.suffix.lower() == '.csv':
|
||||
with open(args.output, "w", encoding='utf-8') as fcsv:
|
||||
jsontocsv(temp, equipment, fcsv)
|
||||
print(f'{ansi_escapes.blue}Saved CSV to {args.output}{ansi_escapes.reset}')
|
||||
else:
|
||||
print(f'{ansi_escapes.red}Cannot save output: neither JSON nor CSV file{ansi_escapes.reset}')
|
||||
sys.exit(1)
|
||||
745
gnpy/tools/convert.py
Executable file
745
gnpy/tools/convert.py
Executable file
@@ -0,0 +1,745 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
gnpy.tools.convert
|
||||
==================
|
||||
|
||||
This module contains utilities for converting between XLS and JSON.
|
||||
|
||||
The input XLS file must contain sheets named "Nodes" and "Links".
|
||||
It may optionally contain a sheet named "Eqpt".
|
||||
|
||||
In the "Nodes" sheet, only the "City" column is mandatory. The column "Type"
|
||||
can be determined automatically given the topology (e.g., if degree 2, ILA;
|
||||
otherwise, ROADM.) Incorrectly specified types (e.g., ILA for node of
|
||||
degree ≠ 2) will be automatically corrected.
|
||||
|
||||
In the "Links" sheet, only the first three columns ("Node A", "Node Z" and
|
||||
"east Distance (km)") are mandatory. Missing "west" information is copied from
|
||||
the "east" information so that it is possible to input undirected data.
|
||||
"""
|
||||
|
||||
from sys import exit
|
||||
from xlrd import open_workbook
|
||||
from argparse import ArgumentParser
|
||||
from collections import namedtuple, Counter, defaultdict
|
||||
from itertools import chain
|
||||
from json import dumps
|
||||
from pathlib import Path
|
||||
from copy import copy
|
||||
from gnpy.core import ansi_escapes
|
||||
from gnpy.core.utils import silent_remove
|
||||
from gnpy.core.exceptions import NetworkTopologyError
|
||||
from gnpy.core.elements import Edfa, Fused, Fiber
|
||||
|
||||
|
||||
def all_rows(sh, start=0):
|
||||
return (sh.row(x) for x in range(start, sh.nrows))
|
||||
|
||||
|
||||
class Node(object):
|
||||
def __init__(self, **kwargs):
|
||||
super(Node, self).__init__()
|
||||
self.update_attr(kwargs)
|
||||
|
||||
def update_attr(self, kwargs):
|
||||
clean_kwargs = {k: v for k, v in kwargs.items() if v != ''}
|
||||
for k, v in self.default_values.items():
|
||||
v = clean_kwargs.get(k, v)
|
||||
setattr(self, k, v)
|
||||
|
||||
default_values = {
|
||||
'city': '',
|
||||
'state': '',
|
||||
'country': '',
|
||||
'region': '',
|
||||
'latitude': 0,
|
||||
'longitude': 0,
|
||||
'node_type': 'ILA',
|
||||
'booster_restriction': '',
|
||||
'preamp_restriction': ''
|
||||
}
|
||||
|
||||
|
||||
class Link(object):
|
||||
"""attribtes from west parse_ept_headers dict
|
||||
+node_a, node_z, west_fiber_con_in, east_fiber_con_in
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(Link, self).__init__()
|
||||
self.update_attr(kwargs)
|
||||
self.distance_units = 'km'
|
||||
|
||||
def update_attr(self, kwargs):
|
||||
clean_kwargs = {k: v for k, v in kwargs.items() if v != ''}
|
||||
for k, v in self.default_values.items():
|
||||
v = clean_kwargs.get(k, v)
|
||||
setattr(self, k, v)
|
||||
k = 'west' + k.split('east')[-1]
|
||||
v = clean_kwargs.get(k, v)
|
||||
setattr(self, k, v)
|
||||
|
||||
def __eq__(self, link):
|
||||
return (self.from_city == link.from_city and self.to_city == link.to_city) \
|
||||
or (self.from_city == link.to_city and self.to_city == link.from_city)
|
||||
|
||||
default_values = {
|
||||
'from_city': '',
|
||||
'to_city': '',
|
||||
'east_distance': 80,
|
||||
'east_fiber': 'SSMF',
|
||||
'east_lineic': 0.2,
|
||||
'east_con_in': None,
|
||||
'east_con_out': None,
|
||||
'east_pmd': 0.1,
|
||||
'east_cable': ''
|
||||
}
|
||||
|
||||
|
||||
class Eqpt(object):
|
||||
def __init__(self, **kwargs):
|
||||
super(Eqpt, self).__init__()
|
||||
self.update_attr(kwargs)
|
||||
|
||||
def update_attr(self, kwargs):
|
||||
clean_kwargs = {k: v for k, v in kwargs.items() if v != ''}
|
||||
for k, v in self.default_values.items():
|
||||
v_east = clean_kwargs.get(k, v)
|
||||
setattr(self, k, v_east)
|
||||
k = 'west' + k.split('east')[-1]
|
||||
v_west = clean_kwargs.get(k, v)
|
||||
setattr(self, k, v_west)
|
||||
|
||||
default_values = {
|
||||
'from_city': '',
|
||||
'to_city': '',
|
||||
'east_amp_type': '',
|
||||
'east_att_in': 0,
|
||||
'east_amp_gain': None,
|
||||
'east_amp_dp': None,
|
||||
'east_tilt': 0,
|
||||
'east_att_out': None
|
||||
}
|
||||
|
||||
|
||||
def read_header(my_sheet, line, slice_):
|
||||
""" return the list of headers !:= ''
|
||||
header_i = [(header, header_column_index), ...]
|
||||
in a {line, slice1_x, slice_y} range
|
||||
"""
|
||||
Param_header = namedtuple('Param_header', 'header colindex')
|
||||
try:
|
||||
header = [x.value.strip() for x in my_sheet.row_slice(line, slice_[0], slice_[1])]
|
||||
header_i = [Param_header(header, i + slice_[0]) for i, header in enumerate(header) if header != '']
|
||||
except Exception:
|
||||
header_i = []
|
||||
if header_i != [] and header_i[-1].colindex != slice_[1]:
|
||||
header_i.append(Param_header('', slice_[1]))
|
||||
return header_i
|
||||
|
||||
|
||||
def read_slice(my_sheet, line, slice_, header):
|
||||
"""return the slice range of a given header
|
||||
in a defined range {line, slice_x, slice_y}"""
|
||||
header_i = read_header(my_sheet, line, slice_)
|
||||
slice_range = (-1, -1)
|
||||
if header_i != []:
|
||||
try:
|
||||
slice_range = next((h.colindex, header_i[i + 1].colindex)
|
||||
for i, h in enumerate(header_i) if header in h.header)
|
||||
except Exception:
|
||||
pass
|
||||
return slice_range
|
||||
|
||||
|
||||
def parse_headers(my_sheet, input_headers_dict, headers, start_line, slice_in):
|
||||
"""return a dict of header_slice
|
||||
key = column index
|
||||
value = header name"""
|
||||
|
||||
for h0 in input_headers_dict:
|
||||
slice_out = read_slice(my_sheet, start_line, slice_in, h0)
|
||||
iteration = 1
|
||||
while slice_out == (-1, -1) and iteration < 10:
|
||||
# try next lines
|
||||
slice_out = read_slice(my_sheet, start_line + iteration, slice_in, h0)
|
||||
iteration += 1
|
||||
if slice_out == (-1, -1):
|
||||
if h0 in ('east', 'Node A', 'Node Z', 'City'):
|
||||
print(f'{ansi_escapes.red}CRITICAL{ansi_escapes.reset}: missing _{h0}_ header: EXECUTION ENDS')
|
||||
exit()
|
||||
else:
|
||||
print(f'missing header {h0}')
|
||||
elif not isinstance(input_headers_dict[h0], dict):
|
||||
headers[slice_out[0]] = input_headers_dict[h0]
|
||||
else:
|
||||
headers = parse_headers(my_sheet, input_headers_dict[h0], headers, start_line + 1, slice_out)
|
||||
if headers == {}:
|
||||
print(f'{ansi_escapes.red}CRITICAL ERROR{ansi_escapes.reset}: could not find any header to read _ ABORT')
|
||||
exit()
|
||||
return headers
|
||||
|
||||
|
||||
def parse_row(row, headers):
|
||||
return {f: r.value for f, r in
|
||||
zip([label for label in headers.values()], [row[i] for i in headers])}
|
||||
|
||||
|
||||
def parse_sheet(my_sheet, input_headers_dict, header_line, start_line, column):
|
||||
headers = parse_headers(my_sheet, input_headers_dict, {}, header_line, (0, column))
|
||||
for row in all_rows(my_sheet, start=start_line):
|
||||
yield parse_row(row[0: column], headers)
|
||||
|
||||
|
||||
def sanity_check(nodes, links, nodes_by_city, links_by_city, eqpts_by_city):
|
||||
|
||||
duplicate_links = []
|
||||
for l1 in links:
|
||||
for l2 in links:
|
||||
if l1 is not l2 and l1 == l2 and l2 not in duplicate_links:
|
||||
print(f'\nWARNING\n \
|
||||
link {l1.from_city}-{l1.to_city} is duplicate \
|
||||
\nthe 1st duplicate link will be removed but you should check Links sheet input')
|
||||
duplicate_links.append(l1)
|
||||
for l in duplicate_links:
|
||||
links.remove(l)
|
||||
|
||||
try:
|
||||
test_nodes = [n for n in nodes_by_city if n not in links_by_city]
|
||||
test_links = [n for n in links_by_city if n not in nodes_by_city]
|
||||
test_eqpts = [n for n in eqpts_by_city if n not in nodes_by_city]
|
||||
assert (test_nodes == [] or test_nodes == [''])\
|
||||
and (test_links == [] or test_links == [''])\
|
||||
and (test_eqpts == [] or test_eqpts == [''])
|
||||
except AssertionError:
|
||||
msg = f'CRITICAL error in excel input: Names in Nodes and Links sheets do no match, check:\
|
||||
\n{test_nodes} in Nodes sheet\
|
||||
\n{test_links} in Links sheet\
|
||||
\n{test_eqpts} in Eqpt sheet'
|
||||
raise NetworkTopologyError(msg)
|
||||
|
||||
for city, link in links_by_city.items():
|
||||
if nodes_by_city[city].node_type.lower() == 'ila' and len(link) != 2:
|
||||
# wrong input: ILA sites can only be Degree 2
|
||||
# => correct to make it a ROADM and remove entry in links_by_city
|
||||
# TODO: put in log rather than print
|
||||
print(f'invalid node type ({nodes_by_city[city].node_type})\
|
||||
specified in {city}, replaced by ROADM')
|
||||
nodes_by_city[city].node_type = 'ROADM'
|
||||
for n in nodes:
|
||||
if n.city == city:
|
||||
n.node_type = 'ROADM'
|
||||
return nodes, links
|
||||
|
||||
|
||||
def xls_to_json_data(input_filename, filter_region=[]):
|
||||
nodes, links, eqpts = parse_excel(input_filename)
|
||||
if filter_region:
|
||||
nodes = [n for n in nodes if n.region.lower() in filter_region]
|
||||
cities = {n.city for n in nodes}
|
||||
links = [lnk for lnk in links if lnk.from_city in cities and
|
||||
lnk.to_city in cities]
|
||||
cities = {lnk.from_city for lnk in links} | {lnk.to_city for lnk in links}
|
||||
nodes = [n for n in nodes if n.city in cities]
|
||||
|
||||
global nodes_by_city
|
||||
nodes_by_city = {n.city: n for n in nodes}
|
||||
|
||||
global links_by_city
|
||||
links_by_city = defaultdict(list)
|
||||
for link in links:
|
||||
links_by_city[link.from_city].append(link)
|
||||
links_by_city[link.to_city].append(link)
|
||||
|
||||
global eqpts_by_city
|
||||
eqpts_by_city = defaultdict(list)
|
||||
for eqpt in eqpts:
|
||||
eqpts_by_city[eqpt.from_city].append(eqpt)
|
||||
|
||||
nodes, links = sanity_check(nodes, links, nodes_by_city, links_by_city, eqpts_by_city)
|
||||
|
||||
return {
|
||||
'elements':
|
||||
[{'uid': f'trx {x.city}',
|
||||
'metadata': {'location': {'city': x.city,
|
||||
'region': x.region,
|
||||
'latitude': x.latitude,
|
||||
'longitude': x.longitude}},
|
||||
'type': 'Transceiver'}
|
||||
for x in nodes_by_city.values() if x.node_type.lower() == 'roadm'] +
|
||||
[{'uid': f'roadm {x.city}',
|
||||
'metadata': {'location': {'city': x.city,
|
||||
'region': x.region,
|
||||
'latitude': x.latitude,
|
||||
'longitude': x.longitude}},
|
||||
'type': 'Roadm'}
|
||||
for x in nodes_by_city.values() if x.node_type.lower() == 'roadm'
|
||||
and x.booster_restriction == '' and x.preamp_restriction == ''] +
|
||||
[{'uid': f'roadm {x.city}',
|
||||
'params': {
|
||||
'restrictions': {
|
||||
'preamp_variety_list': silent_remove(x.preamp_restriction.split(' | '), ''),
|
||||
'booster_variety_list': silent_remove(x.booster_restriction.split(' | '), '')
|
||||
}
|
||||
},
|
||||
'metadata': {'location': {'city': x.city,
|
||||
'region': x.region,
|
||||
'latitude': x.latitude,
|
||||
'longitude': x.longitude}},
|
||||
'type': 'Roadm'}
|
||||
for x in nodes_by_city.values() if x.node_type.lower() == 'roadm' and
|
||||
(x.booster_restriction != '' or x.preamp_restriction != '')] +
|
||||
[{'uid': f'west fused spans in {x.city}',
|
||||
'metadata': {'location': {'city': x.city,
|
||||
'region': x.region,
|
||||
'latitude': x.latitude,
|
||||
'longitude': x.longitude}},
|
||||
'type': 'Fused'}
|
||||
for x in nodes_by_city.values() if x.node_type.lower() == 'fused'] +
|
||||
[{'uid': f'east fused spans in {x.city}',
|
||||
'metadata': {'location': {'city': x.city,
|
||||
'region': x.region,
|
||||
'latitude': x.latitude,
|
||||
'longitude': x.longitude}},
|
||||
'type': 'Fused'}
|
||||
for x in nodes_by_city.values() if x.node_type.lower() == 'fused'] +
|
||||
[{'uid': f'fiber ({x.from_city} \u2192 {x.to_city})-{x.east_cable}',
|
||||
'metadata': {'location': midpoint(nodes_by_city[x.from_city],
|
||||
nodes_by_city[x.to_city])},
|
||||
'type': 'Fiber',
|
||||
'type_variety': x.east_fiber,
|
||||
'params': {'length': round(x.east_distance, 3),
|
||||
'length_units': x.distance_units,
|
||||
'loss_coef': x.east_lineic,
|
||||
'con_in': x.east_con_in,
|
||||
'con_out': x.east_con_out}
|
||||
}
|
||||
for x in links] +
|
||||
[{'uid': f'fiber ({x.to_city} \u2192 {x.from_city})-{x.west_cable}',
|
||||
'metadata': {'location': midpoint(nodes_by_city[x.from_city],
|
||||
nodes_by_city[x.to_city])},
|
||||
'type': 'Fiber',
|
||||
'type_variety': x.west_fiber,
|
||||
'params': {'length': round(x.west_distance, 3),
|
||||
'length_units': x.distance_units,
|
||||
'loss_coef': x.west_lineic,
|
||||
'con_in': x.west_con_in,
|
||||
'con_out': x.west_con_out}
|
||||
} # missing ILA construction
|
||||
for x in links] +
|
||||
[{'uid': f'east edfa in {e.from_city} to {e.to_city}',
|
||||
'metadata': {'location': {'city': nodes_by_city[e.from_city].city,
|
||||
'region': nodes_by_city[e.from_city].region,
|
||||
'latitude': nodes_by_city[e.from_city].latitude,
|
||||
'longitude': nodes_by_city[e.from_city].longitude}},
|
||||
'type': 'Edfa',
|
||||
'type_variety': e.east_amp_type,
|
||||
'operational': {'gain_target': e.east_amp_gain,
|
||||
'delta_p': e.east_amp_dp,
|
||||
'tilt_target': e.east_tilt,
|
||||
'out_voa': e.east_att_out}
|
||||
}
|
||||
for e in eqpts if (e.east_amp_type.lower() != '' and \
|
||||
e.east_amp_type.lower() != 'fused')] +
|
||||
[{'uid': f'west edfa in {e.from_city} to {e.to_city}',
|
||||
'metadata': {'location': {'city': nodes_by_city[e.from_city].city,
|
||||
'region': nodes_by_city[e.from_city].region,
|
||||
'latitude': nodes_by_city[e.from_city].latitude,
|
||||
'longitude': nodes_by_city[e.from_city].longitude}},
|
||||
'type': 'Edfa',
|
||||
'type_variety': e.west_amp_type,
|
||||
'operational': {'gain_target': e.west_amp_gain,
|
||||
'delta_p': e.west_amp_dp,
|
||||
'tilt_target': e.west_tilt,
|
||||
'out_voa': e.west_att_out}
|
||||
}
|
||||
for e in eqpts if (e.west_amp_type.lower() != '' and \
|
||||
e.west_amp_type.lower() != 'fused')] +
|
||||
# fused edfa variety is a hack to indicate that there should not be
|
||||
# booster amplifier out the roadm.
|
||||
# If user specifies ILA in Nodes sheet and fused in Eqpt sheet, then assumes that
|
||||
# this is a fused nodes.
|
||||
[{'uid': f'east edfa in {e.from_city} to {e.to_city}',
|
||||
'metadata': {'location': {'city': nodes_by_city[e.from_city].city,
|
||||
'region': nodes_by_city[e.from_city].region,
|
||||
'latitude': nodes_by_city[e.from_city].latitude,
|
||||
'longitude': nodes_by_city[e.from_city].longitude}},
|
||||
'type': 'Fused',
|
||||
'params': {'loss': 0}
|
||||
}
|
||||
for e in eqpts if e.east_amp_type.lower() == 'fused'] +
|
||||
[{'uid': f'west edfa in {e.from_city} to {e.to_city}',
|
||||
'metadata': {'location': {'city': nodes_by_city[e.from_city].city,
|
||||
'region': nodes_by_city[e.from_city].region,
|
||||
'latitude': nodes_by_city[e.from_city].latitude,
|
||||
'longitude': nodes_by_city[e.from_city].longitude}},
|
||||
'type': 'Fused',
|
||||
'params': {'loss': 0}
|
||||
}
|
||||
for e in eqpts if e.west_amp_type.lower() == 'fused'],
|
||||
'connections':
|
||||
list(chain.from_iterable([eqpt_connection_by_city(n.city)
|
||||
for n in nodes]))
|
||||
+
|
||||
list(chain.from_iterable(zip(
|
||||
[{'from_node': f'trx {x.city}',
|
||||
'to_node': f'roadm {x.city}'}
|
||||
for x in nodes_by_city.values() if x.node_type.lower() == 'roadm'],
|
||||
[{'from_node': f'roadm {x.city}',
|
||||
'to_node': f'trx {x.city}'}
|
||||
for x in nodes_by_city.values() if x.node_type.lower() == 'roadm'])))
|
||||
}
|
||||
|
||||
|
||||
def convert_file(input_filename, filter_region=[], output_json_file_name=None):
|
||||
data = xls_to_json_data(input_filename, filter_region)
|
||||
if output_json_file_name is None:
|
||||
output_json_file_name = input_filename.with_suffix('.json')
|
||||
with open(output_json_file_name, 'w', encoding='utf-8') as edfa_json_file:
|
||||
edfa_json_file.write(dumps(data, indent=2, ensure_ascii=False))
|
||||
return output_json_file_name
|
||||
|
||||
|
||||
def corresp_names(input_filename, network):
|
||||
""" a function that builds the correspondance between names given in the excel,
|
||||
and names used in the json, and created by the autodesign.
|
||||
All names are listed
|
||||
"""
|
||||
nodes, links, eqpts = parse_excel(input_filename)
|
||||
fused = [n.uid for n in network.nodes() if isinstance(n, Fused)]
|
||||
ila = [n.uid for n in network.nodes() if isinstance(n, Edfa)]
|
||||
|
||||
corresp_roadm = {x.city: [f'roadm {x.city}'] for x in nodes
|
||||
if x.node_type.lower() == 'roadm'}
|
||||
corresp_fused = {x.city: [f'west fused spans in {x.city}', f'east fused spans in {x.city}']
|
||||
for x in nodes if x.node_type.lower() == 'fused' and
|
||||
f'west fused spans in {x.city}' in fused and
|
||||
f'east fused spans in {x.city}' in fused}
|
||||
|
||||
# add the special cases when an ila is changed into a fused
|
||||
for my_e in eqpts:
|
||||
name = f'east edfa in {my_e.from_city} to {my_e.to_city}'
|
||||
if my_e.east_amp_type.lower() == 'fused' and name in fused:
|
||||
if my_e.from_city in corresp_fused.keys():
|
||||
corresp_fused[my_e.from_city].append(name)
|
||||
else:
|
||||
corresp_fused[my_e.from_city] = [name]
|
||||
name = f'west edfa in {my_e.from_city} to {my_e.to_city}'
|
||||
if my_e.west_amp_type.lower() == 'fused' and name in fused:
|
||||
if my_e.from_city in corresp_fused.keys():
|
||||
corresp_fused[my_e.from_city].append(name)
|
||||
else:
|
||||
corresp_fused[my_e.from_city] = [name]
|
||||
# build corresp ila based on eqpt sheet
|
||||
# start with east direction
|
||||
corresp_ila = {e.from_city: [f'east edfa in {e.from_city} to {e.to_city}']
|
||||
for e in eqpts if e.east_amp_type.lower() != '' and
|
||||
f'east edfa in {e.from_city} to {e.to_city}' in ila}
|
||||
# west direction, append name or create a new item in dict
|
||||
for my_e in eqpts:
|
||||
if my_e.west_amp_type.lower() != '':
|
||||
name = f'west edfa in {my_e.from_city} to {my_e.to_city}'
|
||||
if name in ila:
|
||||
if my_e.from_city in corresp_ila.keys():
|
||||
corresp_ila[my_e.from_city].append(name)
|
||||
else:
|
||||
corresp_ila[my_e.from_city] = [name]
|
||||
# complete with potential autodesign names: amplifiers
|
||||
for my_l in links:
|
||||
name = f'Edfa0_fiber ({my_l.to_city} \u2192 {my_l.from_city})-{my_l.west_cable}'
|
||||
if name in ila:
|
||||
if my_l.from_city in corresp_ila.keys():
|
||||
# "east edfa in Stbrieuc to Rennes_STA" is equivalent name as
|
||||
# "Edfa0_fiber (Lannion_CAS → Stbrieuc)-F056"
|
||||
# "west edfa in Stbrieuc to Rennes_STA" is equivalent name as
|
||||
# "Edfa0_fiber (Rennes_STA → Stbrieuc)-F057"
|
||||
# does not filter names: all types (except boosters) are created.
|
||||
# in case fibers are splitted the name here is a prefix
|
||||
corresp_ila[my_l.from_city].append(name)
|
||||
else:
|
||||
corresp_ila[my_l.from_city] = [name]
|
||||
name = f'Edfa0_fiber ({my_l.from_city} \u2192 {my_l.to_city})-{my_l.east_cable}'
|
||||
if name in ila:
|
||||
if my_l.to_city in corresp_ila.keys():
|
||||
corresp_ila[my_l.to_city].append(name)
|
||||
else:
|
||||
corresp_ila[my_l.to_city] = [name]
|
||||
|
||||
# merge fused with ila:
|
||||
for key, val in corresp_fused.items():
|
||||
if key in corresp_ila.keys():
|
||||
corresp_ila[key].extend(val)
|
||||
else:
|
||||
corresp_ila[key] = val
|
||||
# no need of roadm booster
|
||||
return corresp_roadm, corresp_fused, corresp_ila
|
||||
|
||||
|
||||
def parse_excel(input_filename):
|
||||
link_headers = {
|
||||
'Node A': 'from_city',
|
||||
'Node Z': 'to_city',
|
||||
'east': {
|
||||
'Distance (km)': 'east_distance',
|
||||
'Fiber type': 'east_fiber',
|
||||
'lineic att': 'east_lineic',
|
||||
'Con_in': 'east_con_in',
|
||||
'Con_out': 'east_con_out',
|
||||
'PMD': 'east_pmd',
|
||||
'Cable id': 'east_cable'
|
||||
},
|
||||
'west': {
|
||||
'Distance (km)': 'west_distance',
|
||||
'Fiber type': 'west_fiber',
|
||||
'lineic att': 'west_lineic',
|
||||
'Con_in': 'west_con_in',
|
||||
'Con_out': 'west_con_out',
|
||||
'PMD': 'west_pmd',
|
||||
'Cable id': 'west_cable'
|
||||
}
|
||||
}
|
||||
node_headers = {
|
||||
'City': 'city',
|
||||
'State': 'state',
|
||||
'Country': 'country',
|
||||
'Region': 'region',
|
||||
'Latitude': 'latitude',
|
||||
'Longitude': 'longitude',
|
||||
'Type': 'node_type',
|
||||
'Booster_restriction': 'booster_restriction',
|
||||
'Preamp_restriction': 'preamp_restriction'
|
||||
}
|
||||
eqpt_headers = {
|
||||
'Node A': 'from_city',
|
||||
'Node Z': 'to_city',
|
||||
'east': {
|
||||
'amp type': 'east_amp_type',
|
||||
'att_in': 'east_att_in',
|
||||
'amp gain': 'east_amp_gain',
|
||||
'delta p': 'east_amp_dp',
|
||||
'tilt': 'east_tilt',
|
||||
'att_out': 'east_att_out'
|
||||
},
|
||||
'west': {
|
||||
'amp type': 'west_amp_type',
|
||||
'att_in': 'west_att_in',
|
||||
'amp gain': 'west_amp_gain',
|
||||
'delta p': 'west_amp_dp',
|
||||
'tilt': 'west_tilt',
|
||||
'att_out': 'west_att_out'
|
||||
}
|
||||
}
|
||||
|
||||
with open_workbook(input_filename) as wb:
|
||||
nodes_sheet = wb.sheet_by_name('Nodes')
|
||||
links_sheet = wb.sheet_by_name('Links')
|
||||
try:
|
||||
eqpt_sheet = wb.sheet_by_name('Eqpt')
|
||||
except Exception:
|
||||
# eqpt_sheet is optional
|
||||
eqpt_sheet = None
|
||||
|
||||
nodes = []
|
||||
for node in parse_sheet(nodes_sheet, node_headers, NODES_LINE, NODES_LINE + 1, NODES_COLUMN):
|
||||
nodes.append(Node(**node))
|
||||
expected_node_types = {'ROADM', 'ILA', 'FUSED'}
|
||||
for n in nodes:
|
||||
if n.node_type not in expected_node_types:
|
||||
n.node_type = 'ILA'
|
||||
|
||||
links = []
|
||||
for link in parse_sheet(links_sheet, link_headers, LINKS_LINE, LINKS_LINE + 2, LINKS_COLUMN):
|
||||
links.append(Link(**link))
|
||||
|
||||
eqpts = []
|
||||
if eqpt_sheet is not None:
|
||||
for eqpt in parse_sheet(eqpt_sheet, eqpt_headers, EQPTS_LINE, EQPTS_LINE + 2, EQPTS_COLUMN):
|
||||
eqpts.append(Eqpt(**eqpt))
|
||||
|
||||
# sanity check
|
||||
all_cities = Counter(n.city for n in nodes)
|
||||
if len(all_cities) != len(nodes):
|
||||
raise ValueError(f'Duplicate city: {all_cities}')
|
||||
bad_links = []
|
||||
for lnk in links:
|
||||
if lnk.from_city not in all_cities or lnk.to_city not in all_cities:
|
||||
bad_links.append([lnk.from_city, lnk.to_city])
|
||||
if bad_links:
|
||||
raise NetworkTopologyError(f'Bad link(s): {bad_links}.')
|
||||
|
||||
return nodes, links, eqpts
|
||||
|
||||
|
||||
def eqpt_connection_by_city(city_name):
|
||||
other_cities = fiber_dest_from_source(city_name)
|
||||
subdata = []
|
||||
if nodes_by_city[city_name].node_type.lower() in {'ila', 'fused'}:
|
||||
# Then len(other_cities) == 2
|
||||
direction = ['west', 'east']
|
||||
for i in range(2):
|
||||
from_ = fiber_link(other_cities[i], city_name)
|
||||
in_ = eqpt_in_city_to_city(city_name, other_cities[0], direction[i])
|
||||
to_ = fiber_link(city_name, other_cities[1 - i])
|
||||
subdata += connect_eqpt(from_, in_, to_)
|
||||
elif nodes_by_city[city_name].node_type.lower() == 'roadm':
|
||||
for other_city in other_cities:
|
||||
from_ = f'roadm {city_name}'
|
||||
in_ = eqpt_in_city_to_city(city_name, other_city)
|
||||
to_ = fiber_link(city_name, other_city)
|
||||
subdata += connect_eqpt(from_, in_, to_)
|
||||
|
||||
from_ = fiber_link(other_city, city_name)
|
||||
in_ = eqpt_in_city_to_city(city_name, other_city, "west")
|
||||
to_ = f'roadm {city_name}'
|
||||
subdata += connect_eqpt(from_, in_, to_)
|
||||
return subdata
|
||||
|
||||
|
||||
def connect_eqpt(from_, in_, to_):
|
||||
connections = []
|
||||
if in_ != '':
|
||||
connections = [{'from_node': from_, 'to_node': in_},
|
||||
{'from_node': in_, 'to_node': to_}]
|
||||
else:
|
||||
connections = [{'from_node': from_, 'to_node': to_}]
|
||||
return connections
|
||||
|
||||
|
||||
def eqpt_in_city_to_city(in_city, to_city, direction='east'):
|
||||
rev_direction = 'west' if direction == 'east' else 'east'
|
||||
amp_direction = f'{direction}_amp_type'
|
||||
amp_rev_direction = f'{rev_direction}_amp_type'
|
||||
return_eqpt = ''
|
||||
if in_city in eqpts_by_city:
|
||||
for e in eqpts_by_city[in_city]:
|
||||
if nodes_by_city[in_city].node_type.lower() == 'roadm':
|
||||
if e.to_city == to_city and getattr(e, amp_direction) != '':
|
||||
return_eqpt = f'{direction} edfa in {e.from_city} to {e.to_city}'
|
||||
elif nodes_by_city[in_city].node_type.lower() == 'ila':
|
||||
if e.to_city != to_city:
|
||||
direction = rev_direction
|
||||
amp_direction = amp_rev_direction
|
||||
if getattr(e, amp_direction) != '':
|
||||
return_eqpt = f'{direction} edfa in {e.from_city} to {e.to_city}'
|
||||
if nodes_by_city[in_city].node_type.lower() == 'fused':
|
||||
return_eqpt = f'{direction} fused spans in {in_city}'
|
||||
return return_eqpt
|
||||
|
||||
|
||||
def corresp_next_node(network, corresp_ila, corresp_roadm):
|
||||
""" for each name in corresp dictionnaries find the next node in network and its name
|
||||
given by user in excel. for meshTopology_exampleV2.xls:
|
||||
user ILA name Stbrieuc covers the two direction. convert.py creates 2 different ILA
|
||||
with possible names (depending on the direction and if the eqpt was defined in eqpt
|
||||
sheet)
|
||||
- east edfa in Stbrieuc to Rennes_STA
|
||||
- west edfa in Stbrieuc to Rennes_STA
|
||||
- Edfa0_fiber (Lannion_CAS → Stbrieuc)-F056
|
||||
- Edfa0_fiber (Rennes_STA → Stbrieuc)-F057
|
||||
next_nodes finds the user defined name of next node to be able to map the path constraints
|
||||
- east edfa in Stbrieuc to Rennes_STA next node = Rennes_STA
|
||||
- west edfa in Stbrieuc to Rennes_STA next node Lannion_CAS
|
||||
|
||||
Edfa0_fiber (Lannion_CAS → Stbrieuc)-F056 and Edfa0_fiber (Rennes_STA → Stbrieuc)-F057
|
||||
do not exist
|
||||
the function supports fiber splitting, fused nodes and shall only be called if
|
||||
excel format is used for both network and service
|
||||
"""
|
||||
next_node = {}
|
||||
# consolidate tables and create next_node table
|
||||
for ila_key, ila_list in corresp_ila.items():
|
||||
temp = copy(ila_list)
|
||||
for ila_elem in ila_list:
|
||||
# find the node with ila_elem string _in_ the node uid. 'in' is used instead of
|
||||
# '==' to find composed nodes due to fiber splitting in autodesign.
|
||||
# eg if elem_ila is 'Edfa0_fiber (Lannion_CAS → Stbrieuc)-F056',
|
||||
# node uid 'Edfa0_fiber (Lannion_CAS → Stbrieuc)-F056_(1/2)' is possible
|
||||
correct_ila_name = next(n.uid for n in network.nodes() if ila_elem in n.uid)
|
||||
temp.remove(ila_elem)
|
||||
temp.append(correct_ila_name)
|
||||
ila_nd = next(n for n in network.nodes() if ila_elem in n.uid)
|
||||
next_nd = next(network.successors(ila_nd))
|
||||
# search for the next ILA or ROADM
|
||||
while isinstance(next_nd, (Fiber, Fused)):
|
||||
next_nd = next(network.successors(next_nd))
|
||||
# if next_nd is a ROADM, add the first found correspondance
|
||||
for key, val in corresp_roadm.items():
|
||||
# val is a list of possible names associated with key
|
||||
if next_nd.uid in val:
|
||||
next_node[correct_ila_name] = key
|
||||
break
|
||||
# if next_nd was not already added in the dict with the previous loop,
|
||||
# add the first found correspondance in ila names
|
||||
if correct_ila_name not in next_node.keys():
|
||||
for key, val in corresp_ila.items():
|
||||
# in case of splitted fibers the ila name might not be exact match
|
||||
if [e for e in val if e in next_nd.uid]:
|
||||
next_node[correct_ila_name] = key
|
||||
break
|
||||
|
||||
corresp_ila[ila_key] = temp
|
||||
return corresp_ila, next_node
|
||||
|
||||
|
||||
def fiber_dest_from_source(city_name):
|
||||
destinations = []
|
||||
links_from_city = links_by_city[city_name]
|
||||
for l in links_from_city:
|
||||
if l.from_city == city_name:
|
||||
destinations.append(l.to_city)
|
||||
else:
|
||||
destinations.append(l.from_city)
|
||||
return destinations
|
||||
|
||||
|
||||
def fiber_link(from_city, to_city):
|
||||
source_dest = (from_city, to_city)
|
||||
links = links_by_city[from_city]
|
||||
link = next(l for l in links if l.from_city in source_dest and l.to_city in source_dest)
|
||||
if link.from_city == from_city:
|
||||
fiber = f'fiber ({link.from_city} \u2192 {link.to_city})-{link.east_cable}'
|
||||
else:
|
||||
fiber = f'fiber ({link.to_city} \u2192 {link.from_city})-{link.west_cable}'
|
||||
return fiber
|
||||
|
||||
|
||||
def midpoint(city_a, city_b):
|
||||
lats = city_a.latitude, city_b.latitude
|
||||
longs = city_a.longitude, city_b.longitude
|
||||
try:
|
||||
result = {
|
||||
'latitude': sum(lats) / 2,
|
||||
'longitude': sum(longs) / 2
|
||||
}
|
||||
except TypeError:
|
||||
result = {
|
||||
'latitude': 0,
|
||||
'longitude': 0
|
||||
}
|
||||
return result
|
||||
|
||||
# TODO get column size automatically from tupple size
|
||||
|
||||
|
||||
NODES_COLUMN = 10
|
||||
NODES_LINE = 4
|
||||
LINKS_COLUMN = 16
|
||||
LINKS_LINE = 3
|
||||
EQPTS_LINE = 3
|
||||
EQPTS_COLUMN = 14
|
||||
|
||||
|
||||
def _do_convert():
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument('workbook', type=Path)
|
||||
parser.add_argument('-f', '--filter-region', action='append', default=[])
|
||||
parser.add_argument('--output', type=Path, help='Name of the generated JSON file')
|
||||
args = parser.parse_args()
|
||||
res = convert_file(args.workbook, args.filter_region, args.output)
|
||||
print(f'XLS -> JSON saved to {res}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
_do_convert()
|
||||
544
gnpy/tools/json_io.py
Normal file
544
gnpy/tools/json_io.py
Normal file
@@ -0,0 +1,544 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
'''
|
||||
gnpy.tools.json_io
|
||||
==================
|
||||
|
||||
Loading and saving data from JSON files in GNPy's internal data format
|
||||
'''
|
||||
|
||||
from networkx import DiGraph
|
||||
from logging import getLogger
|
||||
from pathlib import Path
|
||||
import json
|
||||
from collections import namedtuple
|
||||
from gnpy.core import ansi_escapes, elements
|
||||
from gnpy.core.equipment import trx_mode_params
|
||||
from gnpy.core.exceptions import ConfigurationError, EquipmentConfigError, NetworkTopologyError, ServiceError
|
||||
from gnpy.core.science_utils import estimate_nf_model
|
||||
from gnpy.core.utils import automatic_nch, automatic_fmax, merge_amplifier_restrictions
|
||||
from gnpy.topology.request import PathRequest, Disjunction
|
||||
from gnpy.tools.convert import xls_to_json_data
|
||||
from gnpy.tools.service_sheet import read_service_sheet
|
||||
import time
|
||||
|
||||
|
||||
_logger = getLogger(__name__)
|
||||
|
||||
|
||||
Model_vg = namedtuple('Model_vg', 'nf1 nf2 delta_p')
|
||||
Model_fg = namedtuple('Model_fg', 'nf0')
|
||||
Model_openroadm = namedtuple('Model_openroadm', 'nf_coef')
|
||||
Model_hybrid = namedtuple('Model_hybrid', 'nf_ram gain_ram edfa_variety')
|
||||
Model_dual_stage = namedtuple('Model_dual_stage', 'preamp_variety booster_variety')
|
||||
|
||||
|
||||
class _JsonThing:
|
||||
def update_attr(self, default_values, kwargs, name):
|
||||
clean_kwargs = {k: v for k, v in kwargs.items() if v != ''}
|
||||
for k, v in default_values.items():
|
||||
setattr(self, k, clean_kwargs.get(k, v))
|
||||
if k not in clean_kwargs and name != 'Amp':
|
||||
print(ansi_escapes.red +
|
||||
f'\n WARNING missing {k} attribute in eqpt_config.json[{name}]' +
|
||||
f'\n default value is {k} = {v}' +
|
||||
ansi_escapes.reset)
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
class SI(_JsonThing):
|
||||
default_values = {
|
||||
"f_min": 191.35e12,
|
||||
"f_max": 196.1e12,
|
||||
"baud_rate": 32e9,
|
||||
"spacing": 50e9,
|
||||
"power_dbm": 0,
|
||||
"power_range_db": [0, 0, 0.5],
|
||||
"roll_off": 0.15,
|
||||
"tx_osnr": 45,
|
||||
"sys_margins": 0
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.update_attr(self.default_values, kwargs, 'SI')
|
||||
|
||||
|
||||
class Span(_JsonThing):
|
||||
default_values = {
|
||||
'power_mode': True,
|
||||
'delta_power_range_db': None,
|
||||
'max_fiber_lineic_loss_for_raman': 0.25,
|
||||
'target_extended_gain': 2.5,
|
||||
'max_length': 150,
|
||||
'length_units': 'km',
|
||||
'max_loss': None,
|
||||
'padding': 10,
|
||||
'EOL': 0,
|
||||
'con_in': 0,
|
||||
'con_out': 0
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.update_attr(self.default_values, kwargs, 'Span')
|
||||
|
||||
|
||||
class Roadm(_JsonThing):
|
||||
default_values = {
|
||||
'target_pch_out_db': -17,
|
||||
'add_drop_osnr': 100,
|
||||
'pmd': 0,
|
||||
'restrictions': {
|
||||
'preamp_variety_list': [],
|
||||
'booster_variety_list': []
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.update_attr(self.default_values, kwargs, 'Roadm')
|
||||
|
||||
|
||||
class Transceiver(_JsonThing):
|
||||
default_values = {
|
||||
'type_variety': None,
|
||||
'frequency': None,
|
||||
'mode': {}
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.update_attr(self.default_values, kwargs, 'Transceiver')
|
||||
|
||||
|
||||
class Fiber(_JsonThing):
|
||||
default_values = {
|
||||
'type_variety': '',
|
||||
'dispersion': None,
|
||||
'gamma': 0,
|
||||
'pmd_coef': 0
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.update_attr(self.default_values, kwargs, 'Fiber')
|
||||
|
||||
|
||||
class RamanFiber(_JsonThing):
|
||||
default_values = {
|
||||
'type_variety': '',
|
||||
'dispersion': None,
|
||||
'gamma': 0,
|
||||
'pmd_coef': 0,
|
||||
'raman_efficiency': None
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.update_attr(self.default_values, kwargs, 'RamanFiber')
|
||||
for param in ('cr', 'frequency_offset'):
|
||||
if param not in self.raman_efficiency:
|
||||
raise EquipmentConfigError(f'RamanFiber.raman_efficiency: missing "{param}" parameter')
|
||||
if self.raman_efficiency['frequency_offset'] != sorted(self.raman_efficiency['frequency_offset']):
|
||||
raise EquipmentConfigError(f'RamanFiber.raman_efficiency.frequency_offset is not sorted')
|
||||
|
||||
|
||||
class Amp(_JsonThing):
|
||||
default_values = {
|
||||
'f_min': 191.35e12,
|
||||
'f_max': 196.1e12,
|
||||
'type_variety': '',
|
||||
'type_def': '',
|
||||
'gain_flatmax': None,
|
||||
'gain_min': None,
|
||||
'p_max': None,
|
||||
'nf_model': None,
|
||||
'dual_stage_model': None,
|
||||
'nf_fit_coeff': None,
|
||||
'nf_ripple': None,
|
||||
'dgt': None,
|
||||
'gain_ripple': None,
|
||||
'out_voa_auto': False,
|
||||
'allowed_for_design': False,
|
||||
'raman': False
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.update_attr(self.default_values, kwargs, 'Amp')
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, filename, **kwargs):
|
||||
config = Path(filename).parent / 'default_edfa_config.json'
|
||||
|
||||
type_variety = kwargs['type_variety']
|
||||
type_def = kwargs.get('type_def', 'variable_gain') # default compatibility with older json eqpt files
|
||||
nf_def = None
|
||||
dual_stage_def = None
|
||||
|
||||
if type_def == 'fixed_gain':
|
||||
try:
|
||||
nf0 = kwargs.pop('nf0')
|
||||
except KeyError: # nf0 is expected for a fixed gain amp
|
||||
raise EquipmentConfigError(f'missing nf0 value input for amplifier: {type_variety} in equipment config')
|
||||
for k in ('nf_min', 'nf_max'):
|
||||
try:
|
||||
del kwargs[k]
|
||||
except KeyError:
|
||||
pass
|
||||
nf_def = Model_fg(nf0)
|
||||
elif type_def == 'advanced_model':
|
||||
config = Path(filename).parent / kwargs.pop('advanced_config_from_json')
|
||||
elif type_def == 'variable_gain':
|
||||
gain_min, gain_max = kwargs['gain_min'], kwargs['gain_flatmax']
|
||||
try: # nf_min and nf_max are expected for a variable gain amp
|
||||
nf_min = kwargs.pop('nf_min')
|
||||
nf_max = kwargs.pop('nf_max')
|
||||
except KeyError:
|
||||
raise EquipmentConfigError(f'missing nf_min or nf_max value input for amplifier: {type_variety} in equipment config')
|
||||
try: # remove all remaining nf inputs
|
||||
del kwargs['nf0']
|
||||
except KeyError:
|
||||
pass # nf0 is not needed for variable gain amp
|
||||
nf1, nf2, delta_p = estimate_nf_model(type_variety, gain_min, gain_max, nf_min, nf_max)
|
||||
nf_def = Model_vg(nf1, nf2, delta_p)
|
||||
elif type_def == 'openroadm':
|
||||
try:
|
||||
nf_coef = kwargs.pop('nf_coef')
|
||||
except KeyError: # nf_coef is expected for openroadm amp
|
||||
raise EquipmentConfigError(f'missing nf_coef input for amplifier: {type_variety} in equipment config')
|
||||
nf_def = Model_openroadm(nf_coef)
|
||||
elif type_def == 'dual_stage':
|
||||
try: # nf_ram and gain_ram are expected for a hybrid amp
|
||||
preamp_variety = kwargs.pop('preamp_variety')
|
||||
booster_variety = kwargs.pop('booster_variety')
|
||||
except KeyError:
|
||||
raise EquipmentConfigError(f'missing preamp/booster variety input for amplifier: {type_variety} in equipment config')
|
||||
dual_stage_def = Model_dual_stage(preamp_variety, booster_variety)
|
||||
|
||||
json_data = load_json(config)
|
||||
|
||||
return cls(**{**kwargs, **json_data,
|
||||
'nf_model': nf_def, 'dual_stage_model': dual_stage_def})
|
||||
|
||||
|
||||
def _automatic_spacing(baud_rate):
|
||||
"""return the min possible channel spacing for a given baud rate"""
|
||||
# TODO : this should parametrized in a cfg file
|
||||
# list of possible tuples [(max_baud_rate, spacing_for_this_baud_rate)]
|
||||
spacing_list = [(33e9, 37.5e9), (38e9, 50e9), (50e9, 62.5e9), (67e9, 75e9), (92e9, 100e9)]
|
||||
return min((s[1] for s in spacing_list if s[0] > baud_rate), default=baud_rate * 1.2)
|
||||
|
||||
|
||||
def load_equipment(filename):
|
||||
json_data = load_json(filename)
|
||||
return _equipment_from_json(json_data, filename)
|
||||
|
||||
|
||||
def _update_trx_osnr(equipment):
|
||||
"""add sys_margins to all Transceivers OSNR values"""
|
||||
for trx in equipment['Transceiver'].values():
|
||||
for m in trx.mode:
|
||||
m['OSNR'] = m['OSNR'] + equipment['SI']['default'].sys_margins
|
||||
return equipment
|
||||
|
||||
|
||||
def _update_dual_stage(equipment):
|
||||
edfa_dict = equipment['Edfa']
|
||||
for edfa in edfa_dict.values():
|
||||
if edfa.type_def == 'dual_stage':
|
||||
edfa_preamp = edfa_dict[edfa.dual_stage_model.preamp_variety]
|
||||
edfa_booster = edfa_dict[edfa.dual_stage_model.booster_variety]
|
||||
for key, value in edfa_preamp.__dict__.items():
|
||||
attr_k = 'preamp_' + key
|
||||
setattr(edfa, attr_k, value)
|
||||
for key, value in edfa_booster.__dict__.items():
|
||||
attr_k = 'booster_' + key
|
||||
setattr(edfa, attr_k, value)
|
||||
edfa.p_max = edfa_booster.p_max
|
||||
edfa.gain_flatmax = edfa_booster.gain_flatmax + edfa_preamp.gain_flatmax
|
||||
if edfa.gain_min < edfa_preamp.gain_min:
|
||||
raise EquipmentConfigError(f'Dual stage {edfa.type_variety} minimal gain is lower than its preamp minimal gain')
|
||||
return equipment
|
||||
|
||||
|
||||
def _roadm_restrictions_sanity_check(equipment):
|
||||
""" verifies that booster and preamp restrictions specified in roadm equipment are listed
|
||||
in the edfa.
|
||||
"""
|
||||
restrictions = equipment['Roadm']['default'].restrictions['booster_variety_list'] + \
|
||||
equipment['Roadm']['default'].restrictions['preamp_variety_list']
|
||||
for amp_name in restrictions:
|
||||
if amp_name not in equipment['Edfa']:
|
||||
raise EquipmentConfigError(f'ROADM restriction {amp_name} does not refer to a defined EDFA name')
|
||||
|
||||
|
||||
def _equipment_from_json(json_data, filename):
|
||||
"""build global dictionnary eqpt_library that stores all eqpt characteristics:
|
||||
edfa type type_variety, fiber type_variety
|
||||
from the eqpt_config.json (filename parameter)
|
||||
also read advanced_config_from_json file parameters for edfa if they are available:
|
||||
typically nf_ripple, dfg gain ripple, dgt and nf polynomial nf_fit_coeff
|
||||
if advanced_config_from_json file parameter is not present: use nf_model:
|
||||
requires nf_min and nf_max values boundaries of the edfa gain range
|
||||
"""
|
||||
equipment = {}
|
||||
for key, entries in json_data.items():
|
||||
equipment[key] = {}
|
||||
for entry in entries:
|
||||
subkey = entry.get('type_variety', 'default')
|
||||
if key == 'Edfa':
|
||||
equipment[key][subkey] = Amp.from_json(filename, **entry)
|
||||
elif key == 'Fiber':
|
||||
equipment[key][subkey] = Fiber(**entry)
|
||||
elif key == 'Span':
|
||||
equipment[key][subkey] = Span(**entry)
|
||||
elif key == 'Roadm':
|
||||
equipment[key][subkey] = Roadm(**entry)
|
||||
elif key == 'SI':
|
||||
equipment[key][subkey] = SI(**entry)
|
||||
elif key == 'Transceiver':
|
||||
equipment[key][subkey] = Transceiver(**entry)
|
||||
elif key == 'RamanFiber':
|
||||
equipment[key][subkey] = RamanFiber(**entry)
|
||||
else:
|
||||
raise EquipmentConfigError(f'Unrecognized network element type "{key}"')
|
||||
equipment = _update_trx_osnr(equipment)
|
||||
equipment = _update_dual_stage(equipment)
|
||||
_roadm_restrictions_sanity_check(equipment)
|
||||
return equipment
|
||||
|
||||
|
||||
def load_network(filename, equipment):
|
||||
if filename.suffix.lower() in ('.xls', '.xlsx'):
|
||||
json_data = xls_to_json_data(filename)
|
||||
elif filename.suffix.lower() == '.json':
|
||||
json_data = load_json(filename)
|
||||
else:
|
||||
raise ValueError(f'unsupported topology filename extension {filename.suffix.lower()}')
|
||||
return network_from_json(json_data, equipment)
|
||||
|
||||
|
||||
def save_network(network: DiGraph, filename: str):
|
||||
'''Dump the network into a JSON file
|
||||
|
||||
:param network: network to work on
|
||||
:param filename: file to write to
|
||||
'''
|
||||
save_json(network_to_json(network), filename)
|
||||
|
||||
|
||||
def _cls_for(equipment_type):
|
||||
if equipment_type == 'Edfa':
|
||||
return elements.Edfa
|
||||
if equipment_type == 'Fused':
|
||||
return elements.Fused
|
||||
elif equipment_type == 'Roadm':
|
||||
return elements.Roadm
|
||||
elif equipment_type == 'Transceiver':
|
||||
return elements.Transceiver
|
||||
elif equipment_type == 'Fiber':
|
||||
return elements.Fiber
|
||||
elif equipment_type == 'RamanFiber':
|
||||
return elements.RamanFiber
|
||||
else:
|
||||
raise ConfigurationError(f'Unknown network equipment "{equipment_type}"')
|
||||
|
||||
|
||||
def network_from_json(json_data, equipment):
|
||||
# NOTE|dutc: we could use the following, but it would tie our data format
|
||||
# too closely to the graph library
|
||||
# from networkx import node_link_graph
|
||||
g = DiGraph()
|
||||
for el_config in json_data['elements']:
|
||||
typ = el_config.pop('type')
|
||||
variety = el_config.pop('type_variety', 'default')
|
||||
cls = _cls_for(typ)
|
||||
if typ == 'Fused':
|
||||
# well, there's no variety for the 'Fused' node type
|
||||
pass
|
||||
elif variety in equipment[typ]:
|
||||
extra_params = equipment[typ][variety]
|
||||
temp = el_config.setdefault('params', {})
|
||||
temp = merge_amplifier_restrictions(temp, extra_params.__dict__)
|
||||
el_config['params'] = temp
|
||||
el_config['type_variety'] = variety
|
||||
elif typ in ['Edfa', 'Fiber', 'RamanFiber']: # catch it now because the code will crash later!
|
||||
raise ConfigurationError(f'The {typ} of variety type {variety} was not recognized:'
|
||||
'\nplease check it is properly defined in the eqpt_config json file')
|
||||
el = cls(**el_config)
|
||||
g.add_node(el)
|
||||
|
||||
nodes = {k.uid: k for k in g.nodes()}
|
||||
|
||||
for cx in json_data['connections']:
|
||||
from_node, to_node = cx['from_node'], cx['to_node']
|
||||
try:
|
||||
if isinstance(nodes[from_node], elements.Fiber):
|
||||
edge_length = nodes[from_node].params.length
|
||||
else:
|
||||
edge_length = 0.01
|
||||
g.add_edge(nodes[from_node], nodes[to_node], weight=edge_length)
|
||||
except KeyError:
|
||||
raise NetworkTopologyError(f'can not find {from_node} or {to_node} defined in {cx}')
|
||||
|
||||
return g
|
||||
|
||||
|
||||
def network_to_json(network):
|
||||
data = {
|
||||
'elements': [n.to_json for n in network]
|
||||
}
|
||||
connections = {
|
||||
'connections': [{"from_node": n.uid,
|
||||
"to_node": next_n.uid}
|
||||
for n in network
|
||||
for next_n in network.successors(n) if next_n is not None]
|
||||
}
|
||||
data.update(connections)
|
||||
return data
|
||||
|
||||
|
||||
def load_json(filename):
|
||||
with open(filename, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
return data
|
||||
|
||||
|
||||
def save_json(obj, filename):
|
||||
with open(filename, 'w', encoding='utf-8') as f:
|
||||
json.dump(obj, f, indent=2, ensure_ascii=False)
|
||||
|
||||
|
||||
def load_requests(filename, eqpt, bidir, network, network_filename):
|
||||
""" loads the requests from a json or an excel file into a data string
|
||||
"""
|
||||
if filename.suffix.lower() in ('.xls', '.xlsx'):
|
||||
_logger.info('Automatically converting requests from XLS to JSON')
|
||||
try:
|
||||
return convert_service_sheet(filename, eqpt, network, network_filename=network_filename, bidir=bidir)
|
||||
except ServiceError as this_e:
|
||||
print(f'{ansi_escapes.red}Service error:{ansi_escapes.reset} {this_e}')
|
||||
exit(1)
|
||||
else:
|
||||
return load_json(filename)
|
||||
|
||||
|
||||
def requests_from_json(json_data, equipment):
|
||||
"""Extract list of requests from data parsed from JSON"""
|
||||
requests_list = []
|
||||
|
||||
for req in json_data['path-request']:
|
||||
# init all params from request
|
||||
params = {}
|
||||
params['request_id'] = req['request-id']
|
||||
params['source'] = req['source']
|
||||
params['bidir'] = req['bidirectional']
|
||||
params['destination'] = req['destination']
|
||||
params['trx_type'] = req['path-constraints']['te-bandwidth']['trx_type']
|
||||
if 'trx_mode' in req['path-constraints']['te-bandwidth'].keys():
|
||||
params['trx_mode'] = req['path-constraints']['te-bandwidth']['trx_mode']
|
||||
else:
|
||||
params['trx_mode'] = None
|
||||
params['format'] = params['trx_mode']
|
||||
params['spacing'] = req['path-constraints']['te-bandwidth']['spacing']
|
||||
try:
|
||||
nd_list = req['explicit-route-objects']['route-object-include-exclude']
|
||||
except KeyError:
|
||||
nd_list = []
|
||||
params['nodes_list'] = [n['num-unnum-hop']['node-id'] for n in nd_list]
|
||||
params['loose_list'] = [n['num-unnum-hop']['hop-type'] for n in nd_list]
|
||||
# recover trx physical param (baudrate, ...) from type and mode
|
||||
# in trx_mode_params optical power is read from equipment['SI']['default'] and
|
||||
# nb_channel is computed based on min max frequency and spacing
|
||||
trx_params = trx_mode_params(equipment, params['trx_type'], params['trx_mode'], True)
|
||||
params.update(trx_params)
|
||||
# print(trx_params['min_spacing'])
|
||||
# optical power might be set differently in the request. if it is indicated then the
|
||||
# params['power'] is updated
|
||||
try:
|
||||
if req['path-constraints']['te-bandwidth']['output-power']:
|
||||
params['power'] = req['path-constraints']['te-bandwidth']['output-power']
|
||||
except KeyError:
|
||||
pass
|
||||
# same process for nb-channel
|
||||
f_min = params['f_min']
|
||||
f_max_from_si = params['f_max']
|
||||
try:
|
||||
if req['path-constraints']['te-bandwidth']['max-nb-of-channel'] is not None:
|
||||
nch = req['path-constraints']['te-bandwidth']['max-nb-of-channel']
|
||||
params['nb_channel'] = nch
|
||||
spacing = params['spacing']
|
||||
params['f_max'] = automatic_fmax(f_min, spacing, nch)
|
||||
else:
|
||||
params['nb_channel'] = automatic_nch(f_min, f_max_from_si, params['spacing'])
|
||||
except KeyError:
|
||||
params['nb_channel'] = automatic_nch(f_min, f_max_from_si, params['spacing'])
|
||||
if 'effective-freq-slot' in req['path-constraints']['te-bandwidth']:
|
||||
# temporarily reads only the first slot
|
||||
params['effective_freq_slot'] = req['path-constraints']['te-bandwidth']['effective-freq-slot'][0]
|
||||
else:
|
||||
params['effective_freq_slot'] = None
|
||||
_check_one_request(params, f_max_from_si)
|
||||
|
||||
try:
|
||||
params['path_bandwidth'] = req['path-constraints']['te-bandwidth']['path_bandwidth']
|
||||
except KeyError:
|
||||
pass
|
||||
requests_list.append(PathRequest(**params))
|
||||
return requests_list
|
||||
|
||||
|
||||
def _check_one_request(params, f_max_from_si):
|
||||
"""Checks that the requested parameters are consistant (spacing vs nb channel vs transponder mode...)"""
|
||||
f_min = params['f_min']
|
||||
f_max = params['f_max']
|
||||
max_recommanded_nb_channels = automatic_nch(f_min, f_max, params['spacing'])
|
||||
if params['baud_rate'] is not None:
|
||||
# implicitly means that a mode is defined with min_spacing
|
||||
if params['min_spacing'] > params['spacing']:
|
||||
msg = f'Request {params["request_id"]} has spacing below transponder ' +\
|
||||
f'{params["trx_type"]} {params["trx_mode"]} min spacing value ' +\
|
||||
f'{params["min_spacing"]*1e-9}GHz.\nComputation stopped'
|
||||
print(msg)
|
||||
_logger.critical(msg)
|
||||
raise ServiceError(msg)
|
||||
if f_max > f_max_from_si:
|
||||
msg = f'''Requested channel number {params["nb_channel"]}, baud rate {params["baud_rate"]} GHz
|
||||
and requested spacing {params["spacing"]*1e-9}GHz is not consistent with frequency range
|
||||
{f_min*1e-12} THz, {f_max*1e-12} THz, min recommanded spacing {params["min_spacing"]*1e-9}GHz.
|
||||
max recommanded nb of channels is {max_recommanded_nb_channels}.'''
|
||||
_logger.critical(msg)
|
||||
raise ServiceError(msg)
|
||||
|
||||
|
||||
def disjunctions_from_json(json_data):
|
||||
""" reads the disjunction requests from the json dict and create the list
|
||||
of requested disjunctions for this set of requests
|
||||
"""
|
||||
disjunctions_list = []
|
||||
try:
|
||||
temp_test = json_data['synchronization']
|
||||
except KeyError:
|
||||
temp_test = []
|
||||
if temp_test:
|
||||
for snc in json_data['synchronization']:
|
||||
params = {}
|
||||
params['disjunction_id'] = snc['synchronization-id']
|
||||
params['relaxable'] = snc['svec']['relaxable']
|
||||
params['link_diverse'] = 'link' in snc['svec']['disjointness']
|
||||
params['node_diverse'] = 'node' in snc['svec']['disjointness']
|
||||
params['disjunctions_req'] = snc['svec']['request-id-number']
|
||||
disjunctions_list.append(Disjunction(**params))
|
||||
|
||||
return disjunctions_list
|
||||
|
||||
|
||||
def convert_service_sheet(
|
||||
input_filename,
|
||||
eqpt,
|
||||
network,
|
||||
network_filename=None,
|
||||
output_filename='',
|
||||
bidir=False,
|
||||
filter_region=None):
|
||||
if output_filename == '':
|
||||
output_filename = f'{str(input_filename)[0:len(str(input_filename))-len(str(input_filename.suffixes[0]))]}_services.json'
|
||||
data = read_service_sheet(input_filename, eqpt, network, network_filename, bidir, filter_region)
|
||||
save_json(data, output_filename)
|
||||
return data
|
||||
82
gnpy/tools/plots.py
Executable file
82
gnpy/tools/plots.py
Executable file
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
'''
|
||||
gnpy.tools.plots
|
||||
================
|
||||
|
||||
Graphs and plots usable form a CLI application
|
||||
'''
|
||||
|
||||
from matplotlib.pyplot import show, axis, figure, title, text
|
||||
from networkx import draw_networkx_nodes, draw_networkx_edges, draw_networkx_labels
|
||||
from gnpy.core.elements import Transceiver
|
||||
|
||||
|
||||
def plot_baseline(network):
|
||||
edges = set(network.edges())
|
||||
pos = {n: (n.lng, n.lat) for n in network.nodes()}
|
||||
labels = {n: n.location.city for n in network.nodes() if isinstance(n, Transceiver)}
|
||||
city_labels = set(labels.values())
|
||||
for n in network.nodes():
|
||||
if n.location.city and n.location.city not in city_labels:
|
||||
labels[n] = n.location.city
|
||||
city_labels.add(n.location.city)
|
||||
label_pos = pos
|
||||
|
||||
fig = figure()
|
||||
kwargs = {'figure': fig, 'pos': pos}
|
||||
plot = draw_networkx_nodes(network, nodelist=network.nodes(), node_color='#ababab', **kwargs)
|
||||
draw_networkx_edges(network, edgelist=edges, edge_color='#ababab', **kwargs)
|
||||
draw_networkx_labels(network, labels=labels, font_size=14, **{**kwargs, 'pos': label_pos})
|
||||
axis('off')
|
||||
show()
|
||||
|
||||
|
||||
def plot_results(network, path, source, destination, infos):
|
||||
path_edges = set(zip(path[:-1], path[1:]))
|
||||
edges = set(network.edges()) - path_edges
|
||||
pos = {n: (n.lng, n.lat) for n in network.nodes()}
|
||||
nodes = {}
|
||||
for k, (x, y) in pos.items():
|
||||
nodes.setdefault((round(x, 1), round(y, 1)), []).append(k)
|
||||
labels = {n: n.location.city for n in network.nodes() if isinstance(n, Transceiver)}
|
||||
city_labels = set(labels.values())
|
||||
for n in network.nodes():
|
||||
if n.location.city and n.location.city not in city_labels:
|
||||
labels[n] = n.location.city
|
||||
city_labels.add(n.location.city)
|
||||
label_pos = pos
|
||||
|
||||
fig = figure()
|
||||
kwargs = {'figure': fig, 'pos': pos}
|
||||
all_nodes = [n for n in network.nodes() if n not in path]
|
||||
plot = draw_networkx_nodes(network, nodelist=all_nodes, node_color='#ababab', node_size=50, **kwargs)
|
||||
draw_networkx_nodes(network, nodelist=path, node_color='#ff0000', node_size=55, **kwargs)
|
||||
draw_networkx_edges(network, edgelist=edges, edge_color='#ababab', **kwargs)
|
||||
draw_networkx_edges(network, edgelist=path_edges, edge_color='#ff0000', **kwargs)
|
||||
draw_networkx_labels(network, labels=labels, font_size=14, **{**kwargs, 'pos': label_pos})
|
||||
title(f'Propagating from {source.loc.city} to {destination.loc.city}')
|
||||
axis('off')
|
||||
|
||||
heading = 'Spectral Information\n\n'
|
||||
textbox = text(0.85, 0.20, heading, fontsize=14, fontname='Ubuntu Mono',
|
||||
verticalalignment='top', transform=fig.axes[0].transAxes,
|
||||
bbox={'boxstyle': 'round', 'facecolor': 'wheat', 'alpha': 0.5})
|
||||
|
||||
msgs = {(x, y): heading + '\n\n'.join(str(n) for n in ns if n in path)
|
||||
for (x, y), ns in nodes.items()}
|
||||
|
||||
def hover(event):
|
||||
if event.xdata is None or event.ydata is None:
|
||||
return
|
||||
if fig.contains(event):
|
||||
x, y = round(event.xdata, 1), round(event.ydata, 1)
|
||||
if (x, y) in msgs:
|
||||
textbox.set_text(msgs[x, y])
|
||||
else:
|
||||
textbox.set_text(heading)
|
||||
fig.canvas.draw_idle()
|
||||
|
||||
fig.canvas.mpl_connect('motion_notify_event', hover)
|
||||
show()
|
||||
172
gnpy/tools/rest_example.py
Normal file
172
gnpy/tools/rest_example.py
Normal file
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
'''
|
||||
gnpy.tools.rest_example
|
||||
=======================
|
||||
|
||||
GNPy as a rest API example
|
||||
'''
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from pathlib import Path
|
||||
|
||||
import werkzeug
|
||||
from flask import Flask, request
|
||||
from numpy import mean
|
||||
from werkzeug.exceptions import InternalServerError
|
||||
|
||||
import gnpy.core.ansi_escapes as ansi_escapes
|
||||
import gnpy.core.exceptions as exceptions
|
||||
from gnpy.core.network import build_network
|
||||
from gnpy.core.utils import lin2db, automatic_nch
|
||||
from gnpy.tools.json_io import requests_from_json, disjunctions_from_json, _equipment_from_json, network_from_json
|
||||
from gnpy.topology.request import (ResultElement, compute_path_dsjctn, requests_aggregation,
|
||||
correct_json_route_list,
|
||||
deduplicate_disjunctions, compute_path_with_disjunction)
|
||||
from gnpy.topology.spectrum_assignment import build_oms_list, pth_assign_spectrum
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
_examples_dir = Path(__file__).parent.parent / 'example-data'
|
||||
_reaesc = re.compile(r'\x1b[^m]*m')
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
@app.route('/api/v1/path-computation', methods=['POST'])
|
||||
def compute_path():
|
||||
data = request.json
|
||||
service = data['gnpy-api:service']
|
||||
topology = data['gnpy-api:topology']
|
||||
equipment = _equipment_from_json(data['gnpy-api:equipment'],
|
||||
os.path.join(_examples_dir, 'std_medium_gain_advanced_config.json'))
|
||||
network = network_from_json(topology, equipment)
|
||||
|
||||
propagatedpths, reversed_propagatedpths, rqs = path_requests_run(service, network, equipment)
|
||||
# Generate the output
|
||||
result = []
|
||||
# assumes that list of rqs and list of propgatedpths have same order
|
||||
for i, pth in enumerate(propagatedpths):
|
||||
result.append(ResultElement(rqs[i], pth, reversed_propagatedpths[i]))
|
||||
return {"result": {"response": [n.json for n in result]}}, 201
|
||||
|
||||
|
||||
@app.route('/api/v1/status', methods=['GET'])
|
||||
def api_status():
|
||||
return {"version": "v1", "status": "ok"}, 200
|
||||
|
||||
|
||||
def _init_logger():
|
||||
handler = RotatingFileHandler('api.log', maxBytes=1024 * 1024, backupCount=5, encoding='utf-8')
|
||||
ch = logging.StreamHandler()
|
||||
logging.basicConfig(level=logging.INFO, handlers=[handler, ch],
|
||||
format="%(asctime)s %(levelname)s %(name)s(%(lineno)s) [%(threadName)s - %(thread)d] - %("
|
||||
"message)s")
|
||||
|
||||
|
||||
def path_requests_run(service, network, equipment):
|
||||
# Build the network once using the default power defined in SI in eqpt config
|
||||
# TODO power density: db2linp(ower_dbm": 0)/power_dbm": 0 * nb channels as defined by
|
||||
# spacing, f_min and f_max
|
||||
p_db = equipment['SI']['default'].power_dbm
|
||||
|
||||
p_total_db = p_db + lin2db(automatic_nch(equipment['SI']['default'].f_min,
|
||||
equipment['SI']['default'].f_max, equipment['SI']['default'].spacing))
|
||||
build_network(network, equipment, p_db, p_total_db)
|
||||
oms_list = build_oms_list(network, equipment)
|
||||
rqs = requests_from_json(service, equipment)
|
||||
|
||||
# check that request ids are unique. Non unique ids, may
|
||||
# mess the computation: better to stop the computation
|
||||
all_ids = [r.request_id for r in rqs]
|
||||
if len(all_ids) != len(set(all_ids)):
|
||||
for item in list(set(all_ids)):
|
||||
all_ids.remove(item)
|
||||
msg = f'Requests id {all_ids} are not unique'
|
||||
_logger.critical(msg)
|
||||
raise ValueError('Requests id ' + all_ids + ' are not unique')
|
||||
rqs = correct_json_route_list(network, rqs)
|
||||
|
||||
# pths = compute_path(network, equipment, rqs)
|
||||
dsjn = disjunctions_from_json(service)
|
||||
|
||||
# need to warn or correct in case of wrong disjunction form
|
||||
# disjunction must not be repeated with same or different ids
|
||||
dsjn = deduplicate_disjunctions(dsjn)
|
||||
|
||||
rqs, dsjn = requests_aggregation(rqs, dsjn)
|
||||
# TODO export novel set of aggregated demands in a json file
|
||||
|
||||
_logger.info(f'{ansi_escapes.blue}The following services have been requested:{ansi_escapes.reset}' + str(rqs))
|
||||
|
||||
_logger.info(f'{ansi_escapes.blue}Computing all paths with constraints{ansi_escapes.reset}')
|
||||
pths = compute_path_dsjctn(network, equipment, rqs, dsjn)
|
||||
|
||||
_logger.info(f'{ansi_escapes.blue}Propagating on selected path{ansi_escapes.reset}')
|
||||
propagatedpths, reversed_pths, reversed_propagatedpths = compute_path_with_disjunction(network, equipment, rqs,
|
||||
pths)
|
||||
# Note that deepcopy used in compute_path_with_disjunction returns
|
||||
# a list of nodes which are not belonging to network (they are copies of the node objects).
|
||||
# so there can not be propagation on these nodes.
|
||||
|
||||
pth_assign_spectrum(pths, rqs, oms_list, reversed_pths)
|
||||
return propagatedpths, reversed_propagatedpths, rqs
|
||||
|
||||
|
||||
def common_error_handler(exception):
|
||||
"""
|
||||
|
||||
:type exception: Exception
|
||||
|
||||
"""
|
||||
status_code = 500
|
||||
if not isinstance(exception, werkzeug.exceptions.HTTPException):
|
||||
exception = werkzeug.exceptions.InternalServerError()
|
||||
exception.description = "Something went wrong on our side."
|
||||
|
||||
response = {
|
||||
'message': exception.name,
|
||||
'description': exception.description,
|
||||
'code': exception.code
|
||||
}
|
||||
|
||||
return werkzeug.Response(response=json.dumps(response), status=status_code, mimetype='application/json')
|
||||
|
||||
|
||||
def bad_request_handler(exception):
|
||||
response = {
|
||||
'message': 'bad request',
|
||||
'description': _reaesc.sub('', str(exception)),
|
||||
'code': 400
|
||||
}
|
||||
return werkzeug.Response(response=json.dumps(response), status=400, mimetype='application/json')
|
||||
|
||||
|
||||
def _init_app():
|
||||
app.register_error_handler(KeyError, bad_request_handler)
|
||||
app.register_error_handler(TypeError, bad_request_handler)
|
||||
app.register_error_handler(ValueError, bad_request_handler)
|
||||
app.register_error_handler(exceptions.ConfigurationError, bad_request_handler)
|
||||
app.register_error_handler(exceptions.DisjunctionError, bad_request_handler)
|
||||
app.register_error_handler(exceptions.EquipmentConfigError, bad_request_handler)
|
||||
app.register_error_handler(exceptions.NetworkTopologyError, bad_request_handler)
|
||||
app.register_error_handler(exceptions.ServiceError, bad_request_handler)
|
||||
app.register_error_handler(exceptions.SpectrumError, bad_request_handler)
|
||||
app.register_error_handler(exceptions.ParametersError, bad_request_handler)
|
||||
app.register_error_handler(AssertionError, bad_request_handler)
|
||||
app.register_error_handler(InternalServerError, common_error_handler)
|
||||
for error_code in werkzeug.exceptions.default_exceptions:
|
||||
app.register_error_handler(error_code, common_error_handler)
|
||||
|
||||
|
||||
def main():
|
||||
_init_logger()
|
||||
_init_app()
|
||||
app.run(host='0.0.0.0', port=8080)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
381
gnpy/tools/service_sheet.py
Normal file
381
gnpy/tools/service_sheet.py
Normal file
@@ -0,0 +1,381 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
gnpy.tools.service_sheet
|
||||
========================
|
||||
|
||||
XLS parser that can be called to create a JSON request file in accordance with
|
||||
Yang model for requesting path computation.
|
||||
|
||||
See: draft-ietf-teas-yang-path-computation-01.txt
|
||||
"""
|
||||
|
||||
from xlrd import open_workbook, XL_CELL_EMPTY
|
||||
from collections import namedtuple
|
||||
from logging import getLogger
|
||||
from copy import deepcopy
|
||||
from gnpy.core.utils import db2lin
|
||||
from gnpy.core.exceptions import ServiceError
|
||||
from gnpy.core.elements import Transceiver, Roadm, Edfa, Fiber
|
||||
import gnpy.core.ansi_escapes as ansi_escapes
|
||||
from gnpy.tools.convert import corresp_names, corresp_next_node
|
||||
|
||||
SERVICES_COLUMN = 12
|
||||
|
||||
|
||||
def all_rows(sheet, start=0):
|
||||
return (sheet.row(x) for x in range(start, sheet.nrows))
|
||||
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class Request(namedtuple('Request', 'request_id source destination trx_type mode \
|
||||
spacing power nb_channel disjoint_from nodes_list is_loose path_bandwidth')):
|
||||
def __new__(cls, request_id, source, destination, trx_type, mode=None, spacing=None, power=None, nb_channel=None, disjoint_from='', nodes_list=None, is_loose='', path_bandwidth=None):
|
||||
return super().__new__(cls, request_id, source, destination, trx_type, mode, spacing, power, nb_channel, disjoint_from, nodes_list, is_loose, path_bandwidth)
|
||||
|
||||
|
||||
class Element:
|
||||
def __eq__(self, other):
|
||||
return type(self) == type(other) and self.uid == other.uid
|
||||
|
||||
def __hash__(self):
|
||||
return hash((type(self), self.uid))
|
||||
|
||||
|
||||
class Request_element(Element):
|
||||
def __init__(self, Request, equipment, bidir):
|
||||
# request_id is str
|
||||
# excel has automatic number formatting that adds .0 on integer values
|
||||
# the next lines recover the pure int value, assuming this .0 is unwanted
|
||||
self.request_id = correct_xlrd_int_to_str_reading(Request.request_id)
|
||||
self.source = f'trx {Request.source}'
|
||||
self.destination = f'trx {Request.destination}'
|
||||
# TODO: the automatic naming generated by excel parser requires that source and dest name
|
||||
# be a string starting with 'trx' : this is manually added here.
|
||||
self.srctpid = f'trx {Request.source}'
|
||||
self.dsttpid = f'trx {Request.destination}'
|
||||
self.bidir = bidir
|
||||
# test that trx_type belongs to eqpt_config.json
|
||||
# if not replace it with a default
|
||||
try:
|
||||
if equipment['Transceiver'][Request.trx_type]:
|
||||
self.trx_type = correct_xlrd_int_to_str_reading(Request.trx_type)
|
||||
if Request.mode is not None:
|
||||
Requestmode = correct_xlrd_int_to_str_reading(Request.mode)
|
||||
if [mode for mode in equipment['Transceiver'][Request.trx_type].mode if mode['format'] == Requestmode]:
|
||||
self.mode = Requestmode
|
||||
else:
|
||||
msg = f'Request Id: {self.request_id} - could not find tsp : \'{Request.trx_type}\' with mode: \'{Requestmode}\' in eqpt library \nComputation stopped.'
|
||||
# print(msg)
|
||||
logger.critical(msg)
|
||||
raise ServiceError(msg)
|
||||
else:
|
||||
Requestmode = None
|
||||
self.mode = Request.mode
|
||||
except KeyError:
|
||||
msg = f'Request Id: {self.request_id} - could not find tsp : \'{Request.trx_type}\' with mode: \'{Request.mode}\' in eqpt library \nComputation stopped.'
|
||||
# print(msg)
|
||||
logger.critical(msg)
|
||||
raise ServiceError(msg)
|
||||
# excel input are in GHz and dBm
|
||||
if Request.spacing is not None:
|
||||
self.spacing = Request.spacing * 1e9
|
||||
else:
|
||||
msg = f'Request {self.request_id} missing spacing: spacing is mandatory.\ncomputation stopped'
|
||||
logger.critical(msg)
|
||||
raise ServiceError(msg)
|
||||
if Request.power is not None:
|
||||
self.power = db2lin(Request.power) * 1e-3
|
||||
else:
|
||||
self.power = None
|
||||
if Request.nb_channel is not None:
|
||||
self.nb_channel = int(Request.nb_channel)
|
||||
else:
|
||||
self.nb_channel = None
|
||||
|
||||
value = correct_xlrd_int_to_str_reading(Request.disjoint_from)
|
||||
self.disjoint_from = [n for n in value.split(' | ') if value]
|
||||
self.nodes_list = []
|
||||
if Request.nodes_list:
|
||||
self.nodes_list = Request.nodes_list.split(' | ')
|
||||
self.loose = 'LOOSE'
|
||||
if Request.is_loose.lower() == 'no':
|
||||
self.loose = 'STRICT'
|
||||
self.path_bandwidth = None
|
||||
if Request.path_bandwidth is not None:
|
||||
self.path_bandwidth = Request.path_bandwidth * 1e9
|
||||
else:
|
||||
self.path_bandwidth = 0
|
||||
|
||||
uid = property(lambda self: repr(self))
|
||||
|
||||
@property
|
||||
def pathrequest(self):
|
||||
# Default assumption for bidir is False
|
||||
req_dictionnary = {
|
||||
'request-id': self.request_id,
|
||||
'source': self.source,
|
||||
'destination': self.destination,
|
||||
'src-tp-id': self.srctpid,
|
||||
'dst-tp-id': self.dsttpid,
|
||||
'bidirectional': self.bidir,
|
||||
'path-constraints': {
|
||||
'te-bandwidth': {
|
||||
'technology': 'flexi-grid',
|
||||
'trx_type': self.trx_type,
|
||||
'trx_mode': self.mode,
|
||||
'effective-freq-slot': [{'N': None, 'M': None}],
|
||||
'spacing': self.spacing,
|
||||
'max-nb-of-channel': self.nb_channel,
|
||||
'output-power': self.power
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.nodes_list:
|
||||
req_dictionnary['explicit-route-objects'] = {}
|
||||
temp = {'route-object-include-exclude': [
|
||||
{'explicit-route-usage': 'route-include-ero',
|
||||
'index': self.nodes_list.index(node),
|
||||
'num-unnum-hop': {
|
||||
'node-id': f'{node}',
|
||||
'link-tp-id': 'link-tp-id is not used',
|
||||
'hop-type': f'{self.loose}',
|
||||
}
|
||||
}
|
||||
for node in self.nodes_list]
|
||||
}
|
||||
req_dictionnary['explicit-route-objects'] = temp
|
||||
if self.path_bandwidth is not None:
|
||||
req_dictionnary['path-constraints']['te-bandwidth']['path_bandwidth'] = self.path_bandwidth
|
||||
|
||||
return req_dictionnary
|
||||
|
||||
@property
|
||||
def pathsync(self):
|
||||
if self.disjoint_from:
|
||||
return {'synchronization-id': self.request_id,
|
||||
'svec': {
|
||||
'relaxable': 'false',
|
||||
'disjointness': 'node link',
|
||||
'request-id-number': [self.request_id] + [n for n in self.disjoint_from]
|
||||
}
|
||||
}
|
||||
else:
|
||||
return None
|
||||
# TO-DO: avoid multiple entries with same synchronisation vectors
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
return self.pathrequest, self.pathsync
|
||||
|
||||
|
||||
def read_service_sheet(
|
||||
input_filename,
|
||||
eqpt,
|
||||
network,
|
||||
network_filename=None,
|
||||
bidir=False,
|
||||
filter_region=None):
|
||||
""" converts a service sheet into a json structure
|
||||
"""
|
||||
if filter_region is None:
|
||||
filter_region = []
|
||||
if network_filename is None:
|
||||
network_filename = input_filename
|
||||
service = parse_excel(input_filename)
|
||||
req = [Request_element(n, eqpt, bidir) for n in service]
|
||||
req = correct_xls_route_list(network_filename, network, req)
|
||||
# if there is no sync vector , do not write any synchronization
|
||||
synchro = [n.json[1] for n in req if n.json[1] is not None]
|
||||
if synchro:
|
||||
data = {
|
||||
'path-request': [n.json[0] for n in req],
|
||||
'synchronization': synchro
|
||||
}
|
||||
else:
|
||||
data = {
|
||||
'path-request': [n.json[0] for n in req]
|
||||
}
|
||||
return data
|
||||
|
||||
|
||||
def correct_xlrd_int_to_str_reading(v):
|
||||
if not isinstance(v, str):
|
||||
value = str(int(v))
|
||||
if value.endswith('.0'):
|
||||
value = value[:-2]
|
||||
else:
|
||||
value = v
|
||||
return value
|
||||
|
||||
|
||||
def parse_row(row, fieldnames):
|
||||
return {f: r.value for f, r in zip(fieldnames, row[0:SERVICES_COLUMN])
|
||||
if r.ctype != XL_CELL_EMPTY}
|
||||
|
||||
|
||||
def parse_excel(input_filename):
|
||||
with open_workbook(input_filename) as wb:
|
||||
service_sheet = wb.sheet_by_name('Service')
|
||||
services = list(parse_service_sheet(service_sheet))
|
||||
return services
|
||||
|
||||
|
||||
def parse_service_sheet(service_sheet):
|
||||
""" reads each column according to authorized fieldnames. order is not important.
|
||||
"""
|
||||
logger.info(f'Validating headers on {service_sheet.name!r}')
|
||||
# add a test on field to enable the '' field case that arises when columns on the
|
||||
# right hand side are used as comments or drawing in the excel sheet
|
||||
header = [x.value.strip() for x in service_sheet.row(4)[0:SERVICES_COLUMN]
|
||||
if len(x.value.strip()) > 0]
|
||||
|
||||
# create a service_fieldname independant from the excel column order
|
||||
# to be compatible with any version of the sheet
|
||||
# the following dictionnary records the excel field names and the corresponding parameter's name
|
||||
|
||||
authorized_fieldnames = {
|
||||
'route id': 'request_id', 'Source': 'source', 'Destination': 'destination',
|
||||
'TRX type': 'trx_type', 'Mode': 'mode', 'System: spacing': 'spacing',
|
||||
'System: input power (dBm)': 'power', 'System: nb of channels': 'nb_channel',
|
||||
'routing: disjoint from': 'disjoint_from', 'routing: path': 'nodes_list',
|
||||
'routing: is loose?': 'is_loose', 'path bandwidth': 'path_bandwidth'}
|
||||
try:
|
||||
service_fieldnames = [authorized_fieldnames[e] for e in header]
|
||||
except KeyError:
|
||||
msg = f'Malformed header on Service sheet: {header} field not in {authorized_fieldnames}'
|
||||
logger.critical(msg)
|
||||
raise ValueError(msg)
|
||||
for row in all_rows(service_sheet, start=5):
|
||||
yield Request(**parse_row(row[0:SERVICES_COLUMN], service_fieldnames))
|
||||
|
||||
|
||||
def correct_xls_route_list(network_filename, network, pathreqlist):
|
||||
""" prepares the format of route list of nodes to be consistant with nodes names:
|
||||
remove wrong names, find correct names for ila, roadm and fused if the entry was
|
||||
xls.
|
||||
if it was not xls, all names in list should be exact name in the network.
|
||||
"""
|
||||
|
||||
# first loads the base correspondance dict built with excel naming
|
||||
corresp_roadm, corresp_fused, corresp_ila = corresp_names(network_filename, network)
|
||||
# then correct dict names with names of the autodisign and find next_node name
|
||||
# according to xls naming
|
||||
corresp_ila, next_node = corresp_next_node(network, corresp_ila, corresp_roadm)
|
||||
# finally correct constraints based on these dict
|
||||
trxfibertype = [n.uid for n in network.nodes() if isinstance(n, (Transceiver, Fiber))]
|
||||
roadmtype = [n.uid for n in network.nodes() if isinstance(n, Roadm)]
|
||||
edfatype = [n.uid for n in network.nodes() if isinstance(n, Edfa)]
|
||||
# TODO there is a problem of identification of fibers in case of parallel
|
||||
# fibers between two adjacent roadms so fiber constraint is not supported
|
||||
transponders = [n.uid for n in network.nodes() if isinstance(n, Transceiver)]
|
||||
for pathreq in pathreqlist:
|
||||
# first check that source and dest are transceivers
|
||||
if pathreq.source not in transponders:
|
||||
msg = f'{ansi_escapes.red}Request: {pathreq.request_id}: could not find' +\
|
||||
f' transponder source : {pathreq.source}.{ansi_escapes.reset}'
|
||||
logger.critical(msg)
|
||||
raise ServiceError(msg)
|
||||
|
||||
if pathreq.destination not in transponders:
|
||||
msg = f'{ansi_escapes.red}Request: {pathreq.request_id}: could not find' +\
|
||||
f' transponder destination: {pathreq.destination}.{ansi_escapes.reset}'
|
||||
logger.critical(msg)
|
||||
raise ServiceError(msg)
|
||||
# silently pop source and dest nodes from the list if they were added by the user as first
|
||||
# and last elem in the constraints respectively. Other positions must lead to an error
|
||||
# caught later on
|
||||
if pathreq.nodes_list and pathreq.source == pathreq.nodes_list[0]:
|
||||
pathreq.loose_list.pop(0)
|
||||
pathreq.nodes_list.pop(0)
|
||||
if pathreq.nodes_list and pathreq.destination == pathreq.nodes_list[-1]:
|
||||
pathreq.loose_list.pop(-1)
|
||||
pathreq.nodes_list.pop(-1)
|
||||
# Then process user defined constraints with respect to automatic namings
|
||||
temp = deepcopy(pathreq)
|
||||
# This needs a temporary object since we may suppress/correct elements in the list
|
||||
# during the process
|
||||
for i, n_id in enumerate(temp.nodes_list):
|
||||
# n_id must not be a transceiver and must not be a fiber (non supported, user
|
||||
# can not enter fiber names in excel)
|
||||
if n_id not in trxfibertype:
|
||||
# check that n_id is in the node list, if not find a correspondance name
|
||||
if n_id in roadmtype + edfatype:
|
||||
nodes_suggestion = [n_id]
|
||||
else:
|
||||
# checks first roadm, fused, and ila in this order, because ila automatic name
|
||||
# contain roadm names. If it is a fused node, next ila names might be correct
|
||||
# suggestions, especially if following fibers were splitted and ila names
|
||||
# created with the name of the fused node
|
||||
if n_id in corresp_roadm.keys():
|
||||
nodes_suggestion = corresp_roadm[n_id]
|
||||
elif n_id in corresp_fused.keys():
|
||||
nodes_suggestion = corresp_fused[n_id] + corresp_ila[n_id]
|
||||
elif n_id in corresp_ila.keys():
|
||||
nodes_suggestion = corresp_ila[n_id]
|
||||
else:
|
||||
nodes_suggestion = []
|
||||
if nodes_suggestion:
|
||||
try:
|
||||
if len(nodes_suggestion) > 1:
|
||||
# if there is more than one suggestion, we need to choose the direction
|
||||
# we rely on the next node provided by the user for this purpose
|
||||
new_n = next(n for n in nodes_suggestion
|
||||
if n in next_node.keys() and next_node[n]
|
||||
in temp.nodes_list[i:] + [pathreq.destination] and
|
||||
next_node[n] not in temp.nodes_list[:i])
|
||||
else:
|
||||
new_n = nodes_suggestion[0]
|
||||
if new_n != n_id:
|
||||
# warns the user when the correct name is used only in verbose mode,
|
||||
# eg 'a' is a roadm and correct name is 'roadm a' or when there was
|
||||
# too much ambiguity, 'b' is an ila, its name can be:
|
||||
# Edfa0_fiber (a → b)-xx if next node is c or
|
||||
# Edfa0_fiber (c → b)-xx if next node is a
|
||||
msg = f'{ansi_escapes.yellow}Invalid route node specified:' +\
|
||||
f'\n\t\'{n_id}\', replaced with \'{new_n}\'{ansi_escapes.reset}'
|
||||
logger.info(msg)
|
||||
pathreq.nodes_list[pathreq.nodes_list.index(n_id)] = new_n
|
||||
except StopIteration:
|
||||
# shall not come in this case, unless requested direction does not exist
|
||||
msg = f'{ansi_escapes.yellow}Invalid route specified {n_id}: could' +\
|
||||
f' not decide on direction, skipped!.\nPlease add a valid' +\
|
||||
f' direction in constraints (next neighbour node){ansi_escapes.reset}'
|
||||
print(msg)
|
||||
logger.info(msg)
|
||||
pathreq.loose_list.pop(pathreq.nodes_list.index(n_id))
|
||||
pathreq.nodes_list.remove(n_id)
|
||||
else:
|
||||
if temp.loose_list[i] == 'LOOSE':
|
||||
# if no matching can be found in the network just ignore this constraint
|
||||
# if it is a loose constraint
|
||||
# warns the user that this node is not part of the topology
|
||||
msg = f'{ansi_escapes.yellow}Invalid node specified:\n\t\'{n_id}\'' +\
|
||||
f', could not use it as constraint, skipped!{ansi_escapes.reset}'
|
||||
print(msg)
|
||||
logger.info(msg)
|
||||
pathreq.loose_list.pop(pathreq.nodes_list.index(n_id))
|
||||
pathreq.nodes_list.remove(n_id)
|
||||
else:
|
||||
msg = f'{ansi_escapes.red}Could not find node:\n\t\'{n_id}\' in network' +\
|
||||
f' topology. Strict constraint can not be applied.{ansi_escapes.reset}'
|
||||
logger.critical(msg)
|
||||
raise ServiceError(msg)
|
||||
else:
|
||||
if temp.loose_list[i] == 'LOOSE':
|
||||
print(f'{ansi_escapes.yellow}Invalid route node specified:\n\t\'{n_id}\'' +
|
||||
f' type is not supported as constraint with xls network input,' +
|
||||
f' skipped!{ansi_escapes.reset}')
|
||||
pathreq.loose_list.pop(pathreq.nodes_list.index(n_id))
|
||||
pathreq.nodes_list.remove(n_id)
|
||||
else:
|
||||
msg = f'{ansi_escapes.red}Invalid route node specified \n\t\'{n_id}\'' +\
|
||||
f' type is not supported as constraint with xls network input,' +\
|
||||
f', Strict constraint can not be applied.{ansi_escapes.reset}'
|
||||
logger.critical(msg)
|
||||
raise ServiceError(msg)
|
||||
return pathreqlist
|
||||
3
gnpy/topology/__init__.py
Normal file
3
gnpy/topology/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
'''
|
||||
Tracking :py:mod:`.request` for spectrum and their :py:mod:`.spectrum_assignment`.
|
||||
'''
|
||||
1172
gnpy/topology/request.py
Normal file
1172
gnpy/topology/request.py
Normal file
File diff suppressed because it is too large
Load Diff
447
gnpy/topology/spectrum_assignment.py
Normal file
447
gnpy/topology/spectrum_assignment.py
Normal file
@@ -0,0 +1,447 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
gnpy.topology.spectrum_assignment
|
||||
=================================
|
||||
|
||||
This module contains the :class:`Oms` and :class:`Bitmap` classes and methods to
|
||||
select and assign spectrum. The :func:`spectrum_selection` function identifies the free
|
||||
slots and :func:`select_candidate` selects the candidate spectrum according to
|
||||
strategy: for example first fit
|
||||
oms records its elements, and elements are updated with an oms to have
|
||||
element/oms correspondace
|
||||
"""
|
||||
|
||||
from collections import namedtuple
|
||||
from logging import getLogger
|
||||
from math import ceil
|
||||
from gnpy.core.elements import Roadm, Transceiver
|
||||
from gnpy.core.exceptions import ServiceError, SpectrumError
|
||||
|
||||
LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
class Bitmap:
|
||||
""" records the spectrum occupation
|
||||
"""
|
||||
|
||||
def __init__(self, f_min, f_max, grid, guardband=0.15e12, bitmap=None):
|
||||
# n is the min index including guardband. Guardband is require to be sure
|
||||
# that a channel can be assigned with center frequency fmin (means that its
|
||||
# slot occupation goes below freq_index_min
|
||||
n_min = frequency_to_n(f_min - guardband, grid)
|
||||
n_max = frequency_to_n(f_max + guardband, grid) - 1
|
||||
self.n_min = n_min
|
||||
self.n_max = n_max
|
||||
self.freq_index_min = frequency_to_n(f_min)
|
||||
self.freq_index_max = frequency_to_n(f_max)
|
||||
self.freq_index = list(range(n_min, n_max + 1))
|
||||
if bitmap is None:
|
||||
self.bitmap = [1] * (n_max - n_min + 1)
|
||||
elif len(bitmap) == len(self.freq_index):
|
||||
self.bitmap = bitmap
|
||||
else:
|
||||
raise SpectrumError(f'bitmap is not consistant with f_min{f_min} - n: {n_min} and f_max{f_max}- n :{n_max}')
|
||||
|
||||
def getn(self, i):
|
||||
""" converts the n (itu grid) into a local index
|
||||
"""
|
||||
return self.freq_index[i]
|
||||
|
||||
def geti(self, nvalue):
|
||||
""" converts the local index into n (itu grid)
|
||||
"""
|
||||
return self.freq_index.index(nvalue)
|
||||
|
||||
def insert_left(self, newbitmap):
|
||||
""" insert bitmap on the left to align oms bitmaps if their start frequencies are different
|
||||
"""
|
||||
self.bitmap = newbitmap + self.bitmap
|
||||
temp = list(range(self.n_min - len(newbitmap), self.n_min))
|
||||
self.freq_index = temp + self.freq_index
|
||||
self.n_min = self.freq_index[0]
|
||||
|
||||
def insert_right(self, newbitmap):
|
||||
""" insert bitmap on the right to align oms bitmaps if their stop frequencies are different
|
||||
"""
|
||||
self.bitmap = self.bitmap + newbitmap
|
||||
self.freq_index = self.freq_index + list(range(self.n_max, self.n_max + len(newbitmap)))
|
||||
self.n_max = self.freq_index[-1]
|
||||
|
||||
|
||||
# +'grid available_slots f_min f_max services_list')
|
||||
OMSParams = namedtuple('OMSParams', 'oms_id el_id_list el_list')
|
||||
|
||||
|
||||
class OMS:
|
||||
""" OMS class is the logical container that represent a link between two adjacent ROADMs and
|
||||
records the crossed elements and the occupied spectrum
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **params):
|
||||
params = OMSParams(**params)
|
||||
self.oms_id = params.oms_id
|
||||
self.el_id_list = params.el_id_list
|
||||
self.el_list = params.el_list
|
||||
self.spectrum_bitmap = []
|
||||
self.nb_channels = 0
|
||||
self.service_list = []
|
||||
# TODO
|
||||
|
||||
def __str__(self):
|
||||
return '\n\t'.join([f'{type(self).__name__} {self.oms_id}',
|
||||
f'{self.el_id_list[0]} - {self.el_id_list[-1]}'])
|
||||
|
||||
def __repr__(self):
|
||||
return '\n\t'.join([f'{type(self).__name__} {self.oms_id}',
|
||||
f'{self.el_id_list[0]} - {self.el_id_list[-1]}', '\n'])
|
||||
|
||||
def add_element(self, elem):
|
||||
""" records oms elements
|
||||
"""
|
||||
self.el_id_list.append(elem.uid)
|
||||
self.el_list.append(elem)
|
||||
|
||||
def update_spectrum(self, f_min, f_max, guardband=0.15e12, existing_spectrum=None,
|
||||
grid=0.00625e12):
|
||||
""" frequencies expressed in Hz
|
||||
"""
|
||||
if existing_spectrum is None:
|
||||
# add some 150 GHz margin to enable a center channel on f_min
|
||||
# use ITU-T G694.1
|
||||
# Flexible DWDM grid definition
|
||||
# For the flexible DWDM grid, the allowed frequency slots have a nominal
|
||||
# central frequency (in THz) defined by:
|
||||
# 193.1 + n × 0.00625 where n is a positive or negative integer including 0
|
||||
# and 0.00625 is the nominal central frequency granularity in THz
|
||||
# and a slot width defined by:
|
||||
# 12.5 × m where m is a positive integer and 12.5 is the slot width granularity in
|
||||
# GHz.
|
||||
# Any combination of frequency slots is allowed as long as no two frequency
|
||||
# slots overlap.
|
||||
|
||||
# TODO : add explaination on that / parametrize ....
|
||||
self.spectrum_bitmap = Bitmap(f_min, f_max, grid, guardband)
|
||||
# print(len(self.spectrum_bitmap.bitmap))
|
||||
|
||||
def assign_spectrum(self, nvalue, mvalue):
|
||||
""" change oms spectrum to mark spectrum assigned
|
||||
"""
|
||||
if not isinstance(nvalue, int):
|
||||
raise SpectrumError(f'N must be a signed integer, got {nvalue}')
|
||||
if not isinstance(mvalue, int):
|
||||
raise SpectrumError(f'M must be an integer, got {mvalue}')
|
||||
if mvalue <= 0:
|
||||
raise SpectrumError(f'M must be positive, got {mvalue}')
|
||||
if nvalue > self.spectrum_bitmap.freq_index_max:
|
||||
raise SpectrumError(f'N {nvalue} over the upper spectrum boundary')
|
||||
if nvalue < self.spectrum_bitmap.freq_index_min:
|
||||
raise SpectrumError(f'N {nvalue} below the lower spectrum boundary')
|
||||
startn, stopn = mvalue_to_slots(nvalue, mvalue)
|
||||
if stopn > self.spectrum_bitmap.n_max:
|
||||
raise SpectrumError(f'N {nvalue}, M {mvalue} over the N spectrum bitmap bounds')
|
||||
if startn <= self.spectrum_bitmap.n_min:
|
||||
raise SpectrumError(f'N {nvalue}, M {mvalue} below the N spectrum bitmap bounds')
|
||||
self.spectrum_bitmap.bitmap[self.spectrum_bitmap.geti(startn):self.spectrum_bitmap.geti(stopn) + 1] = [0] * (stopn - startn + 1)
|
||||
|
||||
def add_service(self, service_id, nb_wl):
|
||||
""" record service and mark spectrum as occupied
|
||||
"""
|
||||
self.service_list.append(service_id)
|
||||
self.nb_channels += nb_wl
|
||||
|
||||
|
||||
def frequency_to_n(freq, grid=0.00625e12):
|
||||
""" converts frequency into the n value (ITU grid)
|
||||
reference to Recommendation G.694.1 (02/12), Figure I.3
|
||||
https://www.itu.int/rec/T-REC-G.694.1-201202-I/en
|
||||
|
||||
>>> frequency_to_n(193.1375e12)
|
||||
6
|
||||
>>> frequency_to_n(193.225e12)
|
||||
20
|
||||
|
||||
"""
|
||||
return (int)((freq - 193.1e12) / grid)
|
||||
|
||||
|
||||
def nvalue_to_frequency(nvalue, grid=0.00625e12):
|
||||
""" converts n value into a frequency
|
||||
reference to Recommendation G.694.1 (02/12), Table 1
|
||||
https://www.itu.int/rec/T-REC-G.694.1-201202-I/en
|
||||
|
||||
>>> nvalue_to_frequency(6)
|
||||
193137500000000.0
|
||||
>>> nvalue_to_frequency(-1, 0.1e12)
|
||||
193000000000000.0
|
||||
|
||||
"""
|
||||
return 193.1e12 + nvalue * grid
|
||||
|
||||
|
||||
def mvalue_to_slots(nvalue, mvalue):
|
||||
""" convert center n an m into start and stop n
|
||||
"""
|
||||
startn = nvalue - mvalue
|
||||
stopn = nvalue + mvalue - 1
|
||||
return startn, stopn
|
||||
|
||||
|
||||
def slots_to_m(startn, stopn):
|
||||
""" converts the start and stop n values to the center n and m value
|
||||
reference to Recommendation G.694.1 (02/12), Figure I.3
|
||||
https://www.itu.int/rec/T-REC-G.694.1-201202-I/en
|
||||
|
||||
>>> nval, mval = slots_to_m(6, 20)
|
||||
>>> nval
|
||||
13
|
||||
>>> mval
|
||||
7
|
||||
|
||||
"""
|
||||
nvalue = (int)((startn + stopn + 1) / 2)
|
||||
mvalue = (int)((stopn - startn + 1) / 2)
|
||||
return nvalue, mvalue
|
||||
|
||||
|
||||
def m_to_freq(nvalue, mvalue, grid=0.00625e12):
|
||||
""" converts m into frequency range
|
||||
spectrum(13,7) is (193137500000000.0, 193225000000000.0)
|
||||
reference to Recommendation G.694.1 (02/12), Figure I.3
|
||||
https://www.itu.int/rec/T-REC-G.694.1-201202-I/en
|
||||
|
||||
>>> fstart, fstop = m_to_freq(13, 7)
|
||||
>>> fstart
|
||||
193137500000000.0
|
||||
>>> fstop
|
||||
193225000000000.0
|
||||
|
||||
"""
|
||||
startn, stopn = mvalue_to_slots(nvalue, mvalue)
|
||||
fstart = nvalue_to_frequency(startn, grid)
|
||||
fstop = nvalue_to_frequency(stopn + 1, grid)
|
||||
return fstart, fstop
|
||||
|
||||
|
||||
def align_grids(oms_list):
|
||||
""" used to apply same grid to all oms : same starting n, stop n and slot size
|
||||
out of grid slots are set to 0
|
||||
"""
|
||||
n_min = min([o.spectrum_bitmap.n_min for o in oms_list])
|
||||
n_max = max([o.spectrum_bitmap.n_max for o in oms_list])
|
||||
for this_o in oms_list:
|
||||
if (this_o.spectrum_bitmap.n_min - n_min) > 0:
|
||||
this_o.spectrum_bitmap.insert_left([0] * (this_o.spectrum_bitmap.n_min - n_min))
|
||||
if (n_max - this_o.spectrum_bitmap.n_max) > 0:
|
||||
this_o.spectrum_bitmap.insert_right([0] * (n_max - this_o.spectrum_bitmap.n_max))
|
||||
return oms_list
|
||||
|
||||
|
||||
def build_oms_list(network, equipment):
|
||||
""" initialization of OMS list in the network
|
||||
an oms is build reading all intermediate nodes between two adjacent ROADMs
|
||||
each element within the list is being added an oms and oms_id to record the
|
||||
oms it belongs to.
|
||||
the function supports different spectrum width and supposes that the whole network
|
||||
works with the min range among OMSs
|
||||
"""
|
||||
oms_id = 0
|
||||
oms_list = []
|
||||
for node in [n for n in network.nodes() if isinstance(n, Roadm)]:
|
||||
for edge in network.edges([node]):
|
||||
if not isinstance(edge[1], Transceiver):
|
||||
nd_in = edge[0] # nd_in is a Roadm
|
||||
try:
|
||||
nd_in.oms_list.append(oms_id)
|
||||
except AttributeError:
|
||||
nd_in.oms_list = []
|
||||
nd_in.oms_list.append(oms_id)
|
||||
nd_out = edge[1]
|
||||
|
||||
params = {}
|
||||
params['oms_id'] = oms_id
|
||||
params['el_id_list'] = []
|
||||
params['el_list'] = []
|
||||
oms = OMS(**params)
|
||||
oms.add_element(nd_in)
|
||||
while not isinstance(nd_out, Roadm):
|
||||
oms.add_element(nd_out)
|
||||
# add an oms_id in the element
|
||||
nd_out.oms_id = oms_id
|
||||
nd_out.oms = oms
|
||||
n_temp = nd_out
|
||||
nd_out = next(n[1] for n in network.edges([n_temp]) if n[1].uid != nd_in.uid)
|
||||
nd_in = n_temp
|
||||
|
||||
oms.add_element(nd_out)
|
||||
# nd_out is a Roadm
|
||||
try:
|
||||
nd_out.oms_list.append(oms_id)
|
||||
except AttributeError:
|
||||
nd_out.oms_list = []
|
||||
nd_out.oms_list.append(oms_id)
|
||||
|
||||
oms.update_spectrum(equipment['SI']['default'].f_min,
|
||||
equipment['SI']['default'].f_max, grid=0.00625e12)
|
||||
# oms.assign_spectrum(13,7) gives back (193137500000000.0, 193225000000000.0)
|
||||
# as in the example in the standard
|
||||
# oms.assign_spectrum(13,7)
|
||||
|
||||
oms_list.append(oms)
|
||||
oms_id += 1
|
||||
oms_list = align_grids(oms_list)
|
||||
reversed_oms(oms_list)
|
||||
return oms_list
|
||||
|
||||
|
||||
def reversed_oms(oms_list):
|
||||
""" identifies reversed OMS
|
||||
only applicable for non parallel OMS
|
||||
"""
|
||||
for oms in oms_list:
|
||||
has_reversed = False
|
||||
for this_o in oms_list:
|
||||
if (oms.el_id_list[0] == this_o.el_id_list[-1] and
|
||||
oms.el_id_list[-1] == this_o.el_id_list[0]):
|
||||
oms.reversed_oms = this_o
|
||||
has_reversed = True
|
||||
break
|
||||
if not has_reversed:
|
||||
oms.reversed_oms = None
|
||||
|
||||
|
||||
def bitmap_sum(band1, band2):
|
||||
"""mark occupied bitmap by 0 if the slot is occupied in band1 or in band2"""
|
||||
res = []
|
||||
for i, elem in enumerate(band1):
|
||||
if band2[i] * elem == 0:
|
||||
res.append(0)
|
||||
else:
|
||||
res.append(1)
|
||||
return res
|
||||
|
||||
|
||||
def spectrum_selection(pth, oms_list, requested_m, requested_n=None):
|
||||
"""Collects spectrum availability and call the select_candidate function"""
|
||||
|
||||
# use indexes instead of ITU-T n values
|
||||
path_oms = []
|
||||
for elem in pth:
|
||||
if not isinstance(elem, Roadm) and not isinstance(elem, Transceiver):
|
||||
# only edfa, fused and fibers have oms_id attribute
|
||||
path_oms.append(elem.oms_id)
|
||||
# remove duplicate oms_id, order is not important
|
||||
path_oms = list(set(path_oms))
|
||||
# assuming all oms have same freq index
|
||||
if not path_oms:
|
||||
candidate = (None, None, None)
|
||||
return candidate, path_oms
|
||||
freq_index = oms_list[path_oms[0]].spectrum_bitmap.freq_index
|
||||
freq_index_min = oms_list[path_oms[0]].spectrum_bitmap.freq_index_min
|
||||
freq_index_max = oms_list[path_oms[0]].spectrum_bitmap.freq_index_max
|
||||
|
||||
freq_availability = oms_list[path_oms[0]].spectrum_bitmap.bitmap
|
||||
for oms in path_oms[1:]:
|
||||
freq_availability = bitmap_sum(oms_list[oms].spectrum_bitmap.bitmap, freq_availability)
|
||||
if requested_n is None:
|
||||
# avoid slots reserved on the edge 0.15e-12 on both sides -> 24
|
||||
candidates = [(freq_index[i] + requested_m, freq_index[i], freq_index[i] + 2 * requested_m - 1)
|
||||
for i in range(len(freq_availability))
|
||||
if freq_availability[i:i + 2 * requested_m] == [1] * (2 * requested_m)
|
||||
and freq_index[i] >= freq_index_min
|
||||
and freq_index[i + 2 * requested_m - 1] <= freq_index_max]
|
||||
|
||||
candidate = select_candidate(candidates, policy='first_fit')
|
||||
else:
|
||||
i = oms_list[path_oms[0]].spectrum_bitmap.geti(requested_n)
|
||||
# print(f'N {requested_n} i {i}')
|
||||
# print(freq_availability[i-m:i+m] )
|
||||
# print(freq_index[i-m:i+m])
|
||||
if (freq_availability[i - requested_m:i + requested_m] == [1] * (2 * requested_m) and
|
||||
freq_index[i - requested_m] >= freq_index_min
|
||||
and freq_index[i + requested_m - 1] <= freq_index_max):
|
||||
# candidate is the triplet center_n, startn and stopn
|
||||
candidate = (requested_n, requested_n - requested_m, requested_n + requested_m - 1)
|
||||
else:
|
||||
candidate = (None, None, None)
|
||||
# print("coucou11")
|
||||
# print(candidate)
|
||||
# print(freq_availability[321:321+2*m])
|
||||
# a = [i+321 for i in range(2*m)]
|
||||
# print(a)
|
||||
# print(candidate)
|
||||
return candidate, path_oms
|
||||
|
||||
|
||||
def select_candidate(candidates, policy):
|
||||
""" selects a candidate among all available spectrum
|
||||
"""
|
||||
if policy == 'first_fit':
|
||||
if candidates:
|
||||
return candidates[0]
|
||||
else:
|
||||
return (None, None, None)
|
||||
else:
|
||||
raise ServiceError('Only first_fit spectrum assignment policy is implemented.')
|
||||
|
||||
|
||||
def pth_assign_spectrum(pths, rqs, oms_list, rpths):
|
||||
""" basic first fit assignment
|
||||
if reversed path are provided, means that occupation is bidir
|
||||
"""
|
||||
for i, pth in enumerate(pths):
|
||||
# computes the number of channels required
|
||||
try:
|
||||
if rqs[i].blocking_reason:
|
||||
rqs[i].blocked = True
|
||||
rqs[i].N = None
|
||||
rqs[i].M = 0
|
||||
except AttributeError:
|
||||
nb_wl = ceil(rqs[i].path_bandwidth / rqs[i].bit_rate)
|
||||
# computes the total nb of slots according to requested spacing
|
||||
# TODO : express superchannels
|
||||
# assumes that all channels must be grouped
|
||||
# TODO : enables non contiguous reservation in case of blocking
|
||||
requested_m = ceil(rqs[i].spacing / 0.0125e12) * nb_wl
|
||||
if hasattr(rqs[i], 'M') and rqs[i].M is not None:
|
||||
# Consistency check between the requested M and path_bandwidth
|
||||
# M value should be bigger than the computed requested_m (simple estimate)
|
||||
# TODO: elaborate a more accurate estimate with nb_wl * tx_osnr + possibly guardbands in case of
|
||||
# superchannel closed packing.
|
||||
if requested_m <= rqs[i].M:
|
||||
requested_m = rqs[i].M
|
||||
else:
|
||||
# TODO : create a specific blocking reason and following process for this case instead of an exception
|
||||
raise SpectrumError(f'requested M {rqs[i].M} number of slots for request {rqs[i].request_id} ' +
|
||||
f'should be greater than {requested_m} to support request ' +
|
||||
f'{rqs[i].path_bandwidth * 1e-9} Gbit/s with {rqs[i].tsp} {rqs[i].tsp_mode}')
|
||||
# else: there is no M value so the programs uses the requested_m one
|
||||
if hasattr(rqs[i], 'N'):
|
||||
requested_n = rqs[i].N
|
||||
else:
|
||||
requested_n = None
|
||||
(center_n, startn, stopn), path_oms = spectrum_selection(pth + rpths[i], oms_list, requested_m,
|
||||
requested_n)
|
||||
# checks that requested_m is fitting startm and stopm
|
||||
# if not None, center_n and start, stop frequencies are applicable to all oms of pth
|
||||
# checks that spectrum is not None else indicate blocking reason
|
||||
if center_n is not None:
|
||||
# checks that requested_m is fitting startm and stopm
|
||||
if 2 * requested_m > (stopn - startn + 1):
|
||||
msg = f'candidate: {(center_n, startn, stopn)} is not consistant ' +\
|
||||
f'with {requested_m}'
|
||||
LOGGER.critical(msg)
|
||||
raise ValueError(msg)
|
||||
|
||||
for oms_elem in path_oms:
|
||||
oms_list[oms_elem].assign_spectrum(center_n, requested_m)
|
||||
oms_list[oms_elem].add_service(rqs[i].request_id, nb_wl)
|
||||
rqs[i].blocked = False
|
||||
rqs[i].N = center_n
|
||||
rqs[i].M = requested_m
|
||||
else:
|
||||
rqs[i].blocked = True
|
||||
rqs[i].N = None
|
||||
rqs[i].M = 0
|
||||
rqs[i].blocking_reason = 'NO_SPECTRUM'
|
||||
2777
gnpy/yang/api-request.json
Normal file
2777
gnpy/yang/api-request.json
Normal file
File diff suppressed because it is too large
Load Diff
53
gnpy/yang/gnpy-api@2020-10-22.yang
Normal file
53
gnpy/yang/gnpy-api@2020-10-22.yang
Normal file
@@ -0,0 +1,53 @@
|
||||
module gnpy-api {
|
||||
yang-version 1.1;
|
||||
namespace "gnpy:gnpy-api";
|
||||
prefix gnpyapi;
|
||||
|
||||
import gnpy-network-topology {
|
||||
prefix gnpynt;
|
||||
}
|
||||
import gnpy-path-computation-simplified {
|
||||
prefix gnpypc;
|
||||
}
|
||||
import gnpy-eqpt-config {
|
||||
prefix gnpyeqpt;
|
||||
}
|
||||
|
||||
organization
|
||||
"Telecom Infra Project OOPT PSE Working Group";
|
||||
contact
|
||||
"WG Web: <https://github.com/Telecominfraproject/oopt-gnpy>
|
||||
contact: <mailto:ahmed.triki@orange.com>
|
||||
contact: <mailto:esther.lerouzic@orange.com>
|
||||
";
|
||||
description
|
||||
"YANG model for gnpy api input for path computation - TransportPCE preversion";
|
||||
|
||||
revision 2020-10-22 {
|
||||
description
|
||||
"draft for experimental/2020-candi";
|
||||
reference
|
||||
"YANG model for api input for path computation with gnpy";
|
||||
}
|
||||
|
||||
container service {
|
||||
description
|
||||
"Describe the service file to connect to gnpy";
|
||||
uses gnpypc:service;
|
||||
}
|
||||
container result {
|
||||
uses gnpypc:result;
|
||||
description
|
||||
"Describe the response object to gnpy";
|
||||
}
|
||||
container topology {
|
||||
description
|
||||
"Describe the topology file to connect to gnpy";
|
||||
uses gnpynt:topo;
|
||||
}
|
||||
container equipment {
|
||||
description
|
||||
"Describe the equipment library to connect to gnpy";
|
||||
uses gnpyeqpt:eqpt;
|
||||
}
|
||||
}
|
||||
442
gnpy/yang/gnpy-eqpt-config@2020-10-22.yang
Normal file
442
gnpy/yang/gnpy-eqpt-config@2020-10-22.yang
Normal file
@@ -0,0 +1,442 @@
|
||||
module gnpy-eqpt-config {
|
||||
yang-version 1;
|
||||
namespace "gnpy:gnpy-eqpt-config";
|
||||
|
||||
prefix "gnpyeqpt";
|
||||
|
||||
organization
|
||||
"Telecom Infra Project OOPT PSE
|
||||
Working Group";
|
||||
|
||||
contact
|
||||
"WG Web: <https://github.com/Telecominfraproject/oopt-gnpy>
|
||||
contact: <mailto:ahmed.triki@orange.com>
|
||||
contact: <mailto:esther.lerouzic@orange.com>
|
||||
";
|
||||
|
||||
description "Base YANG model for gnpy equipment library input for path computation - 2020 - candi preversion";
|
||||
|
||||
revision "2020-10-22" {
|
||||
description "draft for experimental/2020-candi";
|
||||
reference "Base YANG model for equipment library input for path computation with gnpy";
|
||||
}
|
||||
|
||||
/*
|
||||
* Identities
|
||||
|
||||
|
||||
identity edfa-type-def {
|
||||
description "base identity for variable gain and fixed gain";
|
||||
}
|
||||
|
||||
identity variable-gain{
|
||||
base edfa-type-def ;
|
||||
description "'variable_gain' is a simplified model simulating a 2-coil
|
||||
EDFA with internal, input and output VOAs. The NF vs gain response is calculated
|
||||
accordingly based on the input parameters: nf_min, nf_max, and gain_flatmax. It
|
||||
is not a simple interpolation but a 2-stage NF calculation.";
|
||||
}
|
||||
|
||||
identity fixed-gain{
|
||||
base edfa-type-def ;
|
||||
description "'fixed_gain' is a fixed gain model. NF == Cte == nf0 if gain_min < gain < gain_flatmax";
|
||||
}
|
||||
|
||||
identity fiber-variety {
|
||||
description "base identity for fiber variety";
|
||||
}
|
||||
|
||||
identity transceiver-variety {
|
||||
description "base identity for transceiver variety";
|
||||
}
|
||||
|
||||
*/
|
||||
grouping variable-gain {
|
||||
leaf nf_min {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
units dB;
|
||||
}
|
||||
leaf nf_max {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
units dB;
|
||||
}
|
||||
leaf out_voa_auto{
|
||||
type boolean ;
|
||||
description "auto_design feature to optimize the amplifier output VOA. If true, output VOA is present
|
||||
and will be used to push amplifier gain to its maximum, within EOL power margins.";
|
||||
}
|
||||
}
|
||||
|
||||
grouping fixed-gain{
|
||||
leaf nf0 {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
units dB;
|
||||
}
|
||||
}
|
||||
|
||||
grouping no-type-def{
|
||||
leaf advanced_config_from_json {
|
||||
type string ;
|
||||
description " filename with json edfa";
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
grouping openroadm{
|
||||
leaf-list nf_coef {
|
||||
type decimal64 {
|
||||
fraction-digits 5;
|
||||
}
|
||||
//default [8.1e-4,6.142e-2,1.558,19.97] ;
|
||||
}
|
||||
}
|
||||
|
||||
grouping dual-stage {
|
||||
leaf raman {
|
||||
type boolean;
|
||||
}
|
||||
leaf preamp_variety {
|
||||
type leafref {
|
||||
path "../../Edfa/type_variety";
|
||||
}
|
||||
}
|
||||
leaf booster_variety {
|
||||
type leafref {
|
||||
path "../../Edfa/type_variety";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
grouping edfa-common {
|
||||
leaf allowed_for_design{
|
||||
type boolean ;
|
||||
description "If false, the amplifier will not be picked by auto-design but it can still be used as a
|
||||
manual input (from JSON or Excel template topology files.)";
|
||||
}
|
||||
leaf gain_flatmax {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
units dB;
|
||||
}
|
||||
leaf gain_min {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
units dB;
|
||||
}
|
||||
leaf p_max {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
units dBm;
|
||||
}
|
||||
leaf type_def {
|
||||
type identityref{
|
||||
base edfa-type-def ;
|
||||
}
|
||||
}
|
||||
choice type_of_model {
|
||||
case variable-gain {
|
||||
when "type_def = 'variable-gain'";
|
||||
uses variable-gain ;
|
||||
}
|
||||
case fixed-gain{
|
||||
when "type_def = 'fixed-gain'";
|
||||
uses fixed-gain;
|
||||
}
|
||||
case no-type-def{
|
||||
when "type_def = 'no-type-def'";
|
||||
uses no-type-def;
|
||||
}
|
||||
case openroadm{
|
||||
when "type_def = 'openroadm'";
|
||||
uses openroadm;
|
||||
}
|
||||
case dual_stage {
|
||||
when "type_def = 'dual_stage'";
|
||||
uses dual-stage ;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
grouping common-fiber {
|
||||
description "common parameters for fiber and raman fiber";
|
||||
leaf type_variety {
|
||||
type string ;
|
||||
}
|
||||
description "a unique name to ID the fiber in the JSON or Excel template topology input file";
|
||||
leaf dispersion{
|
||||
type decimal64 {
|
||||
fraction-digits 8;
|
||||
}
|
||||
units s.m-1.m-1;
|
||||
}
|
||||
leaf gamma{
|
||||
type decimal64 {
|
||||
fraction-digits 8;
|
||||
}
|
||||
units w-1.m-1 ;
|
||||
description "2pi.n2/(lambda*Aeff) (w-2.m-1)";
|
||||
}
|
||||
leaf pmd_coef{
|
||||
type decimal64 {
|
||||
fraction-digits 16;
|
||||
}
|
||||
units s.sqrt(m)-1;
|
||||
}
|
||||
}
|
||||
|
||||
grouping eqpt{
|
||||
list Edfa {
|
||||
key type_variety;
|
||||
leaf type_variety {
|
||||
type string;
|
||||
description "a unique name to ID the amplifier in the JSON/Excel template topology input file";
|
||||
}
|
||||
uses edfa-common;
|
||||
}
|
||||
|
||||
list Fiber {
|
||||
key type_variety;
|
||||
uses common-fiber;
|
||||
}
|
||||
|
||||
list RamanFiber {
|
||||
uses common-fiber;
|
||||
container raman_efficiency {
|
||||
leaf-list cr {
|
||||
type decimal64 {
|
||||
fraction-digits 8;
|
||||
}
|
||||
}
|
||||
leaf-list frequency_offset {
|
||||
type decimal64 {
|
||||
fraction-digits 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
list Span {
|
||||
leaf power_mode {
|
||||
type boolean ;
|
||||
}
|
||||
leaf-list delta_power_range_db {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
}
|
||||
leaf max_length {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
units km;
|
||||
default 150.0 ;
|
||||
}
|
||||
leaf max_loss {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
units dB;
|
||||
}
|
||||
leaf max_fiber_lineic_loss_for_raman {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
units dB.km-1;
|
||||
}
|
||||
leaf target_extended_gain {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
units dB;
|
||||
}
|
||||
leaf length_units{
|
||||
type string ;
|
||||
default "km";
|
||||
}
|
||||
leaf padding{
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
default 10.0 ;
|
||||
}
|
||||
leaf EOL{
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
default 0.0 ;
|
||||
}
|
||||
leaf con_in{
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
default 0.0 ;
|
||||
}
|
||||
leaf con_out{
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
default 0.0 ;
|
||||
}
|
||||
}
|
||||
|
||||
list Roadm {
|
||||
leaf target_pch_out_db {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
}
|
||||
leaf add_drop_osnr {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
}
|
||||
leaf pmd {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
}
|
||||
container restrictions {
|
||||
leaf-list preamp_variety_list {
|
||||
type string;
|
||||
}
|
||||
leaf-list booster_variety_list {
|
||||
type string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
list SI {
|
||||
leaf f_min {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
}
|
||||
leaf f_max {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
}
|
||||
leaf baud_rate {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
}
|
||||
leaf spacing {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
}
|
||||
leaf power_dbm {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
}
|
||||
leaf-list power_range_db {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
}
|
||||
leaf roll_off {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
}
|
||||
leaf tx_osnr {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
}
|
||||
leaf sys_margins {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
list Transceiver {
|
||||
leaf type_variety {
|
||||
type string ;
|
||||
description "a unique name to ID the transceiver in the JSON or Excel template topology input file";
|
||||
}
|
||||
container frequency {
|
||||
leaf min {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
units Hz ;
|
||||
}
|
||||
leaf max {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
units Hz ;
|
||||
}
|
||||
description "Min/max frequency of transponder eg 191.35e12 and 196.1e12";
|
||||
}
|
||||
list mode {
|
||||
leaf format {
|
||||
type string ;
|
||||
description "unique name of the mode";
|
||||
}
|
||||
leaf baud_rate {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
units baud ;
|
||||
description "baud_rate";
|
||||
}
|
||||
leaf OSNR {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
units dB ;
|
||||
description "min required OSNR in 0.1nm (dB)";
|
||||
}
|
||||
leaf tx_osnr {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
units dB ;
|
||||
description "min required OSNR in 0.1nm (dB)";
|
||||
}
|
||||
leaf min_spacing {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
units GHz ;
|
||||
description "...";
|
||||
}
|
||||
leaf bit_rate {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
units bit/s ;
|
||||
description "bit rate";
|
||||
}
|
||||
leaf roll_off {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
description "...";
|
||||
}
|
||||
leaf cost {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
description "arbitrary unit";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
300
gnpy/yang/gnpy-network-topology@2020-10-22.yang
Normal file
300
gnpy/yang/gnpy-network-topology@2020-10-22.yang
Normal file
@@ -0,0 +1,300 @@
|
||||
module gnpy-network-topology {
|
||||
yang-version 1.1;
|
||||
namespace "gnpy:gnpy-network-topology";
|
||||
prefix gnpynt;
|
||||
|
||||
organization
|
||||
"Telecom Infra Project OOPT PSE Working Group";
|
||||
contact
|
||||
"WG Web: <https://github.com/Telecominfraproject/oopt-gnpy>
|
||||
contact: <mailto:ahmed.triki@orange.com>
|
||||
contact: <mailto:esther.lerouzic@orange.com>
|
||||
";
|
||||
description
|
||||
"YANG model for gnpy network input for path computation - 2020 - candi preversion";
|
||||
|
||||
revision 2020-10-22 {
|
||||
description
|
||||
"draft for experimental/2020-candi";
|
||||
reference
|
||||
"YANG model for network input for path computation with gnpy";
|
||||
}
|
||||
|
||||
identity type-element {
|
||||
description
|
||||
"Base identity for element type";
|
||||
}
|
||||
|
||||
identity Transceiver {
|
||||
base type-element;
|
||||
description
|
||||
" Transceiver element";
|
||||
}
|
||||
|
||||
identity Fiber {
|
||||
base type-element;
|
||||
description
|
||||
"Fiber element (unidirectional)";
|
||||
}
|
||||
|
||||
identity Roadm {
|
||||
base type-element;
|
||||
description
|
||||
"Roadm element";
|
||||
}
|
||||
|
||||
identity Edfa {
|
||||
base type-element;
|
||||
description
|
||||
"Edfa element";
|
||||
}
|
||||
|
||||
identity Fused {
|
||||
base type-element;
|
||||
description
|
||||
"Fused element ; non amplified connection between two fiber spans ;
|
||||
can be used to model optical distribution frame, or losses due to
|
||||
connectors or fused in a span";
|
||||
}
|
||||
|
||||
identity length-unit {
|
||||
description
|
||||
"length unit";
|
||||
}
|
||||
|
||||
identity km {
|
||||
base length-unit;
|
||||
description
|
||||
"kilometers";
|
||||
}
|
||||
|
||||
identity m {
|
||||
base length-unit;
|
||||
description
|
||||
"meter";
|
||||
}
|
||||
|
||||
typedef Coordinate {
|
||||
type decimal64 {
|
||||
fraction-digits 6;
|
||||
}
|
||||
}
|
||||
|
||||
typedef Coef {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
}
|
||||
|
||||
grouping location-attributes {
|
||||
container location {
|
||||
leaf city {
|
||||
type string;
|
||||
mandatory true;
|
||||
}
|
||||
leaf region {
|
||||
type string;
|
||||
mandatory true;
|
||||
}
|
||||
leaf latitude {
|
||||
type Coordinate;
|
||||
mandatory true;
|
||||
}
|
||||
leaf longitude {
|
||||
type Coordinate;
|
||||
mandatory true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
grouping fiber-params {
|
||||
description
|
||||
".....";
|
||||
leaf length {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
mandatory true;
|
||||
}
|
||||
leaf loss_coef {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
mandatory true;
|
||||
units db/km;
|
||||
description "Loss coefficient of the fiber span (dB/km)";
|
||||
}
|
||||
leaf length_units {
|
||||
type identityref {
|
||||
base length-unit;
|
||||
}
|
||||
mandatory true;
|
||||
}
|
||||
leaf att_in {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
units "dB";
|
||||
mandatory true;
|
||||
}
|
||||
leaf con_in {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
units "dB";
|
||||
mandatory true;
|
||||
}
|
||||
leaf con_out {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
units "dB";
|
||||
mandatory true;
|
||||
}
|
||||
}
|
||||
|
||||
grouping edfa-params {
|
||||
container operational {
|
||||
description
|
||||
"Operational values for the Edfa ";
|
||||
leaf gain_target {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
units "dB";
|
||||
mandatory true;
|
||||
description
|
||||
"gain target of the amplifier (before VOA and after att_in)";
|
||||
}
|
||||
leaf tilt_target {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
mandatory true;
|
||||
description
|
||||
"..";
|
||||
}
|
||||
leaf out_voa {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
units "dB";
|
||||
mandatory true;
|
||||
description
|
||||
"..";
|
||||
}
|
||||
leaf delta_p {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
units "dB";
|
||||
mandatory true;
|
||||
description
|
||||
"per channel target output power delta with respect to power setting in SI";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
grouping roadm-params {
|
||||
leaf target_pch_out_db {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
units "dB";
|
||||
description
|
||||
"..";
|
||||
}
|
||||
container restrictions {
|
||||
leaf-list preamp_variety_list {
|
||||
type string;
|
||||
description
|
||||
"List of authorized preamp type-variety";
|
||||
}
|
||||
leaf-list booster_variety_list {
|
||||
type string;
|
||||
description
|
||||
"List of authorized booster type-variety";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
grouping transceiver-params;
|
||||
|
||||
grouping fused-params{
|
||||
leaf loss {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
units "dB";
|
||||
description
|
||||
"Concentrated loss of the fused element";
|
||||
}
|
||||
}
|
||||
|
||||
grouping element-type-choice {
|
||||
choice element-type {
|
||||
case Edfa {
|
||||
when "type = 'Edfa'";
|
||||
uses edfa-params;
|
||||
}
|
||||
case FiberRoadm {
|
||||
container params {
|
||||
choice fiberroadmfused {
|
||||
case Fiber {
|
||||
when "type = 'Fiber'";
|
||||
uses fiber-params;
|
||||
}
|
||||
case Roadm {
|
||||
when "type = 'Roadm'";
|
||||
uses roadm-params;
|
||||
}
|
||||
case Fused {
|
||||
when "type = 'Fused'";
|
||||
uses fused-params;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case Transceiver {
|
||||
when "type = 'Transceiver'";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
grouping topo {
|
||||
list elements {
|
||||
key "uid";
|
||||
leaf uid {
|
||||
type string;
|
||||
}
|
||||
leaf type {
|
||||
type identityref {
|
||||
base type-element;
|
||||
}
|
||||
mandatory true;
|
||||
}
|
||||
leaf type_variety {
|
||||
type string;
|
||||
mandatory false;
|
||||
}
|
||||
container metadata {
|
||||
uses location-attributes;
|
||||
}
|
||||
uses element-type-choice;
|
||||
}
|
||||
list connections {
|
||||
config false;
|
||||
leaf from_node {
|
||||
type leafref {
|
||||
path "../../elements/uid";
|
||||
}
|
||||
}
|
||||
leaf to_node {
|
||||
type leafref {
|
||||
path "../../elements/uid";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
559
gnpy/yang/gnpy-path-computation-simplified@2020-10-22.yang
Normal file
559
gnpy/yang/gnpy-path-computation-simplified@2020-10-22.yang
Normal file
@@ -0,0 +1,559 @@
|
||||
module gnpy-path-computation-simplified {
|
||||
yang-version 1.1;
|
||||
namespace "gnpy:path";
|
||||
|
||||
prefix "gnpypc";
|
||||
|
||||
organization
|
||||
"Telecom Infra Project OOPT PSE Working Group";
|
||||
|
||||
contact
|
||||
"WG Web: <https://github.com/Telecominfraproject/oopt-gnpy>
|
||||
contact: <mailto:ahmed.triki@orange.com>
|
||||
contact: <mailto:esther.lerouzic@orange.com>
|
||||
";
|
||||
|
||||
description "YANG model for gnpy path computation simplified for - 2020 - candi preversion";
|
||||
|
||||
revision "2020-10-22" {
|
||||
description
|
||||
"draft for experimental/2020-candi";
|
||||
reference
|
||||
"YANG model for path computation with gnpy inputs";
|
||||
}
|
||||
|
||||
grouping effective-freq-slot{
|
||||
/* content copied from ietf-flexi-grid-media-channel, because only M and N are needed
|
||||
from the initial grouping.
|
||||
*/
|
||||
description "The effective frequency slot is an attribute
|
||||
of a media channel and, being a frequency slot, it is
|
||||
described by its nominal central frequency and slot
|
||||
width";
|
||||
reference "rfc7698";
|
||||
leaf N {
|
||||
type uint32;
|
||||
description
|
||||
"Is used to determine the Nominal Central
|
||||
Frequency. The set of nominal central frequencies
|
||||
can be built using the following expression:
|
||||
f = 193.1 THz + n x 0.00625 THz,
|
||||
where 193.1 THz is ITU-T ''anchor frequency'' for
|
||||
transmission over the C band, n is a positive or
|
||||
negative integer including 0.";
|
||||
reference "rfc7698";
|
||||
}
|
||||
leaf M {
|
||||
type int32;
|
||||
description
|
||||
"Is used to determine the slot width. A slot width
|
||||
is constrained to be M x SWG (that is, M x 12.5 GHz),
|
||||
where M is an integer greater than or equal to 1.";
|
||||
reference "rfc7698";
|
||||
}
|
||||
}
|
||||
|
||||
grouping gnpy-specific-parameters{
|
||||
description
|
||||
"This grouping defines the gnpy specific parameters for requests.";
|
||||
leaf technology {
|
||||
type string;
|
||||
default "flexi-grid";
|
||||
description
|
||||
"Data plane technology type.";
|
||||
}
|
||||
leaf trx_type {
|
||||
type string ;
|
||||
mandatory true;
|
||||
description "name of the transponder type (to be read from equipment library";
|
||||
|
||||
}
|
||||
leaf trx_mode {
|
||||
type string;
|
||||
description "name of the transponder mode (to be read from equipment library";
|
||||
|
||||
}
|
||||
list effective-freq-slot {
|
||||
key "N";
|
||||
uses effective-freq-slot ;
|
||||
}
|
||||
leaf spacing {
|
||||
mandatory true;
|
||||
type decimal64 {
|
||||
fraction-digits 1;
|
||||
}
|
||||
units Hz;
|
||||
description
|
||||
"It is the spacing between channels assuming full load with
|
||||
same channels as the requested one. multiple of 12.5 GHz";
|
||||
|
||||
}
|
||||
leaf max-nb-of-channel{
|
||||
type int32;
|
||||
description "Nb of channel to take into account for the full load case.
|
||||
";
|
||||
|
||||
}
|
||||
leaf output-power{
|
||||
type decimal64 {
|
||||
fraction-digits 5;
|
||||
}
|
||||
units W;
|
||||
description "optical power setting to be used for the propagation";
|
||||
|
||||
}
|
||||
leaf path_bandwidth{
|
||||
type decimal64 {
|
||||
fraction-digits 5;
|
||||
}
|
||||
mandatory true;
|
||||
units bit/s;
|
||||
description "Capacity required";
|
||||
}
|
||||
}
|
||||
|
||||
identity SNR-bandwidth {
|
||||
base path-metric-type;
|
||||
description
|
||||
"A metric that records SNR in signal bandwidth";
|
||||
}
|
||||
|
||||
identity OSNR-bandwidth {
|
||||
base path-metric-type;
|
||||
description
|
||||
"A metric that records OSNR in signal bandwidth";
|
||||
}
|
||||
|
||||
identity SNR-0.1nm {
|
||||
base path-metric-type;
|
||||
description
|
||||
"A metric that records SNR in 0.1nm";
|
||||
}
|
||||
|
||||
identity OSNR-0.1nm {
|
||||
base path-metric-type;
|
||||
description
|
||||
"A metric that records OSNR in 0.1nm";
|
||||
}
|
||||
|
||||
identity reference_power {
|
||||
base path-metric-type;
|
||||
description
|
||||
"to be revised";
|
||||
}
|
||||
|
||||
identity path_bandwidth {
|
||||
base path-metric-type;
|
||||
description
|
||||
"to be revised";
|
||||
}
|
||||
|
||||
grouping transponder{
|
||||
leaf transponder-type {
|
||||
type string ;
|
||||
description
|
||||
"transponder type.";
|
||||
}
|
||||
leaf transponder-mode {
|
||||
type string ;
|
||||
description
|
||||
"transponder mode.";
|
||||
}
|
||||
}
|
||||
|
||||
grouping hop-attribute{
|
||||
description
|
||||
"This grouping defines the hop attribute parameters for request or response";
|
||||
choice hop-type{
|
||||
case tsp {
|
||||
container transponder{
|
||||
uses transponder ;
|
||||
}
|
||||
}
|
||||
case regen {
|
||||
container regenerator{
|
||||
leaf regenerator-id{
|
||||
type string ;
|
||||
}
|
||||
uses transponder ;
|
||||
}
|
||||
}
|
||||
case pow {
|
||||
container optical-power{
|
||||
leaf optical-power{
|
||||
type decimal64 {
|
||||
fraction-digits 5;
|
||||
}
|
||||
units W;
|
||||
description "not used yet. hop output (input??) power";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
identity path-metric-type {
|
||||
description
|
||||
"Base identity for path metric type";
|
||||
}
|
||||
|
||||
identity route-usage-type {
|
||||
description
|
||||
"Base identity for route usage";
|
||||
}
|
||||
|
||||
identity route-include-ero {
|
||||
base route-usage-type;
|
||||
description
|
||||
"Include ERO from route";
|
||||
}
|
||||
|
||||
identity route-exclude-ero {
|
||||
base route-usage-type;
|
||||
description
|
||||
"Exclude ERO from route";
|
||||
}
|
||||
|
||||
identity route-exclude-srlg {
|
||||
base route-usage-type;
|
||||
description
|
||||
"Exclude SRLG from route";
|
||||
}
|
||||
|
||||
typedef te-hop-type {
|
||||
type enumeration {
|
||||
enum LOOSE {
|
||||
description
|
||||
"loose hop in an explicit path";
|
||||
}
|
||||
enum STRICT {
|
||||
description
|
||||
"strict hop in an explicit path";
|
||||
}
|
||||
}
|
||||
description
|
||||
"enumerated type for specifying loose or strict
|
||||
paths";
|
||||
reference "RFC3209: section-4.3.2";
|
||||
}
|
||||
|
||||
typedef te-path-disjointness {
|
||||
type bits {
|
||||
bit node {
|
||||
position 0;
|
||||
description "Node disjoint.";
|
||||
}
|
||||
bit link {
|
||||
position 1;
|
||||
description "Link disjoint.";
|
||||
}
|
||||
bit srlg {
|
||||
position 2;
|
||||
description "SRLG (Shared Risk Link Group) disjoint.";
|
||||
}
|
||||
}
|
||||
description
|
||||
"Type of the resource disjointness for a TE tunnel path.";
|
||||
reference
|
||||
"RFC4872: RSVP-TE Extensions in Support of End-to-End
|
||||
Generalized Multi-Protocol Label Switching (GMPLS)
|
||||
Recovery";
|
||||
} // te-path-disjointness
|
||||
|
||||
typedef accumulated-metric-type {
|
||||
type union {
|
||||
type uint64;
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
}
|
||||
description
|
||||
"type useable for accumulative-value";
|
||||
}
|
||||
|
||||
grouping path-route-objects {
|
||||
description
|
||||
"List of EROs to be included or excluded when performing
|
||||
the path computation.";
|
||||
container explicit-route-objects {
|
||||
description
|
||||
"Container for the route object list";
|
||||
list route-object-include-exclude {
|
||||
description
|
||||
"List of explicit route objects to include or
|
||||
exclude in path computation";
|
||||
leaf explicit-route-usage {
|
||||
type identityref {
|
||||
base route-usage-type;
|
||||
}
|
||||
description "Explicit-route usage.";
|
||||
}
|
||||
key "index";
|
||||
uses explicit-route-hop ;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
grouping generic-path-disjointness {
|
||||
description "Path disjointness grouping";
|
||||
leaf disjointness {
|
||||
type te-path-disjointness;
|
||||
description
|
||||
"The type of resource disjointness.
|
||||
Under primary path, disjointness level applies to
|
||||
all secondary LSPs. Under secondary, disjointness
|
||||
level overrides the one under primary";
|
||||
}
|
||||
}
|
||||
|
||||
grouping common-path-constraints-attributes {
|
||||
description
|
||||
"Common path constraints configuration grouping";
|
||||
uses common-constraints_config;
|
||||
}
|
||||
|
||||
grouping generic-path-constraints {
|
||||
description
|
||||
"Global named path constraints configuration
|
||||
grouping";
|
||||
container path-constraints {
|
||||
description "TE named path constraints container";
|
||||
uses common-path-constraints-attributes;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
grouping explicit-route-hop {
|
||||
description
|
||||
"The explicit route subobject grouping";
|
||||
leaf index {
|
||||
type uint32;
|
||||
description "ERO subobject index";
|
||||
}
|
||||
choice type {
|
||||
description
|
||||
"The explicit route subobject type";
|
||||
case num-unnum-hop {
|
||||
container num-unnum-hop {
|
||||
leaf node-id {
|
||||
//type te-node-id;
|
||||
type string;
|
||||
description
|
||||
"The identifier of a node in the TE topology.";
|
||||
}
|
||||
leaf link-tp-id {
|
||||
//type te-tp-id;
|
||||
type string;
|
||||
description
|
||||
"TE link termination point identifier. The combination
|
||||
of TE link ID and the TE node ID is used to identify an
|
||||
unnumbered TE link.";
|
||||
}
|
||||
leaf hop-type {
|
||||
type te-hop-type;
|
||||
description "strict or loose hop";
|
||||
}
|
||||
description
|
||||
"Numbered and Unnumbered link/node explicit route
|
||||
subobject";
|
||||
}
|
||||
}
|
||||
case label {
|
||||
container label-hop {
|
||||
description "Label hop type";
|
||||
uses effective-freq-slot;
|
||||
}
|
||||
description
|
||||
"The Label ERO subobject";
|
||||
}
|
||||
case hop-attribute{
|
||||
uses gnpypc:hop-attribute ;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
grouping common-constraints_config {
|
||||
description
|
||||
"Common constraints grouping that can be set on
|
||||
a constraint set or directly on the tunnel";
|
||||
|
||||
container te-bandwidth {
|
||||
uses gnpy-specific-parameters ;
|
||||
description
|
||||
"A requested bandwidth to use for path computation";
|
||||
}
|
||||
}
|
||||
|
||||
grouping end-points {
|
||||
description
|
||||
"Common grouping to define the TE tunnel end-points";
|
||||
|
||||
leaf source {
|
||||
type string;
|
||||
description "TE tunnel source address.";
|
||||
}
|
||||
leaf destination {
|
||||
type string;
|
||||
description "P2P tunnel destination address";
|
||||
}
|
||||
leaf src-tp-id {
|
||||
type string;
|
||||
description "TE tunnel source termination point identifier.";
|
||||
}
|
||||
leaf dst-tp-id {
|
||||
type string;
|
||||
description "TE tunnel destination termination point
|
||||
identifier.";
|
||||
}
|
||||
}
|
||||
|
||||
grouping synchronization-info {
|
||||
description "Information for sync";
|
||||
list synchronization {
|
||||
key "synchronization-id";
|
||||
description "sync list";
|
||||
leaf synchronization-id {
|
||||
type string;
|
||||
description "index";
|
||||
}
|
||||
container svec {
|
||||
description
|
||||
"Synchronization VECtor";
|
||||
leaf relaxable {
|
||||
type boolean;
|
||||
default true;
|
||||
description
|
||||
"If this leaf is true, path computation process is free
|
||||
to ignore svec content.
|
||||
otherwise it must take into account this svec.";
|
||||
}
|
||||
uses generic-path-disjointness;
|
||||
leaf-list request-id-number {
|
||||
type string;
|
||||
description "This list reports the set of M path computation requests that must be synchronized.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
grouping path-metric {
|
||||
description "TE path metric type";
|
||||
leaf metric-type {
|
||||
type identityref {
|
||||
base path-metric-type;
|
||||
}
|
||||
description "TE path metric type";
|
||||
}
|
||||
leaf accumulative-value {
|
||||
type decimal64 {
|
||||
fraction-digits 2;
|
||||
}
|
||||
description "TE path metric accumulative value";
|
||||
}
|
||||
}
|
||||
grouping generic-path-properties {
|
||||
description "TE generic path properties grouping";
|
||||
container path-properties {
|
||||
config false;
|
||||
description "The TE path properties";
|
||||
list path-metric {
|
||||
key metric-type;
|
||||
uses path-metric;
|
||||
}
|
||||
list z-a-path-metric {
|
||||
key metric-type;
|
||||
uses path-metric;
|
||||
}
|
||||
list path-route-objects {
|
||||
description
|
||||
"Container for the list of route objects either returned by
|
||||
the computation engine or actually used by an LSP";
|
||||
container path-route-object {
|
||||
description
|
||||
"List of route objects either returned by the computation
|
||||
engine or actually used by an LSP";
|
||||
uses explicit-route-hop;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
grouping path-info {
|
||||
uses generic-path-properties;
|
||||
description "Path computation output information";
|
||||
}
|
||||
|
||||
// adding some blocking reasons and info on path in case of blocking
|
||||
|
||||
grouping no-path-info {
|
||||
description "no-path-info";
|
||||
container no-path {
|
||||
presence "Response without path information, due to failure
|
||||
performing the path computation";
|
||||
leaf no-path {
|
||||
type string;
|
||||
mandatory true ;
|
||||
description
|
||||
"returned blocking reasons:
|
||||
NO_PATH
|
||||
NO_COMPUTED_SNR
|
||||
NO_FEASIBLE_BAUDRATE_WITH_SPACING
|
||||
NO_PATH_WITH_CONSTRAINT
|
||||
NO_FEASIBLE_MODE
|
||||
MODE_NOT_FEASIBLE
|
||||
NO_SPECTRUM
|
||||
";
|
||||
}
|
||||
uses generic-path-properties ;
|
||||
description "if path computation cannot identify a path,
|
||||
rpc returns no path.";
|
||||
}
|
||||
}
|
||||
|
||||
grouping service {
|
||||
list path-request {
|
||||
key "request-id";
|
||||
description "request-list";
|
||||
leaf request-id {
|
||||
type string;
|
||||
mandatory true;
|
||||
description "Each path computation request is uniquely identified by the request-id-number.";
|
||||
}
|
||||
leaf bidirectional {
|
||||
type boolean;
|
||||
mandatory true;
|
||||
description "Specify the bidirectionality of the path";
|
||||
}
|
||||
|
||||
uses end-points;
|
||||
uses path-route-objects;
|
||||
uses generic-path-constraints;
|
||||
}
|
||||
uses synchronization-info;
|
||||
}
|
||||
|
||||
grouping result {
|
||||
list response {
|
||||
key response-id;
|
||||
config false;
|
||||
description "response";
|
||||
leaf response-id {
|
||||
type string;
|
||||
description
|
||||
"The list key that has to reuse request-id-number.";
|
||||
}
|
||||
choice response-type {
|
||||
config false;
|
||||
description "response-type";
|
||||
case no-path-case {
|
||||
uses no-path-info;
|
||||
}
|
||||
case path-case {
|
||||
uses path-info;
|
||||
description "Path computation service.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ Equipment and Network description definitions
|
||||
|
||||
Equipment description defines equipment types and those parameters.
|
||||
Description is made in JSON file with predefined structure. By default
|
||||
**transmission_main_example.py** uses **eqpt_config.json** file and that
|
||||
**gnpy-transmission-example** uses **eqpt_config.json** file and that
|
||||
can be changed with **-e** or **--equipment** command line parameter.
|
||||
Parsing of JSON file is made with
|
||||
**gnpy.core.equipment.load_equipment(equipment_description)** and return
|
||||
@@ -82,7 +82,7 @@ it will be marked with **”default”** value.
|
||||
*******************
|
||||
|
||||
Four types of EDFA definition are possible. Description JSON file
|
||||
location is in **transmission_main_example.py** folder:
|
||||
location is in **gnpy-transmission-example** folder:
|
||||
|
||||
- Advanced – with JSON file describing gain/noise figure tilt and
|
||||
gain/noise figure ripple. **"advanced_config_from_json"** value
|
||||
@@ -314,7 +314,7 @@ Note that ``OSNR`` parameter refers to the receiver's minimal OSNR threshold for
|
||||
Network description defines network elements with additional to
|
||||
equipment description parameters, metadata and elements interconnection.
|
||||
Description is made in JSON file with predefined structure. By default
|
||||
**transmission_main_example.py** uses **edfa_example_network.json** file
|
||||
**gnpy-transmission-example** uses **edfa_example_network.json** file
|
||||
and can be changed from command line. Parsing of JSON file is made with
|
||||
**gnpy.core.network.load_network(network_description,
|
||||
equipment_description)** and return value is **DiGraph** object which
|
||||
@@ -544,7 +544,6 @@ Additional details of the simulation are controlled via ``sim_params.json``:
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"raman_computed_channels": [1, 18, 37, 56, 75],
|
||||
"raman_parameters": {
|
||||
"flag_raman": true,
|
||||
"space_resolution": 10e3,
|
||||
@@ -554,6 +553,7 @@ Additional details of the simulation are controlled via ``sim_params.json``:
|
||||
"nli_method_name": "ggn_spectrally_separated",
|
||||
"wdm_grid_size": 50e9,
|
||||
"dispersion_tolerance": 1,
|
||||
"phase_shift_tollerance": 0.1
|
||||
"phase_shift_tolerance": 0.1,
|
||||
"computed_channels": [1, 18, 37, 56, 75]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
alabaster>=0.7.12,<1
|
||||
docutils==0.15.2
|
||||
matplotlib>=3.1.0,<4
|
||||
networkx>=2.3,<3
|
||||
numpy>=1.16.1,<2
|
||||
pandas==0.24.2
|
||||
pbr>=5.4.4,<6
|
||||
Pygments>=2.4.2,<3
|
||||
pytest>=4.0.0,<5
|
||||
scipy>=1.3.0,<2
|
||||
Sphinx>=2.1.1,<3
|
||||
Sphinx>=2.4.4,<3
|
||||
sphinxcontrib-bibtex>=0.4.2,<1
|
||||
xlrd>=1.2.0,<2
|
||||
flask>=1.1.2
|
||||
51
setup.cfg
Normal file
51
setup.cfg
Normal file
@@ -0,0 +1,51 @@
|
||||
[metadata]
|
||||
name = gnpy
|
||||
description = Route planning and optimization tool for mesh optical networks
|
||||
description-file = README.rst
|
||||
description-content-type = text/x-rst; charset=UTF-8
|
||||
author = Telecom Infra Project
|
||||
author-email = jan.kundrat@telecominfraproject.com
|
||||
license = BSD-3-Clause
|
||||
home-page = https://github.com/Telecominfraproject/oopt-gnpy
|
||||
project_urls =
|
||||
Bug Tracker = https://github.com/Telecominfraproject/oopt-gnpy/issues
|
||||
Documentation = https://gnpy.readthedocs.io/
|
||||
python-requires = >=3.6
|
||||
classifier =
|
||||
Development Status :: 3 - Alpha
|
||||
Intended Audience :: Developers
|
||||
Intended Audience :: Science/Research
|
||||
Intended Audience :: Telecommunications Industry
|
||||
License :: OSI Approved :: BSD License
|
||||
Natural Language :: English
|
||||
Programming Language :: Python
|
||||
Programming Language :: Python :: 3 :: Only
|
||||
Programming Language :: Python :: 3.6
|
||||
Programming Language :: Python :: Implementation :: CPython
|
||||
Topic :: Scientific/Engineering
|
||||
Topic :: Scientific/Engineering :: Physics
|
||||
keywords =
|
||||
optics
|
||||
network
|
||||
fiber
|
||||
communication
|
||||
route
|
||||
planning
|
||||
optimization
|
||||
|
||||
[pbr]
|
||||
warnerrors = True
|
||||
|
||||
[files]
|
||||
packages = gnpy
|
||||
data_files =
|
||||
examples = examples/*
|
||||
# FIXME: solve example data files
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
gnpy-example-data = gnpy.tools.cli_examples:show_example_data_dir
|
||||
gnpy-transmission-example = gnpy.tools.cli_examples:transmission_main_example
|
||||
gnpy-path-request = gnpy.tools.cli_examples:path_requests_run
|
||||
gnpy-convert-xls = gnpy.tools.convert:_do_convert
|
||||
gnpy-rest = gnpy.tools.rest_example:main
|
||||
38
setup.py
38
setup.py
@@ -1,39 +1,5 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
from codecs import open
|
||||
from os import path
|
||||
from setuptools import setup, find_packages
|
||||
import setuptools
|
||||
|
||||
here = path.abspath(path.dirname(__file__))
|
||||
|
||||
with open(path.join(here, 'README.rst'), encoding='utf-8') as f:
|
||||
long_description = f.read()
|
||||
|
||||
setup(
|
||||
name='gnpy',
|
||||
version='1.2.0',
|
||||
description='route planning and optimization tool for mesh optical networks',
|
||||
long_description=long_description,
|
||||
long_description_content_type='text/x-rst; charset=UTF-8',
|
||||
url='https://github.com/Telecominfraproject/gnpy',
|
||||
author='Telecom Infra Project',
|
||||
author_email='jan.kundrat@telecominfraproject.com',
|
||||
classifiers=[
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Intended Audience :: Developers',
|
||||
'Intended Audience :: Science/Research',
|
||||
'Intended Audience :: Telecommunications Industry',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Natural Language :: English',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 3 :: Only',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: Implementation :: CPython',
|
||||
'Topic :: Scientific/Engineering',
|
||||
'Topic :: Scientific/Engineering :: Physics',
|
||||
],
|
||||
keywords='optics network fiber communication route planning optimization',
|
||||
#packages=find_packages(exclude=['examples', 'docs', 'tests']), # Required
|
||||
packages=find_packages(exclude=['docs', 'tests']), # Required
|
||||
install_requires=list(open('requirements.txt'))
|
||||
)
|
||||
setuptools.setup(setup_requires=['pbr'], pbr=True)
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
from json import load, dump
|
||||
from json import dump
|
||||
from pathlib import Path
|
||||
from argparse import ArgumentParser
|
||||
from collections import namedtuple
|
||||
from gnpy.tools.json_io import load_json
|
||||
|
||||
|
||||
class Results(namedtuple('Results', 'missing extra different expected actual')):
|
||||
def _asdict(self):
|
||||
return {'missing': self.missing,
|
||||
'extra': self.extra,
|
||||
return {'missing': self.missing,
|
||||
'extra': self.extra,
|
||||
'different': self.different}
|
||||
|
||||
def __str__(self):
|
||||
rv = []
|
||||
if self.missing:
|
||||
@@ -24,10 +27,12 @@ class Results(namedtuple('Results', 'missing extra different expected actual')):
|
||||
rv.append('All match!')
|
||||
return '\n'.join(rv)
|
||||
|
||||
|
||||
class NetworksResults(namedtuple('NetworksResult', 'elements connections')):
|
||||
def _asdict(self):
|
||||
return {'elements': self.elements._asdict(),
|
||||
return {'elements': self.elements._asdict(),
|
||||
'connections': self.connections._asdict()}
|
||||
|
||||
def __str__(self):
|
||||
return '\n'.join([
|
||||
'Elements'.center(40, '='),
|
||||
@@ -36,10 +41,12 @@ class NetworksResults(namedtuple('NetworksResult', 'elements connections')):
|
||||
str(self.connections),
|
||||
])
|
||||
|
||||
|
||||
class ServicesResults(namedtuple('ServicesResult', 'requests synchronizations')):
|
||||
def _asdict(self):
|
||||
return {'requests': self.requests.asdict(),
|
||||
return {'requests': self.requests.asdict(),
|
||||
'synchronizations': self.synchronizations.asdict()}
|
||||
|
||||
def __str__(self):
|
||||
return '\n'.join([
|
||||
'Requests'.center(40, '='),
|
||||
@@ -48,25 +55,29 @@ class ServicesResults(namedtuple('ServicesResult', 'requests synchronizations'))
|
||||
str(self.synchronizations),
|
||||
])
|
||||
|
||||
|
||||
class PathsResults(namedtuple('PathsResults', 'paths')):
|
||||
def _asdict(self):
|
||||
return {'paths': self.paths.asdict()}
|
||||
|
||||
def __str__(self):
|
||||
return '\n'.join([
|
||||
'Paths'.center(40, '='),
|
||||
str(self.paths),
|
||||
])
|
||||
|
||||
|
||||
def compare(expected, actual, key=lambda x: x):
|
||||
expected = {key(el): el for el in expected}
|
||||
actual = {key(el): el for el in actual}
|
||||
missing = set(expected) - set(actual)
|
||||
extra = set(actual) - set(expected)
|
||||
expected = {key(el): el for el in expected}
|
||||
actual = {key(el): el for el in actual}
|
||||
missing = set(expected) - set(actual)
|
||||
extra = set(actual) - set(expected)
|
||||
different = [(expected[x], actual[x]) for
|
||||
x in set(expected) & set(actual)
|
||||
if expected[x] != actual[x]]
|
||||
return Results(missing, extra, different, expected, actual)
|
||||
|
||||
|
||||
def compare_networks(expected, actual):
|
||||
elements = compare(expected['elements'], actual['elements'],
|
||||
key=lambda el: el['uid'])
|
||||
@@ -74,42 +85,46 @@ def compare_networks(expected, actual):
|
||||
key=lambda el: (el['from_node'], el['to_node']))
|
||||
return NetworksResults(elements, connections)
|
||||
|
||||
|
||||
def compare_services(expected, actual):
|
||||
requests = compare(expected['path-request'], actual['path-request'],
|
||||
key=lambda el: el['request-id'])
|
||||
synchronizations = compare(expected['synchronization'], actual['synchronization'],
|
||||
key=lambda el: el['synchronization-id'])
|
||||
synchronizations = compare(expected['path-request'], expected['path-request'],
|
||||
key=lambda el: el['request-id'])
|
||||
if 'synchronization' in expected.keys():
|
||||
synchronizations = compare(expected['synchronization'], actual['synchronization'],
|
||||
key=lambda el: el['synchronization-id'])
|
||||
return ServicesResults(requests, synchronizations)
|
||||
|
||||
|
||||
def compare_paths(expected_output, actual_output):
|
||||
paths = compare(expected['path'], actual['path'], key=lambda el: el['path-id'])
|
||||
return PathsResults(paths)
|
||||
|
||||
|
||||
COMPARISONS = {
|
||||
'networks': compare_networks,
|
||||
'services': compare_services,
|
||||
'paths': compare_paths,
|
||||
'paths': compare_paths,
|
||||
}
|
||||
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument('expected_output', type=Path, metavar='FILE')
|
||||
parser.add_argument('actual_output', type=Path, metavar='FILE')
|
||||
parser.add_argument('-o', '--output', default=None)
|
||||
parser.add_argument('actual_output', type=Path, metavar='FILE')
|
||||
parser.add_argument('-o', '--output', default=None)
|
||||
parser.add_argument('-c', '--comparison', choices=COMPARISONS, default='networks')
|
||||
|
||||
|
||||
def encode_sets(obj):
|
||||
if isinstance(obj, set):
|
||||
return list(obj)
|
||||
raise TypeError(f'{obj!r} is not JSON serializable!')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = parser.parse_args()
|
||||
|
||||
with open(args.expected_output, encoding='utf-8') as f:
|
||||
expected = load(f)
|
||||
|
||||
with open(args.actual_output, encoding='utf-8') as f:
|
||||
actual = load(f)
|
||||
expected = load_json(args.expected_output)
|
||||
actual = load_json(args.actual_output)
|
||||
|
||||
result = COMPARISONS[args.comparison](expected, actual)
|
||||
|
||||
|
||||
Binary file not shown.
BIN
tests/data/CORONET_Global_Topology.xlsx
Normal file
BIN
tests/data/CORONET_Global_Topology.xlsx
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -63,7 +63,8 @@
|
||||
"Fiber":[{
|
||||
"type_variety": "SSMF",
|
||||
"dispersion": 1.67e-05,
|
||||
"gamma": 0.00127
|
||||
"gamma": 0.00127,
|
||||
"pmd_coef": 1.265e-15
|
||||
}
|
||||
],
|
||||
"Span":[{
|
||||
@@ -83,6 +84,7 @@
|
||||
"Roadm":[{
|
||||
"target_pch_out_db": -20,
|
||||
"add_drop_osnr": 38,
|
||||
"pmd": 0,
|
||||
"restrictions": {
|
||||
"preamp_variety_list":[],
|
||||
"booster_variety_list":[]
|
||||
|
||||
97
tests/data/expected_results_science_utils.csv
Normal file
97
tests/data/expected_results_science_utils.csv
Normal file
@@ -0,0 +1,97 @@
|
||||
,signal,ase,nli
|
||||
0,0.0002869472910749756,3.829244288314411e-08,2.1570435023738975e-07
|
||||
1,0.0002844264441819097,3.810807396068084e-08,2.1799950841473497e-07
|
||||
2,0.00028192866252406385,3.792544000755193e-08,2.2023841125047751e-07
|
||||
3,0.0002794537215642667,3.7744517714620316e-08,2.2242189941355056e-07
|
||||
4,0.00027562432957345563,3.739256592350871e-08,2.2343448272115905e-07
|
||||
5,0.0002718482755003939,3.7044482870002475e-08,2.2437826192962336e-07
|
||||
6,0.00026812479793132313,3.670020704375223e-08,2.2525495466693408e-07
|
||||
7,0.000264450700138397,3.635954085714981e-08,2.2606415187873477e-07
|
||||
8,0.0002608253488030976,3.602242835595967e-08,2.2680748521505387e-07
|
||||
9,0.0002569046888856947,3.564392097524325e-08,2.2718285844823122e-07
|
||||
10,0.0002530414048172964,3.52696660940159e-08,2.2749429758474536e-07
|
||||
11,0.0002492279873569917,3.489974200864255e-08,2.277374766527899e-07
|
||||
12,0.00024546394589921574,3.453407358954537e-08,2.2791414400785136e-07
|
||||
13,0.00024174879169001578,3.4172586853993816e-08,2.280260208417818e-07
|
||||
14,0.00023798746912554602,3.3802283179520985e-08,2.2798420759778034e-07
|
||||
15,0.00023427697848580554,3.343627022987542e-08,2.2788101592695744e-07
|
||||
16,0.0002306167836320285,3.307447309241581e-08,2.2771816297650914e-07
|
||||
17,0.00022700656967539738,3.2716831574363364e-08,2.274975560288182e-07
|
||||
18,0.00022344579480967338,3.236327278261661e-08,2.2361822442592406e-07
|
||||
19,0.00021953361935365365,3.195819964288877e-08,2.1939761734541424e-07
|
||||
20,0.000215683131390894,3.155821693631402e-08,2.152494588710531e-07
|
||||
21,0.0002118936126056039,3.116322947665684e-08,2.1117277567387026e-07
|
||||
22,0.00020816423698459974,3.0773146233359933e-08,2.0716649124095414e-07
|
||||
23,0.000204494186708796,3.0387877710694614e-08,2.0322954179937734e-07
|
||||
24,0.0002011608152067422,3.0044038268833097e-08,1.9963693210325328e-07
|
||||
25,0.0001978756946189507,2.9704204306604607e-08,1.9610141536963302e-07
|
||||
26,0.00019463824873067792,2.9368307297032184e-08,1.9262221997374404e-07
|
||||
27,0.00019144860669288407,2.903632861769827e-08,1.8919927457566036e-07
|
||||
28,0.00018830616497929743,2.870820070744311e-08,1.8583178406705711e-07
|
||||
29,0.0001852103256336822,2.838385708911634e-08,1.8251896218718027e-07
|
||||
30,0.0001821604972098109,2.8063232252848876e-08,1.7926003240910756e-07
|
||||
31,0.00017915618670059162,2.774625963676283e-08,1.76054318231953e-07
|
||||
32,0.00017619680881745593,2.7432875871797347e-08,1.729010553429381e-07
|
||||
33,0.0001732817839023698,2.712301856538676e-08,1.6979948820365403e-07
|
||||
34,0.0001704966413678542,2.6828122477482957e-08,1.6683312331765736e-07
|
||||
35,0.00016775189226190024,2.6536528664560742e-08,1.639139770351803e-07
|
||||
36,0.00016504703499518105,2.624818226917535e-08,1.6104139135569604e-07
|
||||
37,0.00016238266779776653,2.5963117448579666e-08,1.5795381794641793e-07
|
||||
38,0.0001597582427278871,2.568127942199337e-08,1.5492098715709327e-07
|
||||
39,0.0001571732182027887,2.5402614261982925e-08,1.5194201541883415e-07
|
||||
40,0.00015462705891567335,2.5127068868391087e-08,1.4901603171959048e-07
|
||||
41,0.00015212101646395513,2.4854550603641668e-08,1.4614388817380648e-07
|
||||
42,0.00014965447757985992,2.4585009902449718e-08,1.4332463586635585e-07
|
||||
43,0.0001472268380950584,2.4318397887399997e-08,1.4055734193945962e-07
|
||||
44,0.0001447164668892332,2.4034551917480693e-08,1.377259000826997e-07
|
||||
45,0.00014224784112376056,2.3753930444781328e-08,1.3494914625940223e-07
|
||||
46,0.000139820283675003,2.3476479506890216e-08,1.3222606385781202e-07
|
||||
47,0.00013743418748444287,2.3202247900619965e-08,1.295566531341862e-07
|
||||
48,0.00013508884015386686,2.2931181973013504e-08,1.2693987096025158e-07
|
||||
49,0.00013278354172498307,2.2663228905058608e-08,1.2437469442130953e-07
|
||||
50,0.00013051760419724657,2.2398336706395863e-08,1.2186012017917007e-07
|
||||
51,0.00012829168984638487,2.2136423459712534e-08,1.1939640981689728e-07
|
||||
52,0.00012610506317956756,2.1877440279108582e-08,1.1698252030563078e-07
|
||||
53,0.00012395700285919374,2.1621338937233993e-08,1.1461743054419825e-07
|
||||
54,0.00012180241033650921,2.136015630373758e-08,1.1225922783040025e-07
|
||||
55,0.0001196865090578088,2.11019103466444e-08,1.0994951537260489e-07
|
||||
56,0.00011760857776205185,2.0846552296319304e-08,1.0757395097863843e-07
|
||||
57,0.00011556891128259512,2.0594154864038522e-08,1.0524972555992818e-07
|
||||
58,0.00011356676177304645,2.0344670536408355e-08,1.0297570549834491e-07
|
||||
59,0.00011160139690545148,2.009805268169949e-08,1.007507830554809e-07
|
||||
60,0.00010967209909252316,1.9854255584746143e-08,9.857387536569294e-08
|
||||
61,0.00010777915187088834,1.961321154131787e-08,9.644480679617587e-08
|
||||
62,0.00010592181397175025,1.9374877782865603e-08,9.43624842461164e-08
|
||||
63,0.00010409936038609485,1.913921236065976e-08,9.232584080120623e-08
|
||||
64,0.00010246447558376296,1.8936229484424864e-08,9.046927135292076e-08
|
||||
65,0.00010085803630103994,1.873544193319646e-08,8.865067925960422e-08
|
||||
66,9.927950010555374e-05,1.8536821682157304e-08,8.686925127148483e-08
|
||||
67,9.772837346090753e-05,1.834034757300294e-08,8.512422533827403e-08
|
||||
68,9.62041343011343e-05,1.8145993316507615e-08,8.341482250640209e-08
|
||||
69,9.470627135912848e-05,1.7953733512786736e-08,8.174028142913557e-08
|
||||
70,9.32342835979764e-05,1.776354374489084e-08,8.009985766376519e-08
|
||||
71,9.178813743816069e-05,1.757538990695628e-08,7.849321446941075e-08
|
||||
72,9.036733009485282e-05,1.7389250225057777e-08,7.691961625609573e-08
|
||||
73,8.897136946428169e-05,1.7205104136353174e-08,7.537834446343352e-08
|
||||
74,8.760740745801088e-05,1.7025340034280735e-08,7.38751341742058e-08
|
||||
75,8.626710469266231e-05,1.6847609082084475e-08,7.274492099364066e-08
|
||||
76,8.495000573672366e-05,1.6671897815367364e-08,7.16342744751107e-08
|
||||
77,8.365569697520734e-05,1.6498202874185357e-08,7.054284583689086e-08
|
||||
78,8.238374036673638e-05,1.6326516066391613e-08,6.94702656996508e-08
|
||||
79,8.11337070649851e-05,1.615683240442047e-08,6.84161724378069e-08
|
||||
80,7.990517700271111e-05,1.5989150837085435e-08,6.738021182875641e-08
|
||||
81,7.869784230919362e-05,1.5823472723367315e-08,6.63621242598539e-08
|
||||
82,7.751129541079501e-05,1.5659808141896922e-08,6.536156604375558e-08
|
||||
83,7.634513730458697e-05,1.5498175122781168e-08,6.437820072038669e-08
|
||||
84,7.530262080974513e-05,1.5364277079429572e-08,6.349909645089698e-08
|
||||
85,7.427675504203511e-05,1.523236493234819e-08,6.263403294276124e-08
|
||||
86,7.326723873728716e-05,1.510251249079146e-08,6.178275615432246e-08
|
||||
87,7.227232864620995e-05,1.4974078108462424e-08,6.094379608687809e-08
|
||||
88,7.1291797553153e-05,1.4847055996011248e-08,6.011696114034367e-08
|
||||
89,7.032542203609039e-05,1.4721440784517874e-08,5.930206291361685e-08
|
||||
90,6.937298231673965e-05,1.4597227547292096e-08,5.849891607818969e-08
|
||||
91,6.843339696762385e-05,1.447443282270653e-08,5.7706608718023645e-08
|
||||
92,6.750649045006057e-05,1.4353051811356354e-08,5.6924992809748396e-08
|
||||
93,6.65920896785063e-05,1.4233080214004659e-08,5.615392239860827e-08
|
||||
94,6.554258932109667e-05,1.407504972937325e-08,5.5268928972034444e-08
|
||||
95,6.450957734109368e-05,1.3918655180382722e-08,5.439783940506079e-08
|
||||
|
224
tests/data/raman_fiber_config.json
Normal file
224
tests/data/raman_fiber_config.json
Normal file
@@ -0,0 +1,224 @@
|
||||
{
|
||||
"uid": "Span1",
|
||||
"params": {
|
||||
"length": 80,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
"att_in": 0,
|
||||
"con_in": 0.5,
|
||||
"con_out": 0.5,
|
||||
"type_variety": "SSMF",
|
||||
"dispersion": 0.0000167,
|
||||
"gamma": 0.00127,
|
||||
"pmd_coef": 1.265e-15,
|
||||
"raman_efficiency": {
|
||||
"cr": [
|
||||
0,
|
||||
0.0000094,
|
||||
0.0000292,
|
||||
0.0000488,
|
||||
0.0000682,
|
||||
0.0000831,
|
||||
0.000094,
|
||||
0.0001014,
|
||||
0.0001069,
|
||||
0.0001119,
|
||||
0.0001217,
|
||||
0.0001268,
|
||||
0.0001365,
|
||||
0.000149,
|
||||
0.000165,
|
||||
0.000181,
|
||||
0.0001977,
|
||||
0.0002192,
|
||||
0.0002469,
|
||||
0.0002749,
|
||||
0.0002999,
|
||||
0.0003206,
|
||||
0.0003405,
|
||||
0.0003592,
|
||||
0.000374,
|
||||
0.0003826,
|
||||
0.0003841,
|
||||
0.0003826,
|
||||
0.0003802,
|
||||
0.0003756,
|
||||
0.0003549,
|
||||
0.0003795,
|
||||
0.000344,
|
||||
0.0002933,
|
||||
0.0002024,
|
||||
0.0001158,
|
||||
0.0000846,
|
||||
0.0000714,
|
||||
0.0000686,
|
||||
0.000085,
|
||||
0.0000893,
|
||||
0.0000901,
|
||||
0.0000815,
|
||||
0.0000667,
|
||||
0.0000437,
|
||||
0.0000328,
|
||||
0.0000296,
|
||||
0.0000265,
|
||||
0.0000257,
|
||||
0.0000281,
|
||||
0.0000308,
|
||||
0.0000367,
|
||||
0.0000585,
|
||||
0.0000663,
|
||||
0.0000636,
|
||||
0.000055,
|
||||
0.0000406,
|
||||
0.0000277,
|
||||
0.0000242,
|
||||
0.0000187,
|
||||
0.000016,
|
||||
0.000014,
|
||||
0.0000113,
|
||||
0.0000105,
|
||||
0.0000098,
|
||||
0.0000098,
|
||||
0.0000113,
|
||||
0.0000164,
|
||||
0.0000195,
|
||||
0.0000238,
|
||||
0.0000226,
|
||||
0.0000203,
|
||||
0.0000148,
|
||||
0.0000109,
|
||||
0.0000098,
|
||||
0.0000105,
|
||||
0.0000117,
|
||||
0.0000125,
|
||||
0.0000121,
|
||||
0.0000109,
|
||||
0.0000098,
|
||||
0.0000082,
|
||||
0.0000066,
|
||||
0.0000047,
|
||||
0.0000027,
|
||||
0.0000019,
|
||||
0.0000012,
|
||||
4e-7,
|
||||
2e-7,
|
||||
1e-7
|
||||
],
|
||||
"frequency_offset": [
|
||||
0,
|
||||
500000000000,
|
||||
1000000000000,
|
||||
1500000000000,
|
||||
2000000000000,
|
||||
2500000000000,
|
||||
3000000000000,
|
||||
3500000000000,
|
||||
4000000000000,
|
||||
4500000000000,
|
||||
5000000000000,
|
||||
5500000000000,
|
||||
6000000000000,
|
||||
6500000000000,
|
||||
7000000000000,
|
||||
7500000000000,
|
||||
8000000000000,
|
||||
8500000000000,
|
||||
9000000000000,
|
||||
9500000000000,
|
||||
10000000000000,
|
||||
10500000000000,
|
||||
11000000000000,
|
||||
11500000000000,
|
||||
12000000000000,
|
||||
12500000000000,
|
||||
12750000000000,
|
||||
13000000000000,
|
||||
13250000000000,
|
||||
13500000000000,
|
||||
14000000000000,
|
||||
14500000000000,
|
||||
14750000000000,
|
||||
15000000000000,
|
||||
15500000000000,
|
||||
16000000000000,
|
||||
16500000000000,
|
||||
17000000000000,
|
||||
17500000000000,
|
||||
18000000000000,
|
||||
18250000000000,
|
||||
18500000000000,
|
||||
18750000000000,
|
||||
19000000000000,
|
||||
19500000000000,
|
||||
20000000000000,
|
||||
20500000000000,
|
||||
21000000000000,
|
||||
21500000000000,
|
||||
22000000000000,
|
||||
22500000000000,
|
||||
23000000000000,
|
||||
23500000000000,
|
||||
24000000000000,
|
||||
24500000000000,
|
||||
25000000000000,
|
||||
25500000000000,
|
||||
26000000000000,
|
||||
26500000000000,
|
||||
27000000000000,
|
||||
27500000000000,
|
||||
28000000000000,
|
||||
28500000000000,
|
||||
29000000000000,
|
||||
29500000000000,
|
||||
30000000000000,
|
||||
30500000000000,
|
||||
31000000000000,
|
||||
31500000000000,
|
||||
32000000000000,
|
||||
32500000000000,
|
||||
33000000000000,
|
||||
33500000000000,
|
||||
34000000000000,
|
||||
34500000000000,
|
||||
35000000000000,
|
||||
35500000000000,
|
||||
36000000000000,
|
||||
36500000000000,
|
||||
37000000000000,
|
||||
37500000000000,
|
||||
38000000000000,
|
||||
38500000000000,
|
||||
39000000000000,
|
||||
39500000000000,
|
||||
40000000000000,
|
||||
40500000000000,
|
||||
41000000000000,
|
||||
41500000000000,
|
||||
42000000000000
|
||||
]
|
||||
}
|
||||
},
|
||||
"operational": {
|
||||
"temperature": 283,
|
||||
"raman_pumps": [
|
||||
{
|
||||
"power": 0.2,
|
||||
"frequency": 205000000000000,
|
||||
"propagation_direction": "counterprop"
|
||||
},
|
||||
{
|
||||
"power": 0.206,
|
||||
"frequency": 201000000000000,
|
||||
"propagation_direction": "counterprop"
|
||||
}
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"location": {
|
||||
"latitude": 1,
|
||||
"longitude": 0,
|
||||
"city": null,
|
||||
"region": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
14
tests/data/sim_params.json
Normal file
14
tests/data/sim_params.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"raman_parameters": {
|
||||
"flag_raman": true,
|
||||
"space_resolution": 10e3,
|
||||
"tolerance": 1e-8
|
||||
},
|
||||
"nli_parameters": {
|
||||
"nli_method_name": "ggn_spectrally_separated",
|
||||
"wdm_grid_size": 50e9,
|
||||
"dispersion_tolerance": 1,
|
||||
"phase_shift_tolerance": 0.1,
|
||||
"computed_channels": [1, 18, 37, 56, 75]
|
||||
}
|
||||
}
|
||||
BIN
tests/data/testService.xls
Normal file
BIN
tests/data/testService.xls
Normal file
Binary file not shown.
79
tests/data/testService_services_expected.json
Normal file
79
tests/data/testService_services_expected.json
Normal file
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"path-request": [
|
||||
{
|
||||
"request-id": "0",
|
||||
"source": "trx Lorient_KMA",
|
||||
"destination": "trx Vannes_KBE",
|
||||
"src-tp-id": "trx Lorient_KMA",
|
||||
"dst-tp-id": "trx Vannes_KBE",
|
||||
"bidirectional": false,
|
||||
"path-constraints": {
|
||||
"te-bandwidth": {
|
||||
"technology": "flexi-grid",
|
||||
"trx_type": "Voyager",
|
||||
"trx_mode": "mode 1",
|
||||
"effective-freq-slot": [
|
||||
{
|
||||
"N": null,
|
||||
"M": null
|
||||
}
|
||||
],
|
||||
"spacing": 50000000000.0,
|
||||
"max-nb-of-channel": 80,
|
||||
"output-power": null,
|
||||
"path_bandwidth": 100000000000.0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"request-id": "1",
|
||||
"source": "trx Brest_KLA",
|
||||
"destination": "trx Vannes_KBE",
|
||||
"src-tp-id": "trx Brest_KLA",
|
||||
"dst-tp-id": "trx Vannes_KBE",
|
||||
"bidirectional": false,
|
||||
"path-constraints": {
|
||||
"te-bandwidth": {
|
||||
"technology": "flexi-grid",
|
||||
"trx_type": "Voyager",
|
||||
"trx_mode": "mode 1",
|
||||
"effective-freq-slot": [
|
||||
{
|
||||
"N": null,
|
||||
"M": null
|
||||
}
|
||||
],
|
||||
"spacing": 50000000000.0,
|
||||
"max-nb-of-channel": null,
|
||||
"output-power": 0.0012589254117941673,
|
||||
"path_bandwidth": 10000000000.0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"request-id": "3",
|
||||
"source": "trx Lannion_CAS",
|
||||
"destination": "trx Rennes_STA",
|
||||
"src-tp-id": "trx Lannion_CAS",
|
||||
"dst-tp-id": "trx Rennes_STA",
|
||||
"bidirectional": false,
|
||||
"path-constraints": {
|
||||
"te-bandwidth": {
|
||||
"technology": "flexi-grid",
|
||||
"trx_type": "vendorA_trx-type1",
|
||||
"trx_mode": "mode 1",
|
||||
"effective-freq-slot": [
|
||||
{
|
||||
"N": null,
|
||||
"M": null
|
||||
}
|
||||
],
|
||||
"spacing": 50000000000.0,
|
||||
"max-nb-of-channel": 80,
|
||||
"output-power": 0.0012589254117941673,
|
||||
"path_bandwidth": 60000000000.0
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
@@ -251,48 +251,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"uid": "roadm a",
|
||||
"type": "Roadm",
|
||||
"params": {
|
||||
"target_pch_out_db": -20,
|
||||
"restrictions": {
|
||||
"preamp_variety_list": [],
|
||||
"booster_variety_list": [
|
||||
"std_booster"
|
||||
]
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"location": {
|
||||
"latitude": 6.0,
|
||||
"longitude": 0.0,
|
||||
"city": "a",
|
||||
"region": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"uid": "roadm b",
|
||||
"type": "Roadm",
|
||||
"params": {
|
||||
"target_pch_out_db": -20,
|
||||
"restrictions": {
|
||||
"preamp_variety_list": [
|
||||
"std_low_gain"
|
||||
],
|
||||
"booster_variety_list": []
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"location": {
|
||||
"latitude": 5.0,
|
||||
"longitude": 0.0,
|
||||
"city": "b",
|
||||
"region": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"uid": "roadm c",
|
||||
"type": "Roadm",
|
||||
@@ -407,6 +365,48 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"uid": "roadm a",
|
||||
"type": "Roadm",
|
||||
"params": {
|
||||
"target_pch_out_db": -20,
|
||||
"restrictions": {
|
||||
"preamp_variety_list": [],
|
||||
"booster_variety_list": [
|
||||
"std_booster"
|
||||
]
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"location": {
|
||||
"latitude": 6.0,
|
||||
"longitude": 0.0,
|
||||
"city": "a",
|
||||
"region": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"uid": "roadm b",
|
||||
"type": "Roadm",
|
||||
"params": {
|
||||
"target_pch_out_db": -20,
|
||||
"restrictions": {
|
||||
"preamp_variety_list": [
|
||||
"std_low_gain"
|
||||
],
|
||||
"booster_variety_list": []
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"location": {
|
||||
"latitude": 5.0,
|
||||
"longitude": 0.0,
|
||||
"city": "b",
|
||||
"region": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"uid": "west fused spans in Corlay",
|
||||
"type": "Fused",
|
||||
@@ -502,7 +502,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 20.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -524,7 +523,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 50.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -546,7 +544,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 60.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -568,7 +565,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 10.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -590,7 +586,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 60.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -612,7 +607,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 65.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -634,7 +628,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 40.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -656,7 +649,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 35.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -678,7 +670,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 75.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -700,7 +691,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 70.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -722,7 +712,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 50.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -744,7 +733,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 55.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -766,7 +754,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 30.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -788,7 +775,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 30.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -810,7 +796,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 10.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -832,7 +817,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 60.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -854,7 +838,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 70.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -876,7 +859,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 80.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -898,7 +880,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 90.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -920,7 +901,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 100.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -942,7 +922,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 110.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -964,7 +943,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 20.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -986,7 +964,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 50.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -1008,7 +985,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 60.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -1030,7 +1006,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 10.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -1052,7 +1027,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 60.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -1074,7 +1048,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 65.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -1096,7 +1069,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 40.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -1118,7 +1090,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 35.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -1140,7 +1111,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 75.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -1162,7 +1132,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 70.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -1184,7 +1153,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 50.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -1206,7 +1174,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 55.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -1228,7 +1195,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 30.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -1250,7 +1216,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 30.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -1272,7 +1237,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 10.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -1294,7 +1258,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 60.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -1316,7 +1279,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 70.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -1338,7 +1300,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 80.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -1360,7 +1321,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 90.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -1382,7 +1342,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 100.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -1404,7 +1363,6 @@
|
||||
"type": "Fiber",
|
||||
"type_variety": "SSMF",
|
||||
"params": {
|
||||
"type_variety": "SSMF",
|
||||
"length": 110.0,
|
||||
"loss_coef": 0.2,
|
||||
"length_units": "km",
|
||||
@@ -1459,25 +1417,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"uid": "east edfa in Stbrieuc to Rennes_STA",
|
||||
"type": "Edfa",
|
||||
"type_variety": "std_medium_gain",
|
||||
"operational": {
|
||||
"gain_target": 18.5,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"out_voa": 0
|
||||
},
|
||||
"metadata": {
|
||||
"location": {
|
||||
"latitude": 3.0,
|
||||
"longitude": 0.0,
|
||||
"city": "Stbrieuc",
|
||||
"region": "RLD"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"uid": "east edfa in Lannion_CAS to Morlaix",
|
||||
"type": "Edfa",
|
||||
@@ -1497,6 +1436,25 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"uid": "east edfa in Stbrieuc to Rennes_STA",
|
||||
"type": "Edfa",
|
||||
"type_variety": "std_medium_gain",
|
||||
"operational": {
|
||||
"gain_target": 18.5,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"out_voa": 0
|
||||
},
|
||||
"metadata": {
|
||||
"location": {
|
||||
"latitude": 3.0,
|
||||
"longitude": 0.0,
|
||||
"city": "Stbrieuc",
|
||||
"region": "RLD"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"uid": "east edfa in Brest_KLA to Quimper",
|
||||
"type": "Edfa",
|
||||
@@ -1593,7 +1551,7 @@
|
||||
"type": "Edfa",
|
||||
"type_variety": "test_fixed_gain",
|
||||
"operational": {
|
||||
"gain_target": 20.0,
|
||||
"gain_target": 20,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"out_voa": 0
|
||||
@@ -1612,7 +1570,7 @@
|
||||
"type": "Edfa",
|
||||
"type_variety": "test_fixed_gain",
|
||||
"operational": {
|
||||
"gain_target": 20.0,
|
||||
"gain_target": 20,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"out_voa": 0
|
||||
@@ -1740,88 +1698,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"uid": "Edfa0_roadm a",
|
||||
"type": "Edfa",
|
||||
"type_variety": "std_booster",
|
||||
"operational": {
|
||||
"gain_target": 20,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"out_voa": 0
|
||||
},
|
||||
"metadata": {
|
||||
"location": {
|
||||
"latitude": 5.75,
|
||||
"longitude": 0.0,
|
||||
"city": "a",
|
||||
"region": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"uid": "Edfa1_roadm a",
|
||||
"type": "Edfa",
|
||||
"type_variety": "std_booster",
|
||||
"operational": {
|
||||
"gain_target": 20,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"out_voa": 0
|
||||
},
|
||||
"metadata": {
|
||||
"location": {
|
||||
"latitude": 6.0,
|
||||
"longitude": 0.25,
|
||||
"city": "a",
|
||||
"region": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"uid": "Edfa0_roadm b",
|
||||
"type": "Edfa",
|
||||
"type_variety": "test_fixed_gain",
|
||||
"operational": {
|
||||
"gain_target": 20,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"out_voa": 0
|
||||
},
|
||||
"metadata": {
|
||||
"location": {
|
||||
"latitude": 5.25,
|
||||
"longitude": 0.0,
|
||||
"city": "b",
|
||||
"region": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"uid": "Edfa1_roadm b",
|
||||
"type": "Edfa",
|
||||
"type_variety": "test_fixed_gain",
|
||||
"operational": {
|
||||
"gain_target": 20,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"out_voa": 0
|
||||
},
|
||||
"metadata": {
|
||||
"location": {
|
||||
"latitude": 5.0,
|
||||
"longitude": 0.25,
|
||||
"city": "b",
|
||||
"region": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"uid": "Edfa0_roadm c",
|
||||
"type": "Edfa",
|
||||
"type_variety": "test_fixed_gain",
|
||||
"operational": {
|
||||
"gain_target": 22,
|
||||
"gain_target": 22.0,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"out_voa": 0
|
||||
@@ -1840,7 +1722,7 @@
|
||||
"type": "Edfa",
|
||||
"type_variety": "test_fixed_gain",
|
||||
"operational": {
|
||||
"gain_target": 22,
|
||||
"gain_target": 22.0,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"out_voa": 0
|
||||
@@ -2063,6 +1945,82 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"uid": "Edfa0_roadm a",
|
||||
"type": "Edfa",
|
||||
"type_variety": "std_booster",
|
||||
"operational": {
|
||||
"gain_target": 20,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"out_voa": 0
|
||||
},
|
||||
"metadata": {
|
||||
"location": {
|
||||
"latitude": 5.75,
|
||||
"longitude": 0.0,
|
||||
"city": "a",
|
||||
"region": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"uid": "Edfa1_roadm a",
|
||||
"type": "Edfa",
|
||||
"type_variety": "std_booster",
|
||||
"operational": {
|
||||
"gain_target": 20,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"out_voa": 0
|
||||
},
|
||||
"metadata": {
|
||||
"location": {
|
||||
"latitude": 6.0,
|
||||
"longitude": 0.25,
|
||||
"city": "a",
|
||||
"region": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"uid": "Edfa0_roadm b",
|
||||
"type": "Edfa",
|
||||
"type_variety": "test_fixed_gain",
|
||||
"operational": {
|
||||
"gain_target": 20,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"out_voa": 0
|
||||
},
|
||||
"metadata": {
|
||||
"location": {
|
||||
"latitude": 5.25,
|
||||
"longitude": 0.0,
|
||||
"city": "b",
|
||||
"region": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"uid": "Edfa1_roadm b",
|
||||
"type": "Edfa",
|
||||
"type_variety": "test_fixed_gain",
|
||||
"operational": {
|
||||
"gain_target": 20,
|
||||
"delta_p": null,
|
||||
"tilt_target": 0,
|
||||
"out_voa": 0
|
||||
},
|
||||
"metadata": {
|
||||
"location": {
|
||||
"latitude": 5.0,
|
||||
"longitude": 0.25,
|
||||
"city": "b",
|
||||
"region": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"uid": "Edfa0_fiber (Loudeac → Lorient_KMA)-F054",
|
||||
"type": "Edfa",
|
||||
@@ -2797,30 +2755,6 @@
|
||||
"from_node": "roadm c",
|
||||
"to_node": "east edfa in c to d"
|
||||
},
|
||||
{
|
||||
"from_node": "roadm a",
|
||||
"to_node": "trx a"
|
||||
},
|
||||
{
|
||||
"from_node": "roadm a",
|
||||
"to_node": "Edfa0_roadm a"
|
||||
},
|
||||
{
|
||||
"from_node": "roadm a",
|
||||
"to_node": "Edfa1_roadm a"
|
||||
},
|
||||
{
|
||||
"from_node": "roadm b",
|
||||
"to_node": "trx b"
|
||||
},
|
||||
{
|
||||
"from_node": "roadm b",
|
||||
"to_node": "Edfa0_roadm b"
|
||||
},
|
||||
{
|
||||
"from_node": "roadm b",
|
||||
"to_node": "Edfa1_roadm b"
|
||||
},
|
||||
{
|
||||
"from_node": "roadm c",
|
||||
"to_node": "trx c"
|
||||
@@ -2897,6 +2831,30 @@
|
||||
"from_node": "roadm h",
|
||||
"to_node": "Edfa1_roadm h"
|
||||
},
|
||||
{
|
||||
"from_node": "roadm a",
|
||||
"to_node": "trx a"
|
||||
},
|
||||
{
|
||||
"from_node": "roadm a",
|
||||
"to_node": "Edfa0_roadm a"
|
||||
},
|
||||
{
|
||||
"from_node": "roadm a",
|
||||
"to_node": "Edfa1_roadm a"
|
||||
},
|
||||
{
|
||||
"from_node": "roadm b",
|
||||
"to_node": "trx b"
|
||||
},
|
||||
{
|
||||
"from_node": "roadm b",
|
||||
"to_node": "Edfa0_roadm b"
|
||||
},
|
||||
{
|
||||
"from_node": "roadm b",
|
||||
"to_node": "Edfa1_roadm b"
|
||||
},
|
||||
{
|
||||
"from_node": "west fused spans in Corlay",
|
||||
"to_node": "fiber (Corlay → Loudeac)-F010"
|
||||
@@ -3097,14 +3055,14 @@
|
||||
"from_node": "east edfa in Lannion_CAS to Stbrieuc",
|
||||
"to_node": "fiber (Lannion_CAS → Stbrieuc)-F056"
|
||||
},
|
||||
{
|
||||
"from_node": "east edfa in Stbrieuc to Rennes_STA",
|
||||
"to_node": "fiber (Stbrieuc → Rennes_STA)-F057"
|
||||
},
|
||||
{
|
||||
"from_node": "east edfa in Lannion_CAS to Morlaix",
|
||||
"to_node": "fiber (Lannion_CAS → Morlaix)-F059"
|
||||
},
|
||||
{
|
||||
"from_node": "east edfa in Stbrieuc to Rennes_STA",
|
||||
"to_node": "fiber (Stbrieuc → Rennes_STA)-F057"
|
||||
},
|
||||
{
|
||||
"from_node": "east edfa in Brest_KLA to Quimper",
|
||||
"to_node": "fiber (Brest_KLA → Quimper)-"
|
||||
@@ -3157,22 +3115,6 @@
|
||||
"from_node": "Edfa0_roadm Brest_KLA",
|
||||
"to_node": "fiber (Brest_KLA → Morlaix)-F060"
|
||||
},
|
||||
{
|
||||
"from_node": "Edfa0_roadm a",
|
||||
"to_node": "fiber (a → b)-"
|
||||
},
|
||||
{
|
||||
"from_node": "Edfa1_roadm a",
|
||||
"to_node": "fiber (a → c)-"
|
||||
},
|
||||
{
|
||||
"from_node": "Edfa0_roadm b",
|
||||
"to_node": "fiber (b → a)-"
|
||||
},
|
||||
{
|
||||
"from_node": "Edfa1_roadm b",
|
||||
"to_node": "fiber (b → f)-"
|
||||
},
|
||||
{
|
||||
"from_node": "Edfa0_roadm c",
|
||||
"to_node": "fiber (c → a)-"
|
||||
@@ -3225,6 +3167,22 @@
|
||||
"from_node": "Edfa1_roadm h",
|
||||
"to_node": "fiber (h → g)-"
|
||||
},
|
||||
{
|
||||
"from_node": "Edfa0_roadm a",
|
||||
"to_node": "fiber (a → b)-"
|
||||
},
|
||||
{
|
||||
"from_node": "Edfa1_roadm a",
|
||||
"to_node": "fiber (a → c)-"
|
||||
},
|
||||
{
|
||||
"from_node": "Edfa0_roadm b",
|
||||
"to_node": "fiber (b → a)-"
|
||||
},
|
||||
{
|
||||
"from_node": "Edfa1_roadm b",
|
||||
"to_node": "fiber (b → f)-"
|
||||
},
|
||||
{
|
||||
"from_node": "Edfa0_fiber (Loudeac → Lorient_KMA)-F054",
|
||||
"to_node": "roadm Lorient_KMA"
|
||||
@@ -3354,4 +3312,4 @@
|
||||
"to_node": "roadm h"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user