32 Commits

Author SHA1 Message Date
Jan Kundrát
049b077ee4 Use real IP addresses for the US-based Cassinis
Change-Id: I158bb84261a56d71074155880c4359033b2f1044
2020-03-07 16:49:39 -08:00
Jan Kundrát
ab2080a805 Update IP addresses and hostnames for the OFC2020 demo
Change-Id: Ie8d30d56f94d1ce14f8ac62ceec7f0e57a3486b2
2020-02-12 17:32:07 +01:00
Jan Kundrát
8ab54e76df Merge branch 'develop' into experimental/2020-ofc
Change-Id: I4f7d3cc91734a03251b4ad4d82b05aad68d0ef5f
2020-02-12 17:18:50 +01:00
Jan Kundrát
f015c6abed ROADM module replacements 2020-01-07 16:29:15 +01:00
Jan Kundrát
71293c1c18 demo: reduce the spectrum so that it's safely and conveniently deep in the C-band 2019-11-12 20:26:08 +01:00
Jan Kundrát
bd7c70f902 demo: Fix ONOS dev-id mapping for Ams-L2
A duplicate key in the dict means that bad things happen.
2019-11-12 13:20:16 +01:00
Jan Kundrát
20c92d4338 demo: add an endpoint which return success so that ONOS can verify connectivity 2019-11-12 11:53:41 +01:00
Jan Kundrát
f0158e7202 demo: fix transponder name and type 2019-11-12 11:41:51 +01:00
Jan Kundrát
62408ddc98 demo: hardcode the device IP addresses 2019-11-11 16:48:53 +01:00
Jan Kundrát
b4f87b36db REST API: output detailed info about the reversed path for bidi requests 2019-11-08 15:27:21 +01:00
Jan Kundrát
9f49a115a1 Add a path-route-object with EDFA-specific per-channel power and output VOA settings
...once again. for the demo.
2019-11-08 13:00:40 +01:00
Jan Kundrát
c7d2305589 REST: return element type for EDFA, TXP and ROADM elements
...as requested by Andrea during today's call.
2019-11-08 12:50:11 +01:00
Jan Kundrát
5826a649de sync topology with Esther's proposal 2019-11-05 13:52:16 +01:00
EstherLerouzic
fa826391f6 Add some tests to support partial per degree target power definition
Signed-off-by: EstherLerouzic <esther.lerouzic@orange.com>
2019-11-05 13:18:37 +01:00
EstherLerouzic
3481ba8ee3 add the degree info of next node during path propagation
when node is a roadm, add the degree info of next node during
path propagation.

Signed-off-by: EstherLerouzic <esther.lerouzic@orange.com>
2019-11-05 13:18:18 +01:00
EstherLerouzic
b4ab0b55de use the per degree target_pch_out_db for the target power in network build
Signed-off-by: EstherLerouzic <esther.lerouzic@orange.com>
2019-11-05 13:17:02 +01:00
EstherLerouzic
0370b45d8a Add per degree power information in ROADM
- add the per degree info using the EXACT next node uid as identifier
  of the degree
- add the degree identifier on the propagate and call functions

Signed-off-by: EstherLerouzic <esther.lerouzic@orange.com>
2019-11-05 13:16:54 +01:00
EstherLerouzic
468e689094 Add per channel power target out
Works OK only for roadms that face the line.... but maybe a problem
for the express path ....
to be checked

Signed-off-by: EstherLerouzic <esther.lerouzic@orange.com>
2019-11-05 13:16:38 +01:00
Jan Kundrát
aafd82b16d Docker: run the TIP Summit 2019 demo by default
Here's a TL;DR of how to use this. First, start the container so that it
exports its port 5000 for incoming HTTP requests:

- docker run -P -it --rm telecominfraproject/oopt-gnpy-experimental:$SOME_VERSION

You'll have to replace `$SOME_VERSION` by acn actual version. There is
no default `latest` tag on this particular Docker repository.

Then find out what port Docker currently uses:

```console-session
~ # docker ps
CONTAINER ID        IMAGE                                                          COMMAND                  CREATED             STATUS              PORTS                     NAMES
6004c3a9b741        telecominfraproject/oopt-gnpy-experimental:v1.8-114-g7e0abc4   "/oopt-gnpy/.docker-…"   48 seconds ago      Up 44 seconds       0.0.0.0:32768->5000/tcp   eloquent_hawking
~ # docker port 6004c3a9b741
5000/tcp -> 0.0.0.0:32768
```

Path computation can then be requested like this:

- curl -v -X POST -H "Content-Type: application/json" -d @examples/2019-demo-services.json http://127.0.0.1:32768/gnpy-experimental

This one will try to compute two disjoint optical paths and output their
respective optical performance.
2019-10-28 17:02:08 +01:00
Jan Kundrát
60ee331153 demo: simplify the REST API interface
The topology will be provisioned out-of-band, so let's simplify the REST
API so that it reflects that design. Also, let's make it obvious that
the API is subject to change and should not be relied upon at this time.
It's meant to be an experimental interface with data I/O format which
*will* change as we adapt a proper YANG model for both directions.
2019-10-28 15:59:18 +01:00
Jan Kundrát
3a8ce74355 topologies and service requests for the TIP Summit demo
The examples/2019-generate-tip-demo.py helper script can be used to
generate a ring topology where each "ROADM node" consists of three
separate ROADMs and two pairs of booster+preamp EDFAs. This will be used
at the TIP Summit to show integration between ONOS and GNPy.

The topology *and the equipment library) more or less corresponds to the
CzechLight OLS that is planned for the exhibition.
2019-10-28 15:55:18 +01:00
Jan Kundrát
fd44463238 REST: do not use HTTP auth
I do not think that proof-of-concept demos should implement HTTP auth
because GNPy has no concept of access lists.  If people want to use this
in a "real scenario", they will likely wrap Python's HTTP server behind
a real HTTP reverse proxy, and they can then implement proper ACL at
that layer.
2019-10-28 15:45:36 +01:00
EstherLerouzic
84ba2da553 add 400 return with msg in case of service error
Signed-off-by: EstherLerouzic <esther.lerouzic@orange.com>
2019-10-14 17:04:07 +01:00
EstherLerouzic
e693d96ca1 limit generators to support fused in preamps
Signed-off-by: EstherLerouzic <esther.lerouzic@orange.com>
2019-10-14 15:52:49 +01:00
EstherLerouzic
81cb7f8133 corrections due to codacy report
- remove unused abort and marshall
- change variable names to conform to upper letter rule,
  [a-z_][a-z0-9_]{2,30}$
- add docstrings

Signed-off-by: EstherLerouzic <esther.lerouzic@orange.com>
2019-10-14 15:37:58 +01:00
EstherLerouzic
3471969956 add flesk_restfull and flask_httpauth packages to requirements
Signed-off-by: EstherLerouzic <esther.lerouzic@orange.com>
2019-10-10 12:21:28 +01:00
EstherLerouzic
7a0985c362 add flask import in requirements
Signed-off-by: EstherLerouzic <esther.lerouzic@orange.com>
2019-10-10 12:21:28 +01:00
EstherLerouzic
b79a9e2e67 example of result in json format
Signed-off-by: EstherLerouzic <esther.lerouzic@orange.com>
2019-10-10 12:21:28 +01:00
EstherLerouzic
1e037fe6f5 add example for simple topology
on the same topology file find the triangle topology

        site_c
        /  \
       /    \
site_a ------ Site_b

and the simple parallel link
site_a ------ Site_b
        \  /
         --

this topo includes only sinple span hops and roadm have boosters and amplifiers

the serviceDemov1.json gives the example of how the requests must be formulated

- 0 simple one
- 1 request with the forced Span (case of parallel link)
- 2 request with the forced roadm (case of triangle topo)
- 3 and 4 request with the disjunction

Signed-off-by: EstherLerouzic <esther.lerouzic@orange.com>
2019-10-10 12:21:10 +01:00
EstherLerouzic
0897be57c1 use a default topology file when api input topo is empty
Signed-off-by: EstherLerouzic <esther.lerouzic@orange.com>
2019-10-10 09:04:36 +01:00
EstherLerouzic
4172b06b19 Update service and result json templates
Signed-off-by: EstherLerouzic <esther.lerouzic@orange.com>
2019-10-10 09:04:36 +01:00
ahmed
32a4875e46 add the option "rest" to activate the api-rest
- add the function launch_cli to launch the "cli" mode
- add the the class Gnpy_API to launch the "api" mode
- modify the main to enable the launch of Gnpy with two
modes "rest" and "cli"

Signed-off-by: ahmed <ahmed.triki@orange.com>
2019-10-10 09:04:36 +01:00
150 changed files with 15593 additions and 15570 deletions

View File

@@ -1,2 +1 @@
[bandit] skips: ['B101']
skips: B101

View File

@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
cp -nr /opt/application/oopt-gnpy/gnpy/example-data /shared cp -nr /oopt-gnpy/examples /shared
exec "$@" exec "$@"

View File

@@ -5,17 +5,16 @@ set -e
IMAGE_NAME=telecominfraproject/oopt-gnpy IMAGE_NAME=telecominfraproject/oopt-gnpy
IMAGE_TAG=$(git describe --tags) IMAGE_TAG=$(git describe --tags)
if [[ "${TRAVIS_BRANCH}" == "experimental/2019-summit" ]]; then
IMAGE_NAME=telecominfraproject/oopt-gnpy-experimental
fi
ALREADY_FOUND=0 ALREADY_FOUND=0
docker pull ${IMAGE_NAME}:${IMAGE_TAG} && ALREADY_FOUND=1 docker pull ${IMAGE_NAME}:${IMAGE_TAG} && ALREADY_FOUND=1
if [[ $ALREADY_FOUND == 0 ]]; then if [[ $ALREADY_FOUND == 0 ]]; then
docker build . -t ${IMAGE_NAME} docker build . -t ${IMAGE_NAME}
docker tag ${IMAGE_NAME} ${IMAGE_NAME}:${IMAGE_TAG} docker tag ${IMAGE_NAME} ${IMAGE_NAME}:${IMAGE_TAG}
# shared directory setup: do not clobber the real data
mkdir trash
cd trash
docker run -it --rm --volume $(pwd):/shared ${IMAGE_NAME} gnpy-transmission-example
else else
echo "Image ${IMAGE_NAME}:${IMAGE_TAG} already available, will just update the other tags" echo "Image ${IMAGE_NAME}:${IMAGE_TAG} already available, will just update the other tags"
fi fi
@@ -43,5 +42,11 @@ if [[ "${TRAVIS_PULL_REQUEST}" == "false" ]]; then
docker push ${IMAGE_NAME}:${IMAGE_TAG} docker push ${IMAGE_NAME}:${IMAGE_TAG}
fi fi
docker push ${IMAGE_NAME}:stable docker push ${IMAGE_NAME}:stable
elif [[ "${TRAVIS_BRANCH}" == "experimental/2019-summit" ]]; then
echo "Publishing ad-hoc image for the TIP Summit demo"
do_docker_login
if [[ $ALREADY_FOUND == 0 ]]; then
docker push ${IMAGE_NAME}:${IMAGE_TAG}
fi
fi fi
fi fi

View File

@@ -1 +0,0 @@
venv/

3
.gitignore vendored
View File

@@ -3,7 +3,6 @@ __pycache__/
*.py[cod] *.py[cod]
*$py.class *$py.class
.ipynb_checkpoints .ipynb_checkpoints
.idea
# C extensions # C extensions
*.so *.so
@@ -65,5 +64,3 @@ target/
# MacOS DS_store # MacOS DS_store
.DS_Store .DS_Store
venv/

View File

@@ -1,5 +0,0 @@
[gerrit]
host=review.gerrithub.io
project=Telecominfraproject/oopt-gnpy
defaultrebase=0
defaultbranch=develop

View File

@@ -7,11 +7,14 @@ python:
- "3.7" - "3.7"
install: skip install: skip
script: script:
- python setup.py develop - python setup.py install
- pip install pytest-cov rstcheck - pip install pytest-cov rstcheck
- pytest --cov-report=xml --cov=gnpy -v - pytest --cov-report=xml --cov=gnpy
- rstcheck --ignore-roles cite *.rst - rstcheck --ignore-roles cite --ignore-directives automodule --recursive --ignore-messages '(Duplicate explicit target name.*)' .
- sphinx-build -W --keep-going docs/ x-throwaway-location - ./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
after_success: after_success:
- bash <(curl -s https://codecov.io/bash) - bash <(curl -s https://codecov.io/bash)
jobs: jobs:

View File

@@ -2,43 +2,7 @@
- project: - project:
check: check:
jobs: jobs:
- tox-py36-cover - noop
- 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: gate:
jobs: jobs:
- tox-py36-el8 - noop
- 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=

View File

@@ -7,7 +7,7 @@ To learn how to contribute, please see CONTRIBUTING.md
- Alessio Ferrari (Politecnico di Torino) <alessio.ferrari@polito.it> - Alessio Ferrari (Politecnico di Torino) <alessio.ferrari@polito.it>
- Anders Lindgren (Telia Company) <Anders.X.Lindgren@teliacompany.com> - 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> - Brian Taylor (Facebook) <briantaylor@fb.com>
- David Boertjes (Ciena) <dboertje@ciena.com> - David Boertjes (Ciena) <dboertje@ciena.com>
- Diego Landa (Facebook) <dlanda@fb.com> - Diego Landa (Facebook) <dlanda@fb.com>

View File

@@ -1,18 +1,8 @@
FROM python:3.7-slim FROM python:3.7-slim
WORKDIR /opt/application/oopt-gnpy COPY . /oopt-gnpy
RUN mkdir -p /shared/example-data \ WORKDIR /oopt-gnpy
&& groupadd gnpy \ RUN python setup.py install
&& useradd -u 1000 -g gnpy -m gnpy \ WORKDIR /shared
&& apt-get update \ ENTRYPOINT ["/oopt-gnpy/.docker-entry.sh"]
&& apt-get install git -y \ CMD ["python", "examples/path_requests_run.py", "examples/2019-demo-topology.json", "examples/2019-demo-services.json", "examples/2019-demo-equipment.json", "--rest"]
&& rm -rf /var/lib/apt/lists/* EXPOSE 5000
COPY . /opt/application/oopt-gnpy
WORKDIR /opt/application/oopt-gnpy
RUN mkdir topology \
&& mkdir equipment \
&& mkdir autodesign \
&& 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"]

View File

@@ -1,7 +1,8 @@
Excel (XLS, XLSX) input files
=============================
``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. 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.
The file named 'meshTopologyExampleV2.xls' is an example. The file named 'meshTopologyExampleV2.xls' is an example.
@@ -15,8 +16,6 @@ In order to work the excel file MUST contain at least 2 sheets:
- Eqt - Eqt
- Service - Service
.. _excel-nodes-sheet:
Nodes sheet Nodes sheet
----------- -----------
@@ -35,7 +34,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. - 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. - *State*, *Country*, *Region* are not mandatory.
"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. "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.
- *Longitude*, *Latitude* are not mandatory. If filled they should contain numbers. - *Longitude*, *Latitude* are not mandatory. If filled they should contain numbers.
@@ -45,8 +44,6 @@ 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** **There MUST NOT be empty line(s) between two nodes lines**
.. _excel-links-sheet:
Links sheet Links sheet
----------- -----------
@@ -83,11 +80,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 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 below 150 km, it is considered as a single (bidirectional) fiber span.
- 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” - 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”
- **Fiber type** is not mandatory. - **Fiber type** is not mandatory.
If filled it must contain types listed in `eqpt_config.json <gnpy/example-data/eqpt_config.json>`_ in "Fiber" list "type_variety". If filled it must contain types listed in `eqpt_config.json <examples/eqpt_config.json>`_ in "Fiber" list "type_variety".
If not filled it takes "SSMF" as default value. If not filled it takes "SSMF" as default value.
- **Lineic att** is not mandatory. - **Lineic att** is not mandatory.
@@ -116,8 +113,6 @@ and a fiber span from node3 to node6::
(in progress) (in progress)
.. _excel-equipment-sheet:
Eqpt sheet Eqpt sheet
---------- ----------
@@ -125,7 +120,7 @@ Eqt sheet is optional. It lists the amplifiers types and characteristics on each
Eqpt sheet must contain twelve columns:: Eqpt sheet must contain twelve columns::
<-- east cable from a to z --> <-- west from z to a --> <-- 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 Node A ; Node Z ; amp type ; att_in ; amp gain ; tilt ; att_out ; amp type ; att_in ; amp gain ; tilt ; att_out
If the sheet is present, it MUST have as many lines as egress directions of ROADMs defined in Links Sheet. If the sheet is present, it MUST have as many lines as egress directions of ROADMs defined in Links Sheet.
@@ -155,11 +150,11 @@ then Eqpt sheet should contain:
C - amp3 C - amp3
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. 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.
.. code-block:: shell .. code-block:: shell
$ cd $(gnpy-example-data) $ cd examples
$ python create_eqpt_sheet.py meshTopologyExampleV2.xls $ 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. 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.
@@ -172,7 +167,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. - **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. - **amp type** is not mandatory.
If filled it must contain types listed in `eqpt_config.json <gnpy/example-data/eqpt_config.json>`_ in "Edfa" list "type_variety". If filled it must contain types listed in `eqpt_config.json <examples/eqpt_config.json>`_ in "Edfa" list "type_variety".
If not filled it takes "std_medium_gain" as default value. 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. 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.
@@ -180,23 +175,19 @@ 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 not filled, it will be determined with design rules in the convert.py file.
If filled, it must contain positive numbers. 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 attenuator 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 attenautor at input and output of amplifier (in dB).
If filled they must contain positive numbers. If filled they must contain positive numbers.
- *tilt* --TODO-- - *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 # # to be completed #
(in progress) (in progress)
.. _excel-service-sheet:
Service sheet Service sheet
------------- -------------
Service sheet is optional. It lists the services for which path and feasibility must be computed with ``gnpy-path_request``. Service sheet is optional. It lists the services for which path and feasibility must be computed with path_requests_run.py.
Service sheet must contain 11 columns:: Service sheet must contain 11 columns::
@@ -225,4 +216,36 @@ 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. - 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. - 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 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. - ** 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)

View File

@@ -7,7 +7,7 @@
`gnpy`: mesh optical network route planning and optimization library `gnpy`: mesh optical network route planning and optimization library
==================================================================== ====================================================================
|docs| |travis| |doi| |contributors| |codacy-quality| |codecov| |docs| |build| |doi|
**`gnpy` is an open-source, community-developed library for building route **`gnpy` is an open-source, community-developed library for building route
planning and optimization tools in real-world mesh optical networks.** planning and optimization tools in real-world mesh optical networks.**
@@ -31,10 +31,108 @@ There are `weekly calls <https://telecominfraproject.workplace.com/events/702894
Newcomers, users and telecom operators are especially welcome there. 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/>`__. 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 How to Install
-------------- --------------
Install either via `Docker <docs/install.rst#install-docker>`__, or as a `Python package <docs/install.rst#install-pip>`__. 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
Instructions for First Use Instructions for First Use
-------------------------- --------------------------
@@ -59,24 +157,25 @@ This example demonstrates how GNPy can be used to check the expected SNR at the
:target: https://asciinema.org/a/252295 :target: https://asciinema.org/a/252295
By default, this script operates on a single span network defined in By default, this script operates on a single span network defined in
`gnpy/example-data/edfa_example_network.json <gnpy/example-data/edfa_example_network.json>`_ `examples/edfa_example_network.json <examples/edfa_example_network.json>`_
You can specify a different network at the command line as follows. For You can specify a different network at the command line as follows. For
example, to use the CORONET Global network defined in example, to use the CORONET Global network defined in
`gnpy/example-data/CORONET_Global_Topology.json <gnpy/example-data/CORONET_Global_Topology.json>`_: `examples/CORONET_Global_Topology.json <examples/CORONET_Global_Topology.json>`_:
.. code-block:: shell-session .. code-block:: shell-session
$ gnpy-transmission-example $(gnpy-example-data)/CORONET_Global_Topology.json $ ./examples/transmission_main_example.py examples/CORONET_Global_Topology.json
It is also possible to use an Excel file input (for example It is also possible to use an Excel file input (for example
`gnpy/example-data/CORONET_Global_Topology.xlsx <gnpy/example-data/CORONET_Global_Topology.xlsx>`_). `examples/CORONET_Global_Topology.xls <examples/CORONET_Global_Topology.xls>`_).
The Excel file will be processed into a JSON file with the same prefix. The Excel file will be processed into a JSON file with the same prefix. For
Further details about the Excel data structure are available `in the documentation <docs/excel.rst>`__. further instructions on how to prepare the Excel input file, see
`Excel_userguide.rst <Excel_userguide.rst>`_.
The main transmission example will calculate the average signal OSNR and SNR The main transmission example will calculate the average signal OSNR and SNR
across network elements (transceiver, ROADMs, fibers, and amplifiers) across network elements (transceiver, ROADMs, fibers, and amplifiers)
between two transceivers selected by the user. Additional details are provided by doing ``gnpy-transmission-example -h``. (By default, for the CORONET Global between two transceivers selected by the user. Additional details are provided by doing ``transmission_main_example.py -h``. (By default, for the CORONET Global
network, it will show the transmission of spectral information between Abilene and Albany) network, it will show the transmission of spectral information between Abilene and Albany)
This script calculates the average signal OSNR = |OSNR| and SNR = |SNR|. This script calculates the average signal OSNR = |OSNR| and SNR = |SNR|.
@@ -90,74 +189,352 @@ interference noise.
.. |Pase| replace:: P\ :sub:`ase` .. |Pase| replace:: P\ :sub:`ase`
.. |Pnli| replace:: P\ :sub:`nli` .. |Pnli| replace:: P\ :sub:`nli`
Further Instructions for Use Further Instructions for Use (`transmission_main_example.py`, `path_requests_run.py`)
---------------------------- -------------------------------------------------------------------------------------
Simulations are driven by a set of `JSON <docs/json.rst>`__ or `XLS <docs/excel.rst>`__ files. 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).
The ``gnpy-transmission-example`` script propagates a spectrum of channels at 32 Gbaud, 50 GHz spacing and 0 dBm/channel. 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.
Launch power can be overridden by using the ``--power`` argument. 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. 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. The number of channel is computed based on ``spacing`` and ``f_min``, ``f_max`` values.
An experimental support for Raman amplification is available: An experimental support for Raman amplification is available:
.. code-block:: shell-session .. code-block:: shell
$ gnpy-transmission-example \ $ ./examples/transmission_main_example.py \
$(gnpy-example-data)/raman_edfa_example_network.json \ examples/raman_edfa_example_network.json \
--sim $(gnpy-example-data)/sim_params.json --show-channels --sim examples/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 <gnpy/example-data/raman_edfa_example_network.json>`_. 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 `gnpy/example-data/sim_params.json <gnpy/example-data/sim_params.json>`_. General numeric parameters for simulaiton control are provided in the `examples/sim_params.json <examples/sim_params.json>`_.
Use ``gnpy-path-request`` to request several paths at once: Use `examples/path_requests_run.py <examples/path_requests_run.py>`_ to run multiple optimizations as follows:
.. code-block:: shell-session .. code-block:: shell
$ cd $(gnpy-example-data) $ python path_requests_run.py -h
$ gnpy-path-request -o output_file.json \ Usage: path_requests_run.py [-h] [-v] [-o OUTPUT] [network_filename] [service_filename] [eqpt_filename]
meshTopologyExampleV2.xls meshTopologyExampleV2_services.json
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 ``network_filename`` and ``service_filename`` can be an XLS or JSON file. The ``eqpt_filename`` must be a JSON file.
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`.
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. To see an example of it, run:
As a result transponder type is not part of the network info. it is related to the list of services requests.
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. .. code-block:: shell
REST API (experimental) $ cd examples
----------------------- $ python path_requests_run.py meshTopologyExampleV2.xls meshTopologyExampleV2_services.json eqpt_config.json -o output_file.json
``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 This program requires a list of connections to be estimated and the equipment
library. The program computes performances for the list of services (accepts
$ gnpy-rest JSON or Excel format) using the same spectrum propagation modules as
``transmission_main_example.py``. Explanation on the Excel template is provided in
.. code-block:: shell-session the `Excel_userguide.rst <Excel_userguide.rst#service-sheet>`_. Template for
the JSON format can be found here: `service-template.json
$ docker run -p 8080:8080 -it emmanuelledelfour/gnpy-experimental:candi-1.0 gnpy-rest <service-template.json>`_.
When starting the api server will aks for an encryption/decryption key. This key i used to encrypt equipment file when using /api/v1/equipments endpoint.
This key is a Fernet key and can be generated this way:
.. code-block:: python
from cryptography.fernet import Fernet
Fernet.generate_key()
After typing the key, you can detach the container by typing ^P^Q.
After starting the api server, you can launch a request
.. code-block:: shell-session
$ curl -v -X POST -H "Content-Type: application/json" -d @<PATH_TO_JSON_REQUEST_FILE> https://localhost:8080/api/v1/path-computation -k
TODO: api documentation, unit tests, real WSGI server with trusted certificates
Contributing Contributing
------------ ------------
@@ -214,14 +591,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 source simulation model which can be used freely across multiple vendor
implementations. implementations.
.. |docs| image:: https://readthedocs.org/projects/gnpy/badge/?version=master .. |docs| image:: https://readthedocs.org/projects/gnpy/badge/?version=develop
:target: http://gnpy.readthedocs.io/en/master/?badge=master :target: http://gnpy.readthedocs.io/en/develop/?badge=develop
:alt: Documentation Status :alt: Documentation Status
:scale: 100% :scale: 100%
.. |travis| image:: https://travis-ci.com/Telecominfraproject/oopt-gnpy.svg?branch=master .. |build| image:: https://travis-ci.com/Telecominfraproject/oopt-gnpy.svg?branch=develop
:target: https://travis-ci.com/Telecominfraproject/oopt-gnpy :target: https://travis-ci.com/Telecominfraproject/oopt-gnpy
:alt: Build Status via Travis CI :alt: Build Status
:scale: 100% :scale: 100%
.. |doi| image:: https://zenodo.org/badge/96894149.svg .. |doi| image:: https://zenodo.org/badge/96894149.svg
@@ -229,21 +606,6 @@ implementations.
:alt: DOI :alt: DOI
:scale: 100% :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 TIP OOPT/PSE & PSE WG Charter
----------------------------- -----------------------------

View File

@@ -32,9 +32,7 @@ sys.path.insert(0, os.path.abspath('../'))
# ones. # ones.
extensions = ['sphinx.ext.autodoc', extensions = ['sphinx.ext.autodoc',
'sphinx.ext.mathjax', 'sphinx.ext.mathjax',
'sphinx.ext.githubpages', 'sphinx.ext.githubpages','sphinxcontrib.bibtex']
'sphinxcontrib.bibtex',
'pbr.sphinxext',]
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates'] templates_path = ['_templates']
@@ -53,6 +51,15 @@ project = 'gnpy'
copyright = '2018, Telecom InfraProject - OOPT PSE Group' copyright = '2018, Telecom InfraProject - OOPT PSE Group'
author = '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 # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.
# #
@@ -80,17 +87,8 @@ todo_include_todos = False
on_rtd = os.environ.get('READTHEDOCS') == 'True' on_rtd = os.environ.get('READTHEDOCS') == 'True'
if on_rtd: if on_rtd:
html_theme = 'default' html_theme = 'default'
html_theme_options = {
'logo_only': True,
}
else: else:
html_theme = 'alabaster' 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 # 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 # further. For a list of options available for each theme, see the
@@ -101,7 +99,7 @@ html_logo = 'images/GNPy-logo.png'
# Add any paths that contain custom static files (such as style sheets) here, # 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, # relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css". # so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = [] html_static_path = ['_static']
# Custom sidebar templates, must be a dictionary that maps document names # Custom sidebar templates, must be a dictionary that maps document names
# to template names. # to template names.
@@ -175,9 +173,4 @@ texinfo_documents = [
'Miscellaneous'), 'Miscellaneous'),
] ]
autodoc_default_options = { autodoc_default_flags = ['members', 'undoc-members', 'private-members', 'show-inheritance']
'members': True,
'undoc-members': True,
'private-members': True,
'show-inheritance': True,
}

View File

@@ -1,13 +0,0 @@
``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

View File

@@ -1,9 +0,0 @@
``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

View File

@@ -1,6 +0,0 @@
``gnpy.topology``
-----------------
.. automodule:: gnpy.topology
.. automodule:: gnpy.topology.request
.. automodule:: gnpy.topology.spectrum_assignment

View File

@@ -1,14 +0,0 @@
***************************
API Reference Documentation
***************************
``gnpy`` package
================
.. automodule:: gnpy
.. toctree::
gnpy-api-core
gnpy-api-topology
gnpy-api-tools

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -1,18 +1,33 @@
GNPy: Optical Route Planning Library .. 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 <http://github.com/telecominfraproject/gnpy>`_ is an open-source, Welcome to gnpy's documentation!
community-developed library for building route planning and optimization tools ================================
in real-world mesh optical networks. It is based on the Gaussian Noise Model.
**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.
.. toctree:: .. toctree::
:maxdepth: 4 :maxdepth: 2
install
json
excel
model model
gnpy-api
Indices and tables Indices and tables
================== ==================
@@ -21,3 +36,67 @@ Indices and tables
* :ref:`modindex` * :ref:`modindex`
* :ref:`search` * :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.

View File

@@ -1,111 +0,0 @@
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

View File

@@ -1,339 +0,0 @@
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. |
+----------------------+-----------+-------------------------------------------+

View File

@@ -1,5 +1,5 @@
Physical Model used in GNPy The QoT estimation in the PSE framework of TIP-OOPT
=========================== =======================================================
QoT-E including ASE noise and NLI accumulation QoT-E including ASE noise and NLI accumulation
---------------------------------------------- ----------------------------------------------

94
docs/source/gnpy.core.rst Normal file
View File

@@ -0,0 +1,94 @@
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

14
docs/source/gnpy.rst Normal file
View File

@@ -0,0 +1,14 @@
gnpy package
============
Subpackages
-----------
.. toctree::
gnpy.core
Module contents
---------------
.. automodule:: gnpy

7
docs/source/modules.rst Normal file
View File

@@ -0,0 +1,7 @@
gnpy
====
.. toctree::
:maxdepth: 4
gnpy

View File

@@ -0,0 +1,124 @@
{ "Edfa":[
{
"type_variety": "fixed27",
"type_def": "fixed_gain",
"gain_flatmax": 27,
"gain_min": 27,
"p_max": 21,
"nf0": 5.5,
"allowed_for_design": false
},
{
"type_variety": "fixed22",
"type_def": "fixed_gain",
"gain_flatmax": 22,
"gain_min": 22,
"p_max": 21,
"nf0": 5.5,
"allowed_for_design": false
}
],
"Fiber":[{
"type_variety": "SSMF",
"dispersion": 1.67e-05,
"gamma": 0.00127
},
{
"type_variety": "NZDF",
"dispersion": 0.5e-05,
"gamma": 0.00146
},
{
"type_variety": "LOF",
"dispersion": 2.2e-05,
"gamma": 0.000843
}
],
"Span":[{
"power_mode": false,
"delta_power_range_db": [-2,3,0.5],
"max_fiber_lineic_loss_for_raman": 0.25,
"target_extended_gain": 2.5,
"max_length": 150,
"length_units": "km",
"max_loss": 28,
"padding": 10,
"EOL": 0,
"con_in": 0,
"con_out": 0
}
],
"Roadm":[{
"target_pch_out_db": -25,
"add_drop_osnr": 30.00,
"restrictions": {
"preamp_variety_list":[],
"booster_variety_list":[]
}
}],
"SI":[{
"f_min": 191.6e12,
"baud_rate": 32e9,
"f_max":195.1e12,
"spacing": 50e9,
"power_dbm": 0,
"power_range_db": [0,0,1],
"roll_off": 0.15,
"tx_osnr": 40,
"sys_margins": 2
}],
"Transceiver":[
{
"type_variety": "Cassini",
"frequency":{
"min": 191.35e12,
"max": 196.1e12
},
"mode":[
{
"format": "dp-qpsk",
"baud_rate": 32e9,
"OSNR": 11,
"bit_rate": 100e9,
"roll_off": 0.15,
"tx_osnr": 40,
"min_spacing": 37.5e9,
"cost":1
},
{
"format": "16-qam",
"baud_rate": 66e9,
"OSNR": 15,
"bit_rate": 200e9,
"roll_off": 0.15,
"tx_osnr": 40,
"min_spacing": 75e9,
"cost":1
}
]
},
{
"type_variety": "Voyager",
"frequency":{
"min": 191.35e12,
"max": 196.1e12
},
"mode":[
{
"format": "mode 1",
"baud_rate": 32e9,
"OSNR": 12,
"bit_rate": 100e9,
"roll_off": 0.15,
"tx_osnr": 40,
"min_spacing": 37.5e9,
"cost":1
}
]
}
]
}

View File

@@ -0,0 +1,67 @@
{
"path-request": [
{
"request-id": "first",
"source": "netconf:10.0.254.93:830",
"destination": "netconf:10.0.254.94:830",
"src-tp-id": "trx-Amsterdam",
"dst-tp-id": "trx-Bremen",
"bidirectional": true,
"path-constraints": {
"te-bandwidth": {
"technology": "flexi-grid",
"trx_type": "Cassini",
"trx_mode": null,
"effective-freq-slot": [
{
"N": "null",
"M": "null"
}
],
"spacing": 50000000000.0,
"max-nb-of-channel": null,
"output-power": null,
"path_bandwidth": 100000000000.0
}
}
},
{
"request-id": "second",
"source": "netconf:10.0.254.93:830",
"destination": "netconf:10.0.254.94:830",
"src-tp-id": "trx-Amsterdam",
"dst-tp-id": "trx-Bremen",
"bidirectional": true,
"path-constraints": {
"te-bandwidth": {
"technology": "flexi-grid",
"trx_type": "Cassini",
"trx_mode": null,
"effective-freq-slot": [
{
"N": "null",
"M": "null"
}
],
"spacing": 50000000000.0,
"max-nb-of-channel": null,
"output-power": null,
"path_bandwidth": 100000000000.0
}
}
}
],
"synchronization": [
{
"synchronization-id": "some redundancy please",
"svec": {
"relaxable": "false",
"disjointness": "node link",
"request-id-number": [
"first",
"second"
]
}
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,179 @@
# How many nodes in the ring topology? Up to eight is supported, then I ran out of cities..
HOW_MANY = 3
# city names
ALL_CITIES = [
'Amsterdam',
'Bremen',
'Cologne',
'Dueseldorf',
'Eindhoven',
'Frankfurt',
'Ghent',
'Hague',
]
# end of configurable parameters
J = {
"elements": [],
"connections": [],
}
def unidir_join(a, b):
global J
J["connections"].append(
{"from_node": a, "to_node": b}
)
def mk_edfa(name, gain, voa=0.0):
global J
J["elements"].append(
{"uid": name, "type": "Edfa", "type_variety": f"fixed{gain}", "operational": {"gain_target": gain, "out_voa": voa}}
)
def add_att(a, b, att):
global J
if att > 0:
uid = f"att-({a})-({b})"
else:
uid = f"splice-({a})-({b})"
J["elements"].append(
{"uid": uid, "type": "Fused", "params": {"loss": att}},
)
unidir_join(a, uid)
unidir_join(uid, b)
return uid
def build_fiber(city1, city2):
global J
J["elements"].append(
{
"uid": f"fiber-{city1}-{city2}",
"type": "Fiber",
"type_variety": "SSMF",
"params": {
"length": 50,
"length_units": "km",
"loss_coef": 0.2,
"con_in": 1.5,
"con_out": 1.5,
}
}
)
def unidir_patch(a, b):
global J
uid = f"patch-({a})-({b})"
J["elements"].append(
{
"uid": uid,
"type": "Fiber",
"type_variety": "SSMF",
"params": {
"length": 0,
"length_units": "km",
"loss_coef": 0.2,
"con_in": 0.5,
"con_out": 0.5,
}
}
)
add_att(a, uid, 0.0)
add_att(uid, b, 0.0)
for CITY in (ALL_CITIES[x] for x in range(0, HOW_MANY)):
J["elements"].append(
{"uid": f"trx-{CITY}", "type": "Transceiver"}
)
target_pwr = [
{"to_node": f"trx-{CITY}", "target_pch_out_db": -25},
{"to_node": f"splice-(roadm-{CITY}-AD)-(patch-(roadm-{CITY}-AD)-(roadm-{CITY}-L1))", "target_pch_out_db": -12},
{"to_node": f"splice-(roadm-{CITY}-AD)-(patch-(roadm-{CITY}-AD)-(roadm-{CITY}-L2))", "target_pch_out_db": -12},
]
J["elements"].append(
{"uid": f"roadm-{CITY}-AD", "type": "Roadm", "params": {"target_pch_out_db": -2.0, "per_degree_target_pch_out_db": target_pwr}}
)
unidir_join(f"trx-{CITY}", f"roadm-{CITY}-AD")
unidir_join(f"roadm-{CITY}-AD", f"trx-{CITY}")
for n in (1,2):
target_pwr = [
{"to_node": f"roadm-{CITY}-L{n}-booster", "target_pch_out_db": -23},
{"to_node": f"splice-(roadm-{CITY}-L{n})-(patch-(roadm-{CITY}-L{n})-(roadm-{CITY}-AD))", "target_pch_out_db": -12},
]
for m in (1,2):
if m == n:
continue
target_pwr.append(
{"to_node": f"splice-(roadm-{CITY}-L{n})-(patch-(roadm-{CITY}-L{n})-(roadm-{CITY}-L{m}))", "target_pch_out_db": -12},
)
J["elements"].append(
{"uid": f"roadm-{CITY}-L{n}", "type": "Roadm", "params": {"target_pch_out_db": -23.0, "per_degree_target_pch_out_db": target_pwr}}
)
mk_edfa(f"roadm-{CITY}-L{n}-booster", 22)
mk_edfa(f"roadm-{CITY}-L{n}-preamp", 27)
unidir_join(f"roadm-{CITY}-L{n}", f"roadm-{CITY}-L{n}-booster")
unidir_join(f"roadm-{CITY}-L{n}-preamp", f"roadm-{CITY}-L{n}")
unidir_patch(f"roadm-{CITY}-AD", f"roadm-{CITY}-L{n}")
unidir_patch(f"roadm-{CITY}-L{n}", f"roadm-{CITY}-AD")
for m in (1,2):
if m == n:
continue
#add_att(f"roadm-{CITY}-L{n}", f"roadm-{CITY}-L{m}", 22)
unidir_patch(f"roadm-{CITY}-L{n}", f"roadm-{CITY}-L{m}")
for city1, city2 in ((ALL_CITIES[i], ALL_CITIES[i + 1] if i < HOW_MANY - 1 else ALL_CITIES[0]) for i in range(0, HOW_MANY)):
build_fiber(city1, city2)
unidir_join(f"roadm-{city1}-L1-booster", f"fiber-{city1}-{city2}")
unidir_join(f"fiber-{city1}-{city2}", f"roadm-{city2}-L2-preamp")
build_fiber(city2, city1)
unidir_join(f"roadm-{city2}-L2-booster", f"fiber-{city2}-{city1}")
unidir_join(f"fiber-{city2}-{city1}", f"roadm-{city1}-L1-preamp")
for _, E in enumerate(J["elements"]):
uid = E["uid"]
if uid.startswith("roadm-") and (uid.endswith("-L1-booster") or uid.endswith("-L2-booster")):
E["operational"]["out_voa"] = 12.0
#if uid.endswith("-AD-add"):
# E["operational"]["out_voa"] = 21
translate = {
#"trx-Amsterdam": "10.0.254.93",
#"trx-Bremen": "10.0.254.94",
"trx-Amsterdam": "10.0.254.76",
"trx-Bremen": "10.0.254.77",
# Amsterdam A/D: coherent-v9u
"roadm-Amsterdam-AD": "10.0.254.107",
# Bremen A/D: -spi
"roadm-Bremen-AD": "10.0.254.225",
# Amsterdam -> Bremen ...QR79
"roadm-Amsterdam-L1": "10.0.254.78",
# Bremen -> Amsterdam ...QCP9
"roadm-Bremen-L2": "10.0.254.102",
# Bremen -> Cologne ...WKP
"roadm-Bremen-L1": "10.0.254.100",
# Cologne -> Bremen ...QLK6
"roadm-Cologne-L2": "10.0.254.104",
# Cologne -> Amsterdam ...TQQ
"roadm-Cologne-L1": "10.0.254.99",
# Amsterdam -> Cologne ...Q7JS
"roadm-Amsterdam-L2": "10.0.254.79",
# spare Line/Degree ...QC8B
"spare-line-degree": "10.0.254.101",
# spare Add/Drop: ...NNN
"spare-add-drop": "10.0.254.228",
}
import json
s = json.dumps(J, indent=2)
for (old, new) in translate.items():
s = s.replace(f'"{old}"', f'"netconf:{new}:830"')
print(s)

0
examples/__init__.py Normal file
View File

View File

@@ -11,22 +11,20 @@ If not present in the "Nodes" sheet, the "Type" column will be implicitly
determined based on the topology. determined based on the topology.
""" """
try:
from xlrd import open_workbook from xlrd import open_workbook
except ModuleNotFoundError:
exit('Required: `pip install xlrd`')
from argparse import ArgumentParser from argparse import ArgumentParser
PARSER = ArgumentParser() PARSER = ArgumentParser()
PARSER.add_argument('workbook', nargs='?', default='meshTopologyExampleV2.xls', PARSER.add_argument('workbook', nargs='?', default='meshTopologyExampleV2.xls',
help='create the mandatory columns in Eqpt sheet') 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: class Node:
""" Node element contains uid, list of connected nodes and eqpt type """ Node element contains uid, list of connected nodes and eqpt type
""" """
def __init__(self, uid, to_node): def __init__(self, uid, to_node):
self.uid = uid self.uid = uid
self.to_node = to_node self.to_node = to_node
@@ -38,7 +36,6 @@ class Node:
def __str__(self): def __str__(self):
return f'uid {self.uid} \nto_node {[node for node in self.to_node]}\neqpt {self.eqpt}\n' return f'uid {self.uid} \nto_node {[node for node in self.to_node]}\neqpt {self.eqpt}\n'
def read_excel(input_filename): def read_excel(input_filename):
""" read excel Nodes and Links sheets and create a dict of nodes with """ read excel Nodes and Links sheets and create a dict of nodes with
their to_nodes and type of eqpt their to_nodes and type of eqpt
@@ -76,7 +73,6 @@ def read_excel(input_filename):
exit() exit()
return nodes return nodes
def create_eqt_template(nodes, input_filename): def create_eqt_template(nodes, input_filename):
""" writes list of node A node Z corresponding to Nodes and Links sheets in order """ writes list of node A node Z corresponding to Nodes and Links sheets in order
to help user populating Eqpt to help user populating Eqpt
@@ -89,6 +85,7 @@ def create_eqt_template(nodes, input_filename):
\nNode A \tNode Z \tamp type \tatt_in \tamp gain \ttilt \tatt_out\ \nNode A \tNode Z \tamp type \tatt_in \tamp gain \ttilt \tatt_out\
amp type \tatt_in \tamp gain \ttilt \tatt_out\n') amp type \tatt_in \tamp gain \ttilt \tatt_out\n')
for node in nodes.values(): for node in nodes.values():
if node.eqpt == 'ILA': if node.eqpt == 'ILA':
my_file.write(f'{node.uid}\t{node.to_node[0]}\n') my_file.write(f'{node.uid}\t{node.to_node[0]}\n')
@@ -96,8 +93,8 @@ def create_eqt_template(nodes, input_filename):
for to_node in node.to_node: for to_node in node.to_node:
my_file.write(f'{node.uid}\t{to_node}\n') 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__': if __name__ == '__main__':
ARGS = PARSER.parse_args() ARGS = PARSER.parse_args()

View File

@@ -1,8 +1,198 @@
{ {
"nf_ripple": [ "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 0.0
], ],
"gain_ripple": [ "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 0.0
], ],
"dgt": [ "dgt": [

1033
examples/demo.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ Amplifier models and configuration
Equipment description defines equipment types and parameters. Equipment description defines equipment types and parameters.
It takes place in the default **eqpt_config.json** file. It takes place in the default **eqpt_config.json** file.
By default **gnpy-transmission-example** uses **eqpt_config.json** file and that By default **transmission_main_example.py** uses **eqpt_config.json** file and that
can be changed with **-e** or **--equipment** command line parameter. can be changed with **-e** or **--equipment** command line parameter.
2. Amplifier parameters and subtypes 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 4. advanced_config_from_json
####################################### #######################################
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: 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:
Update an existing json file with all the 96ch txt files for a given amplifier type 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 amplifier type 'OA_type1' is hard coded but can be modified and other types added

View File

@@ -13,6 +13,7 @@ import re
import sys import sys
import json import json
import numpy as np import numpy as np
from gnpy.core.utils import lin2db, db2lin
"""amplifier file names """amplifier file names
convert a set of amplifier files + input json definiton file into a valid edfa_json_file: convert a set of amplifier files + input json definiton file into a valid edfa_json_file:
@@ -40,7 +41,6 @@ gain_ripple_field = "gain_ripple"
nf_ripple_field = "nf_ripple" nf_ripple_field = "nf_ripple"
nf_fit_coeff = "nf_fit_coeff" nf_fit_coeff = "nf_fit_coeff"
def read_file(field, file_name): def read_file(field, file_name):
"""read and format the 96 channels txt files describing the amplifier NF and ripple """read and format the 96 channels txt files describing the amplifier NF and ripple
convert dfg into gain ripple by removing the mean component convert dfg into gain ripple by removing the mean component
@@ -63,7 +63,6 @@ def read_file(field, file_name):
data = data.tolist() data = data.tolist()
return data return data
def input_json(path): def input_json(path):
"""read the json input file and add all the 96 channels txt files """read the json input file and add all the 96 channels txt files
create the output json file with output_json_file_name""" create the output json file with output_json_file_name"""
@@ -80,7 +79,6 @@ def input_json(path):
with open(output_json_file_name,'w') as edfa_json_file: with open(output_json_file_name,'w') as edfa_json_file:
edfa_json_file.write(amp_text) edfa_json_file.write(amp_text)
if __name__ == '__main__': if __name__ == '__main__':
if len(sys.argv) == 2: if len(sys.argv) == 2:
path = sys.argv[1] path = sys.argv[1]

View File

@@ -146,27 +146,23 @@
"Fiber":[{ "Fiber":[{
"type_variety": "SSMF", "type_variety": "SSMF",
"dispersion": 1.67e-05, "dispersion": 1.67e-05,
"gamma": 0.00127, "gamma": 0.00127
"pmd_coef": 1.265e-15
}, },
{ {
"type_variety": "NZDF", "type_variety": "NZDF",
"dispersion": 0.5e-05, "dispersion": 0.5e-05,
"gamma": 0.00146, "gamma": 0.00146
"pmd_coef": 1.265e-15
}, },
{ {
"type_variety": "LOF", "type_variety": "LOF",
"dispersion": 2.2e-05, "dispersion": 2.2e-05,
"gamma": 0.000843, "gamma": 0.000843
"pmd_coef": 1.265e-15
} }
], ],
"RamanFiber":[{ "RamanFiber":[{
"type_variety": "SSMF", "type_variety": "SSMF",
"dispersion": 1.67e-05, "dispersion": 1.67e-05,
"gamma": 0.00127, "gamma": 0.00127,
"pmd_coef": 1.265e-15,
"raman_efficiency": { "raman_efficiency": {
"cr":[ "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, 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,
@@ -210,7 +206,6 @@
"Roadm":[{ "Roadm":[{
"target_pch_out_db": -20, "target_pch_out_db": -20,
"add_drop_osnr": 38, "add_drop_osnr": 38,
"pmd": 0,
"restrictions": { "restrictions": {
"preamp_variety_list":[], "preamp_variety_list":[],
"booster_variety_list":[] "booster_variety_list":[]

View File

@@ -643,6 +643,44 @@
"out_voa": null "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", "uid": "east edfa in Lannion_CAS to Stbrieuc",
"metadata": { "metadata": {
@@ -795,6 +833,44 @@
"out_voa": null "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", "uid": "west edfa in Lorient_KMA to Vannes_KBE",
"metadata": { "metadata": {

View File

@@ -14,8 +14,8 @@
"trx_mode": null, "trx_mode": null,
"effective-freq-slot": [ "effective-freq-slot": [
{ {
"N": null, "N": "null",
"M": null "M": "null"
} }
], ],
"spacing": 50000000000.0, "spacing": 50000000000.0,
@@ -39,8 +39,8 @@
"trx_mode": "mode 1", "trx_mode": "mode 1",
"effective-freq-slot": [ "effective-freq-slot": [
{ {
"N": null, "N": "null",
"M": null "M": "null"
} }
], ],
"spacing": 50000000000.0, "spacing": 50000000000.0,
@@ -104,8 +104,8 @@
"trx_mode": "mode 1", "trx_mode": "mode 1",
"effective-freq-slot": [ "effective-freq-slot": [
{ {
"N": null, "N": "null",
"M": null "M": "null"
} }
], ],
"spacing": 50000000000.0, "spacing": 50000000000.0,
@@ -129,8 +129,8 @@
"trx_mode": null, "trx_mode": null,
"effective-freq-slot": [ "effective-freq-slot": [
{ {
"N": null, "N": "null",
"M": null "M": "null"
} }
], ],
"spacing": 75000000000.0, "spacing": 75000000000.0,
@@ -154,8 +154,8 @@
"trx_mode": "mode 2", "trx_mode": "mode 2",
"effective-freq-slot": [ "effective-freq-slot": [
{ {
"N": null, "N": "null",
"M": null "M": "null"
} }
], ],
"spacing": 75000000000.0, "spacing": 75000000000.0,
@@ -179,8 +179,8 @@
"trx_mode": "mode 1", "trx_mode": "mode 1",
"effective-freq-slot": [ "effective-freq-slot": [
{ {
"N": null, "N": "null",
"M": null "M": "null"
} }
], ],
"spacing": 50000000000.0, "spacing": 50000000000.0,
@@ -204,8 +204,8 @@
"trx_mode": "mode 1", "trx_mode": "mode 1",
"effective-freq-slot": [ "effective-freq-slot": [
{ {
"N": null, "N": "null",
"M": null "M": "null"
} }
], ],
"spacing": 50000000000.0, "spacing": 50000000000.0,
@@ -229,8 +229,8 @@
"trx_mode": "mode 1", "trx_mode": "mode 1",
"effective-freq-slot": [ "effective-freq-slot": [
{ {
"N": null, "N": "null",
"M": null "M": "null"
} }
], ],
"spacing": 75000000000.0, "spacing": 75000000000.0,

569
examples/path_requests_run.py Executable file
View File

@@ -0,0 +1,569 @@
#!/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 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, network_from_json
from gnpy.core.equipment import load_equipment, trx_mode_params, automatic_nch
from gnpy.core.elements import Transceiver, Roadm
from gnpy.core.utils import db2lin, lin2db
from gnpy.core.request import (Path_request, Result_element,
propagate, jsontocsv, Disjunction, compute_path_dsjctn,
requests_aggregation, propagate_and_optimize_mode,
BLOCKING_NOPATH, BLOCKING_NOMODE,
find_reversed_path)
from gnpy.core.exceptions import (ConfigurationError, EquipmentConfigError, NetworkTopologyError,
ServiceError, DisjunctionError)
import gnpy.core.ansi_escapes as ansi_escapes
from gnpy.core.spectrum_assignment import (build_oms_list, pth_assign_spectrum)
from copy import copy, deepcopy
from textwrap import dedent
from math import ceil
from flask import Flask, jsonify, make_response, request
from flask_restful import Api, Resource, reqparse, fields
#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',\
help='input topology file in xls or json')
PARSER.add_argument('service_filename', nargs='?', type=Path,\
default=Path(__file__).parent / 'meshTopologyExampleV2.xls',\
help='input service file in xls or json')
PARSER.add_argument('eqpt_filename', nargs='?', type=Path,\
default=Path(__file__).parent / 'eqpt_config.json',\
help='input equipment library in json. Default is eqpt_config.json')
PARSER.add_argument('-bi', '--bidir', action='store_true',\
help='considers that all demands are bidir')
PARSER.add_argument('-v', '--verbose', action='count', default=0,\
help='increases verbosity for each occurence')
PARSER.add_argument('-o', '--output', type=Path)
PARSER.add_argument('-r', '--rest', action='count', default=0, help='use the REST API')
NETWORK_FILENAME = 'topoDemov1.json' #'disagregatedTopoDemov1.json' #
APP = Flask(__name__, static_url_path="")
API = Api(APP)
def requests_from_json(json_data, equipment):
""" converts the json data into a list of requests elements
"""
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']
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):
""" 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:
#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 ' +\
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 = 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)
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 load_requests(filename, eqpt_filename, bidir):
""" loads the requests from a json or an excel file into a data string
"""
if filename.suffix.lower() == '.xls':
LOGGER.info('Automatically converting requests from XLS to JSON')
try:
json_data = convert_service_sheet(filename, eqpt_filename, bidir=bidir)
except ServiceError as this_e:
print(f'{ansi_escapes.red}Service error:{ansi_escapes.reset} {this_e}')
exit(1)
else:
with open(filename, encoding='utf-8') as my_f:
json_data = loads(my_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 based on request_id
TODO change all these req, dsjct, res lists into dict !
"""
path_res_list = []
reversed_path_res_list = []
propagated_reversed_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}')
# adding first node to be clearer on the output
print(f'with path constraint: {[pathreq.source] + pathreq.nodes_list}')
# pathlist[i] contains the whole path information for request i
# last element is a transciver and where the result of the propagation is
# recorded.
# 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 simulation. This is why
# we use deepcopy: to ensure that each propagation is recorded and not overwritten
total_path = deepcopy(pathlist[i])
print(f'Computed path (roadms):{[e.uid for e in total_path if isinstance(e, Roadm)]}')
# for debug
# print(f'{pathreq.baud_rate} {pathreq.power} {pathreq.spacing} {pathreq.nb_channel}')
if total_path:
if pathreq.baud_rate is not None:
# means that at this point the mode was entered/forced by user and thus a
# baud_rate was defined
total_path = propagate(total_path, pathreq, equipment)
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' +\
f' {pathreq.source} to {pathreq.destination} does not pass with' +\
f' {pathreq.tsp_mode}\n\tcomputedSNR in 0.1nm = {temp_snr01nm} ' +\
f'- required osnr {pathreq.OSNR}'
print(msg)
LOGGER.warning(msg)
pathreq.blocking_reason = 'MODE_NOT_FEASIBLE'
else:
total_path, mode = propagate_and_optimize_mode(total_path, pathreq, equipment)
# if no baudrate satisfies spacing, no mode is returned and the last explored mode
# a warning is shown in the propagate_and_optimize_mode
# propagate_and_optimize_mode function returns the mode with the highest bitrate
# that passes. if no mode passes, then a attribute blocking_reason is added on
# pathreq that contains the reason for blocking: 'NO_PATH', 'NO_FEASIBLE_MODE', ...
try:
if pathreq.blocking_reason in BLOCKING_NOPATH:
total_path = []
elif pathreq.blocking_reason in BLOCKING_NOMODE:
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']
# other blocking reason should not appear at this point
except AttributeError:
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']
# reversed path is needed for correct spectrum assignment
reversed_path = find_reversed_path(pathlist[i])
if pathreq.bidir:
# only propagate if bidir is true, but needs the reversed path anyway for
# correct spectrum assignment
rev_p = deepcopy(reversed_path)
print(f'\n\tPropagating Z to A direction {pathreq.destination} to {pathreq.source}')
print(f'\tPath (roadsm) {[r.uid for r in rev_p if isinstance(r,Roadm)]}\n')
propagated_reversed_path = propagate(rev_p, pathreq, equipment)
temp_snr01nm = round(mean(propagated_reversed_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' +\
f' {pathreq.source} to {pathreq.destination} does not pass with' +\
f' {pathreq.tsp_mode}\n' +\
f'\tcomputedSNR in 0.1nm = {temp_snr01nm} - required osnr {pathreq.OSNR}'
print(msg)
LOGGER.warning(msg)
# TODO selection of mode should also be on reversed direction !!
pathreq.blocking_reason = 'MODE_NOT_FEASIBLE'
else:
propagated_reversed_path = []
else:
msg = 'Total path is empty. No propagation'
print(msg)
LOGGER.info(msg)
reversed_path = []
propagated_reversed_path = []
path_res_list.append(total_path)
reversed_path_res_list.append(reversed_path)
propagated_reversed_path_res_list.append(propagated_reversed_path)
# print to have a nice output
print('')
return path_res_list, reversed_path_res_list, propagated_reversed_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
# 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:
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}\',' +\
f' 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.' +\
f' 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' +\
f' transponder source: {pathreq.source}.'+'\x1b[0m'
LOGGER.critical(msg)
print(f'{msg}\nComputation stopped.')
raise ServiceError(msg)
if pathreq.destination not in transponders:
msg = f'\x1b[1;31;40m'+f'Request: {pathreq.request_id}: could not find' +\
f' transponder destination: {pathreq.destination}.'+'\x1b[0m'
LOGGER.critical(msg)
print(f'{msg}\nComputation stopped.')
raise ServiceError(msg)
# 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):
""" clean disjunctions to remove possible repetition
"""
local_disjn = disjn.copy()
for elem in local_disjn:
for dis_elem in local_disjn:
if set(elem.disjunctions_req) == set(dis_elem.disjunctions_req) and\
elem.disjunction_id != dis_elem.disjunction_id:
local_disjn.remove(dis_elem)
return local_disjn
def path_result_json(pathresult):
""" create the response dictionnary
"""
data = {
'response': [n.json for n in pathresult]
}
return data
def compute_requests(network, data, equipment):
""" Main program calling functions
"""
# 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)
oms_list = build_oms_list(network, equipment)
try:
rqs = requests_from_json(data, equipment)
except ServiceError as this_e:
print(f'{ansi_escapes.red}Service error:{ansi_escapes.reset} {this_e}')
raise this_e
# 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 ServiceError(msg)
try:
rqs = correct_route_list(network, rqs)
except ServiceError as this_e:
print(f'{ansi_escapes.red}Service error:{ansi_escapes.reset} {this_e}')
raise this_e
#exit(1)
# 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')
try:
pths = compute_path_dsjctn(network, equipment, rqs, dsjn)
except DisjunctionError as this_e:
print(f'{ansi_escapes.red}Disjunction error:{ansi_escapes.reset} {this_e}')
raise this_e
print('\x1b[1;34;40m' + f'Propagating on selected path' + '\x1b[0m')
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('\x1b[1;34;40m'+f'Result summary'+ '\x1b[0m')
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('\x1b[1;33;40m'+f'Result summary shows mean SNR and OSNR (average over all channels)' +\
'\x1b[0m')
return propagatedpths, reversed_propagatedpths, rqs
def launch_cli(network, data, equipment):
""" Compute requests using network, data and equipment with client line interface
"""
propagatedpths, reversed_propagatedpths, rqs = compute_requests(network, data, equipment)
#Generate the output
if ARGS.output :
result = []
# assumes that list of rqs and list of propgatedpths have same order
for i, pth in enumerate(propagatedpths):
result.append(Result_element(rqs[i], pth, reversed_propagatedpths[i]))
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 fjson:
fjson.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')
class GnpyAPI(Resource):
""" Compute requests using network, data and equipment with rest api
"""
def get(self):
return {"ping": True}, 200
def post(self):
data = request.get_json()
equipment = load_equipment('examples/2019-demo-equipment.json')
topo_json = load_json('examples/2019-demo-topology.json')
network = network_from_json(topo_json, equipment)
try:
propagatedpths, reversed_propagatedpths, rqs = compute_requests(network, data, 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(Result_element(rqs[i], pth, reversed_propagatedpths[i]))
return {"result":path_result_json(result)}, 201
except ServiceError as this_e:
msg = f'Service error: {this_e}'
return {"result": msg}, 400
API.add_resource(GnpyAPI, '/gnpy-experimental')
def main(args):
""" main function that calls all functions
"""
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, args.bidir)
equipment = load_equipment(args.eqpt_filename)
network = load_network(args.network_filename, equipment)
except EquipmentConfigError as this_e:
print(f'{ansi_escapes.red}Configuration error in the equipment library:{ansi_escapes.reset} {this_e}')
exit(1)
except NetworkTopologyError as this_e:
print(f'{ansi_escapes.red}Invalid network definition:{ansi_escapes.reset} {this_e}')
exit(1)
except ConfigurationError as this_e:
print(f'{ansi_escapes.red}Configuration error:{ansi_escapes.reset} {this_e}')
exit(1)
except ServiceError as this_e:
print(f'{ansi_escapes.red}Service error:{ansi_escapes.reset} {this_e}')
exit(1)
# input_str = raw_input("How will you use your program: c:[cli] , a:[api] ?")
# print(input_str)
#
if ((args.rest == 1) and (args.output is None)):
print('you have chosen the rest mode')
APP.run(host='0.0.0.0', port=5000, debug=True)
elif ((args.rest > 1) or ((args.rest == 1) and (args.output is not None))):
print('command is not well formulated')
else:
launch_cli(network, data, equipment)
if __name__ == '__main__':
ARGS = PARSER.parse_args()
basicConfig(level={2: DEBUG, 1: INFO, 0: CRITICAL}.get(ARGS.verbose, DEBUG))
main(ARGS)

180
examples/serviceDemov1.json Normal file
View File

@@ -0,0 +1,180 @@
{
"path-request": [
{
"request-id": "0",
"source": "trx site_a",
"destination": "trx site_b",
"src-tp-id": "trx site_a",
"dst-tp-id": "trx site_b",
"bidirectional": false,
"path-constraints": {
"te-bandwidth": {
"technology": "flexi-grid",
"trx_type": "Voyager",
"trx_mode": null,
"effective-freq-slot": [
{
"N": "null",
"M": "null"
}
],
"spacing": 50000000000.0,
"max-nb-of-channel": null,
"output-power": null,
"path_bandwidth": 100000000000.0
}
}
},
{
"request-id": "1",
"source": "trx site_a",
"destination": "trx site_b",
"src-tp-id": "trx site_a",
"dst-tp-id": "trx site_b",
"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": null,
"path_bandwidth": 200000000000.0
}
},
"explicit-route-objects": {
"route-object-include-exclude": [
{
"explicit-route-usage": "route-include-ero",
"index": 0,
"num-unnum-hop": {
"node-id": "Span1ab",
"link-tp-id": "link-tp-id is not used",
"hop-type": "STRICT"
}
}
]
}
},
{
"request-id": "2",
"source": "trx site_a",
"destination": "trx site_b",
"src-tp-id": "trx site_a",
"dst-tp-id": "trx site_b",
"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": null,
"path_bandwidth": 200000000000.0
}
},
"explicit-route-objects": {
"route-object-include-exclude": [
{
"explicit-route-usage": "route-include-ero",
"index": 0,
"num-unnum-hop": {
"node-id": "roadm site_c",
"link-tp-id": "link-tp-id is not used",
"hop-type": "STRICT"
}
}
]
}
},
{
"request-id": "3",
"source": "trx site_a",
"destination": "trx site_b",
"src-tp-id": "trx site_a",
"dst-tp-id": "trx site_b",
"bidirectional": false,
"path-constraints": {
"te-bandwidth": {
"technology": "flexi-grid",
"trx_type": "Voyager",
"trx_mode": null,
"effective-freq-slot": [
{
"N": "null",
"M": "null"
}
],
"spacing": 50000000000.0,
"max-nb-of-channel": null,
"output-power": null,
"path_bandwidth": 100000000000.0
}
}
},
{
"request-id": "4",
"source": "trx site_a",
"destination": "trx site_b",
"src-tp-id": "trx site_a",
"dst-tp-id": "trx site_b",
"bidirectional": false,
"path-constraints": {
"te-bandwidth": {
"technology": "flexi-grid",
"trx_type": "Voyager",
"trx_mode": null,
"effective-freq-slot": [
{
"N": "null",
"M": "null"
}
],
"spacing": 50000000000.0,
"max-nb-of-channel": null,
"output-power": null,
"path_bandwidth": 100000000000.0
}
}
}
],
"synchronization": [
{
"synchronization-id": "x",
"svec": {
"relaxable": "false",
"disjointness": "node link",
"request-id-number": [
"3",
"0"
]
}
},
{
"synchronization-id": "y",
"svec": {
"relaxable": "false",
"disjointness": "node link",
"request-id-number": [
"4",
"3",
"0"
]
}
}
]
}

View File

@@ -1,4 +1,5 @@
{ {
"raman_computed_channels": [1, 18, 37, 56, 75],
"raman_parameters": { "raman_parameters": {
"flag_raman": true, "flag_raman": true,
"space_resolution": 10e3, "space_resolution": 10e3,
@@ -8,7 +9,6 @@
"nli_method_name": "ggn_spectrally_separated", "nli_method_name": "ggn_spectrally_separated",
"wdm_grid_size": 50e9, "wdm_grid_size": 50e9,
"dispersion_tolerance": 1, "dispersion_tolerance": 1,
"phase_shift_tolerance": 0.1, "phase_shift_tollerance": 0.1
"computed_channels": [1, 18, 37, 56, 75]
} }
} }

703
examples/topoDemov1.json Normal file
View File

@@ -0,0 +1,703 @@
{
"elements": [
{
"uid": "trx site_a",
"type": "Transceiver",
"metadata": {
"location": {
"latitude": 0,
"longitude": 0,
"city": "Site a",
"region": ""
}
}
},
{
"uid": "roadm site_a",
"type": "Roadm",
"params": {
"target_pch_out_db": -20,
"restrictions": {
"preamp_variety_list": [],
"booster_variety_list": []
}
},
"metadata": {
"location": {
"latitude": 0,
"longitude": 0,
"city": "Site a",
"region": ""
}
}
},
{
"uid": "Span1ab",
"type": "Fiber",
"type_variety": "SSMF",
"params": {
"type_variety": "SSMF",
"length": 100.0,
"loss_coef": 0.2,
"length_units": "km",
"att_in": 0,
"con_in": 0.5,
"con_out": 0.5
},
"metadata": {
"location": {
"latitude": 1,
"longitude": 0,
"city": null,
"region": ""
}
}
},
{
"uid": "Span1ba",
"type": "Fiber",
"type_variety": "SSMF",
"params": {
"type_variety": "SSMF",
"length": 100.0,
"loss_coef": 0.2,
"length_units": "km",
"att_in": 0,
"con_in": 0.5,
"con_out": 0.5
},
"metadata": {
"location": {
"latitude": 1,
"longitude": 0,
"city": null,
"region": ""
}
}
},
{
"uid": "Span2ab",
"type": "Fiber",
"type_variety": "SSMF",
"params": {
"type_variety": "SSMF",
"length": 80.0,
"loss_coef": 0.2,
"length_units": "km",
"att_in": 0,
"con_in": 0.5,
"con_out": 0.5
},
"metadata": {
"location": {
"latitude": 1,
"longitude": 0,
"city": null,
"region": ""
}
}
},
{
"uid": "Span2ba",
"type": "Fiber",
"type_variety": "SSMF",
"params": {
"type_variety": "SSMF",
"length": 80.0,
"loss_coef": 0.2,
"length_units": "km",
"att_in": 0,
"con_in": 0.5,
"con_out": 0.5
},
"metadata": {
"location": {
"latitude": 1,
"longitude": 0,
"city": null,
"region": ""
}
}
},
{
"uid": "roadm site_b",
"type": "Roadm",
"params": {
"target_pch_out_db": -20,
"restrictions": {
"preamp_variety_list": [],
"booster_variety_list": []
}
},
"metadata": {
"location": {
"latitude": 0,
"longitude": 0,
"city": "Site b",
"region": ""
}
}
},
{
"uid": "trx site_b",
"type": "Transceiver",
"metadata": {
"location": {
"latitude": 2,
"longitude": 0,
"city": "Site b",
"region": ""
}
}
},
{
"uid": "booster1 site_a",
"type": "Edfa",
"type_variety": "std_medium_gain",
"operational": {
"gain_target": 19.0,
"delta_p": -1.0,
"tilt_target": 0,
"out_voa": 0
},
"metadata": {
"location": {
"latitude": 0.5,
"longitude": 0.0,
"city": "Site a",
"region": ""
}
}
},
{
"uid": "preamp site_b",
"type": "Edfa",
"type_variety": "std_low_gain",
"operational": {
"gain_target": 18.0,
"delta_p": 0,
"tilt_target": 0,
"out_voa": 0
},
"metadata": {
"location": {
"latitude": 0.5,
"longitude": 0.0,
"city": "Site b",
"region": ""
}
}
},
{
"uid": "booster1 site_b",
"type": "Edfa",
"type_variety": "std_medium_gain",
"operational": {
"gain_target": 19.0,
"delta_p": -1.0,
"tilt_target": 0,
"out_voa": 0
},
"metadata": {
"location": {
"latitude": 0.5,
"longitude": 0.0,
"city": "Site b",
"region": ""
}
}
},
{
"uid": "preamp1 site_a",
"type": "Edfa",
"type_variety": "std_low_gain",
"operational": {
"gain_target": 18.0,
"delta_p": 0,
"tilt_target": 0,
"out_voa": 0
},
"metadata": {
"location": {
"latitude": 0.5,
"longitude": 0.0,
"city": "Site_a",
"region": ""
}
}
},
{
"uid": "booster2 site_a",
"type": "Edfa",
"type_variety": "std_medium_gain",
"operational": {
"gain_target": 19.0,
"delta_p": -1.0,
"tilt_target": 0,
"out_voa": 0
},
"metadata": {
"location": {
"latitude": 0.5,
"longitude": 0.0,
"city": "Site a",
"region": ""
}
}
},
{
"uid": "preamp2 site_b",
"type": "Edfa",
"type_variety": "std_low_gain",
"operational": {
"gain_target": 18.0,
"delta_p": 0,
"tilt_target": 0,
"out_voa": 0
},
"metadata": {
"location": {
"latitude": 0.5,
"longitude": 0.0,
"city": "Site_b",
"region": ""
}
}
},
{
"uid": "booster2 site_b",
"type": "Edfa",
"type_variety": "std_medium_gain",
"operational": {
"gain_target": 19.0,
"delta_p": -1.0,
"tilt_target": 0,
"out_voa": 0
},
"metadata": {
"location": {
"latitude": 0.5,
"longitude": 0.0,
"city": "Site b",
"region": ""
}
}
},
{
"uid": "preamp2 site_a",
"type": "Edfa",
"type_variety": "std_low_gain",
"operational": {
"gain_target": 18.0,
"delta_p": 0,
"tilt_target": 0,
"out_voa": 0
},
"metadata": {
"location": {
"latitude": 0.5,
"longitude": 0.0,
"city": "Site_a",
"region": ""
}
}
},
{
"uid": "booster3 site_a",
"type": "Edfa",
"type_variety": "std_medium_gain",
"operational": {
"gain_target": 19.0,
"delta_p": -1.0,
"tilt_target": 0,
"out_voa": 0
},
"metadata": {
"location": {
"latitude": 0.5,
"longitude": 0.0,
"city": "Site a",
"region": ""
}
}
},
{
"uid": "preamp3 site_b",
"type": "Edfa",
"type_variety": "std_low_gain",
"operational": {
"gain_target": 18.0,
"delta_p": 0,
"tilt_target": 0,
"out_voa": 0
},
"metadata": {
"location": {
"latitude": 0.5,
"longitude": 0.0,
"city": "Site_b",
"region": ""
}
}
},
{
"uid": "booster3 site_b",
"type": "Edfa",
"type_variety": "std_medium_gain",
"operational": {
"gain_target": 19.0,
"delta_p": -1.0,
"tilt_target": 0,
"out_voa": 0
},
"metadata": {
"location": {
"latitude": 0.5,
"longitude": 0.0,
"city": "Site b",
"region": ""
}
}
},
{
"uid": "preamp3 site_a",
"type": "Edfa",
"type_variety": "std_low_gain",
"operational": {
"gain_target": 18.0,
"delta_p": 0,
"tilt_target": 0,
"out_voa": 0
},
"metadata": {
"location": {
"latitude": 0.5,
"longitude": 0.0,
"city": "Site_a",
"region": ""
}
}
},
{
"uid": "roadm site_c",
"type": "Roadm",
"params": {
"target_pch_out_db": -20,
"restrictions": {
"preamp_variety_list": [],
"booster_variety_list": []
}
},
"metadata": {
"location": {
"latitude": 0,
"longitude": 0,
"city": "Site c",
"region": ""
}
}
},
{
"uid": "booster1 site_c",
"type": "Edfa",
"type_variety": "std_medium_gain",
"operational": {
"gain_target": 19.0,
"delta_p": -1.0,
"tilt_target": 0,
"out_voa": 0
},
"metadata": {
"location": {
"latitude": 0.5,
"longitude": 0.0,
"city": "Site c",
"region": ""
}
}
},
{
"uid": "preamp1 site_c",
"type": "Edfa",
"type_variety": "std_low_gain",
"operational": {
"gain_target": 18.0,
"delta_p": 0,
"tilt_target": 0,
"out_voa": 0
},
"metadata": {
"location": {
"latitude": 0.5,
"longitude": 0.0,
"city": "Site_c",
"region": ""
}
}
},
{
"uid": "booster2 site_c",
"type": "Edfa",
"type_variety": "std_medium_gain",
"operational": {
"gain_target": 19.0,
"delta_p": -1.0,
"tilt_target": 0,
"out_voa": 0
},
"metadata": {
"location": {
"latitude": 0.5,
"longitude": 0.0,
"city": "Site c",
"region": ""
}
}
},
{
"uid": "preamp2 site_c",
"type": "Edfa",
"type_variety": "std_low_gain",
"operational": {
"gain_target": 18.0,
"delta_p": 0,
"tilt_target": 0,
"out_voa": 0
},
"metadata": {
"location": {
"latitude": 0.5,
"longitude": 0.0,
"city": "Site_c",
"region": ""
}
}
},
{
"uid": "Span1ac",
"type": "Fiber",
"type_variety": "SSMF",
"params": {
"type_variety": "SSMF",
"length": 80.0,
"loss_coef": 0.2,
"length_units": "km",
"att_in": 0,
"con_in": 0.5,
"con_out": 0.5
},
"metadata": {
"location": {
"latitude": 1,
"longitude": 0,
"city": null,
"region": ""
}
}
},
{
"uid": "Span1ca",
"type": "Fiber",
"type_variety": "SSMF",
"params": {
"type_variety": "SSMF",
"length": 80.0,
"loss_coef": 0.2,
"length_units": "km",
"att_in": 0,
"con_in": 0.5,
"con_out": 0.5
},
"metadata": {
"location": {
"latitude": 1,
"longitude": 0,
"city": null,
"region": ""
}
}
},
{
"uid": "Span1bc",
"type": "Fiber",
"type_variety": "SSMF",
"params": {
"type_variety": "SSMF",
"length": 80.0,
"loss_coef": 0.2,
"length_units": "km",
"att_in": 0,
"con_in": 0.5,
"con_out": 0.5
},
"metadata": {
"location": {
"latitude": 1,
"longitude": 0,
"city": null,
"region": ""
}
}
},
{
"uid": "Span1cb",
"type": "Fiber",
"type_variety": "SSMF",
"params": {
"type_variety": "SSMF",
"length": 80.0,
"loss_coef": 0.2,
"length_units": "km",
"att_in": 0,
"con_in": 0.5,
"con_out": 0.5
},
"metadata": {
"location": {
"latitude": 1,
"longitude": 0,
"city": null,
"region": ""
}
}
}
],
"connections": [
{
"from_node": "trx site_a",
"to_node": "roadm site_a"
},
{
"from_node": "roadm site_a",
"to_node": "booster1 site_a"
},
{
"from_node": "booster1 site_a",
"to_node": "Span1ab"
},
{
"from_node": "Span1ab",
"to_node": "preamp site_b"
},
{
"from_node": "preamp site_b",
"to_node": "roadm site_b"
},
{
"from_node": "roadm site_b",
"to_node": "trx site_b"
},
{
"from_node": "roadm site_a",
"to_node": "booster2 site_a"
},
{
"from_node": "booster2 site_a",
"to_node": "Span2ab"
},
{
"from_node": "Span2ab",
"to_node": "preamp2 site_b"
},
{
"from_node": "preamp2 site_b",
"to_node": "roadm site_b"
},
{
"from_node": "roadm site_b",
"to_node": "booster1 site_b"
},
{
"from_node": "booster1 site_b",
"to_node": "Span1ba"
},
{
"from_node": "Span1ba",
"to_node": "preamp1 site_a"
},
{
"from_node": "preamp1 site_a",
"to_node": "roadm site_a"
},
{
"from_node": "roadm site_b",
"to_node": "booster2 site_b"
},
{
"from_node": "booster2 site_b",
"to_node": "Span2ba"
},
{
"from_node": "Span2ba",
"to_node": "preamp2 site_a"
},
{
"from_node": "preamp2 site_a",
"to_node": "roadm site_a"
},
{
"from_node": "roadm site_a",
"to_node": "booster3 site_a"
},
{
"from_node": "booster3 site_a",
"to_node": "Span1ac"
},
{
"from_node": "Span1ac",
"to_node": "preamp1 site_c"
},
{
"from_node": "preamp1 site_c",
"to_node": "roadm site_c"
},
{
"from_node": "roadm site_c",
"to_node": "booster1 site_c"
},
{
"from_node": "booster1 site_c",
"to_node": "Span1cb"
},
{
"from_node": "Span1cb",
"to_node": "preamp3 site_b"
},
{
"from_node": "preamp3 site_b",
"to_node": "roadm site_b"
},
{
"from_node": "roadm site_b",
"to_node": "booster3 site_b"
},
{
"from_node": "booster3 site_b",
"to_node": "Span1bc"
},
{
"from_node": "Span1bc",
"to_node": "preamp2 site_c"
},
{
"from_node": "preamp2 site_c",
"to_node": "roadm site_c"
},
{
"from_node": "roadm site_c",
"to_node": "booster2 site_c"
},
{
"from_node": "booster2 site_c",
"to_node": "Span1ca"
},
{
"from_node": "Span1ca",
"to_node": "preamp3 site_a"
},
{
"from_node": "preamp3 site_a",
"to_node": "roadm site_a"
}
]
}

View File

@@ -0,0 +1,319 @@
#!/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['bidir'] = False
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)

View File

@@ -14,8 +14,8 @@ See: draft-ietf-teas-yang-path-computation-01.txt
from argparse import ArgumentParser from argparse import ArgumentParser
from pathlib import Path from pathlib import Path
from json import loads from json import loads
from gnpy.tools.json_io import load_equipment from gnpy.core.equipment import load_equipment
from gnpy.topology.request import jsontocsv from gnpy.core.request import jsontocsv
parser = ArgumentParser(description = 'A function that writes json path results in an excel sheet.') parser = ArgumentParser(description = 'A function that writes json path results in an excel sheet.')
@@ -33,3 +33,4 @@ if __name__ == '__main__':
equipment = load_equipment(args.eqpt_filename) equipment = load_equipment(args.eqpt_filename)
print(f'Writing in {args.output_filename}') print(f'Writing in {args.output_filename}')
jsontocsv(json_data,equipment,file) jsontocsv(json_data,equipment,file)

View File

@@ -1,8 +0,0 @@
'''
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`.
'''

View File

@@ -1,9 +0,0 @@
# coding: utf-8
from flask import Flask
app = Flask(__name__)
import gnpy.api.route.path_request_route
import gnpy.api.route.status_route
import gnpy.api.route.topology_route
import gnpy.api.route.equipments_route

View File

@@ -1 +0,0 @@
# coding: utf-8

View File

@@ -1,14 +0,0 @@
# coding: utf-8
class ConfigError(Exception):
""" Exception raise for configuration file error
Attributes:
message -- explanation of the error
"""
def __init__(self, message):
self.message = message
def __str__(self):
return self.message

View File

@@ -1,14 +0,0 @@
# coding: utf-8
class EquipmentError(Exception):
""" Exception raise for equipment error
Attributes:
message -- explanation of the error
"""
def __init__(self, message):
self.message = message
def __str__(self):
return self.message

View File

@@ -1,33 +0,0 @@
# coding: utf-8
import json
import re
import werkzeug
from gnpy.api.model.error import Error
_reaesc = re.compile(r'\x1b[^m]*m')
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."
else:
status_code = exception.code
response = Error(message=exception.name, description=exception.description,
code=status_code)
return werkzeug.Response(response=json.dumps(response.__dict__), status=status_code, mimetype='application/json')
def bad_request_handler(exception):
response = Error(message='bad request', description=_reaesc.sub('', str(exception)),
code=400)
return werkzeug.Response(response=json.dumps(response.__dict__), status=400, mimetype='application/json')

View File

@@ -1,14 +0,0 @@
# coding: utf-8
class PathComputationError(Exception):
""" Exception raise for path computation error error
Attributes:
message -- explanation of the error
"""
def __init__(self, message):
self.message = message
def __str__(self):
return self.message

View File

@@ -1,14 +0,0 @@
# coding: utf-8
class TopologyError(Exception):
""" Exception raise for topology error
Attributes:
message -- explanation of the error
"""
def __init__(self, message):
self.message = message
def __str__(self):
return self.message

View File

@@ -1 +0,0 @@
# coding: utf-8

View File

@@ -1,17 +0,0 @@
# coding: utf-8
class Error:
def __init__(self, code: int = None, message: str = None, description: str = None):
"""Error
:param code: The code of this Error.
:type code: int
:param message: The message of this Error.
:type message: str
:param description: The description of this Error.
:type description: str
"""
self.code = code
self.message = message
self.description = description

View File

@@ -1,8 +0,0 @@
# coding: utf-8
class Result:
def __init__(self, message: str = None, description: str = None):
self.message = message
self.description = description

View File

@@ -1,83 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
'''
gnpy.tools.rest_example
=======================
GNPy as a rest API example
'''
import logging
from logging.handlers import RotatingFileHandler
import werkzeug
from flask_injector import FlaskInjector
from injector import singleton
from werkzeug.exceptions import InternalServerError
import gnpy.core.exceptions as exceptions
from gnpy.api import app
from gnpy.api.exception.exception_handler import bad_request_handler, common_error_handler
from gnpy.api.exception.path_computation_error import PathComputationError
from gnpy.api.exception.topology_error import TopologyError
from gnpy.api.service import config_service
from gnpy.api.service.encryption_service import EncryptionService
from gnpy.api.service.equipment_service import EquipmentService
from gnpy.api.service.path_request_service import PathRequestService
_logger = logging.getLogger(__name__)
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 _init_app(key):
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)
app.register_error_handler(TopologyError, bad_request_handler)
app.register_error_handler(PathComputationError, bad_request_handler)
for error_code in werkzeug.exceptions.default_exceptions:
app.register_error_handler(error_code, common_error_handler)
config = config_service.init_config()
config.add_section('SECRET')
config.set('SECRET', 'equipment', key)
app.config['properties'] = config
def _configure(binder):
binder.bind(EquipmentService,
to=EquipmentService(EncryptionService(app.config['properties'].get('SECRET', 'equipment'))),
scope=singleton)
binder.bind(PathRequestService,
to=PathRequestService(EncryptionService(app.config['properties'].get('SECRET', 'equipment'))),
scope=singleton)
app.config['properties'].pop('SECRET', None)
def main():
key = input('Enter encryption/decryption key: ')
_init_logger()
_init_app(key)
FlaskInjector(app=app, modules=[_configure])
app.run(host='0.0.0.0', port=8080, ssl_context='adhoc')
if __name__ == '__main__':
main()

View File

@@ -1,2 +0,0 @@
# coding: utf-8

View File

@@ -1,38 +0,0 @@
# coding: utf-8
import http
import json
from flask import request
from gnpy.api import app
from gnpy.api.exception.equipment_error import EquipmentError
from gnpy.api.model.result import Result
from gnpy.api.service.equipment_service import EquipmentService
EQUIPMENT_BASE_PATH = '/api/v1/equipments'
EQUIPMENT_ID_PATH = EQUIPMENT_BASE_PATH + '/<equipment_id>'
@app.route(EQUIPMENT_BASE_PATH, methods=['POST'])
def create_equipment(equipment_service: EquipmentService):
if not request.is_json:
raise EquipmentError('Request body is not json')
equipment_identifier = equipment_service.save_equipment(request.json)
response = Result(message='Equipment creation ok', description=equipment_identifier)
return json.dumps(response.__dict__), 201, {'location': EQUIPMENT_BASE_PATH + '/' + equipment_identifier}
@app.route(EQUIPMENT_ID_PATH, methods=['PUT'])
def update_equipment(equipment_id, equipment_service: EquipmentService):
if not request.is_json:
raise EquipmentError('Request body is not json')
equipment_identifier = equipment_service.update_equipment(request.json, equipment_id)
response = Result(message='Equipment update ok', description=equipment_identifier)
return json.dumps(response.__dict__), http.HTTPStatus.OK, {
'location': EQUIPMENT_BASE_PATH + '/' + equipment_identifier}
@app.route(EQUIPMENT_ID_PATH, methods=['DELETE'])
def delete_equipment(equipment_id, equipment_service: EquipmentService):
equipment_service.delete_equipment(equipment_id)
return '', http.HTTPStatus.NO_CONTENT

View File

@@ -1,63 +0,0 @@
# coding: utf-8
import http
import os
from pathlib import Path
from flask import request
from gnpy.api import app
from gnpy.api.exception.equipment_error import EquipmentError
from gnpy.api.exception.topology_error import TopologyError
from gnpy.api.service import topology_service
from gnpy.api.service.equipment_service import EquipmentService
from gnpy.api.service.path_request_service import PathRequestService
from gnpy.tools.json_io import _equipment_from_json, network_from_json
from gnpy.topology.request import ResultElement
PATH_COMPUTATION_BASE_PATH = '/api/v1/path-computation'
AUTODESIGN_PATH = PATH_COMPUTATION_BASE_PATH + '/<path_computation_id>/autodesign'
_examples_dir = Path(__file__).parent.parent.parent / 'example-data'
@app.route(PATH_COMPUTATION_BASE_PATH, methods=['POST'])
def compute_path(equipment_service: EquipmentService, path_request_service: PathRequestService):
data = request.json
service = data['gnpy-api:service']
if 'gnpy-api:topology' in data:
topology = data['gnpy-api:topology']
elif 'gnpy-api:topology_id' in data:
topology = topology_service.get_topology(data['gnpy-api:topology_id'])
else:
raise TopologyError('No topology found in request')
if 'gnpy-api:equipment' in data:
equipment = data['gnpy-api:equipment']
elif 'gnpy-api:equipment_id' in data:
equipment = equipment_service.get_equipment(data['gnpy-api:equipment_id'])
else:
raise EquipmentError('No equipment found in request')
equipment = _equipment_from_json(equipment,
os.path.join(_examples_dir, 'std_medium_gain_advanced_config.json'))
network = network_from_json(topology, equipment)
propagatedpths, reversed_propagatedpths, rqs, path_computation_id = path_request_service.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, {
'location': AUTODESIGN_PATH.replace('<path_computation_id>', path_computation_id)}
@app.route(AUTODESIGN_PATH, methods=['GET'])
def get_autodesign(path_computation_id, path_request_service: PathRequestService):
return path_request_service.get_autodesign(path_computation_id), http.HTTPStatus.OK
@app.route(AUTODESIGN_PATH, methods=['DELETE'])
def delete_autodesign(path_computation_id, path_request_service: PathRequestService):
path_request_service.delete_autodesign(path_computation_id)
return '', http.HTTPStatus.NO_CONTENT

View File

@@ -1,7 +0,0 @@
# coding: utf-8
from gnpy.api import app
@app.route('/api/v1/status', methods=['GET'])
def api_status():
return {"version": "v1", "status": "ok"}, 200

View File

@@ -1,43 +0,0 @@
# coding: utf-8
import http
import json
from flask import request
from gnpy.api import app
from gnpy.api.exception.topology_error import TopologyError
from gnpy.api.model.result import Result
from gnpy.api.service import topology_service
TOPOLOGY_BASE_PATH = '/api/v1/topologies'
TOPOLOGY_ID_PATH = TOPOLOGY_BASE_PATH + '/<topology_id>'
@app.route(TOPOLOGY_BASE_PATH, methods=['POST'])
def create_topology():
if not request.is_json:
raise TopologyError('Request body is not json')
topology_identifier = topology_service.save_topology(request.json)
response = Result(message='Topology creation ok', description=topology_identifier)
return json.dumps(response.__dict__), 201, {'location': TOPOLOGY_BASE_PATH + '/' + topology_identifier}
@app.route(TOPOLOGY_ID_PATH, methods=['PUT'])
def update_topology(topology_id):
if not request.is_json:
raise TopologyError('Request body is not json')
topology_identifier = topology_service.update_topology(request.json, topology_id)
response = Result(message='Topology update ok', description=topology_identifier)
return json.dumps(response.__dict__), http.HTTPStatus.OK, {
'location': TOPOLOGY_BASE_PATH + '/' + topology_identifier}
@app.route(TOPOLOGY_ID_PATH, methods=['GET'])
def get_topology(topology_id):
return topology_service.get_topology(topology_id), http.HTTPStatus.OK
@app.route(TOPOLOGY_ID_PATH, methods=['DELETE'])
def delete_topology(topology_id):
topology_service.delete_topology(topology_id)
return '', http.HTTPStatus.NO_CONTENT

View File

@@ -1 +0,0 @@
# coding: utf-8

View File

@@ -1,45 +0,0 @@
# coding: utf-8
import configparser
import os
from flask import current_app
from gnpy.api.exception.config_error import ConfigError
def init_config(properties_file_path: str = os.path.join(os.path.dirname(__file__),
'properties.ini')) -> configparser.ConfigParser:
"""
Read config from properties_file_path
@param properties_file_path: the properties file to read
@return: config parser
"""
if not os.path.exists(properties_file_path):
raise ConfigError('Properties file does not exist ' + properties_file_path)
config = configparser.ConfigParser()
config.read(properties_file_path)
return config
def get_topology_dir() -> str:
"""
Get the base dir where topologies are saved
@return: the directory of topologies
"""
return current_app.config['properties'].get('DIRECTORY', 'topology')
def get_equipment_dir() -> str:
"""
Get the base dir where equipments are saved
@return: the directory of equipments
"""
return current_app.config['properties'].get('DIRECTORY', 'equipment')
def get_autodesign_dir() -> str:
"""
Get the base dir where autodesign are saved
@return: the directory of equipments
"""
return current_app.config['properties'].get('DIRECTORY', 'autodesign')

View File

@@ -1,13 +0,0 @@
# coding: utf-8
from cryptography.fernet import Fernet
class EncryptionService:
def __init__(self, key):
self._fernet = Fernet(key)
def encrypt(self, data):
return self._fernet.encrypt(data)
def decrypt(self, data):
return self._fernet.decrypt(data)

View File

@@ -1,66 +0,0 @@
# coding: utf-
import json
import os
import uuid
from injector import Inject
from gnpy.api.exception.equipment_error import EquipmentError
from gnpy.api.service import config_service
from gnpy.api.service.encryption_service import EncryptionService
class EquipmentService:
def __init__(self, encryption_service: EncryptionService):
self.encryption = encryption_service
def save_equipment(self, equipment):
"""
Save equipment to file.
@param equipment: json content
@return: a UUID identifier to identify the equipment
"""
equipment_identifier = str(uuid.uuid4())
# TODO: validate json content
self._write_equipment(equipment, equipment_identifier)
return equipment_identifier
def update_equipment(self, equipment, equipment_identifier):
"""
Update equipment with identifier equipment_identifier.
@param equipment_identifier: the identifier of the equipment to be updated
@param equipment: json content
@return: a UUID identifier to identify the equipment
"""
# TODO: validate json content
self._write_equipment(equipment, equipment_identifier)
return equipment_identifier
def _write_equipment(self, equipment, equipment_identifier):
equipment_dir = config_service.get_equipment_dir()
with(open(os.path.join(equipment_dir, '.'.join([equipment_identifier, 'json'])), 'wb')) as file:
file.write(self.encryption.encrypt(json.dumps(equipment).encode()))
def get_equipment(self, equipment_id: str) -> dict:
"""
Get the equipment with id equipment_id
@param equipment_id:
@return: the equipment in json format
"""
equipment_dir = config_service.get_equipment_dir()
equipment_file = os.path.join(equipment_dir, '.'.join([equipment_id, 'json']))
if not os.path.exists(equipment_file):
raise EquipmentError('Equipment with id {} does not exist '.format(equipment_id))
with(open(equipment_file, 'rb')) as file:
return json.loads(self.encryption.decrypt(file.read()))
def delete_equipment(self, equipment_id: str):
"""
Delete equipment with id equipment_id
@param equipment_id:
"""
equipment_dir = config_service.get_equipment_dir()
equipment_file = os.path.join(equipment_dir, '.'.join([equipment_id, 'json']))
if os.path.exists(equipment_file):
os.remove(equipment_file)

View File

@@ -1,100 +0,0 @@
# -*- coding: utf-8 -*-
import json
import logging
import os
import uuid
import gnpy.core.ansi_escapes as ansi_escapes
from gnpy.api.exception.path_computation_error import PathComputationError
from gnpy.api.service import config_service
from gnpy.api.service.encryption_service import EncryptionService
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, network_to_json
from gnpy.topology.request import (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__)
class PathRequestService:
def __init__(self, encryption_service: EncryptionService):
self.encryption = encryption_service
def path_requests_run(self, 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)
path_computation_identifier = str(uuid.uuid4())
autodesign_dir = config_service.get_autodesign_dir()
with(open(os.path.join(autodesign_dir, '.'.join([path_computation_identifier, 'json'])), 'wb')) as file:
file.write(self.encryption.encrypt(json.dumps(network_to_json(network)).encode()))
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, path_computation_identifier
def get_autodesign(self, path_computation_id):
"""
Get the autodesign with id topology_id
@param path_computation_id:
@return: the autodesign in json format
"""
autodesign_dir = config_service.get_autodesign_dir()
autodesign_file = os.path.join(autodesign_dir, '.'.join([path_computation_id, 'json']))
if not os.path.exists(autodesign_file):
raise PathComputationError('Autodesign with id {} does not exist '.format(path_computation_id))
with(open(autodesign_file, 'rb')) as file:
return json.loads(self.encryption.decrypt(file.read()))
def delete_autodesign(self, path_computation_id: str):
"""
Delete autodesign with id equipment_id
@param path_computation_id:
"""
autodesign_dir = config_service.get_autodesign_dir()
autodesign_file = os.path.join(autodesign_dir, '.'.join([path_computation_id, 'json']))
if os.path.exists(autodesign_file):
os.remove(autodesign_file)

View File

@@ -1,4 +0,0 @@
[DIRECTORY]
topology: /opt/application/oopt-gnpy/topology
equipment: /opt/application/oopt-gnpy/equipment
autodesign: /opt/application/oopt-gnpy/autodesign

View File

@@ -1,62 +0,0 @@
# coding: utf-
import json
import os
import uuid
from gnpy.api.exception.topology_error import TopologyError
from gnpy.api.service import config_service
def save_topology(topology):
"""
Save topology to file.
@param topology: json content
@return: a UUID identifier to identify the topology
"""
topology_identifier = str(uuid.uuid4())
# TODO: validate json content
_write_topology(topology, topology_identifier)
return topology_identifier
def update_topology(topology, topology_identifier):
"""
Update topology with identifier topology_identifier.
@param topology_identifier: the identifier of the topology to be updated
@param topology: json content
@return: a UUID identifier to identify the topology
"""
# TODO: validate json content
_write_topology(topology, topology_identifier)
return topology_identifier
def _write_topology(topology, topology_identifier):
topology_dir = config_service.get_topology_dir()
with(open(os.path.join(topology_dir, '.'.join([topology_identifier, 'json'])), 'w')) as file:
json.dump(topology, file)
def get_topology(topology_id: str) -> dict:
"""
Get the topology with id topology_id
@param topology_id:
@return: the topology in json format
"""
topology_dir = config_service.get_topology_dir()
topology_file = os.path.join(topology_dir, '.'.join([topology_id, 'json']))
if not os.path.exists(topology_file):
raise TopologyError('Topology with id {} does not exist '.format(topology_id))
with(open(topology_file, 'r')) as file:
return json.load(file)
def delete_topology(topology_id: str):
"""
Delete topology with id topology_id
@param topology_id:
"""
topology_dir = config_service.get_topology_dir()
topology_file = os.path.join(topology_dir, '.'.join([topology_id, 'json']))
if os.path.exists(topology_file):
os.remove(topology_file)

View File

@@ -1,9 +1,30 @@
''' #!/usr/bin/env python3
Simulation of signal propagation in the DWDM network # -*- coding: utf-8 -*-
########################################################################
# _____ ___ ____ ____ ____ _____ #
# |_ _|_ _| _ \ | _ \/ ___|| ____| #
# | | | || |_) | | |_) \___ \| _| #
# | | | || __/ | __/ ___) | |___ #
# |_| |___|_| |_| |____/|_____| #
# #
# == Physical Simulation Environment == #
# #
########################################################################
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`.
''' '''
gnpy route planning and optimization library
============================================
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.
'''
from . import elements
from .execute import *
from .network import *
from .utils import *

View File

@@ -9,7 +9,5 @@ A random subset of ANSI terminal escape codes for colored messages
''' '''
red = '\x1b[1;31;40m' red = '\x1b[1;31;40m'
blue = '\x1b[1;34;40m'
cyan = '\x1b[1;36;40m' cyan = '\x1b[1;36;40m'
yellow = '\x1b[1;33;40m'
reset = '\x1b[0m' reset = '\x1b[0m'

631
gnpy/core/convert.py Executable file
View File

@@ -0,0 +1,631 @@
#!/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
from gnpy.core.exceptions import NetworkTopologyError
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}')
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 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)

View File

@@ -5,7 +5,7 @@
gnpy.core.elements gnpy.core.elements
================== ==================
Standard network elements which propagate optical spectrum This module contains standard network elements.
A network element is a Python callable. It takes a :class:`.info.SpectralInformation` A network element is a Python callable. It takes a :class:`.info.SpectralInformation`
object and returns a copy with appropriate fields affected. This structure object and returns a copy with appropriate fields affected. This structure
@@ -14,66 +14,21 @@ Network elements must have only a local "view" of the network and propogate
:class:`.info.SpectralInformation` using only this information. They should be independent and :class:`.info.SpectralInformation` using only this information. They should be independent and
self-contained. self-contained.
Network elements MUST implement two attributes :py:attr:`uid` and :py:attr:`name` representing a Network elements MUST implement two attributes .uid and .name representing a
unique identifier and a printable name, and provide the :py:meth:`__call__` method taking a unique identifier and a printable name.
:class:`SpectralInformation` as an input and returning another :class:`SpectralInformation`
instance as a result.
''' '''
from numpy import abs, arange, array, divide, errstate, ones from numpy import abs, arange, array, exp, divide, errstate
from numpy import interp, mean, pi, polyfit, polyval, sum, sqrt from numpy import interp, log10, mean, pi, polyfit, polyval, sum
from scipy.constants import h, c from scipy.constants import c, h
from collections import namedtuple from collections import namedtuple
from gnpy.core.node import Node
from gnpy.core.units import UNITS
from gnpy.core.utils import lin2db, db2lin, arrange_frequencies, snr_sum from gnpy.core.utils import lin2db, db2lin, arrange_frequencies, snr_sum
from gnpy.core.parameters import FiberParams, PumpParams from gnpy.core.science_utils import propagate_raman_fiber, _psi
from gnpy.core.science_utils import NliSolver, RamanSolver, propagate_raman_fiber, _psi
class Transceiver(Node):
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:
'''Convenience class for providing common functionality of all network elements
This class is just an internal implementation detail; do **not** assume that all network elements
inherit from :class:`_Node`.
'''
def __init__(self, uid, name=None, params=None, metadata=None, operational=None, type_variety=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
if type_variety:
self.type_variety = type_variety
@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
class Transceiver(_Node):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.osnr_ase_01nm = None self.osnr_ase_01nm = None
@@ -82,18 +37,6 @@ class Transceiver(_Node):
self.snr = None self.snr = None
self.passive = False self.passive = False
self.baud_rate = None self.baud_rate = None
self.chromatic_dispersion = None
self.pmd = None
def _calc_cd(self, spectral_info):
""" Updates the Transceiver property with the CD of the received channels. CD in ps/nm.
"""
self.chromatic_dispersion = [carrier.chromatic_dispersion * 1e3 for carrier in spectral_info.carriers]
def _calc_pmd(self, spectral_info):
"""Updates the Transceiver property with the PMD of the received channels. PMD in ps.
"""
self.pmd = [carrier.pmd*1e12 for carrier in spectral_info.carriers]
def _calc_snr(self, spectral_info): def _calc_snr(self, spectral_info):
with errstate(divide='ignore'): with errstate(divide='ignore'):
@@ -124,7 +67,7 @@ class Transceiver(_Node):
only applied in request.py / propagate on the last Trasceiver node of the path only applied in request.py / propagate on the last Trasceiver node of the path
all penalties are added in a single call because to avoid uncontrolled cumul all penalties are added in a single call because to avoid uncontrolled cumul
""" """
# use raw_values so that the added SNR penalties are not cumulated #use raw_values so that the added snr penalties are not cumulated
snr_added = 0 snr_added = 0
for s in args: for s in args:
snr_added += db2lin(-s) snr_added += db2lin(-s)
@@ -153,9 +96,7 @@ class Transceiver(_Node):
f'osnr_ase_01nm={self.osnr_ase_01nm!r}, ' f'osnr_ase_01nm={self.osnr_ase_01nm!r}, '
f'osnr_ase={self.osnr_ase!r}, ' f'osnr_ase={self.osnr_ase!r}, '
f'osnr_nli={self.osnr_nli!r}, ' f'osnr_nli={self.osnr_nli!r}, '
f'snr={self.snr!r}, ' f'snr={self.snr!r})')
f'chromatic_dispersion={self.chromatic_dispersion!r}, '
f'pmd={self.pmd!r})')
def __str__(self): def __str__(self):
if self.snr is None or self.osnr_ase is None: if self.snr is None or self.osnr_ase is None:
@@ -165,36 +106,31 @@ class Transceiver(_Node):
osnr_ase = round(mean(self.osnr_ase),2) osnr_ase = round(mean(self.osnr_ase),2)
osnr_ase_01nm = round(mean(self.osnr_ase_01nm), 2) osnr_ase_01nm = round(mean(self.osnr_ase_01nm), 2)
snr_01nm = round(mean(self.snr_01nm),2) snr_01nm = round(mean(self.snr_01nm),2)
cd = mean(self.chromatic_dispersion)
pmd = mean(self.pmd)
return '\n'.join([f'{type(self).__name__} {self.uid}', return '\n'.join([f'{type(self).__name__} {self.uid}',
f' OSNR ASE (0.1nm, dB): {osnr_ase_01nm:.2f}', f' OSNR ASE (0.1nm, dB): {osnr_ase_01nm:.2f}',
f' OSNR ASE (signal bw, dB): {osnr_ase:.2f}', f' OSNR ASE (signal bw, dB): {osnr_ase:.2f}',
f' SNR total (signal bw, dB): {snr:.2f}', f' SNR total (signal bw, dB): {snr:.2f}',
f' SNR total (0.1nm, dB): {snr_01nm:.2f}', f' SNR total (0.1nm, dB): {snr_01nm:.2f}'])
f' CD (ps/nm): {cd:.2f}',
f' PMD (ps): {pmd:.2f}'])
def __call__(self, spectral_info): def __call__(self, spectral_info):
self._calc_snr(spectral_info) self._calc_snr(spectral_info)
self._calc_cd(spectral_info)
self._calc_pmd(spectral_info)
return spectral_info return spectral_info
RoadmParams = namedtuple('RoadmParams', 'target_pch_out_db add_drop_osnr restrictions per_degree_target_pch_out_db')
RoadmParams = namedtuple('RoadmParams', 'target_pch_out_db add_drop_osnr pmd restrictions') class Roadm(Node):
class Roadm(_Node):
def __init__(self, *args, params, **kwargs): def __init__(self, *args, params, **kwargs):
if 'per_degree_target_pch_out_db' not in params.keys():
params['per_degree_target_pch_out_db'] = []
super().__init__(*args, params=RoadmParams(**params), **kwargs) super().__init__(*args, params=RoadmParams(**params), **kwargs)
self.loss = 0 #auto-design interest self.loss = 0 #auto-design interest
self.effective_loss = None self.effective_loss = None
self.effective_pch_out_db = self.params.target_pch_out_db self.effective_pch_out_db = self.params.target_pch_out_db
self.passive = True self.passive = True
self.restrictions = self.params.restrictions self.restrictions = self.params.restrictions
self.per_degree_target_pch_out_db = self.params.per_degree_target_pch_out_db
@property @property
def to_json(self): def to_json(self):
@@ -202,7 +138,8 @@ class Roadm(_Node):
'type' : type(self).__name__, 'type' : type(self).__name__,
'params' : { 'params' : {
'target_pch_out_db' : self.effective_pch_out_db, 'target_pch_out_db' : self.effective_pch_out_db,
'restrictions': self.restrictions 'restrictions' : self.restrictions,
'per_degree_target_pch_out_db': self.per_degree_target_pch_out_db
}, },
'metadata' : { 'metadata' : {
'location': self.metadata['location']._asdict() 'location': self.metadata['location']._asdict()
@@ -213,22 +150,30 @@ class Roadm(_Node):
return f'{type(self).__name__}(uid={self.uid!r}, loss={self.loss!r})' return f'{type(self).__name__}(uid={self.uid!r}, loss={self.loss!r})'
def __str__(self): def __str__(self):
if self.effective_loss is None:
return f'{type(self).__name__} {self.uid}'
return '\n'.join([f'{type(self).__name__} {self.uid}', return '\n'.join([f'{type(self).__name__} {self.uid}',
f' effective loss (dB): {self.effective_loss:.2f}', f' effective loss (dB): {self.effective_loss:.2f}',
f' pch out (dBm): {self.effective_pch_out_db!r}']) f' pch out (dBm): {self.effective_pch_out_db!r}'])
def propagate(self, pref, *carriers): def propagate(self, pref, *carriers, degree):
#pin_target and loss are read from eqpt_config.json['Roadm'] #pin_target and loss are read from eqpt_config.json['Roadm']
#all ingress channels in xpress are set to this power level #all ingress channels in xpress are set to this power level
#but add channels are not, so we define an effective loss #but add channels are not, so we define an effective loss
#in the case of add channels #in the case of add channels
self.effective_pch_out_db = min(pref.p_spani, self.params.target_pch_out_db) if self.per_degree_target_pch_out_db:
# find the target power on this degree
try:
temp = next(el['target_pch_out_db'] \
for el in self.per_degree_target_pch_out_db if el['to_node']==degree)
except StopIteration:
# if no target power is defined on this degree use the global one
temp = self.params.target_pch_out_db
else:
# if no per degree target power are defined, use the global one
temp = self.params.target_pch_out_db
self.effective_pch_out_db = min(pref.p_spani, temp)
self.effective_loss = pref.p_spani - self.effective_pch_out_db self.effective_loss = pref.p_spani - self.effective_pch_out_db
carriers_power = array([c.power.signal +c.power.nli+c.power.ase for c in carriers]) carriers_power = array([c.power.signal +c.power.nli+c.power.ase for c in carriers])
carriers_att = list(map(lambda x: lin2db(x * 1e3) - self.params.target_pch_out_db, carriers_power)) carriers_att = list(map(lambda x : lin2db(x*1e3)-self.effective_pch_out_db, carriers_power))
exceeding_att = -min(list(filter(lambda x: x < 0, carriers_att)), default = 0) exceeding_att = -min(list(filter(lambda x: x < 0, carriers_att)), default = 0)
carriers_att = list(map(lambda x: db2lin(x+exceeding_att), carriers_att)) carriers_att = list(map(lambda x: db2lin(x+exceeding_att), carriers_att))
for carrier_att, carrier in zip(carriers_att, carriers) : for carrier_att, carrier in zip(carriers_att, carriers) :
@@ -236,22 +181,19 @@ class Roadm(_Node):
pwr = pwr._replace( signal = pwr.signal/carrier_att, pwr = pwr._replace( signal = pwr.signal/carrier_att,
nli = pwr.nli/carrier_att, nli = pwr.nli/carrier_att,
ase = pwr.ase/carrier_att) ase = pwr.ase/carrier_att)
pmd = sqrt(carrier.pmd**2 + self.params.pmd**2) yield carrier._replace(power=pwr)
yield carrier._replace(power=pwr, pmd=pmd)
def update_pref(self, pref): def update_pref(self, pref):
return pref._replace(p_span0=pref.p_span0, p_spani=self.effective_pch_out_db) return pref._replace(p_span0=pref.p_span0, p_spani=self.effective_pch_out_db)
def __call__(self, spectral_info): def __call__(self, spectral_info, degree):
carriers = tuple(self.propagate(spectral_info.pref, *spectral_info.carriers)) carriers = tuple(self.propagate(spectral_info.pref, *spectral_info.carriers, degree=degree))
pref = self.update_pref(spectral_info.pref) pref = self.update_pref(spectral_info.pref)
return spectral_info._replace(carriers=carriers, pref=pref) return spectral_info._replace(carriers=carriers, pref=pref)
FusedParams = namedtuple('FusedParams', 'loss') FusedParams = namedtuple('FusedParams', 'loss')
class Fused(Node):
class Fused(_Node):
def __init__(self, *args, params=None, **kwargs): def __init__(self, *args, params=None, **kwargs):
if params is None: if params is None:
# default loss value if not mentioned in loaded network json # default loss value if not mentioned in loaded network json
@@ -297,16 +239,37 @@ class Fused(_Node):
pref = self.update_pref(spectral_info.pref) pref = self.update_pref(spectral_info.pref)
return spectral_info._replace(carriers=carriers, pref=pref) return spectral_info._replace(carriers=carriers, pref=pref)
FiberParams = namedtuple('FiberParams', 'type_variety length loss_coef length_units \
att_in con_in con_out dispersion gamma')
class Fiber(_Node): class Fiber(Node):
def __init__(self, *args, params=None, **kwargs): def __init__(self, *args, params=None, **kwargs):
if not params: if params is None:
params = {} params = {}
if 'con_in' not in params:
# 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]
params['con_in'] = None
params['con_out'] = None
if 'att_in' not in params:
#fixed attenuator for padding
params['att_in'] = 0
super().__init__(*args, params=FiberParams(**params), **kwargs) super().__init__(*args, params=FiberParams(**params), **kwargs)
self.type_variety = self.params.type_variety
self.length = self.params.length * UNITS[self.params.length_units] # in m
self.loss_coef = self.params.loss_coef * 1e-3 # lineic loss dB/m
self.lin_loss_coef = self.params.loss_coef / (20 * log10(exp(1)))
self.att_in = self.params.att_in
self.con_in = self.params.con_in
self.con_out = self.params.con_out
self.dispersion = self.params.dispersion # s/m/m
self.gamma = self.params.gamma # 1/W/m
self.pch_out_db = None
self.carriers_in = None self.carriers_in = None
self.carriers_out = None self.carriers_out = None
self.pch_out_db = None # TODO|jla: discuss factor 2 in the linear lineic attenuation
self.nli_solver = NliSolver(self)
@property @property
def to_json(self): def to_json(self):
@@ -315,12 +278,13 @@ class Fiber(_Node):
'type_variety' : self.type_variety, 'type_variety' : self.type_variety,
'params' : { 'params' : {
#have to specify each because namedtupple cannot be updated :( #have to specify each because namedtupple cannot be updated :(
'length': round(self.params.length * 1e-3, 6), 'type_variety' : self.type_variety,
'loss_coef': self.params.loss_coef * 1e3, 'length' : self.length/UNITS[self.params.length_units],
'length_units': 'km', 'loss_coef' : self.loss_coef*1e3,
'att_in': self.params.att_in, 'length_units' : self.params.length_units,
'con_in': self.params.con_in, 'att_in' : self.att_in,
'con_out': self.params.con_out 'con_in' : self.con_in,
'con_out' : self.con_out
}, },
'metadata' : { 'metadata' : {
'location': self.metadata['location']._asdict() 'location': self.metadata['location']._asdict()
@@ -328,38 +292,48 @@ class Fiber(_Node):
} }
def __repr__(self): def __repr__(self):
return f'{type(self).__name__}(uid={self.uid!r}, ' \ return f'{type(self).__name__}(uid={self.uid!r}, length={round(self.length*1e-3,1)!r}km, loss={round(self.loss,1)!r}dB)'
f'length={round(self.params.length * 1e-3,1)!r}km, ' \
f'loss={round(self.loss,1)!r}dB)'
def __str__(self): def __str__(self):
if self.pch_out_db is None:
return f'{type(self).__name__} {self.uid}'
return '\n'.join([f'{type(self).__name__} {self.uid}', return '\n'.join([f'{type(self).__name__} {self.uid}',
f' type_variety: {self.type_variety}', f' type_variety: {self.type_variety}',
f' length (km): ' f' length (km): {round(self.length*1e-3):.2f}',
f'{round(self.params.length * 1e-3):.2f}', f' pad att_in (dB): {self.att_in:.2f}',
f' pad att_in (dB): {self.params.att_in:.2f}',
f' total loss (dB): {self.loss:.2f}', f' total loss (dB): {self.loss:.2f}',
f' (includes conn loss (dB) in: {self.params.con_in:.2f} out: {self.params.con_out:.2f})', f' (includes conn loss (dB) in: {self.con_in:.2f} out: {self.con_out:.2f})',
f' (conn loss out includes EOL margin defined in eqpt_config.json)', f' (conn loss out includes EOL margin defined in eqpt_config.json)',
f' pch out (dBm): {self.pch_out_db!r}']) f' pch out (dBm): {self.pch_out_db!r}'])
@property @property
def fiber_loss(self): def fiber_loss(self):
"""Fiber loss in dB, not including padding attenuator""" """Fiber loss in dB, not including padding attenuator"""
return self.params.loss_coef * self.params.length + self.params.con_in + self.params.con_out return self.loss_coef * self.length + self.con_in + self.con_out
@property @property
def loss(self): def loss(self):
"""total loss including padding att_in: useful for polymorphism with roadm loss""" """total loss including padding att_in: useful for polymorphism with roadm loss"""
return self.params.loss_coef * self.params.length + self.params.con_in + self.params.con_out + self.params.att_in return self.loss_coef * self.length + self.con_in + self.con_out + self.att_in
@property @property
def passive(self): def passive(self):
return True return True
@property
def lin_attenuation(self):
return db2lin(self.length * self.loss_coef)
@property
def effective_length(self):
_, alpha = self.dbkm_2_lin()
leff = (1 - exp(-2 * alpha * self.length)) / (2 * alpha)
return leff
@property
def asymptotic_length(self):
_, alpha = self.dbkm_2_lin()
aleff = 1 / (2 * alpha)
return aleff
def carriers(self, loc, attr): def carriers(self, loc, attr):
"""retrieve carriers information """retrieve carriers information
@@ -376,46 +350,25 @@ class Fiber(_Node):
else: else:
yield c.power._asdict().get(attr, None) yield c.power._asdict().get(attr, None)
def alpha(self, frequencies): def beta2(self, ref_wavelength=1550e-9):
"""It returns the values of the series expansion of attenuation coefficient alpha(f) for all f in frequencies """Returns beta2 from dispersion parameter.
Dispersion is entered in ps/nm/km.
Disperion can be a numpy array or a single value.
:param frequencies: frequencies of series expansion [Hz] :param ref_wavelength: can be a numpy array; default: 1550nm
:return: alpha: power attenuation coefficient for f in frequencies [Neper/m]
""" """
if type(self.params.loss_coef) == dict: # TODO|jla: discuss beta2 as method or attribute
alpha = interp(frequencies, self.params.f_loss_ref, self.params.lin_loss_exp) D = abs(self.dispersion)
else: b2 = (ref_wavelength ** 2) * D / (2 * pi * c) # 10^21 scales [ps^2/km]
alpha = self.params.lin_loss_exp * ones(frequencies.shape) return b2 # s/Hz/m
return alpha def dbkm_2_lin(self):
"""calculates the linear loss coefficient"""
def alpha0(self, f_ref=193.5e12): # linear loss coefficient in dB/km^-1
"""It returns the zero element of the series expansion of attenuation coefficient alpha(f) in the alpha_pcoef = self.loss_coef
reference frequency f_ref # linear loss field amplitude coefficient in m^-1
alpha_acoef = alpha_pcoef / (2 * 10 * log10(exp(1)))
:param f_ref: reference frequency of series expansion [Hz] return alpha_pcoef, alpha_acoef
:return: alpha0: power attenuation coefficient in f_ref [Neper/m]
"""
return self.alpha(f_ref * ones(1))[0]
def chromatic_dispersion(self, freq=193.5e12):
"""Returns accumulated chromatic dispersion (CD).
:param freq: the frequency at which the chromatic dispersion is computed
:return: chromatic dispersion: the accumulated dispersion [s/m]
"""
beta2 = self.params.beta2
beta3 = self.params.beta3
ref_f = self.params.ref_frequency
length = self.params.length
beta = beta2 + 2 * pi * beta3 * (freq - ref_f)
dispersion = -beta * 2 * pi * ref_f**2 / c
return dispersion * length
@property
def pmd(self):
"""differential group delay (PMD) [s]"""
return self.params.pmd_coef * sqrt(self.params.length)
def _gn_analytic(self, carrier, *carriers): def _gn_analytic(self, carrier, *carriers):
"""Computes the nonlinear interference power on a single carrier. """Computes the nonlinear interference power on a single carrier.
@@ -428,27 +381,20 @@ class Fiber(_Node):
g_nli = 0 g_nli = 0
for interfering_carrier in carriers: for interfering_carrier in carriers:
psi = _psi(carrier, interfering_carrier, beta2=self.params.beta2, psi = _psi(carrier, interfering_carrier, beta2=self.beta2(), asymptotic_length=self.asymptotic_length)
asymptotic_length=self.params.asymptotic_length)
g_nli += (interfering_carrier.power.signal/interfering_carrier.baud_rate)**2 \ g_nli += (interfering_carrier.power.signal/interfering_carrier.baud_rate)**2 \
* (carrier.power.signal/carrier.baud_rate) * psi * (carrier.power.signal/carrier.baud_rate) * psi
g_nli *= (16 / 27) * (self.params.gamma * self.params.effective_length)**2 \ g_nli *= (16 / 27) * (self.gamma * self.effective_length)**2 \
/ (2 * pi * abs(self.params.beta2) * self.params.asymptotic_length) / (2 * pi * abs(self.beta2()) * self.asymptotic_length)
carrier_nli = carrier.baud_rate * g_nli carrier_nli = carrier.baud_rate * g_nli
return carrier_nli return carrier_nli
def propagate(self, *carriers): def propagate(self, *carriers):
r"""Generator that computes the fiber propagation: attenuation, non-linear interference generation, CD
accumulation and PMD accumulation.
:param: \*carriers: the channels at the input of the fiber
:yield: carrier: the next channel at the output of the fiber
"""
# apply connector_att_in on all carriers before computing gn analytics premiere partie pas bonne # apply connector_att_in on all carriers before computing gn analytics premiere partie pas bonne
attenuation = db2lin(self.params.con_in + self.params.att_in) attenuation = db2lin(self.con_in + self.att_in)
chan = [] chan = []
for carrier in carriers: for carrier in carriers:
@@ -462,16 +408,14 @@ class Fiber(_Node):
carriers = tuple(f for f in chan) carriers = tuple(f for f in chan)
# propagate in the fiber and apply attenuation out # propagate in the fiber and apply attenuation out
attenuation = db2lin(self.params.con_out) attenuation = db2lin(self.con_out)
for carrier in carriers: for carrier in carriers:
pwr = carrier.power pwr = carrier.power
carrier_nli = self._gn_analytic(carrier, *carriers) carrier_nli = self._gn_analytic(carrier, *carriers)
pwr = pwr._replace(signal=pwr.signal / self.params.lin_attenuation / attenuation, pwr = pwr._replace(signal=pwr.signal/self.lin_attenuation/attenuation,
nli=(pwr.nli + carrier_nli) / self.params.lin_attenuation / attenuation, nli=(pwr.nli+carrier_nli)/self.lin_attenuation/attenuation,
ase=pwr.ase / self.params.lin_attenuation / attenuation) ase=pwr.ase/self.lin_attenuation/attenuation)
chromatic_dispersion = carrier.chromatic_dispersion + self.chromatic_dispersion(carrier.frequency) yield carrier._replace(power=pwr)
pmd = sqrt(carrier.pmd**2 + self.pmd**2)
yield carrier._replace(power=pwr, chromatic_dispersion=chromatic_dispersion, pmd=pmd)
def update_pref(self, pref): def update_pref(self, pref):
self.pch_out_db = round(pref.p_spani - self.loss, 2) self.pch_out_db = round(pref.p_spani - self.loss, 2)
@@ -484,16 +428,46 @@ class Fiber(_Node):
self.carriers_out = carriers self.carriers_out = carriers
return spectral_info._replace(carriers=carriers, pref=pref) return spectral_info._replace(carriers=carriers, pref=pref)
RamanFiberParams = namedtuple('RamanFiberParams', 'type_variety length loss_coef length_units \
att_in con_in con_out dispersion gamma raman_efficiency')
class RamanFiber(Fiber): class RamanFiber(Fiber):
def __init__(self, *args, params=None, **kwargs): def __init__(self, *args, params=None, **kwargs):
super().__init__(*args, params=params, **kwargs) if params is None:
if self.operational and 'raman_pumps' in self.operational: params = {}
self.raman_pumps = tuple(PumpParams(p['power'], p['frequency'], p['propagation_direction']) if 'con_in' not in params:
for p in self.operational['raman_pumps']) # if not defined in the network json connector loss in/out
else: # the None value will be updated in network.py[build_network]
self.raman_pumps = None # with default values from eqpt_config.json[Spans]
self.raman_solver = RamanSolver(self) params['con_in'] = None
params['con_out'] = None
if 'att_in' not in params:
#fixed attenuator for padding
params['att_in'] = 0
# TODO: can we re-use the Fiber constructor in a better way?
Node.__init__(self, *args, params=RamanFiberParams(**params), **kwargs)
self.type_variety = self.params.type_variety
self.length = self.params.length * UNITS[self.params.length_units] # in m
self.loss_coef = self.params.loss_coef * 1e-3 # lineic loss dB/m
self.lin_loss_coef = self.params.loss_coef / (20 * log10(exp(1)))
self.att_in = self.params.att_in
self.con_in = self.params.con_in
self.con_out = self.params.con_out
self.dispersion = self.params.dispersion # s/m/m
self.gamma = self.params.gamma # 1/W/m
self.pch_out_db = None
self.carriers_in = None
self.carriers_out = None
# TODO|jla: discuss factor 2 in the linear lineic attenuation
@property
def sim_params(self):
return self._sim_params
@sim_params.setter
def sim_params(self, sim_params=None):
self._sim_params = sim_params
def update_pref(self, pref, *carriers): def update_pref(self, pref, *carriers):
pch_out_db = lin2db(mean([carrier.power.signal for carrier in carriers])) + 30 pch_out_db = lin2db(mean([carrier.power.signal for carrier in carriers])) + 30
@@ -509,13 +483,8 @@ class RamanFiber(Fiber):
def propagate(self, *carriers): def propagate(self, *carriers):
for propagated_carrier in propagate_raman_fiber(self, *carriers): for propagated_carrier in propagate_raman_fiber(self, *carriers):
chromatic_dispersion = propagated_carrier.chromatic_dispersion + \
self.chromatic_dispersion(propagated_carrier.frequency)
pmd = sqrt(propagated_carrier.pmd**2 + self.pmd**2)
propagated_carrier = propagated_carrier._replace(chromatic_dispersion=chromatic_dispersion, pmd=pmd)
yield propagated_carrier yield propagated_carrier
class EdfaParams: class EdfaParams:
def __init__(self, **params): def __init__(self, **params):
self.update_params(params) self.update_params(params)
@@ -535,11 +504,12 @@ class EdfaParams:
def update_params(self, kwargs): def update_params(self, kwargs):
for k,v in kwargs.items() : for k,v in kwargs.items() :
setattr(self, k, self.update_params(**v) if isinstance(v, dict) else v) setattr(self, k, update_params(**v)
if isinstance(v, dict) else v)
class EdfaOperational: class EdfaOperational:
default_values = { default_values = \
{
'gain_target': None, 'gain_target': None,
'delta_p': None, 'delta_p': None,
'out_voa': None, 'out_voa': None,
@@ -559,8 +529,7 @@ class EdfaOperational:
f'gain_target={self.gain_target!r}, ' f'gain_target={self.gain_target!r}, '
f'tilt_target={self.tilt_target!r})') f'tilt_target={self.tilt_target!r})')
class Edfa(Node):
class Edfa(_Node):
def __init__(self, *args, params=None, operational=None, **kwargs): def __init__(self, *args, params=None, operational=None, **kwargs):
if params is None: if params is None:
params = {} params = {}
@@ -657,17 +626,15 @@ class Edfa(_Node):
def interpol_params(self, frequencies, pin, baud_rates, pref): def interpol_params(self, frequencies, pin, baud_rates, pref):
"""interpolate SI channel frequencies with the edfa dgt and gain_ripple frquencies from JSON """interpolate SI channel frequencies with the edfa dgt and gain_ripple frquencies from JSON
set the edfa class __init__ None parameters :
self.channel_freq, self.nf, self.interpol_dgt and self.interpol_gain_ripple
""" """
# TODO|jla: read amplifier actual frequencies from additional params in json # TODO|jla: read amplifier actual frequencies from additional params in json
self.channel_freq = frequencies
amplifier_freq = arrange_frequencies(len(self.params.dgt), self.params.f_min, self.params.f_max) # Hz amplifier_freq = arrange_frequencies(len(self.params.dgt), self.params.f_min, self.params.f_max) # Hz
self.channel_freq = frequencies
self.interpol_dgt = interp(self.channel_freq, amplifier_freq, self.params.dgt) self.interpol_dgt = interp(self.channel_freq, amplifier_freq, self.params.dgt)
amplifier_freq = arrange_frequencies(len(self.params.gain_ripple), self.params.f_min, self.params.f_max) # Hz
self.interpol_gain_ripple = interp(self.channel_freq, amplifier_freq, self.params.gain_ripple) self.interpol_gain_ripple = interp(self.channel_freq, amplifier_freq, self.params.gain_ripple)
amplifier_freq = arrange_frequencies(len(self.params.nf_ripple), self.params.f_min, self.params.f_max) # Hz
self.interpol_nf_ripple =interp(self.channel_freq, amplifier_freq, self.params.nf_ripple) self.interpol_nf_ripple =interp(self.channel_freq, amplifier_freq, self.params.nf_ripple)
self.nch = frequencies.size self.nch = frequencies.size
@@ -757,9 +724,8 @@ class Edfa(_Node):
return self.interpol_nf_ripple + nf_avg # input VOA = 1 for 1 NF degradation return self.interpol_nf_ripple + nf_avg # input VOA = 1 for 1 NF degradation
def noise_profile(self, df): def noise_profile(self, df):
"""noise_profile(bw) computes amplifier ASE (W) in signal bandwidth (Hz) """noise_profile(bw) computes amplifier ase (W) in signal bw (Hz)
noise is calculated at amplifier input
Noise is calculated at amplifier input
:bw: signal bandwidth = baud rate in Hz :bw: signal bandwidth = baud rate in Hz
:type bw: float :type bw: float
@@ -767,8 +733,8 @@ class Edfa(_Node):
:return: the asepower in W in the signal bandwidth bw for 96 channels :return: the asepower in W in the signal bandwidth bw for 96 channels
:return type: numpy array of float :return type: numpy array of float
ASE power using per channel gain profile inputs: ASE POWER USING PER CHANNEL GAIN PROFILE
INPUTS:
NF_dB - Noise figure in dB, vector of length number of channels or NF_dB - Noise figure in dB, vector of length number of channels or
spectral slices spectral slices
G_dB - Actual gain calculated for the EDFA, vector of length number of G_dB - Actual gain calculated for the EDFA, vector of length number of
@@ -777,14 +743,9 @@ class Edfa(_Node):
THz, vector of length number of channels or spectral slices THz, vector of length number of channels or spectral slices
dF - width of each channel or spectral slice in THz, dF - width of each channel or spectral slice in THz,
vector of length number of channels or spectral slices vector of length number of channels or spectral slices
OUTPUT: OUTPUT:
ase_dBm - ase in dBm per channel or spectral slice ase_dBm - ase in dBm per channel or spectral slice
NOTE: the output is the total ASE in the channel or spectral slice. For
NOTE:
The output is the total ASE in the channel or spectral slice. For
50GHz channels the ASE BW is effectively 0.4nm. To get to noise power 50GHz channels the ASE BW is effectively 0.4nm. To get to noise power
in 0.1nm, subtract 6dB. in 0.1nm, subtract 6dB.
@@ -808,32 +769,39 @@ class Edfa(_Node):
:param gain_ripple: design flat gain :param gain_ripple: design flat gain
:param dgt: design gain tilt :param dgt: design gain tilt
:param Pin: total input power in W :param Pin: total input power in W
:param gp: Average gain setpoint in dB units (provisioned gain) :param gp: Average gain setpoint in dB units
:param gtp: gain tilt setting (provisioned tilt) :param gtp: gain tilt setting
:type gain_ripple: numpy.ndarray :type gain_ripple: numpy.ndarray
:type dgt: numpy.ndarray :type dgt: numpy.ndarray
:type Pin: numpy.ndarray :type Pin: numpy.ndarray
:type gp: float :type gp: float
:type gtp: float :type gtp: float
:return: gain profile in dBm, per channel or spectral slice :return: gain profile in dBm
:rtype: numpy.ndarray :rtype: numpy.ndarray
Checking of output power clamping is implemented in interpol_params(). AMPLIFICATION USING INPUT PROFILE
INPUTS:
gain_ripple - vector of length number of channels or spectral slices
Based on: DGT - vector of length number of channels or spectral slices
Pin - input powers vector of length number of channels or
R. di Muro, "The Er3+ fiber gain coefficient derived from a dynamic spectral slices
gain tilt technique", Journal of Lightwave Technology, Vol. 18, Gp - provisioned gain length 1
Iss. 3, Pp. 343-347, 2000. GTp - provisioned tilt length 1
OUTPUT:
amp gain per channel or spectral slice
NOTE: there is no checking done for violations of the total output
power capability of the amp.
EDIT OF PREVIOUS NOTE: power violation now added in interpol_params
Ported from Matlab version written by David Boerges at Ciena. Ported from Matlab version written by David Boerges at Ciena.
Based on:
R. di Muro, "The Er3+ fiber gain coefficient derived from a dynamic
gain
tilt technique", Journal of Lightwave Technology, Vol. 18, Iss. 3,
Pp. 343-347, 2000.
""" """
# TODO|jla: check what param should be used (currently length(dgt)) # TODO|jla: check what param should be used (currently length(dgt))
if len(self.interpol_dgt) == 1:
return array([self.effective_gain])
nb_channel = arange(len(self.interpol_dgt)) nb_channel = arange(len(self.interpol_dgt))
# TODO|jla: find a way to use these or lose them. Primarily we should have # TODO|jla: find a way to use these or lose them. Primarily we should have

View File

@@ -8,9 +8,261 @@ gnpy.core.equipment
This module contains functionality for specifying equipment. This module contains functionality for specifying equipment.
''' '''
from gnpy.core.utils import automatic_nch, db2lin 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.exceptions import EquipmentConfigError 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): 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)""" """return the trx and SI parameters from eqpt_config for a given type_variety and mode (ie format)"""
@@ -22,15 +274,15 @@ def trx_mode_params(equipment, trx_type_variety='', trx_mode='', error_message=F
#if called from path_requests_run.py, trx_mode is filled with None when not specified by user #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 transmission_main.py, trx_mode is ''
if trx_mode is not None: if trx_mode is not None:
mode_params = next(mode for trx in trxs mode_params = next(mode for trx in trxs \
if trx == trx_type_variety if trx == trx_type_variety \
for mode in trxs[trx].mode for mode in trxs[trx].mode \
if mode['format'] == trx_mode) if mode['format'] == trx_mode)
trx_params = {**mode_params} trx_params = {**mode_params}
# sanity check: spacing baudrate must be smaller than min spacing # sanity check: spacing baudrate must be smaller than min spacing
if trx_params['baud_rate'] > trx_params['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"]}" ' + 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}.') f'has baud rate: {trx_params["baud_rate"]*1e-9} GHz greater than min_spacing {trx_params["min_spacing"]*1e-9}.')
else: else:
mode_params = {"format": "undetermined", mode_params = {"format": "undetermined",
"baud_rate": None, "baud_rate": None,
@@ -45,12 +297,12 @@ def trx_mode_params(equipment, trx_type_variety='', trx_mode='', error_message=F
trx_params['f_max'] = equipment['Transceiver'][trx_type_variety].frequency['max'] trx_params['f_max'] = equipment['Transceiver'][trx_type_variety].frequency['max']
# TODO: novel automatic feature maybe unwanted if spacing is specified # 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'] # temp = trx_params['spacing']
# print(f'spacing {temp}') # print(f'spacing {temp}')
except StopIteration : except StopIteration :
if error_message: if error_message:
raise EquipmentConfigError(f'Could not find transponder "{trx_type_variety}" with mode "{trx_mode}" in equipment library') raise EquipmentConfigError(f'Computation stoped: could not find tsp : {trx_type_variety} with mode: {trx_mode} in eqpt library')
else: else:
# default transponder charcteristics # default transponder charcteristics
# mainly used with transmission_main_example.py # mainly used with transmission_main_example.py
@@ -71,3 +323,79 @@ def trx_mode_params(equipment, trx_type_variety='', trx_mode='', error_message=F
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 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

View File

@@ -12,26 +12,18 @@ Exceptions thrown by other gnpy modules
class ConfigurationError(Exception): class ConfigurationError(Exception):
'''User-provided configuration contains an error''' '''User-provided configuration contains an error'''
class EquipmentConfigError(ConfigurationError): class EquipmentConfigError(ConfigurationError):
'''Incomplete or wrong configuration within the equipment library''' '''Incomplete or wrong configuration within the equipment library'''
class NetworkTopologyError(ConfigurationError): class NetworkTopologyError(ConfigurationError):
'''Topology of user-provided network is wrong''' '''Topology of user-provided network is wrong'''
class ServiceError(Exception): class ServiceError(Exception):
'''Service of user-provided request is wrong''' '''Service of user-provided request is wrong'''
class DisjunctionError(ServiceError): class DisjunctionError(ServiceError):
'''Disjunction of user-provided request can not be satisfied''' '''Disjunction of user-provided request can not be satisfied'''
class SpectrumError(Exception): class SpectrumError(Exception):
'''Spectrum errors of the program''' '''Spectrum errors of the program'''
class ParametersError(ConfigurationError):
'''Incomplete or wrong configurations within parameters json'''

10
gnpy/core/execute.py Normal file
View File

@@ -0,0 +1,10 @@
#!/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.
'''

View File

@@ -10,24 +10,18 @@ This module contains classes for modelling :class:`SpectralInformation`.
from collections import namedtuple from collections import namedtuple
from gnpy.core.utils import automatic_nch, lin2db 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
class Power(namedtuple('Power', 'signal nli ase')): class Power(namedtuple('Power', 'signal nli ase')):
"""carriers power in W""" """carriers power in W"""
class Channel(namedtuple('Channel', 'channel_number frequency baud_rate roll_off power chromatic_dispersion pmd')): class Channel(namedtuple('Channel', 'channel_number frequency baud_rate roll_off power')):
""" Class containing the parameters of a WDM signal. pass
: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 ')): class Pref(namedtuple('Pref', 'p_span0, p_spani, neq_ch ')):
@@ -51,7 +45,28 @@ def create_input_spectral_information(f_min, f_max, roll_off, baud_rate, power,
pref=Pref(pref, pref, lin2db(nb_channel)), pref=Pref(pref, pref, lin2db(nb_channel)),
carriers=[ carriers=[
Channel(f, (f_min+spacing*f), Channel(f, (f_min+spacing*f),
baud_rate, roll_off, Power(power, 0, 0), 0, 0) for f in range(1, nb_channel + 1) baud_rate, roll_off, Power(power, 0, 0)) for f in range(1,nb_channel+1)
] ])
)
return si 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
)
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}')

View File

@@ -5,31 +5,92 @@
gnpy.core.network gnpy.core.network
================= =================
Working with networks which consist of network elements This module contains functions for constructing networks of network elements.
''' '''
from gnpy.core.convert import convert_file
from networkx import DiGraph
from numpy import arange
from scipy.interpolate import interp1d from scipy.interpolate import interp1d
from operator import attrgetter from logging import getLogger
from gnpy.core import ansi_escapes, elements 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 gnpy.core.exceptions import ConfigurationError, NetworkTopologyError from gnpy.core.exceptions import ConfigurationError, NetworkTopologyError
from gnpy.core.utils import round2float, convert_length 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 collections import namedtuple from collections import namedtuple
logger = getLogger(__name__)
def edfa_nf(gain_target, variety_type, equipment): def load_network(filename, equipment, name_matching = False):
amp_params = equipment['Edfa'][variety_type] json_filename = ''
amp = elements.Edfa( if filename.suffix.lower() == '.xls':
uid='calc_NF', logger.info('Automatically generating topology JSON file')
params=amp_params.__dict__, json_filename = convert_file(filename, name_matching)
operational={ elif filename.suffix.lower() == '.json':
'gain_target': gain_target, json_filename = filename
'tilt_target': 0 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]
} }
) connections = {
amp.pin_db = 0 'connections': [{"from_node": n.uid,
amp.nch = 88 "to_node": next_n.uid}
return amp._calc_nf(True) for n in network
for next_n in network.successors(n) if next_n is not None]
}
data.update(connections)
return data
def select_edfa(raman_allowed, gain_target, power_target, equipment, uid, restrictions=None): def select_edfa(raman_allowed, gain_target, power_target, equipment, uid, restrictions=None):
"""amplifer selection algorithm """amplifer selection algorithm
@@ -61,9 +122,10 @@ def select_edfa(raman_allowed, gain_target, power_target, equipment, uid, restri
edfa.p_max edfa.p_max
) )
-power_target, -power_target,
gain_min=gain_target + 3 gain_min=
gain_target+3
-edfa.gain_min, -edfa.gain_min,
nf=edfa_nf(gain_target, edfa_variety, equipment)) nf=edfa_nf(gain_target, edfa_variety, equipment)) \
for edfa_variety, edfa in edfa_dict.items() for edfa_variety, edfa in edfa_dict.items()
if ((edfa.allowed_for_design or restrictions is not None) and not edfa.raman)] if ((edfa.allowed_for_design or restrictions is not None) and not edfa.raman)]
@@ -78,7 +140,8 @@ def select_edfa(raman_allowed, gain_target, power_target, equipment, uid, restri
edfa.p_max edfa.p_max
) )
-power_target, -power_target,
gain_min=gain_target gain_min=
gain_target
-edfa.gain_min, -edfa.gain_min,
nf=edfa_nf(gain_target, edfa_variety, equipment)) nf=edfa_nf(gain_target, edfa_variety, equipment))
for edfa_variety, edfa in edfa_dict.items() for edfa_variety, edfa in edfa_dict.items()
@@ -104,8 +167,10 @@ def select_edfa(raman_allowed, gain_target, power_target, equipment, uid, restri
else: else:
# TODO: convert to logging # TODO: convert to logging
print( print(
f'{ansi_escapes.red}WARNING:{ansi_escapes.reset} target gain in node {uid} is below all available amplifiers min gain: \ f'\x1b[1;31;40m'\
amplifier input padding will be assumed, consider increase span fiber padding instead' + 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'
) )
acceptable_gain_min_list = edfa_list acceptable_gain_min_list = edfa_list
@@ -122,6 +187,7 @@ def select_edfa(raman_allowed, gain_target, power_target, equipment, uid, restri
acceptable_power_list = [x for x in acceptable_gain_min_list 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, # gain and power requirements are resolved,
# =>chose the amp with the best NF among the acceptable ones: # =>chose the amp with the best NF among the acceptable ones:
selected_edfa = min(acceptable_power_list, key=attrgetter('nf')) #filter on NF selected_edfa = min(acceptable_power_list, key=attrgetter('nf')) #filter on NF
@@ -129,17 +195,20 @@ def select_edfa(raman_allowed, gain_target, power_target, equipment, uid, restri
power_reduction = round(min(selected_edfa.power, 0),2) power_reduction = round(min(selected_edfa.power, 0),2)
if power_reduction < -0.5: if power_reduction < -0.5:
print( print(
f'{ansi_escapes.red}WARNING:{ansi_escapes.reset} target gain and power in node {uid}\n \ f'\x1b[1;31;40m'\
+ f'WARNING: target gain and power in node {uid}\n \
is beyond all available amplifiers capabilities and/or extended_gain_range:\n\ is beyond all available amplifiers capabilities and/or extended_gain_range:\n\
a power reduction of {power_reduction} is applied\n' a power reduction of {power_reduction} is applied\n'\
+ '\x1b[0m'
) )
return selected_edfa.variety, power_reduction
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 SPAN_LOSS_REF = 20
POWER_SLOPE = 0.3 POWER_SLOPE = 0.3
power_mode = equipment['Span']['default'].power_mode
dp_range = list(equipment['Span']['default'].delta_power_range_db) dp_range = list(equipment['Span']['default'].delta_power_range_db)
node_loss = span_loss(network, node) node_loss = span_loss(network, node)
@@ -151,12 +220,11 @@ def target_power(network, node, equipment): # get_fiber_dp
raise ConfigurationError(f'invalid delta_power_range_db definition in eqpt_config[Span]' 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, elements.Roadm): if isinstance(node, Roadm):
dp = 0 dp = 0
return dp return dp
def prev_node_generator(network, node): def prev_node_generator(network, node):
"""fused spans interest: """fused spans interest:
iterate over all predecessors while they are Fused or Fiber type""" iterate over all predecessors while they are Fused or Fiber type"""
@@ -165,13 +233,12 @@ def prev_node_generator(network, node):
except StopIteration: except StopIteration:
raise NetworkTopologyError(f'Node {node.uid} is not properly connected, please check network topology') raise NetworkTopologyError(f'Node {node.uid} is not properly connected, please check network topology')
# yield and re-iterate # yield and re-iterate
if isinstance(prev_node, elements.Fused) or isinstance(node, elements.Fused): if isinstance(prev_node, Fused) or isinstance(node, Fused) and not isinstance(prev_node, Roadm):
yield prev_node yield prev_node
yield from prev_node_generator(network, prev_node) yield from prev_node_generator(network, prev_node)
else: else:
StopIteration StopIteration
def next_node_generator(network, node): def next_node_generator(network, node):
"""fused spans interest: """fused spans interest:
iterate over all successors while they are Fused or Fiber type""" iterate over all successors while they are Fused or Fiber type"""
@@ -180,32 +247,30 @@ def next_node_generator(network, node):
except StopIteration: except StopIteration:
raise NetworkTopologyError('Node {node.uid} is not properly connected, please check network topology') raise NetworkTopologyError('Node {node.uid} is not properly connected, please check network topology')
# yield and re-iterate # yield and re-iterate
if isinstance(next_node, elements.Fused) or isinstance(node, elements.Fused): if isinstance(next_node, Fused) or isinstance(node, Fused) and not isinstance(next_node, Roadm):
yield next_node yield next_node
yield from next_node_generator(network, next_node) yield from next_node_generator(network, next_node)
else: else:
StopIteration StopIteration
def span_loss(network, node): def span_loss(network, node):
"""Fused span interest: """Fused span interest:
return the total span loss of all the fibers spliced by a Fused node""" return the total span loss of all the fibers spliced by a Fused node"""
loss = node.loss if node.passive else 0 loss = node.loss if node.passive else 0
try: try:
prev_node = next(n for n in network.predecessors(node)) prev_node = next(n for n in network.predecessors(node))
if isinstance(prev_node, elements.Fused): if isinstance(prev_node, Fused):
loss += sum(n.loss for n in prev_node_generator(network, node)) loss += sum(n.loss for n in prev_node_generator(network, node))
except StopIteration: except StopIteration:
pass pass
try: try:
next_node = next(n for n in network.successors(node)) next_node = next(n for n in network.successors(node))
if isinstance(next_node, elements.Fused): if isinstance(next_node, Fused):
loss += sum(n.loss for n in next_node_generator(network, node)) loss += sum(n.loss for n in next_node_generator(network, node))
except StopIteration: except StopIteration:
pass pass
return loss return loss
def find_first_node(network, node): def find_first_node(network, node):
"""Fused node interest: """Fused node interest:
returns the 1st node at the origin of a succession of fused nodes returns the 1st node at the origin of a succession of fused nodes
@@ -215,7 +280,6 @@ def find_first_node(network, node):
pass pass
return this_node return this_node
def find_last_node(network, node): def find_last_node(network, node):
"""Fused node interest: """Fused node interest:
returns the last node in a succession of fused nodes returns the last node in a succession of fused nodes
@@ -225,11 +289,11 @@ def find_last_node(network, node):
pass pass
return this_node return this_node
def set_amplifier_voa(amp, power_target, power_mode): 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 amp.out_voa is None:
if power_mode: if power_mode:
gain_target = amp.effective_gain
voa = min(amp.params.p_max-power_target, voa = min(amp.params.p_max-power_target,
amp.params.gain_flatmax-amp.effective_gain) 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 voa = max(round2float(max(voa, 0), 0.5) - VOA_MARGIN, 0) if amp.params.out_voa_auto else 0
@@ -239,26 +303,36 @@ def set_amplifier_voa(amp, power_target, power_mode):
voa = 0 # no output voa optimization in gain mode voa = 0 # no output voa optimization in gain mode
amp.out_voa = voa amp.out_voa = voa
def set_egress_amplifier(network, roadm, equipment, pref_total_db): def set_egress_amplifier(network, roadm, equipment, pref_total_db):
power_mode = equipment['Span']['default'].power_mode power_mode = equipment['Span']['default'].power_mode
next_oms = (n for n in network.successors(roadm) if not isinstance(n, elements.Transceiver)) next_oms = (n for n in network.successors(roadm) if not isinstance(n, Transceiver))
for oms in next_oms: 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 node = roadm
prev_node = roadm prev_node = roadm
next_node = oms next_node = oms
# if isinstance(next_node, elements.Fused): #support ROADM wo egress amp for metro applications # if isinstance(next_node, Fused): #support ROADM wo egress amp for metro applications
# node = find_last_node(next_node) # node = find_last_node(next_node)
# next_node = next(n for n in network.successors(node)) # next_node = next(n for n in network.successors(node))
# next_node = find_last_node(next_node) # next_node = find_last_node(next_node)
if node.per_degree_target_pch_out_db:
# find the target power on this degree
try:
prev_dp = next(el["target_pch_out_db"] for el in \
node.per_degree_target_pch_out_db if el["to_node"]==next_node.uid)
except StopIteration:
# if no target power is defined on this degree use the global one
prev_dp = getattr(node.params, 'target_pch_out_db', 0)
else:
# if no per degree target power is given use the global one
prev_dp = getattr(node.params, 'target_pch_out_db', 0) prev_dp = getattr(node.params, 'target_pch_out_db', 0)
dp = prev_dp dp = prev_dp
prev_voa = 0 prev_voa = 0
voa = 0 voa = 0
while True: while True:
#go through all nodes in the OMS (loop until next Roadm instance) #go through all nodes in the OMS (loop until next Roadm instance)
if isinstance(node, elements.Edfa): if isinstance(node, Edfa):
node_loss = span_loss(network, prev_node) node_loss = span_loss(network, prev_node)
voa = node.out_voa if node.out_voa else 0 voa = node.out_voa if node.out_voa else 0
if node.delta_p is None: if node.delta_p is None:
@@ -275,18 +349,18 @@ def set_egress_amplifier(network, roadm, equipment, pref_total_db):
power_target = pref_total_db + dp power_target = pref_total_db + dp
raman_allowed = False raman_allowed = False
if isinstance(prev_node, elements.Fiber): if isinstance(prev_node, Fiber):
max_fiber_lineic_loss_for_raman = \ 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 raman_allowed = prev_node.params.loss_coef < max_fiber_lineic_loss_for_raman
# implementation of restrictions on roadm boosters # implementation of restrictions on roadm boosters
if isinstance(prev_node, elements.Roadm): if isinstance(prev_node,Roadm):
if prev_node.restrictions['booster_variety_list']: if prev_node.restrictions['booster_variety_list']:
restrictions = prev_node.restrictions['booster_variety_list'] restrictions = prev_node.restrictions['booster_variety_list']
else: else:
restrictions = None restrictions = None
elif isinstance(next_node, elements.Roadm): elif isinstance(next_node,Roadm):
# implementation of restrictions on roadm preamp # implementation of restrictions on roadm preamp
if next_node.restrictions['preamp_variety_list']: if next_node.restrictions['preamp_variety_list']:
restrictions = next_node.restrictions['preamp_variety_list'] restrictions = next_node.restrictions['preamp_variety_list']
@@ -296,18 +370,24 @@ def set_egress_amplifier(network, roadm, equipment, pref_total_db):
restrictions = None restrictions = None
if node.params.type_variety == '': if node.params.type_variety == '':
edfa_variety, power_reduction = select_edfa(raman_allowed, gain_target, power_target, equipment, node.uid, restrictions) edfa_variety, power_reduction = select_edfa(raman_allowed,
gain_target, power_target, equipment, node.uid, restrictions)
extra_params = equipment['Edfa'][edfa_variety] extra_params = equipment['Edfa'][edfa_variety]
node.params.update_params(extra_params.__dict__) node.params.update_params(extra_params.__dict__)
dp += power_reduction dp += power_reduction
gain_target += power_reduction gain_target += power_reduction
elif node.params.raman and not raman_allowed: elif node.params.raman and not raman_allowed:
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') 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'
)
node.delta_p = dp if power_mode else None node.delta_p = dp if power_mode else None
node.effective_gain = gain_target node.effective_gain = gain_target
set_amplifier_voa(node, power_target, power_mode) set_amplifier_voa(node, power_target, power_mode)
if isinstance(next_node, elements.Roadm) or isinstance(next_node, elements.Transceiver): if isinstance(next_node, Roadm) or isinstance(next_node, Transceiver):
break break
prev_dp = dp prev_dp = dp
prev_voa = voa prev_voa = voa
@@ -319,11 +399,11 @@ def set_egress_amplifier(network, roadm, equipment, pref_total_db):
def add_egress_amplifier(network, node): def add_egress_amplifier(network, node):
next_nodes = [n for n in network.successors(node) next_nodes = [n for n in network.successors(node)
if not (isinstance(n, elements.Transceiver) or isinstance(n, elements.Fused) or isinstance(n, elements.Edfa))] if not (isinstance(n, Transceiver) or isinstance(n, Fused) or isinstance(n, Edfa))]
#no amplification for fused spans or TRX #no amplification for fused spans or TRX
for i, next_node in enumerate(next_nodes): for i, next_node in enumerate(next_nodes):
network.remove_edge(node, next_node) network.remove_edge(node, next_node)
amp = elements.Edfa( amp = Edfa(
uid = f'Edfa{i}_{node.uid}', uid = f'Edfa{i}_{node.uid}',
params = {}, params = {},
metadata = { metadata = {
@@ -339,7 +419,7 @@ def add_egress_amplifier(network, node):
'tilt_target': 0, 'tilt_target': 0,
}) })
network.add_node(amp) network.add_node(amp)
if isinstance(node, elements.Fiber): if isinstance(node,Fiber):
edgeweight = node.params.length edgeweight = node.params.length
else: else:
edgeweight = 0.01 edgeweight = 0.01
@@ -372,7 +452,7 @@ def calculate_new_length(fiber_length, bounds, target_length):
def split_fiber(network, fiber, bounds, target_length, equipment): def split_fiber(network, fiber, bounds, target_length, equipment):
new_length, n_spans = calculate_new_length(fiber.params.length, bounds, target_length) new_length, n_spans = calculate_new_length(fiber.length, bounds, target_length)
if n_spans == 1: if n_spans == 1:
return return
@@ -384,14 +464,16 @@ def split_fiber(network, fiber, bounds, target_length, equipment):
network.remove_node(fiber) network.remove_node(fiber)
fiber.params.length = new_length 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
f = interp1d([prev_node.lng, next_node.lng], [prev_node.lat, next_node.lat]) 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) ypos = f(xpos)
for span, lng, lat in zip(range(n_spans), xpos, ypos): for span, lng, lat in zip(range(n_spans), xpos, ypos):
new_span = elements.Fiber(uid=f'{fiber.uid}_({span+1}/{n_spans})', new_span = Fiber(uid = f'{fiber.uid}_({span+1}/{n_spans})',
type_variety=fiber.type_variety,
metadata = { metadata = {
'location': { 'location': {
'latitude': lat, 'latitude': lat,
@@ -400,58 +482,53 @@ def split_fiber(network, fiber, bounds, target_length, equipment):
'region': fiber.loc.region, 'region': fiber.loc.region,
} }
}, },
params=fiber.params.asdict()) params = fiber_params)
if isinstance(prev_node, elements.Fiber): if isinstance(prev_node,Fiber):
edgeweight = prev_node.params.length edgeweight = prev_node.params.length
else: else:
edgeweight = 0.01 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 prev_node = new_span
if isinstance(prev_node, elements.Fiber): if isinstance(prev_node,Fiber):
edgeweight = prev_node.params.length edgeweight = prev_node.params.length
else: else:
edgeweight = 0.01 edgeweight = 0.01
network.add_edge(prev_node, next_node, weight = edgeweight) network.add_edge(prev_node, next_node, weight = edgeweight)
def add_connector_loss(network, fibers, default_con_in, default_con_out, EOL): def add_connector_loss(network, fibers, default_con_in, default_con_out, EOL):
for fiber in fibers: for fiber in fibers:
if fiber.params.con_in is None: if fiber.con_in is None: fiber.con_in = default_con_in
fiber.params.con_in = default_con_in if fiber.con_out is None: fiber.con_out = default_con_out
if fiber.params.con_out is None:
fiber.params.con_out = default_con_out
next_node = next(n for n in network.successors(fiber)) next_node = next(n for n in network.successors(fiber))
if not isinstance(next_node, elements.Fused): if not isinstance(next_node, Fused):
fiber.params.con_out += EOL fiber.con_out += EOL
def add_fiber_padding(network, fibers, padding): def add_fiber_padding(network, fibers, padding):
"""last_fibers = (fiber for n in network.nodes() """last_fibers = (fiber for n in network.nodes()
if not (isinstance(n, elements.Fiber) or isinstance(n, elements.Fused)) if not (isinstance(n, Fiber) or isinstance(n, Fused))
for fiber in network.predecessors(n) for fiber in network.predecessors(n)
if isinstance(fiber, elements.Fiber))""" if isinstance(fiber, Fiber))"""
for fiber in fibers: for fiber in fibers:
this_span_loss = span_loss(network, fiber) this_span_loss = span_loss(network, fiber)
try: try:
next_node = next(network.successors(fiber)) next_node = next(network.successors(fiber))
except StopIteration: except StopIteration:
raise NetworkTopologyError(f'Fiber {fiber.uid} is not properly connected, please check network topology') raise NetworkTopologyError(f'Fiber {fiber.uid} is not properly connected, please check network topology')
if this_span_loss < padding and not (isinstance(next_node, elements.Fused)): if this_span_loss < padding and not (isinstance(next_node, Fused)):
#add a padding att_in at the input of the 1st fiber: #add a padding att_in at the input of the 1st fiber:
#address the case when several fibers are spliced together #address the case when several fibers are spliced together
first_fiber = find_first_node(network, fiber) first_fiber = find_first_node(network, fiber)
# in order to support no booster , fused might be placed # in order to support no booster , fused might be placed
# just after a roadm: need to check that first_fiber is really a fiber # just after a roadm: need to check that first_fiber is really a fiber
if isinstance(first_fiber, elements.Fiber): if isinstance(first_fiber,Fiber):
if first_fiber.params.att_in is None: if first_fiber.att_in is None:
first_fiber.params.att_in = padding - this_span_loss first_fiber.att_in = padding - this_span_loss
else: else:
first_fiber.params.att_in = first_fiber.params.att_in + padding - this_span_loss first_fiber.att_in = first_fiber.att_in + padding - this_span_loss
def build_network(network, equipment, pref_ch_db, pref_total_db): def build_network(network, equipment, pref_ch_db, pref_total_db):
default_span_data = equipment['Span']['default'] default_span_data = equipment['Span']['default']
max_length = int(convert_length(default_span_data.max_length, default_span_data.length_units)) 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) min_length = max(int(default_span_data.padding/0.2*1e3),50_000)
bounds = range(min_length, max_length) bounds = range(min_length, max_length)
target_length = max(min_length, 90_000) target_length = max(min_length, 90_000)
@@ -460,7 +537,7 @@ def build_network(network, equipment, pref_ch_db, pref_total_db):
padding = default_span_data.padding padding = default_span_data.padding
#set roadm loss for gain_mode before to build network #set roadm loss for gain_mode before to build network
fibers = [f for f in network.nodes() if isinstance(f, elements.Fiber)] fibers = [f for f in network.nodes() if isinstance(f, Fiber)]
add_connector_loss(network, fibers, default_con_in, default_con_out, default_span_data.EOL) add_connector_loss(network, fibers, default_con_in, default_con_out, default_span_data.EOL)
add_fiber_padding(network, fibers, padding) add_fiber_padding(network, fibers, padding)
# don't group split fiber and add amp in the same loop # don't group split fiber and add amp in the same loop
@@ -468,17 +545,27 @@ def build_network(network, equipment, pref_ch_db, pref_total_db):
for fiber in fibers: for fiber in fibers:
split_fiber(network, fiber, bounds, target_length, equipment) split_fiber(network, fiber, bounds, target_length, equipment)
amplified_nodes = [n for n in network.nodes() if isinstance(n, elements.Fiber) or isinstance(n, elements.Roadm)] amplified_nodes = [n for n in network.nodes()
if isinstance(n, Fiber) or isinstance(n, Roadm)]
for node in amplified_nodes: for node in amplified_nodes:
add_egress_amplifier(network, node) add_egress_amplifier(network, node)
roadms = [r for r in network.nodes() if isinstance(r, elements.Roadm)] roadms = [r for r in network.nodes() if isinstance(r, Roadm)]
for roadm in roadms: for roadm in roadms:
set_egress_amplifier(network, roadm, equipment, pref_total_db) 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: if len(roadms) == 0:
trx = [t for t in network.nodes() if isinstance(t, elements.Transceiver)] trx = [t for t in network.nodes() if isinstance(t, Transceiver)]
for t in trx: for t in trx:
set_egress_amplifier(network, t, equipment, pref_total_db) 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

56
gnpy/core/node.py Normal file
View File

@@ -0,0 +1,56 @@
#! /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

View File

@@ -1,287 +0,0 @@
#!/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

View File

@@ -2,8 +2,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
gnpy.topology.request gnpy.core.request
===================== =================
This module contains path request functionality. This module contains path request functionality.
@@ -16,17 +16,16 @@ See: draft-ietf-teas-yang-path-computation-01.txt
""" """
from collections import namedtuple, OrderedDict from collections import namedtuple, OrderedDict
from logging import getLogger from logging import getLogger, basicConfig, CRITICAL, DEBUG, INFO
from networkx import (dijkstra_path, NetworkXNoPath, from networkx import (dijkstra_path, NetworkXNoPath, all_simple_paths)
all_simple_paths, shortest_simple_paths)
from networkx.utils import pairwise from networkx.utils import pairwise
from numpy import mean from numpy import mean
from gnpy.core.elements import Transceiver, Roadm from gnpy.core.service_sheet import convert_service_sheet, Request_element, Element
from gnpy.core.utils import lin2db from gnpy.core.elements import Transceiver, Roadm, Edfa, Fused
from gnpy.core.info import create_input_spectral_information from gnpy.core.utils import db2lin, lin2db
from gnpy.core.info import create_input_spectral_information, SpectralInformation, Channel, Power
from gnpy.core.exceptions import ServiceError, DisjunctionError from gnpy.core.exceptions import ServiceError, DisjunctionError
import gnpy.core.ansi_escapes as ansi_escapes from copy import copy, deepcopy
from copy import deepcopy
from csv import writer from csv import writer
from math import ceil from math import ceil
@@ -35,15 +34,13 @@ LOGGER = getLogger(__name__)
RequestParams = namedtuple('RequestParams', 'request_id source destination bidir trx_type' + RequestParams = namedtuple('RequestParams', 'request_id source destination bidir trx_type' +
' trx_mode nodes_list loose_list spacing power nb_channel f_min' + ' trx_mode nodes_list loose_list spacing power nb_channel f_min' +
' f_max format baud_rate OSNR bit_rate roll_off tx_osnr' + ' f_max format baud_rate OSNR bit_rate roll_off tx_osnr' +
' min_spacing cost path_bandwidth effective_freq_slot') ' min_spacing cost path_bandwidth')
DisjunctionParams = namedtuple('DisjunctionParams', 'disjunction_id relaxable link' + DisjunctionParams = namedtuple('DisjunctionParams', 'disjunction_id relaxable link' +
'_diverse node_diverse disjunctions_req') '_diverse node_diverse disjunctions_req')
class Path_request:
class PathRequest:
""" the class that contains all attributes related to a request """ the class that contains all attributes related to a request
""" """
def __init__(self, *args, **params): def __init__(self, *args, **params):
params = RequestParams(**params) params = RequestParams(**params)
self.request_id = params.request_id self.request_id = params.request_id
@@ -68,15 +65,11 @@ class PathRequest:
self.min_spacing = params.min_spacing self.min_spacing = params.min_spacing
self.cost = params.cost self.cost = params.cost
self.path_bandwidth = params.path_bandwidth self.path_bandwidth = params.path_bandwidth
if params.effective_freq_slot is not None:
self.N = params.effective_freq_slot['N']
self.M = params.effective_freq_slot['M']
def __str__(self): def __str__(self):
return '\n\t'.join([f'{type(self).__name__} {self.request_id}', return '\n\t'.join([f'{type(self).__name__} {self.request_id}',
f'source: {self.source}', f'source: {self.source}',
f'destination: {self.destination}']) f'destination: {self.destination}'])
def __repr__(self): def __repr__(self):
if self.baud_rate is not None: if self.baud_rate is not None:
temp = self.baud_rate * 1e-9 temp = self.baud_rate * 1e-9
@@ -99,12 +92,9 @@ class PathRequest:
f'nodes-list:\t{self.nodes_list}', f'nodes-list:\t{self.nodes_list}',
f'loose-list:\t{self.loose_list}' f'loose-list:\t{self.loose_list}'
'\n']) '\n'])
class Disjunction: class Disjunction:
""" the class that contains all attributes related to disjunction constraints """ the class that contains all attributes related to disjunction constraints
""" """
def __init__(self, *args, **params): def __init__(self, *args, **params):
params = DisjunctionParams(**params) params = DisjunctionParams(**params)
self.disjunction_id = params.disjunction_id self.disjunction_id = params.disjunction_id
@@ -118,7 +108,6 @@ class Disjunction:
f'link-diverse: {self.link_diverse}', f'link-diverse: {self.link_diverse}',
f'node-diverse: {self.node_diverse}', f'node-diverse: {self.node_diverse}',
f'request-id-numbers: {self.disjunctions_req}']) f'request-id-numbers: {self.disjunctions_req}'])
def __repr__(self): def __repr__(self):
return '\n\t'.join([f'{type(self).__name__} {self.disjunction_id}', return '\n\t'.join([f'{type(self).__name__} {self.disjunction_id}',
f'relaxable: {self.relaxable}', f'relaxable: {self.relaxable}',
@@ -127,15 +116,22 @@ class Disjunction:
f'request-id-numbers: {self.disjunctions_req}' f'request-id-numbers: {self.disjunctions_req}'
'\n']) '\n'])
BLOCKING_NOPATH = ['NO_PATH', 'NO_PATH_WITH_CONSTRAINT',\
BLOCKING_NOPATH = ['NO_PATH', 'NO_PATH_WITH_CONSTRAINT', 'NO_FEASIBLE_BAUDRATE_WITH_SPACING',\
'NO_FEASIBLE_BAUDRATE_WITH_SPACING',
'NO_COMPUTED_SNR'] 'NO_COMPUTED_SNR']
BLOCKING_NOMODE = ['NO_FEASIBLE_MODE', 'MODE_NOT_FEASIBLE'] BLOCKING_NOMODE = ['NO_FEASIBLE_MODE', 'MODE_NOT_FEASIBLE']
BLOCKING_NOSPECTRUM = 'NO_SPECTRUM' BLOCKING_NOSPECTRUM = 'NO_SPECTRUM'
def element_to_node_type(element):
if isinstance(element, Transceiver):
return "transceiver"
if isinstance(element, Edfa):
return "EDFA"
if isinstance(element, Roadm):
return "ROADM"
return None
class ResultElement: class Result_element(Element):
def __init__(self, path_request, computed_path, reversed_computed_path=None): def __init__(self, path_request, computed_path, reversed_computed_path=None):
self.path_id = path_request.request_id self.path_id = path_request.request_id
self.path_request = path_request self.path_request = path_request
@@ -143,16 +139,14 @@ class ResultElement:
# starting implementing reversed properties in case of bidir demand # starting implementing reversed properties in case of bidir demand
if reversed_computed_path is not None: if reversed_computed_path is not None:
self.reversed_computed_path = reversed_computed_path self.reversed_computed_path = reversed_computed_path
uid = property(lambda self: repr(self)) uid = property(lambda self: repr(self))
@property def detailed_path_json(self, path):
def detailed_path_json(self):
""" a function that builds path object for normal and blocking cases """ a function that builds path object for normal and blocking cases
""" """
index = 0 index = 0
pro_list = [] pro_list = []
for element in self.computed_path: for element in path:
temp = { temp = {
'path-route-object': { 'path-route-object': {
'index': index, 'index': index,
@@ -163,6 +157,9 @@ class ResultElement:
} }
} }
} }
node_type = element_to_node_type(element)
if (node_type is not None):
temp['path-route-object']['num-unnum-hop']['gnpy-node-type'] = node_type
pro_list.append(temp) pro_list.append(temp)
index += 1 index += 1
if self.path_request.M > 0: if self.path_request.M > 0:
@@ -195,8 +192,32 @@ class ResultElement:
} }
pro_list.append(temp) pro_list.append(temp)
index += 1 index += 1
if isinstance(element, Roadm):
temp = {
'path-route-object': {
'index': index,
'target-channel-power' : {
'value' : element.effective_pch_out_db,
}
}
}
pro_list.append(temp)
index += 1
if isinstance(element, Edfa):
temp = {
'path-route-object': {
'index': index,
'target-channel-power' : {
'value': element.effective_pch_out_db,
},
'output-voa': {
'value': element.out_voa,
}
}
}
pro_list.append(temp)
index += 1
return pro_list return pro_list
@property @property
def path_properties(self): def path_properties(self):
""" a function that returns the path properties (metrics, crossed elements) into a dict """ a function that returns the path properties (metrics, crossed elements) into a dict
@@ -234,12 +255,13 @@ class ResultElement:
path_properties = { path_properties = {
'path-metric': path_metric(self.computed_path, self.path_request), 'path-metric': path_metric(self.computed_path, self.path_request),
'z-a-path-metric': path_metric(self.reversed_computed_path, self.path_request), 'z-a-path-metric': path_metric(self.reversed_computed_path, self.path_request),
'path-route-objects': self.detailed_path_json 'path-route-objects': self.detailed_path_json(self.computed_path),
'reversed-path-route-objects': self.detailed_path_json(self.reversed_computed_path),
} }
else: else:
path_properties = { path_properties = {
'path-metric': path_metric(self.computed_path, self.path_request), 'path-metric': path_metric(self.computed_path, self.path_request),
'path-route-objects': self.detailed_path_json 'path-route-objects': self.detailed_path_json(self.computed_path)
} }
return path_properties return path_properties
@@ -276,85 +298,155 @@ class ResultElement:
def json(self): def json(self):
return self.pathresult return self.pathresult
def compute_constrained_path(network, req): 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_elem in req.nodes_list:
# for debug excel print(n)
nodes_list.append(next(el for el in anytypenode if el.uid == n_elem))
# nodes_list contains at least the destination # nodes_list contains at least the destination
if nodes_list is None:
# only arrive here if there is a bug in the program because route lists have
# been corrected and harmonized before
msg = f'Request {req.request_id} problem in the constitution of nodes_list: ' +\
'should at least include destination'
LOGGER.critical(msg)
raise ValueError(msg)
if req.nodes_list[-1] != req.destination: if req.nodes_list[-1] != req.destination:
# only arrive here if there is a bug in the program because route lists have # only arrive here if there is a bug in the program because route lists have
# been corrected and harmonized before # been corrected and harmonized before
msg = (f'Request {req.request_id} malformed list of nodes: last node should ' msg = f'Request {req.request_id} malformed list of nodes: last node should '+\
'be destination trx') 'be destination trx'
LOGGER.critical(msg) LOGGER.critical(msg)
raise ValueError() raise ValueError()
trx = [n for n in network if isinstance(n, Transceiver)] if len(nodes_list) == 1:
source = next(el for el in trx if el.uid == req.source)
destination = next(el for el in trx if el.uid == req.destination)
nodes_list = []
for node in req.nodes_list[:-1]:
nodes_list.append(next(el for el in network if el.uid == node))
try: try:
path_generator = shortest_simple_paths(network, source, destination, weight='weight') total_path = dijkstra_path(network, source, destination, weight='weight')
total_path = next(path for path in path_generator if ispart(nodes_list, path)) # 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: except NetworkXNoPath:
msg = (f'{ansi_escapes.yellow}Request {req.request_id} could not find a path from' msg = f'\x1b[1;33;40m'+f'Request {req.request_id} could not find a path from' +\
f' {source.uid} to node: {destination.uid} in network topology{ansi_escapes.reset}') f' {source.uid} to node: {destination.uid} in network topology'+ '\x1b[0m'
LOGGER.critical(msg) LOGGER.critical(msg)
print(msg) print(msg)
req.blocking_reason = 'NO_PATH' req.blocking_reason = 'NO_PATH'
total_path = [] total_path = []
except StopIteration: else:
all_simp_pths = list(all_simple_paths(network, source=source,\
target=destination, cutoff=120))
candidate = []
for pth in all_simp_pths:
if ispart(nodes_list, pth):
# print(f'selection{[el.uid for el in p if el in roadm]}')
candidate.append(pth)
# 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 # TODO: better account for individual loose and strict node
# to ease: suppose that one strict makes the whole liste strict (except for the # to ease: suppose that one strict makes the whole liste strict (except for the
# last node which is the transceiver) # last node which is the transceiver)
# if all nodes i n node_list are LOOSE constraint, skip the constraints and find # 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 # a path w/o constraints, else there is no possible path
print(f'{ansi_escapes.yellow}Request {req.request_id} could not find a path crossing ' if nodes_list[:-len("STRICT")]:
f'{[el.uid for el in nodes_list[:-1]]} in network topology{ansi_escapes.reset}') 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')
if 'STRICT' not in req.loose_list[:-1]: else:
msg = (f'{ansi_escapes.yellow}Request {req.request_id} could not find a path with user_' print(f'\x1b[1;33;40m'+f'User include_node constraints could not be applied ' +\
f'include node constraints{ansi_escapes.reset}') 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) LOGGER.info(msg)
print(f'constraint ignored') print(f'constraint ignored')
total_path = dijkstra_path(network, source, destination, weight='weight') total_path = dijkstra_path(network, source, destination, weight='weight')
else: else:
# one STRICT makes the whole list STRICT msg = f'\x1b[1;33;40m'+f'Request {req.request_id} could not find a path with user ' +\
msg = (f'{ansi_escapes.yellow}Request {req.request_id} could not find a path with user ' f'include node constraints.\nNo path computed'+ '\x1b[0m'
f'include node constraints.\nNo path computed{ansi_escapes.reset}')
LOGGER.critical(msg) LOGGER.critical(msg)
print(msg) print(msg)
req.blocking_reason = 'NO_PATH_WITH_CONSTRAINT' req.blocking_reason = 'NO_PATH_WITH_CONSTRAINT'
total_path = [] total_path = []
return total_path # the following method was initially used but abandonned: compute per segment:
# 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 segments 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
return total_path
def propagate(path, req, equipment): def propagate(path, req, equipment):
si = create_input_spectral_information( si = create_input_spectral_information(
req.f_min, req.f_max, req.roll_off, req.baud_rate, req.f_min, req.f_max, req.roll_off, req.baud_rate,
req.power, req.spacing) req.power, req.spacing)
for el in path: for i, el in enumerate(path):
if isinstance(el, Roadm):
next_el = path[i+1]
si = el(si, degree=next_el.uid)
else:
si = el(si) si = el(si)
print(el)
path[-1].update_snr(req.tx_osnr, equipment['Roadm']['default'].add_drop_osnr) path[-1].update_snr(req.tx_osnr, equipment['Roadm']['default'].add_drop_osnr)
return path return path
def propagate2(path, req, equipment): def propagate2(path, req, equipment):
si = create_input_spectral_information( si = create_input_spectral_information(
req.f_min, req.f_max, req.roll_off, req.baud_rate, req.f_min, req.f_max, req.roll_off, req.baud_rate,
req.power, req.spacing) req.power, req.spacing)
infos = {} infos = {}
for el in path: for i, el in enumerate(path):
before_si = si before_si = si
if isinstance(el, Roadm):
next_el = path[i+1]
after_si = si = el(si, degree=next_el.uid)
else:
after_si = si = el(si) after_si = si = el(si)
infos[el] = before_si, after_si infos[el] = before_si, after_si
path[-1].update_snr(req.tx_osnr, equipment['Roadm']['default'].add_drop_osnr) path[-1].update_snr(req.tx_osnr, equipment['Roadm']['default'].add_drop_osnr)
return infos return infos
def propagate_and_optimize_mode(path, req, equipment): def propagate_and_optimize_mode(path, req, equipment):
# if mode is unknown : loops on the modes starting from the highest baudrate fiting in the # 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 # step 1: create an ordered list of modes based on baudrate
@@ -373,23 +465,30 @@ def propagate_and_optimize_mode(path, req, equipment):
key=lambda x: x['bit_rate'], reverse=True) key=lambda x: x['bit_rate'], reverse=True)
# print(modes_to_explore) # print(modes_to_explore)
# step2: computes propagation for each baudrate: stop and select the first that passes # 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: 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 # TODO: if the loop in mode optimization does not have a feasible path, then bugs
spc_info = create_input_spectral_information(req.f_min, req.f_max, spc_info = create_input_spectral_information(req.f_min, req.f_max,
equipment['SI']['default'].roll_off, equipment['SI']['default'].roll_off,
this_br, req.power, req.spacing) this_br, req.power, req.spacing)
for el in path: for i, el in enumerate(path):
if isinstance(el, Roadm):
next_el = path[i+1]
spc_info = el(spc_info, degree=next_el.uid)
else:
spc_info = el(spc_info) spc_info = el(spc_info)
for this_mode in modes_to_explore: for this_mode in modes_to_explore:
if path[-1].snr is not None: if path[-1].snr is not None:
path[-1].update_snr(this_mode['tx_osnr'], equipment['Roadm']['default'].add_drop_osnr) path[-1].update_snr(this_mode['tx_osnr'], equipment['Roadm']['default'].add_drop_osnr)
if round(min(path[-1].snr+lin2db(this_br/(12.5e9))), 2) > this_mode['OSNR']: if round(min(path[-1].snr+lin2db(this_br/(12.5e9))), 2) > this_mode['OSNR']:
found_a_feasible_mode = True
return path, this_mode return path, this_mode
else: else:
last_explored_mode = this_mode last_explored_mode = this_mode
else: else:
req.blocking_reason = 'NO_COMPUTED_SNR' req.blocking_reason = 'NO_COMPUTED_SNR'
return path, None return path, None
# only get to this point if no baudrate/mode satisfies OSNR requirement # only get to this point if no baudrate/mode satisfies OSNR requirement
# returns the last propagated path and mode # returns the last propagated path and mode
@@ -406,7 +505,6 @@ def propagate_and_optimize_mode(path, req, equipment):
req.blocking_reason = 'NO_FEASIBLE_BAUDRATE_WITH_SPACING' req.blocking_reason = 'NO_FEASIBLE_BAUDRATE_WITH_SPACING'
return [], None return [], None
def jsontopath_metric(path_metric): def jsontopath_metric(path_metric):
""" a functions that reads resulting metric from json string """ a functions that reads resulting metric from json string
""" """
@@ -425,7 +523,6 @@ def jsontopath_metric(path_metric):
for e in path_metric if e['metric-type'] == 'path_bandwidth') for e in path_metric if e['metric-type'] == 'path_bandwidth')
return output_snr, output_snrbandwidth, output_osnr, power, path_bandwidth return output_snr, output_snrbandwidth, output_osnr, power, path_bandwidth
def jsontoparams(my_p, tsp, mode, equipment): def jsontoparams(my_p, tsp, mode, equipment):
""" a function that derives optical params from transponder type and mode """ a function that derives optical params from transponder type and mode
supports the no mode case supports the no mode case
@@ -439,7 +536,7 @@ def jsontoparams(my_p, tsp, mode, equipment):
temp2 = [] temp2 = []
for elem in my_p['path-properties']['path-route-objects']: for elem in my_p['path-properties']['path-route-objects']:
if 'label-hop' in elem['path-route-object'].keys(): if 'label-hop' in elem['path-route-object'].keys():
temp2.append(f'{elem["path-route-object"]["label-hop"]["N"]}, ' + temp2.append(f'{elem["path-route-object"]["label-hop"]["N"]}, ' + \
f'{elem["path-route-object"]["label-hop"]["M"]}') f'{elem["path-route-object"]["label-hop"]["M"]}')
# OrderedDict.fromkeys returns the unique set of strings. # OrderedDict.fromkeys returns the unique set of strings.
# TODO: if spectrum changes along the path, we should be able to give the segments # TODO: if spectrum changes along the path, we should be able to give the segments
@@ -462,7 +559,6 @@ def jsontoparams(my_p, tsp, mode, equipment):
return pth, minosnr, baud_rate, bit_rate, cost, output_snr, \ return pth, minosnr, baud_rate, bit_rate, cost, output_snr, \
output_snrbandwidth, output_osnr, power, path_bandwidth, sptrm output_snrbandwidth, output_osnr, power, path_bandwidth, sptrm
def jsontocsv(json_data, equipment, fileout): def jsontocsv(json_data, equipment, fileout):
""" reads json path result file in accordance with: """ reads json path result file in accordance with:
Yang model for requesting Path Computation Yang model for requesting Path Computation
@@ -470,10 +566,10 @@ def jsontocsv(json_data, equipment, fileout):
and write results in an CSV file and write results in an CSV file
""" """
mywriter = writer(fileout) mywriter = writer(fileout)
mywriter.writerow(('response-id', 'source', 'destination', 'path_bandwidth', 'Pass?', mywriter.writerow(('response-id', 'source', 'destination', 'path_bandwidth', 'Pass?',\
'nb of tsp pairs', 'total cost', 'transponder-type', 'transponder-mode', 'nb of tsp pairs', 'total cost', 'transponder-type', 'transponder-mode',\
'OSNR-0.1nm', 'SNR-0.1nm', 'SNR-bandwidth', 'baud rate (Gbaud)', 'OSNR-0.1nm', 'SNR-0.1nm', 'SNR-bandwidth', 'baud rate (Gbaud)',\
'input power (dBm)', 'path', 'spectrum (N,M)', 'reversed path OSNR-0.1nm', 'input power (dBm)', 'path', 'spectrum (N,M)', 'reversed path OSNR-0.1nm',\
'reversed path SNR-0.1nm', 'reversed path SNR-bandwidth')) 'reversed path SNR-0.1nm', 'reversed path SNR-bandwidth'))
for pth_el in json_data['response']: for pth_el in json_data['response']:
@@ -506,9 +602,12 @@ def jsontocsv(json_data, equipment, fileout):
# as spectrum assignment is not performed for blocked demands: there is no label object in the answer # as spectrum assignment is not performed for blocked demands: there is no label object in the answer
# so the hop_attribute with tsp and mode is second object or last object, while id of hop is first and # so the hop_attribute with tsp and mode is second object or last object, while id of hop is first and
# penultimate # penultimate
source = pth_el['no-path']['path-properties']['path-route-objects'][0]['path-route-object']['num-unnum-hop']['node-id'] source = pth_el['no-path']['path-properties']['path-route-objects'][0]\
destination = pth_el['no-path']['path-properties']['path-route-objects'][-2]['path-route-object']['num-unnum-hop']['node-id'] ['path-route-object']['num-unnum-hop']['node-id']
temp_tsp = pth_el['no-path']['path-properties']['path-route-objects'][1]['path-route-object']['transponder'] destination = pth_el['no-path']['path-properties']['path-route-objects'][-2]\
['path-route-object']['num-unnum-hop']['node-id']
temp_tsp = pth_el['no-path']['path-properties']['path-route-objects'][1]\
['path-route-object']['transponder']
tsp = temp_tsp['transponder-type'] tsp = temp_tsp['transponder-type']
mode = temp_tsp['transponder-mode'] mode = temp_tsp['transponder-mode']
isok = pth_el['no-path']['no-path'] isok = pth_el['no-path']['no-path']
@@ -535,10 +634,13 @@ def jsontocsv(json_data, equipment, fileout):
revsnrb = '' revsnrb = ''
else: else:
# when label will be assigned destination will be with index -3, and transponder with index 2 # when label will be assigned destination will be with index -3, and transponder with index 2
source = pth_el['path-properties']['path-route-objects'][0]['path-route-object']['num-unnum-hop']['node-id'] source = pth_el['path-properties']['path-route-objects'][0]\
destination = pth_el['path-properties']['path-route-objects'][-3]['path-route-object']['num-unnum-hop']['node-id'] ['path-route-object']['num-unnum-hop']['node-id']
destination = pth_el['path-properties']['path-route-objects'][-3]\
['path-route-object']['num-unnum-hop']['node-id']
# selects only roadm nodes # selects only roadm nodes
temp_tsp = pth_el['path-properties']['path-route-objects'][2]['path-route-object']['transponder'] temp_tsp = pth_el['path-properties']['path-route-objects'][2]\
['path-route-object']['transponder']
tsp = temp_tsp['transponder-type'] tsp = temp_tsp['transponder-type']
mode = temp_tsp['transponder-mode'] mode = temp_tsp['transponder-mode']
@@ -589,9 +691,8 @@ def jsontocsv(json_data, equipment, fileout):
revsnrb revsnrb
)) ))
def compute_path_dsjctn(network, equipment, pathreqlist, disjunctions_list): def compute_path_dsjctn(network, equipment, pathreqlist, disjunctions_list):
# pathreqlist is a list of PathRequest objects # pathreqlist is a list of Path_request objects
# disjunctions_list a list of Disjunction objects # disjunctions_list a list of Disjunction objects
# given a network, a list of requests with the set of disjunction features between # given a network, a list of requests with the set of disjunction features between
@@ -650,14 +751,13 @@ def compute_path_dsjctn(network, equipment, pathreqlist, disjunctions_list):
simple_rqs = {} simple_rqs = {}
simple_rqs_reversed = {} simple_rqs_reversed = {}
for pathreq in pathreqlist_disjt: for pathreq in pathreqlist_disjt:
all_simp_pths = list(all_simple_paths(network, all_simp_pths = list(all_simple_paths(network,\
source=next(el for el in network.nodes() if el.uid == pathreq.source), source=next(el for el in network.nodes() if el.uid == pathreq.source),\
target=next(el for el in network.nodes() target=next(el for el in network.nodes() if el.uid == pathreq.destination),\
if el.uid == pathreq.destination),
cutoff=80)) cutoff=80))
# sort them in km length instead of hop # 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 path: len(path))
all_simp_pths = sorted(all_simp_pths, key=lambda 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))) 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 # reversed direction paths required to check disjunction on both direction
all_simp_pths_reversed = [] all_simp_pths_reversed = []
@@ -668,7 +768,7 @@ def compute_path_dsjctn(network, equipment, pathreqlist, disjunctions_list):
for pth in all_simp_pths: for pth in all_simp_pths:
# build a short list representing each roadm+direction with the first item # build a short list representing each roadm+direction with the first item
# start enumeration at 1 to avoid Trx in the list # start enumeration at 1 to avoid Trx in the list
short_list = [e.uid for i, e in enumerate(pth[1:-1]) short_list = [e.uid for i, e in enumerate(pth[1:-1]) \
if isinstance(e, Roadm) | (isinstance(pth[i], Roadm))] if isinstance(e, Roadm) | (isinstance(pth[i], Roadm))]
temp.append(short_list) temp.append(short_list)
# id(short_list) is unique even if path is the same: two objects with same # id(short_list) is unique even if path is the same: two objects with same
@@ -679,7 +779,7 @@ def compute_path_dsjctn(network, equipment, pathreqlist, disjunctions_list):
for pth in all_simp_pths_reversed: for pth in all_simp_pths_reversed:
# build a short list representing each roadm+direction with the first item # build a short list representing each roadm+direction with the first item
# start enumeration at 1 to avoid Trx in the list # start enumeration at 1 to avoid Trx in the list
temp.append([e.uid for i, e in enumerate(pth[1:-1]) temp.append([e.uid for i, e in enumerate(pth[1:-1]) \
if isinstance(e, Roadm) | (isinstance(pth[i], Roadm))]) if isinstance(e, Roadm) | (isinstance(pth[i], Roadm))])
simple_rqs_reversed[pathreq.request_id] = temp simple_rqs_reversed[pathreq.request_id] = temp
# step 2 # step 2
@@ -687,12 +787,13 @@ def compute_path_dsjctn(network, equipment, pathreqlist, disjunctions_list):
# select the disjoint path combination # select the disjoint path combination
candidates = {} candidates = {}
for dis in disjunctions_list: for d in disjunctions_list:
dlist = dis.disjunctions_req.copy() dlist = d.disjunctions_req.copy()
# each line of dpath is one combination of path that satisfies disjunction # each line of dpath is one combination of path that satisfies disjunction
dpath = [] dpath = []
for i, pth in enumerate(simple_rqs[dlist[0]]): for i, pth in enumerate(simple_rqs[dlist[0]]):
dpath.append([pth]) dpath.append([pth])
# allpaths[id(p)].d_id = d.disjunction_id
# in each loop, dpath is updated with a path for rq that satisfies # in each loop, dpath is updated with a path for rq that satisfies
# disjunction with each path in dpath # disjunction with each path in dpath
# for example, assume set of requests in the vector (disjunction_list) is {rq1,rq2, rq3} # for example, assume set of requests in the vector (disjunction_list) is {rq1,rq2, rq3}
@@ -738,7 +839,7 @@ def compute_path_dsjctn(network, equipment, pathreqlist, disjunctions_list):
# print(f' coucou {elem1}: \t{temp}') # print(f' coucou {elem1}: \t{temp}')
dpath = temp dpath = temp
# print(dpath) # print(dpath)
candidates[dis.disjunction_id] = dpath candidates[d.disjunction_id] = dpath
# for i in disjunctions_list: # for i in disjunctions_list:
# print(f'\n{candidates[i.disjunction_id]}') # print(f'\n{candidates[i.disjunction_id]}')
@@ -753,6 +854,7 @@ def compute_path_dsjctn(network, equipment, pathreqlist, disjunctions_list):
# candidate[s2] = [[p3 p6]] # candidate[s2] = [[p3 p6]]
# for rq1 p3 should be preferred # for rq1 p3 should be preferred
for pathreq in pathreqlist_disjt: for pathreq in pathreqlist_disjt:
concerned_d_id = [d.disjunction_id for d in disjunctions_list concerned_d_id = [d.disjunction_id for d in disjunctions_list
if pathreq.request_id in d.disjunctions_req] if pathreq.request_id in d.disjunctions_req]
@@ -807,7 +909,7 @@ def compute_path_dsjctn(network, equipment, pathreqlist, disjunctions_list):
else: else:
if 'LOOSE' in allpaths[id(pth)].req.loose_list: if 'LOOSE' in allpaths[id(pth)].req.loose_list:
LOGGER.info(f'Could not apply route constraint'+ LOGGER.info(f'Could not apply route constraint'+
f'{allpaths[id(pth)].req.nodes_list} on request' + f'{allpaths[id(pth)].req.nodes_list} on request' +\
f' {allpaths[id(pth)].req.request_id}') f' {allpaths[id(pth)].req.request_id}')
else: else:
LOGGER.info(f'removing last solution from candidate paths\n{sol}') LOGGER.info(f'removing last solution from candidate paths\n{sol}')
@@ -852,7 +954,6 @@ def compute_path_dsjctn(network, equipment, pathreqlist, disjunctions_list):
path_res_list.append(pathreslist_disjoint[req]) path_res_list.append(pathreslist_disjoint[req])
return path_res_list return path_res_list
def isdisjoint(pth1, pth2): def isdisjoint(pth1, pth2):
""" returns 0 if disjoint """ returns 0 if disjoint
""" """
@@ -863,7 +964,6 @@ def isdisjoint(pth1, pth2):
return 1 return 1
return 0 return 0
def find_reversed_path(pth): def find_reversed_path(pth):
""" select of intermediate roadms and find the path between them """ select of intermediate roadms and find the path between them
note that this function may not give an exact result in case of multiple note that this function may not give an exact result in case of multiple
@@ -878,7 +978,7 @@ def find_reversed_path(pth):
# the OrderedDict.fromkeys function does this. eg # the OrderedDict.fromkeys function does this. eg
# pth = [el1_oms1 el2_oms1 el3_oms1 el1_oms2 el2_oms2 el3_oms2] # pth = [el1_oms1 el2_oms1 el3_oms1 el1_oms2 el2_oms2 el3_oms2]
# p_oms should be = [oms1 oms2] # p_oms should be = [oms1 oms2]
p_oms = list(OrderedDict.fromkeys(reversed([el.oms.reversed_oms for el in pth p_oms = list(OrderedDict.fromkeys(reversed([el.oms.reversed_oms for el in pth \
if not isinstance(el, Transceiver) and not isinstance(el, Roadm)]))) if not isinstance(el, Transceiver) and not isinstance(el, Roadm)])))
reversed_path = [pth[-1]] reversed_path = [pth[-1]]
for oms in p_oms: for oms in p_oms:
@@ -898,7 +998,6 @@ def find_reversed_path(pth):
return reversed_path return reversed_path
def ispart(ptha, pthb): def ispart(ptha, pthb):
""" the functions takes two paths a and b and retrns True """ the functions takes two paths a and b and retrns True
if all a elements are part of b and in the same order if all a elements are part of b and in the same order
@@ -914,7 +1013,6 @@ def ispart(ptha, pthb):
return False return False
return True return True
def remove_candidate(candidates, allpaths, rqst, pth): def remove_candidate(candidates, allpaths, rqst, pth):
""" filter duplicate candidates """ filter duplicate candidates
""" """
@@ -930,7 +1028,6 @@ def remove_candidate(candidates, allpaths, rqst, pth):
candidates[key] = temp candidates[key] = temp
return candidates return candidates
def compare_reqs(req1, req2, disjlist): def compare_reqs(req1, req2, disjlist):
""" compare two requests: returns True or False """ compare two requests: returns True or False
""" """
@@ -971,7 +1068,6 @@ def compare_reqs(req1, req2, disjlist):
else: else:
return False return False
def requests_aggregation(pathreqlist, disjlist): def requests_aggregation(pathreqlist, disjlist):
""" this function aggregates requests so that if several requests """ this function aggregates requests so that if several requests
exist between same source and destination and with same transponder type exist between same source and destination and with same transponder type
@@ -999,174 +1095,3 @@ def requests_aggregation(pathreqlist, disjlist):
disjlist.remove(this_d) disjlist.remove(this_d)
break break
return local_list, disjlist return local_list, disjlist
def correct_json_route_list(network, pathreqlist):
""" all names in list should be exact name in the network, and there is no ambiguity
This function only checks that list is correct, warns user if the name is incorrect and
suppresses the constraint it it is loose or raises an error if it is strict
"""
all_uid = [n.uid for n in network.nodes()]
transponders = [n.uid for n in network.nodes() if isinstance(n, Transceiver)]
for pathreq in pathreqlist:
if pathreq.source not in transponders:
msg = f'{ansi_escapes.red}Request: {pathreq.request_id}: could not find transponder' +\
f' 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 transponder' +\
f' destination : {pathreq.destination}.{ansi_escapes.reset}'
LOGGER.critical(msg)
raise ServiceError(msg)
# silently remove source and dest nodes from the list
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)
temp = deepcopy(pathreq)
for i, n_id in enumerate(temp.nodes_list):
# a node within this list must be part of the topology and should not be a transceiver,
# because only source and dest are transceivers
if n_id not in all_uid or n_id in transponders:
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 route 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)
return pathreqlist
def deduplicate_disjunctions(disjn):
""" clean disjunctions to remove possible repetition
"""
local_disjn = disjn.copy()
for elem in local_disjn:
for dis_elem in local_disjn:
if set(elem.disjunctions_req) == set(dis_elem.disjunctions_req) and \
elem.disjunction_id != dis_elem.disjunction_id:
local_disjn.remove(dis_elem)
return local_disjn
def compute_path_with_disjunction(network, equipment, pathreqlist, pathlist):
""" use a list but a dictionnary might be helpful to find path based on request_id
TODO change all these req, dsjct, res lists into dict !
"""
path_res_list = []
reversed_path_res_list = []
propagated_reversed_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
print(f'request {pathreq.request_id}')
print(f'Computing path from {pathreq.source} to {pathreq.destination}')
# adding first node to be clearer on the output
print(f'with path constraint: {[pathreq.source] + pathreq.nodes_list}')
# pathlist[i] contains the whole path information for request i
# last element is a transciver and where the result of the propagation is
# recorded.
# 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 simulation. This is why
# we use deepcopy: to ensure that each propagation is recorded and not overwritten
total_path = deepcopy(pathlist[i])
print(f'Computed path (roadms):{[e.uid for e in total_path if isinstance(e, Roadm)]}')
# for debug
# print(f'{pathreq.baud_rate} {pathreq.power} {pathreq.spacing} {pathreq.nb_channel}')
if total_path:
if pathreq.baud_rate is not None:
# means that at this point the mode was entered/forced by user and thus a
# baud_rate was defined
total_path = propagate(total_path, pathreq, equipment)
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' +\
f' {pathreq.source} to {pathreq.destination} does not pass with' +\
f' {pathreq.tsp_mode}\n\tcomputedSNR in 0.1nm = {temp_snr01nm} ' +\
f'- required osnr {pathreq.OSNR}'
print(msg)
LOGGER.warning(msg)
pathreq.blocking_reason = 'MODE_NOT_FEASIBLE'
else:
total_path, mode = propagate_and_optimize_mode(total_path, pathreq, equipment)
# if no baudrate satisfies spacing, no mode is returned and the last explored mode
# a warning is shown in the propagate_and_optimize_mode
# propagate_and_optimize_mode function returns the mode with the highest bitrate
# that passes. if no mode passes, then a attribute blocking_reason is added on
# pathreq that contains the reason for blocking: 'NO_PATH', 'NO_FEASIBLE_MODE', ...
try:
if pathreq.blocking_reason in BLOCKING_NOPATH:
total_path = []
elif pathreq.blocking_reason in BLOCKING_NOMODE:
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']
# other blocking reason should not appear at this point
except AttributeError:
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']
# reversed path is needed for correct spectrum assignment
reversed_path = find_reversed_path(pathlist[i])
if pathreq.bidir:
# only propagate if bidir is true, but needs the reversed path anyway for
# correct spectrum assignment
rev_p = deepcopy(reversed_path)
print(f'\n\tPropagating Z to A direction {pathreq.destination} to {pathreq.source}')
print(f'\tPath (roadsm) {[r.uid for r in rev_p if isinstance(r,Roadm)]}\n')
propagated_reversed_path = propagate(rev_p, pathreq, equipment)
temp_snr01nm = round(mean(propagated_reversed_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' +\
f' {pathreq.source} to {pathreq.destination} does not pass with' +\
f' {pathreq.tsp_mode}\n' +\
f'\tcomputedSNR in 0.1nm = {temp_snr01nm} - required osnr {pathreq.OSNR}'
print(msg)
LOGGER.warning(msg)
# TODO selection of mode should also be on reversed direction !!
if not hasattr(pathreq, 'blocking_reason'):
pathreq.blocking_reason = 'MODE_NOT_FEASIBLE'
else:
propagated_reversed_path = []
else:
msg = 'Total path is empty. No propagation'
print(msg)
LOGGER.info(msg)
reversed_path = []
propagated_reversed_path = []
path_res_list.append(total_path)
reversed_path_res_list.append(reversed_path)
propagated_reversed_path_res_list.append(propagated_reversed_path)
# print to have a nice output
print('')
return path_res_list, reversed_path_res_list, propagated_reversed_path_res_list

View File

@@ -1,27 +1,162 @@
import numpy as np import numpy as np
from operator import attrgetter from operator import attrgetter
from collections import namedtuple
from logging import getLogger from logging import getLogger
import scipy.constants as ph import scipy.constants as ph
from scipy.integrate import solve_bvp from scipy.integrate import solve_bvp
from scipy.integrate import cumtrapz from scipy.integrate import cumtrapz
from scipy.interpolate import interp1d from scipy.interpolate import interp1d
from scipy.optimize import OptimizeResult from scipy.optimize import OptimizeResult
from math import isclose
from gnpy.core.utils import db2lin, lin2db from gnpy.core.utils import db2lin
from gnpy.core.exceptions import EquipmentConfigError
logger = getLogger(__name__) 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): def propagate_raman_fiber(fiber, *carriers):
simulation = Simulation.get_simulation() sim_params = fiber.sim_params
sim_params = simulation.sim_params raman_params = fiber.sim_params.raman_params
raman_params = sim_params.raman_params nli_params = fiber.sim_params.nli_params
nli_params = sim_params.nli_params
# apply input attenuation to carriers # apply input attenuation to carriers
attenuation_in = db2lin(fiber.params.con_in + fiber.params.att_in) attenuation_in = db2lin(fiber.con_in + fiber.att_in)
chan = [] chan = []
for carrier in carriers: for carrier in carriers:
pwr = carrier.power pwr = carrier.power
@@ -31,32 +166,36 @@ def propagate_raman_fiber(fiber, *carriers):
carrier = carrier._replace(power=pwr) carrier = carrier._replace(power=pwr)
chan.append(carrier) chan.append(carrier)
carriers = tuple(f for f in chan) carriers = tuple(f for f in chan)
fiber_params = FiberParams(fiber)
# evaluate fiber attenuation involving also SRS if required by sim_params # evaluate fiber attenuation involving also SRS if required by sim_params
raman_solver = fiber.raman_solver if 'raman_pumps' in fiber.operational:
raman_solver.carriers = carriers raman_pumps = tuple(pump(p['power'], p['frequency'], p['propagation_direction'])
raman_solver.raman_pumps = fiber.raman_pumps for p in fiber.operational['raman_pumps'])
stimulated_raman_scattering = raman_solver.stimulated_raman_scattering 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)
fiber_attenuation = (stimulated_raman_scattering.rho[:, -1])**-2 fiber_attenuation = (stimulated_raman_scattering.rho[:, -1])**-2
if not raman_params.flag_raman: if not raman_params.flag_raman:
fiber_attenuation = tuple(fiber.params.lin_attenuation for _ in carriers) fiber_attenuation = tuple(fiber.lin_attenuation for _ in carriers)
# evaluate Raman ASE noise if required by sim_params and if raman pumps are present # evaluate Raman ASE noise if required by sim_params and if raman pumps are present
if raman_params.flag_raman and fiber.raman_pumps: if raman_params.flag_raman and raman_pumps:
raman_ase = raman_solver.spontaneous_raman_scattering.power[:, -1] raman_ase = raman_solver.spontaneous_raman_scattering.power[:, -1]
else: else:
raman_ase = tuple(0 for _ in carriers) raman_ase = tuple(0 for _ in carriers)
# evaluate nli and propagate in fiber # evaluate nli and propagate in fiber
attenuation_out = db2lin(fiber.params.con_out) attenuation_out = db2lin(fiber.con_out)
nli_solver = fiber.nli_solver nli_solver = NliSolver(nli_params=nli_params, fiber_params=fiber_params)
nli_solver.stimulated_raman_scattering = stimulated_raman_scattering nli_solver.stimulated_raman_scattering = stimulated_raman_scattering
nli_frequencies = [] nli_frequencies = []
computed_nli = [] computed_nli = []
for carrier in (c for c in carriers if c.channel_number in sim_params.nli_params.computed_channels): 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) resolution_param = frequency_resolution(carrier, carriers, sim_params, fiber_params)
f_cut_resolution, f_pump_resolution, _, _ = resolution_param f_cut_resolution, f_pump_resolution, _, _ = resolution_param
nli_params.f_cut_resolution = f_cut_resolution nli_params.f_cut_resolution = f_cut_resolution
nli_params.f_pump_resolution = f_pump_resolution nli_params.f_pump_resolution = f_pump_resolution
@@ -73,8 +212,7 @@ def propagate_raman_fiber(fiber, *carriers):
new_carriers.append(carrier._replace(power=pwr)) new_carriers.append(carrier._replace(power=pwr))
return new_carriers 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): 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_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) res_k = _get_freq_res_dispersion_attenuation(delta_count, grid_size, alpha0, beta2, k_tol)
@@ -90,10 +228,10 @@ def frequency_resolution(carrier, carriers, sim_params, fiber):
grid_size = sim_params.nli_params.wdm_grid_size grid_size = sim_params.nli_params.wdm_grid_size
delta_z = sim_params.raman_params.space_resolution delta_z = sim_params.raman_params.space_resolution
alpha0 = fiber.alpha0() alpha0 = fiber_params.alpha0()
beta2 = fiber.params.beta2 beta2 = fiber_params.beta2
k_tol = sim_params.nli_params.dispersion_tolerance k_tol = sim_params.nli_params.dispersion_tolerance
phi_tol = sim_params.nli_params.phase_shift_tolerance phi_tol = sim_params.nli_params.phase_shift_tollerance
f_pump_resolution, method_f_pump, res_dict_pump = \ 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) _get_freq_res_k_phi(0, grid_size, alpha0, delta_z, beta2, k_tol, phi_tol)
f_cut_resolution = {} f_cut_resolution = {}
@@ -109,7 +247,6 @@ def frequency_resolution(carrier, carriers, sim_params, fiber):
res_dict_cut[delta_number] = res_dict 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)] 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): def raised_cosine_comb(f, *carriers):
""" Returns an array storing the PSD of a WDM comb of raised cosine shaped """ Returns an array storing the PSD of a WDM comb of raised cosine shaped
channels at the input frequencies defined in array f channels at the input frequencies defined in array f
@@ -133,59 +270,30 @@ def raised_cosine_comb(f, *carriers):
np.where(tf > 0, 1., 0.) * np.where(np.abs(ff) <= stopband, 1., 0.)) + psd np.where(tf > 0, 1., 0.) * np.where(np.abs(ff) <= stopband, 1., 0.)) + psd
return 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: class RamanSolver:
def __init__(self, fiber=None): def __init__(self, raman_params=None, fiber_params=None):
""" Initialize the Raman solver object. """ Initialize the fiber object with its physical parameters
:param fiber: instance of elements.py/Fiber. :param length: fiber length in m.
:param carriers: tuple of carrier objects :param alphap: fiber power attenuation coefficient vs frequency in 1/m. numpy array
:param raman_pumps: tuple containing pumps characteristics :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).
""" """
self._fiber = fiber self.fiber_params = fiber_params
self.raman_params = raman_params
self._carriers = None self._carriers = None
self._raman_pumps = None
self._stimulated_raman_scattering = None self._stimulated_raman_scattering = None
self._spontaneous_raman_scattering = None self._spontaneous_raman_scattering = None
@property @property
def fiber(self): def fiber_params(self):
return self._fiber return self._fiber_params
@fiber_params.setter
def fiber_params(self, fiber_params):
self._stimulated_raman_scattering = None
self._fiber_params = fiber_params
@property @property
def carriers(self): def carriers(self):
@@ -193,8 +301,11 @@ class RamanSolver:
@carriers.setter @carriers.setter
def carriers(self, carriers): def carriers(self, carriers):
"""
:param carriers: tuple of namedtuples containing information about carriers
:return:
"""
self._carriers = carriers self._carriers = carriers
self._spontaneous_raman_scattering = None
self._stimulated_raman_scattering = None self._stimulated_raman_scattering = None
@property @property
@@ -207,43 +318,62 @@ class RamanSolver:
self._stimulated_raman_scattering = None self._stimulated_raman_scattering = None
@property @property
def stimulated_raman_scattering(self): def raman_params(self):
if self._stimulated_raman_scattering is None: return self._raman_params
self.calculate_stimulated_raman_scattering(self.carriers, self.raman_pumps)
return self._stimulated_raman_scattering @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
@property @property
def spontaneous_raman_scattering(self): def spontaneous_raman_scattering(self):
if self._spontaneous_raman_scattering is None: if self._spontaneous_raman_scattering is None:
self.calculate_spontaneous_raman_scattering(self.carriers, self.raman_pumps) # SET STUFF
return self._spontaneous_raman_scattering loss_coef = self.fiber_params.loss_coef
raman_efficiency = self.fiber_params.raman_efficiency
def calculate_spontaneous_raman_scattering(self, carriers, raman_pumps): temperature = self.fiber_params.temperature
raman_efficiency = self.fiber.params.raman_efficiency carriers = self.carriers
temperature = self.fiber.operational['temperature'] raman_pumps = self.raman_pumps
logger.debug('Start computing fiber Spontaneous Raman Scattering') logger.debug('Start computing fiber Spontaneous Raman Scattering')
power_spectrum, freq_array, prop_direct, bn_array = self._compute_power_spectrum(carriers, raman_pumps) power_spectrum, freq_array, prop_direct, bn_array = self._compute_power_spectrum(carriers, raman_pumps)
alphap_fiber = self.fiber.alpha(freq_array) 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))) freq_diff = abs(freq_array - np.reshape(freq_array, (len(freq_array), 1)))
interp_cr = interp1d(raman_efficiency['frequency_offset'], raman_efficiency['cr']) interp_cr = interp1d(raman_efficiency['frequency_offset'], raman_efficiency['cr'])
cr = interp_cr(freq_diff) cr = interp_cr(freq_diff)
# z propagation axis # z propagation axis
z_array = self.stimulated_raman_scattering.z z_array = self._stimulated_raman_scattering.z
ase_bc = np.zeros(freq_array.shape) ase_bc = np.zeros(freq_array.shape)
# calculate ase power # calculate ase power
int_spontaneous_raman = self._int_spontaneous_raman(z_array, self._stimulated_raman_scattering.power, spontaneous_raman_scattering = self._int_spontaneous_raman(z_array, self._stimulated_raman_scattering.power,
alphap_fiber, freq_array, cr, freq_diff, ase_bc, alphap_fiber, freq_array, cr, freq_diff, ase_bc,
bn_array, temperature) bn_array, temperature)
spontaneous_raman_scattering = SpontaneousRamanScattering(freq_array, z_array, int_spontaneous_raman.x) setattr(spontaneous_raman_scattering, 'frequency', freq_array)
logger.debug("Spontaneous Raman Scattering evaluated successfully") 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._spontaneous_raman_scattering = spontaneous_raman_scattering
return self._spontaneous_raman_scattering
@staticmethod @staticmethod
def _compute_power_spectrum(carriers, raman_pumps=None): def _compute_power_spectrum(carriers, raman_pumps=None):
""" """
@@ -282,14 +412,10 @@ class RamanSolver:
return pow_array, f_array, propagation_direction, noise_bandwidth_array return pow_array, f_array, propagation_direction, noise_bandwidth_array
def _int_spontaneous_raman(self, z_array, raman_matrix, alphap_fiber, freq_array, def _int_spontaneous_raman(self, z_array, raman_matrix, alphap_fiber, freq_array, cr_raman_matrix, freq_diff, ase_bc, bn_array, temperature):
cr_raman_matrix, freq_diff, ase_bc, bn_array, temperature):
spontaneous_raman_scattering = OptimizeResult() spontaneous_raman_scattering = OptimizeResult()
simulation = Simulation.get_simulation() dx = self.raman_params.space_resolution
sim_params = simulation.sim_params
dx = sim_params.raman_params.space_resolution
h = ph.value('Planck constant') h = ph.value('Planck constant')
kb = ph.value('Boltzmann constant') kb = ph.value('Boltzmann constant')
@@ -302,46 +428,53 @@ class RamanSolver:
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_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(), int_raman_loss = np.sum((cr_raman[:f_ind] * vibrational_loss * int_pump[:f_ind, :].transpose()).transpose(), axis=0)
axis=0)
int_raman_gain = np.sum((cr_raman[f_ind + 1:] * int_pump[f_ind + 1:, :].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 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() 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)
* h * f_ase * bn_array[f_ind], axis=0)
bc_evolution = ase_bc[f_ind] * np.exp(int_gain_loss) bc_evolution = ase_bc[f_ind] * np.exp(int_gain_loss)
ase_evolution = np.exp(int_gain_loss) * cumtrapz(new_ase * ase_evolution = np.exp(int_gain_loss) * cumtrapz(new_ase*np.exp(-int_gain_loss), z_array, dx=dx, initial=0)
np.exp(-int_gain_loss), z_array, dx=dx, initial=0)
power_ase[f_ind, :] = bc_evolution + ase_evolution power_ase[f_ind, :] = bc_evolution + ase_evolution
spontaneous_raman_scattering.x = 2 * power_ase 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 return spontaneous_raman_scattering
def calculate_stimulated_raman_scattering(self, carriers, raman_pumps): def stimulated_raman_scattering(self, carriers, raman_pumps=None):
""" Returns stimulated Raman scattering solution including """ Returns stimulated Raman scattering solution including
fiber gain/loss profile. fiber gain/loss profile.
:return: None :return: self._stimulated_raman_scattering: the SRS problem solution.
scipy.interpolate.PPoly instance
""" """
# 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 not sim_params.raman_params.flag_raman: if self._stimulated_raman_scattering is None:
raman_efficiency['cr'] = np.zeros(len(raman_efficiency['cr'])) # 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 # raman solver parameters
z_resolution = sim_params.raman_params.space_resolution z_resolution = self.raman_params.space_resolution
tolerance = sim_params.raman_params.tolerance tolerance = self.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)
alphap_fiber = self.fiber.alpha(freq_array) 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))) freq_diff = abs(freq_array - np.reshape(freq_array, (len(freq_array), 1)))
interp_cr = interp1d(raman_efficiency['frequency_offset'], raman_efficiency['cr']) interp_cr = interp1d(raman_efficiency['frequency_offset'], raman_efficiency['cr'])
@@ -350,23 +483,28 @@ class RamanSolver:
# z propagation axis # z propagation axis
z = np.arange(0, fiber_length+1, z_resolution) z = np.arange(0, fiber_length+1, z_resolution)
def ode_function(z, p): ode_function = lambda z, p: self._ode_stimulated_raman(z, p, alphap_fiber, freq_array, cr, prop_direct)
return 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)
def boundary_residual(ya, yb):
return 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) initial_guess_conditions = self._initial_guess_stimulated_raman(z, power_spectrum, alphap_fiber, prop_direct)
# ODE SOLVER # ODE SOLVER
bvp_solution = solve_bvp(ode_function, boundary_residual, z, initial_guess_conditions, tol=tolerance) stimulated_raman_scattering = solve_bvp(ode_function, boundary_residual, z, initial_guess_conditions, tol=tolerance)
rho = (bvp_solution.y.transpose() / power_spectrum).transpose() rho = (stimulated_raman_scattering.y.transpose() / power_spectrum).transpose()
rho = np.sqrt(rho) # From power attenuation to field attenuation rho = np.sqrt(rho) # From power attenuation to field attenuation
stimulated_raman_scattering = StimulatedRamanScattering(freq_array, bvp_solution.x, rho, bvp_solution.y) 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')
self.carriers = carriers
self.raman_pumps = raman_pumps
self._stimulated_raman_scattering = stimulated_raman_scattering self._stimulated_raman_scattering = stimulated_raman_scattering
return self._stimulated_raman_scattering
def _residuals_stimulated_raman(self, ya, yb, power_spectrum, prop_direct): def _residuals_stimulated_raman(self, ya, yb, power_spectrum, prop_direct):
computed_boundary_value = np.zeros(ya.size) computed_boundary_value = np.zeros(ya.size)
@@ -382,14 +520,11 @@ class RamanSolver:
def _initial_guess_stimulated_raman(self, z, power_spectrum, alphap_fiber, prop_direct): def _initial_guess_stimulated_raman(self, z, power_spectrum, alphap_fiber, prop_direct):
""" Computes the initial guess knowing the boundary conditions """ Computes the initial guess knowing the boundary conditions
:param z: patial axis [m]. numpy array :param z: patial axis [m]. numpy array
:param power_spectrum: power in each frequency slice [W]. :param power_spectrum: power in each frequency slice [W]. Frequency axis is defined by freq_array. numpy array
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 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: :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 +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]. :return: power_guess: guess on the initial conditions [W]. The first ndarray index identifies the frequency slice,
The first ndarray index identifies the frequency slice,
the second ndarray index identifies the step in z. ndarray the second ndarray index identifies the step in z. ndarray
""" """
@@ -403,19 +538,14 @@ class RamanSolver:
return power_guess return power_guess
def _ode_stimulated_raman(self, z, power_spectrum, alphap_fiber, freq_array, cr_raman_matrix, prop_direct): 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) """ Aim of ode_raman is to implement the set of ordinary differential equations (ODEs) describing the Raman effect.
describing the Raman effect.
:param z: spatial axis (unused). :param z: spatial axis (unused).
:param power_spectrum: power in each frequency slice [W]. :param power_spectrum: power in each frequency slice [W]. Frequency axis is defined by freq_array. numpy array. Size n
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 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 freq_array: reference frequency axis [Hz]. numpy array. Size n
:param cr_raman: Cr(f) Raman gain efficiency variation in frequency [1/W/m]. :param cr_raman: Cr(f) Raman gain efficiency variation in frequency [1/W/m]. Frequency defined by freq_array. numpy ndarray. Size nxn
Frequency defined by freq_array. numpy ndarray. Size nxn
:param prop_direct: indicates the propagation direction of each power slice in power_spectrum: :param prop_direct: indicates the propagation direction of each power slice in power_spectrum:
+1 for forward propagation and -1 for backward propagation. +1 for forward propagation and -1 for backward propagation. Frequency defined by freq_array. numpy array. Size n
Frequency defined by freq_array. numpy array. Size n
:return: dP/dz: the power variation in dz [W/m]. numpy array. Size n :return: dP/dz: the power variation in dz [W/m]. numpy array. Size n
""" """
@@ -433,25 +563,28 @@ class RamanSolver:
return np.vstack(dpdz) return np.vstack(dpdz)
class NliSolver: class NliSolver:
""" This class implements the NLI models. """ This class implements the NLI models.
Model and method can be specified in `sim_params.nli_params.method`. Model and method can be specified in `self.nli_params.method`.
List of implemented methods: List of implemented methods:
'gn_model_analytic': brute force triple integral solution 'gn_model_analytic': brute force triple integral solution
'ggn_spectrally_separated_xpm_spm': XPM plus SPM 'ggn_spectrally_separated_xpm_spm': XPM plus SPM
""" """
def __init__(self, fiber=None): def __init__(self, nli_params=None, fiber_params=None):
""" Initialize the Nli solver object. """ Initialize the fiber object with its physical parameters
:param fiber: instance of elements.py/Fiber.
""" """
self._fiber = fiber self.fiber_params = fiber_params
self._stimulated_raman_scattering = None self.nli_params = nli_params
self.stimulated_raman_scattering = None
@property @property
def fiber(self): def fiber_params(self):
return self._fiber return self._fiber_params
@fiber_params.setter
def fiber_params(self, fiber_params):
self._fiber_params = fiber_params
@property @property
def stimulated_raman_scattering(self): def stimulated_raman_scattering(self):
@@ -461,19 +594,28 @@ class NliSolver:
def stimulated_raman_scattering(self, stimulated_raman_scattering): def stimulated_raman_scattering(self, stimulated_raman_scattering):
self._stimulated_raman_scattering = 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): def compute_nli(self, carrier, *carriers):
""" Compute NLI power generated by the WDM comb `*carriers` on the channel under test `carrier` """ Compute NLI power generated by the WDM comb `*carriers` on the channel under test `carrier`
at the end of the fiber span. at the end of the fiber span.
""" """
simulation = Simulation.get_simulation() if 'gn_model_analytic' == self.nli_params.nli_method_name.lower():
sim_params = simulation.sim_params
if 'gn_model_analytic' == sim_params.nli_params.nli_method_name.lower():
carrier_nli = self._gn_analytic(carrier, *carriers) carrier_nli = self._gn_analytic(carrier, *carriers)
elif 'ggn_spectrally_separated' in sim_params.nli_params.nli_method_name.lower(): elif 'ggn_spectrally_separated' in self.nli_params.nli_method_name.lower():
eta_matrix = self._compute_eta_matrix(carrier, *carriers) eta_matrix = self._compute_eta_matrix(carrier, *carriers)
carrier_nli = self._carrier_nli_from_eta_matrix(eta_matrix, carrier, *carriers) carrier_nli = self._carrier_nli_from_eta_matrix(eta_matrix, carrier, *carriers)
else: else:
raise ValueError(f'Method {sim_params.nli_params.method_nli} not implemented.') raise ValueError(f'Method {self.nli_params.method_nli} not implemented.')
return carrier_nli return carrier_nli
@@ -490,8 +632,6 @@ class NliSolver:
def _compute_eta_matrix(self, carrier_cut, *carriers): def _compute_eta_matrix(self, carrier_cut, *carriers):
cut_index = carrier_cut.channel_number - 1 cut_index = carrier_cut.channel_number - 1
simulation = Simulation.get_simulation()
sim_params = simulation.sim_params
# Matrix initialization # Matrix initialization
matrix_size = max(carriers, key=lambda x: getattr(x, 'channel_number')).channel_number matrix_size = max(carriers, key=lambda x: getattr(x, 'channel_number')).channel_number
eta_matrix = np.zeros(shape=(matrix_size, matrix_size)) eta_matrix = np.zeros(shape=(matrix_size, matrix_size))
@@ -499,10 +639,10 @@ class NliSolver:
# SPM # SPM
logger.debug(f'Start computing SPM on channel #{carrier_cut.channel_number}') logger.debug(f'Start computing SPM on channel #{carrier_cut.channel_number}')
# SPM GGN # SPM GGN
if 'ggn' in sim_params.nli_params.nli_method_name.lower(): if 'ggn' in self.nli_params.nli_method_name.lower():
partial_nli = self._generalized_spectrally_separated_spm(carrier_cut) partial_nli = self._generalized_spectrally_separated_spm(carrier_cut)
# SPM GN # SPM GN
elif 'gn' in sim_params.nli_params.nli_method_name.lower(): elif 'gn' in self.nli_params.nli_method_name.lower():
partial_nli = self._gn_analytic(carrier_cut, *[carrier_cut]) partial_nli = self._gn_analytic(carrier_cut, *[carrier_cut])
eta_matrix[cut_index, cut_index] = partial_nli / (carrier_cut.power.signal**3) eta_matrix[cut_index, cut_index] = partial_nli / (carrier_cut.power.signal**3)
@@ -513,10 +653,10 @@ class NliSolver:
logger.debug(f'Start computing XPM on channel #{carrier_cut.channel_number} ' logger.debug(f'Start computing XPM on channel #{carrier_cut.channel_number} '
f'from channel #{pump_carrier.channel_number}') f'from channel #{pump_carrier.channel_number}')
# XPM GGN # XPM GGN
if 'ggn' in sim_params.nli_params.nli_method_name.lower(): if 'ggn' in self.nli_params.nli_method_name.lower():
partial_nli = self._generalized_spectrally_separated_xpm(carrier_cut, pump_carrier) partial_nli = self._generalized_spectrally_separated_xpm(carrier_cut, pump_carrier)
# XPM GGN # XPM GGN
elif 'gn' in sim_params.nli_params.nli_method_name.lower(): elif 'gn' in self.nli_params.nli_method_name.lower():
partial_nli = self._gn_analytic(carrier_cut, *[pump_carrier]) partial_nli = self._gn_analytic(carrier_cut, *[pump_carrier])
eta_matrix[pump_index, pump_index] = partial_nli /\ 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)
@@ -530,17 +670,19 @@ class NliSolver:
:param carriers: the full WDM comb :param carriers: the full WDM comb
:return: carrier_nli: the amount of nonlinear interference in W on the carrier under analysis :return: carrier_nli: the amount of nonlinear interference in W on the carrier under analysis
""" """
beta2 = self.fiber.params.beta2 alpha = self.fiber_params.alpha0() / 2
gamma = self.fiber.params.gamma beta2 = self.fiber_params.beta2
effective_length = self.fiber.params.effective_length gamma = self.fiber_params.gamma
asymptotic_length = self.fiber.params.asymptotic_length length = self.fiber_params.length
effective_length = (1 - np.exp(-2 * alpha * length)) / (2 * alpha)
asymptotic_length = 1 / (2 * alpha)
g_nli = 0 g_nli = 0
for interfering_carrier in carriers: for interfering_carrier in carriers:
g_interfearing = interfering_carrier.power.signal / interfering_carrier.baud_rate g_interfearing = interfering_carrier.power.signal / interfering_carrier.baud_rate
g_signal = carrier.power.signal / carrier.baud_rate g_signal = carrier.power.signal / carrier.baud_rate
g_nli += g_interfearing**2 * g_signal \ g_nli += g_interfearing**2 * g_signal \
* _psi(carrier, interfering_carrier, beta2=beta2, asymptotic_length=asymptotic_length) * _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 /\ g_nli *= (16.0 / 27.0) * (gamma * effective_length)**2 /\
(2 * np.pi * abs(beta2) * asymptotic_length) (2 * np.pi * abs(beta2) * asymptotic_length)
carrier_nli = carrier.baud_rate * g_nli carrier_nli = carrier.baud_rate * g_nli
@@ -548,33 +690,27 @@ class NliSolver:
# Methods for computing the GGN-model # Methods for computing the GGN-model
def _generalized_spectrally_separated_spm(self, carrier): def _generalized_spectrally_separated_spm(self, carrier):
gamma = self.fiber.params.gamma f_cut_resolution = self.nli_params.f_cut_resolution['delta_0']
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 f_eval = carrier.frequency
g_cut = (carrier.power.signal / carrier.baud_rate) g_cut = (carrier.power.signal / carrier.baud_rate)
spm_nli = carrier.baud_rate * (16.0 / 27.0) * gamma ** 2 * g_cut ** 3 * \ 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) self._generalized_psi(carrier, carrier, f_eval, f_cut_resolution, f_cut_resolution)
return spm_nli return spm_nli
def _generalized_spectrally_separated_xpm(self, carrier_cut, pump_carrier): 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 delta_index = pump_carrier.channel_number - carrier_cut.channel_number
f_cut_resolution = sim_params.nli_params.f_cut_resolution[f'delta_{delta_index}'] f_cut_resolution = self.nli_params.f_cut_resolution[f'delta_{delta_index}']
f_pump_resolution = sim_params.nli_params.f_pump_resolution f_pump_resolution = self.nli_params.f_pump_resolution
f_eval = carrier_cut.frequency f_eval = carrier_cut.frequency
g_pump = (pump_carrier.power.signal / pump_carrier.baud_rate) g_pump = (pump_carrier.power.signal / pump_carrier.baud_rate)
g_cut = (carrier_cut.power.signal / carrier_cut.baud_rate) g_cut = (carrier_cut.power.signal / carrier_cut.baud_rate)
frequency_offset_threshold = self._frequency_offset_threshold(pump_carrier.baud_rate) frequency_offset_threshold = self._frequency_offset_threshold(pump_carrier.baud_rate)
if abs(carrier_cut.frequency - pump_carrier.frequency) <= frequency_offset_threshold: if abs(carrier_cut.frequency - pump_carrier.frequency) <= frequency_offset_threshold:
xpm_nli = carrier_cut.baud_rate * (16.0 / 27.0) * gamma ** 2 * g_pump**2 * g_cut * \ 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) 2 * self._generalized_psi(carrier_cut, pump_carrier, f_eval, f_cut_resolution, f_pump_resolution)
else: else:
xpm_nli = carrier_cut.baud_rate * (16.0 / 27.0) * gamma ** 2 * g_pump**2 * g_cut * \ 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) 2 * self._fast_generalized_psi(carrier_cut, pump_carrier, f_eval, f_cut_resolution)
return xpm_nli return xpm_nli
@@ -583,15 +719,15 @@ class NliSolver:
:return: generalized_psi :return: generalized_psi
""" """
# Fiber parameters # Fiber parameters
alpha0 = self.fiber.alpha0(f_eval) alpha0 = self.fiber_params.alpha0(f_eval)
beta2 = self.fiber.params.beta2 beta2 = self.fiber_params.beta2
beta3 = self.fiber.params.beta3 beta3 = self.fiber_params.beta3
f_ref_beta = self.fiber.params.ref_frequency f_ref_beta = self.fiber_params.f_ref_beta
z = self.stimulated_raman_scattering.z z = self.stimulated_raman_scattering.z
frequency_rho = self.stimulated_raman_scattering.frequency frequency_rho = self.stimulated_raman_scattering.frequency
rho_norm = self.stimulated_raman_scattering.rho * np.exp(np.abs(alpha0) * z / 2) rho_norm = self.stimulated_raman_scattering.rho * np.exp(np.abs(alpha0) * z / 2)
if len(frequency_rho) == 1: if len(frequency_rho) == 1:
def rho_function(f): return rho_norm[0, :] rho_function = lambda f: rho_norm[0, :]
else: else:
rho_function = interp1d(frequency_rho, rho_norm, axis=0, fill_value='extrapolate') rho_function = interp1d(frequency_rho, rho_norm, axis=0, fill_value='extrapolate')
rho_norm_pump = rho_function(pump_carrier.frequency) rho_norm_pump = rho_function(pump_carrier.frequency)
@@ -616,15 +752,15 @@ class NliSolver:
:return: generalized_psi :return: generalized_psi
""" """
# Fiber parameters # Fiber parameters
alpha0 = self.fiber.alpha0(f_eval) alpha0 = self.fiber_params.alpha0(f_eval)
beta2 = self.fiber.params.beta2 beta2 = self.fiber_params.beta2
beta3 = self.fiber.params.beta3 beta3 = self.fiber_params.beta3
f_ref_beta = self.fiber.params.ref_frequency f_ref_beta = self.fiber_params.f_ref_beta
z = self.stimulated_raman_scattering.z z = self.stimulated_raman_scattering.z
frequency_rho = self.stimulated_raman_scattering.frequency frequency_rho = self.stimulated_raman_scattering.frequency
rho_norm = self.stimulated_raman_scattering.rho * np.exp(np.abs(alpha0) * z / 2) rho_norm = self.stimulated_raman_scattering.rho * np.exp(np.abs(alpha0) * z / 2)
if len(frequency_rho) == 1: if len(frequency_rho) == 1:
def rho_function(f): return rho_norm[0, :] rho_function = lambda f: rho_norm[0, :]
else: else:
rho_function = interp1d(frequency_rho, rho_norm, axis=0, fill_value='extrapolate') rho_function = interp1d(frequency_rho, rho_norm, axis=0, fill_value='extrapolate')
rho_norm_pump = rho_function(pump_carrier.frequency) rho_norm_pump = rho_function(pump_carrier.frequency)
@@ -667,11 +803,9 @@ class NliSolver:
beta2_ref = 21.3e-27 beta2_ref = 21.3e-27
delta_f_ref = 50e9 delta_f_ref = 50e9
rs_ref = 32e9 rs_ref = 32e9
beta2 = abs(self.fiber.params.beta2) freq_offset_th = ((k_ref * delta_f_ref) * rs_ref * beta2_ref) / (self.fiber_params.beta2 * symbol_rate)
freq_offset_th = ((k_ref * delta_f_ref) * rs_ref * beta2_ref) / (beta2 * symbol_rate)
return freq_offset_th return freq_offset_th
def _psi(carrier, interfering_carrier, beta2, asymptotic_length): def _psi(carrier, interfering_carrier, beta2, asymptotic_length):
"""Calculates eq. 123 from `arXiv:1209.0394 <https://arxiv.org/abs/1209.0394>`__""" """Calculates eq. 123 from `arXiv:1209.0394 <https://arxiv.org/abs/1209.0394>`__"""
@@ -684,49 +818,3 @@ def _psi(carrier, interfering_carrier, beta2, asymptotic_length):
psi -= np.arcsinh(np.pi**2 * asymptotic_length * abs(beta2) * psi -= np.arcsinh(np.pi**2 * asymptotic_length * abs(beta2) *
carrier.baud_rate * (delta_f - 0.5 * interfering_carrier.baud_rate)) carrier.baud_rate * (delta_f - 0.5 * interfering_carrier.baud_rate))
return psi 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

268
gnpy/core/service_sheet.py Normal file
View File

@@ -0,0 +1,268 @@
#!/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
from gnpy.core.exceptions import ServiceError
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, 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
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)
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(' | ')
# 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):
# 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': '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='', bidir=False, filter_region=None):
""" converts a service sheet into a json structure
"""
if filter_region is None:
filter_region = []
service = parse_excel(input_filename)
req = [Request_element(n, eqpt_filename, bidir) 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):
""" 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))

View File

@@ -2,12 +2,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
gnpy.topology.spectrum_assignment gnpy.core.spectrum_assignment
================================= =============================
This module contains the :class:`Oms` and :class:`Bitmap` classes and methods to This module contains the Oms and Bitmap classes and the different method to
select and assign spectrum. The :func:`spectrum_selection` function identifies the free select and assign spectrum. Spectrum_selection function identifies the free
slots and :func:`select_candidate` selects the candidate spectrum according to slots and select_candidate selects the candidate spectrum according to
strategy: for example first fit strategy: for example first fit
oms records its elements, and elements are updated with an oms to have oms records its elements, and elements are updated with an oms to have
element/oms correspondace element/oms correspondace
@@ -17,15 +17,13 @@ from collections import namedtuple
from logging import getLogger from logging import getLogger
from math import ceil from math import ceil
from gnpy.core.elements import Roadm, Transceiver from gnpy.core.elements import Roadm, Transceiver
from gnpy.core.exceptions import ServiceError, SpectrumError from gnpy.core.exceptions import SpectrumError
LOGGER = getLogger(__name__) LOGGER = getLogger(__name__)
class Bitmap: class Bitmap:
""" records the spectrum occupation """ records the spectrum occupation
""" """
def __init__(self, f_min, f_max, grid, guardband=0.15e12, bitmap=None): 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 # 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 # that a channel can be assigned with center frequency fmin (means that its
@@ -48,12 +46,10 @@ class Bitmap:
""" converts the n (itu grid) into a local index """ converts the n (itu grid) into a local index
""" """
return self.freq_index[i] return self.freq_index[i]
def geti(self, nvalue): def geti(self, nvalue):
""" converts the local index into n (itu grid) """ converts the local index into n (itu grid)
""" """
return self.freq_index.index(nvalue) return self.freq_index.index(nvalue)
def insert_left(self, newbitmap): def insert_left(self, newbitmap):
""" insert bitmap on the left to align oms bitmaps if their start frequencies are different """ insert bitmap on the left to align oms bitmaps if their start frequencies are different
""" """
@@ -61,7 +57,6 @@ class Bitmap:
temp = list(range(self.n_min-len(newbitmap), self.n_min)) temp = list(range(self.n_min-len(newbitmap), self.n_min))
self.freq_index = temp + self.freq_index self.freq_index = temp + self.freq_index
self.n_min = self.freq_index[0] self.n_min = self.freq_index[0]
def insert_right(self, newbitmap): def insert_right(self, newbitmap):
""" insert bitmap on the right to align oms bitmaps if their stop frequencies are different """ insert bitmap on the right to align oms bitmaps if their stop frequencies are different
""" """
@@ -69,16 +64,13 @@ class Bitmap:
self.freq_index = self.freq_index + list(range(self.n_max, self.n_max+len(newbitmap))) self.freq_index = self.freq_index + list(range(self.n_max, self.n_max+len(newbitmap)))
self.n_max = self.freq_index[-1] self.n_max = self.freq_index[-1]
# +'grid available_slots f_min f_max services_list') # +'grid available_slots f_min f_max services_list')
OMSParams = namedtuple('OMSParams', 'oms_id el_id_list el_list') OMSParams = namedtuple('OMSParams', 'oms_id el_id_list el_list')
class OMS: class OMS:
""" OMS class is the logical container that represent a link between two adjacent ROADMs and """ OMS class is the logical container that represent a link between two adjacent ROADMs and
records the crossed elements and the occupied spectrum records the crossed elements and the occupied spectrum
""" """
def __init__(self, *args, **params): def __init__(self, *args, **params):
params = OMSParams(**params) params = OMSParams(**params)
self.oms_id = params.oms_id self.oms_id = params.oms_id
@@ -88,11 +80,9 @@ class OMS:
self.nb_channels = 0 self.nb_channels = 0
self.service_list = [] self.service_list = []
# TODO # TODO
def __str__(self): def __str__(self):
return '\n\t'.join([f'{type(self).__name__} {self.oms_id}', return '\n\t'.join([f'{type(self).__name__} {self.oms_id}',
f'{self.el_id_list[0]} - {self.el_id_list[-1]}']) f'{self.el_id_list[0]} - {self.el_id_list[-1]}'])
def __repr__(self): def __repr__(self):
return '\n\t'.join([f'{type(self).__name__} {self.oms_id}', return '\n\t'.join([f'{type(self).__name__} {self.oms_id}',
f'{self.el_id_list[0]} - {self.el_id_list[-1]}', '\n']) f'{self.el_id_list[0]} - {self.el_id_list[-1]}', '\n'])
@@ -128,22 +118,25 @@ class OMS:
def assign_spectrum(self, nvalue, mvalue): def assign_spectrum(self, nvalue, mvalue):
""" change oms spectrum to mark spectrum assigned """ change oms spectrum to mark spectrum assigned
""" """
if not isinstance(nvalue, int): if (nvalue is None or mvalue is None or isinstance(nvalue, float)
raise SpectrumError(f'N must be a signed integer, got {nvalue}') or isinstance(mvalue, float) or mvalue == 0):
if not isinstance(mvalue, int): raise SpectrumError('could not assign None values')
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) startn, stopn = mvalue_to_slots(nvalue, mvalue)
if stopn > self.spectrum_bitmap.n_max: # print(f'startn stop n {startn} , {stopn}')
raise SpectrumError(f'N {nvalue}, M {mvalue} over the N spectrum bitmap bounds') # assumes that guardbands are sufficient to ensure that assigning a center channel
if startn <= self.spectrum_bitmap.n_min: # at fmin or fmax is OK is startn > self.spectrum_bitmap.n_min
raise SpectrumError(f'N {nvalue}, M {mvalue} below the N spectrum bitmap bounds') if (nvalue <= self.spectrum_bitmap.freq_index_max and
nvalue >= self.spectrum_bitmap.freq_index_min and
stopn <= self.spectrum_bitmap.n_max and
startn > self.spectrum_bitmap.n_min):
# verification that both length are identical
self.spectrum_bitmap.bitmap[self.spectrum_bitmap.geti(startn):self.spectrum_bitmap.geti(stopn)+1] = [0] * (stopn-startn+1) self.spectrum_bitmap.bitmap[self.spectrum_bitmap.geti(startn):self.spectrum_bitmap.geti(stopn)+1] = [0] * (stopn-startn+1)
return True
else:
msg = f'Could not assign n {nvalue}, m {mvalue} values:' +\
f' one or several slots are not available'
LOGGER.info(msg)
return False
def add_service(self, service_id, nb_wl): def add_service(self, service_id, nb_wl):
""" record service and mark spectrum as occupied """ record service and mark spectrum as occupied
@@ -151,35 +144,16 @@ class OMS:
self.service_list.append(service_id) self.service_list.append(service_id)
self.nb_channels += nb_wl self.nb_channels += nb_wl
def frequency_to_n(freq, grid=0.00625e12): def frequency_to_n(freq, grid=0.00625e12):
""" converts frequency into the n value (ITU grid) """ 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) return (int)((freq-193.1e12)/grid)
def nvalue_to_frequency(nvalue, grid=0.00625e12): def nvalue_to_frequency(nvalue, grid=0.00625e12):
""" converts n value into a frequency """ 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 return 193.1e12 + nvalue * grid
def mvalue_to_slots(nvalue, mvalue): def mvalue_to_slots(nvalue, mvalue):
""" convert center n an m into start and stop n """ convert center n an m into start and stop n
""" """
@@ -187,43 +161,21 @@ def mvalue_to_slots(nvalue, mvalue):
stopn = nvalue + mvalue -1 stopn = nvalue + mvalue -1
return startn, stopn return startn, stopn
def slots_to_m(startn, stopn): def slots_to_m(startn, stopn):
""" converts the start and stop n values to the center n and m value """ 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) nvalue = (int)((startn+stopn+1)/2)
mvalue = (int)((stopn-startn+1)/2) mvalue = (int)((stopn-startn+1)/2)
return nvalue, mvalue return nvalue, mvalue
def m_to_freq(nvalue, mvalue, grid=0.00625e12): def m_to_freq(nvalue, mvalue, grid=0.00625e12):
""" converts m into frequency range """ 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) startn, stopn = mvalue_to_slots(nvalue, mvalue)
fstart = nvalue_to_frequency(startn, grid) fstart = nvalue_to_frequency(startn, grid)
fstop = nvalue_to_frequency(stopn+1, grid) fstop = nvalue_to_frequency(stopn+1, grid)
return fstart, fstop return fstart, fstop
def align_grids(oms_list): def align_grids(oms_list):
""" used to apply same grid to all oms : same starting n, stop n and slot size """ used to apply same grid to all oms : same starting n, stop n and slot size
out of grid slots are set to 0 out of grid slots are set to 0
@@ -237,7 +189,6 @@ def align_grids(oms_list):
this_o.spectrum_bitmap.insert_right([0] * (n_max - this_o.spectrum_bitmap.n_max)) this_o.spectrum_bitmap.insert_right([0] * (n_max - this_o.spectrum_bitmap.n_max))
return oms_list return oms_list
def build_oms_list(network, equipment): def build_oms_list(network, equipment):
""" initialization of OMS list in the network """ initialization of OMS list in the network
an oms is build reading all intermediate nodes between two adjacent ROADMs an oms is build reading all intermediate nodes between two adjacent ROADMs
@@ -294,7 +245,6 @@ def build_oms_list(network, equipment):
reversed_oms(oms_list) reversed_oms(oms_list)
return oms_list return oms_list
def reversed_oms(oms_list): def reversed_oms(oms_list):
""" identifies reversed OMS """ identifies reversed OMS
only applicable for non parallel OMS only applicable for non parallel OMS
@@ -312,7 +262,8 @@ def reversed_oms(oms_list):
def bitmap_sum(band1, band2): def bitmap_sum(band1, band2):
"""mark occupied bitmap by 0 if the slot is occupied in band1 or in band2""" """ a functions that marks occupied bitmap by 0 if the slot is occupied in band1 or in band2
"""
res = [] res = []
for i, elem in enumerate(band1): for i, elem in enumerate(band1):
if band2[i] * elem == 0: if band2[i] * elem == 0:
@@ -321,9 +272,15 @@ def bitmap_sum(band1, band2):
res.append(1) res.append(1)
return res return res
def spectrum_selection(pth, oms_list, requested_m, requested_n=None): def spectrum_selection(pth, oms_list, requested_m, requested_n=None):
"""Collects spectrum availability and call the select_candidate function""" """ collects spectrum availability and call the select_candidate function
# step 1 collects pth spectrum availability
# step 2 if n is not None try to assign the spectrum
# if the spectrum is not available then sends back an "error"
# if n is None selects candidate spectrum
# select spectrum that fits the policy ( first fit, random, ABP...)
# step3 returns the selection
"""
# use indexes instead of ITU-T n values # use indexes instead of ITU-T n values
path_oms = [] path_oms = []
@@ -373,7 +330,6 @@ def spectrum_selection(pth, oms_list, requested_m, requested_n=None):
# print(candidate) # print(candidate)
return candidate, path_oms return candidate, path_oms
def select_candidate(candidates, policy): def select_candidate(candidates, policy):
""" selects a candidate among all available spectrum """ selects a candidate among all available spectrum
""" """
@@ -385,7 +341,6 @@ def select_candidate(candidates, policy):
else: else:
raise ServiceError('Only first_fit spectrum assignment policy is implemented.') raise ServiceError('Only first_fit spectrum assignment policy is implemented.')
def pth_assign_spectrum(pths, rqs, oms_list, rpths): def pth_assign_spectrum(pths, rqs, oms_list, rpths):
""" basic first fit assignment """ basic first fit assignment
if reversed path are provided, means that occupation is bidir if reversed path are provided, means that occupation is bidir
@@ -395,7 +350,7 @@ def pth_assign_spectrum(pths, rqs, oms_list, rpths):
try: try:
if rqs[i].blocking_reason: if rqs[i].blocking_reason:
rqs[i].blocked = True rqs[i].blocked = True
rqs[i].N = None rqs[i].N = 0
rqs[i].M = 0 rqs[i].M = 0
except AttributeError: except AttributeError:
nb_wl = ceil(rqs[i].path_bandwidth / rqs[i].bit_rate) nb_wl = ceil(rqs[i].path_bandwidth / rqs[i].bit_rate)
@@ -404,25 +359,9 @@ def pth_assign_spectrum(pths, rqs, oms_list, rpths):
# assumes that all channels must be grouped # assumes that all channels must be grouped
# TODO : enables non contiguous reservation in case of blocking # TODO : enables non contiguous reservation in case of blocking
requested_m = ceil(rqs[i].spacing / 0.0125e12) * nb_wl requested_m = ceil(rqs[i].spacing / 0.0125e12) * nb_wl
if hasattr(rqs[i], 'M') and rqs[i].M is not None: # concatenate all path and reversed path elements to derive slots availability
# 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, (center_n, startn, stopn), path_oms = spectrum_selection(pth + rpths[i], oms_list, requested_m,
requested_n) requested_n=None)
# checks that requested_m is fitting startm and stopm # 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 # 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 # checks that spectrum is not None else indicate blocking reason
@@ -442,6 +381,6 @@ def pth_assign_spectrum(pths, rqs, oms_list, rpths):
rqs[i].M = requested_m rqs[i].M = requested_m
else: else:
rqs[i].blocked = True rqs[i].blocked = True
rqs[i].N = None rqs[i].N = 0
rqs[i].M = 0 rqs[i].M = 0
rqs[i].blocking_reason = 'NO_SPECTRUM' rqs[i].blocking_reason = 'NO_SPECTRUM'

5
gnpy/core/units.py Normal file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
UNITS = {'m': 1,
'km': 1E3}

View File

@@ -9,17 +9,28 @@ This module contains utility functions that are used with gnpy.
''' '''
import json
from csv import writer from csv import writer
import numpy as np import numpy as np
from numpy import pi, cos, sqrt, log10 from numpy import pi, cos, sqrt, log10
from scipy import constants 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): 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':
[ [
@@ -36,9 +47,7 @@ def write_csv(obj, filename):
] ]
} }
The generated csv file will be: the generated csv file will be:
::
result_category 1 result_category 1
header 1 header 2 header 1 header 2
value_xxx value_yyy value_xxx value_yyy
@@ -57,56 +66,41 @@ def write_csv(obj, filename):
for data_dict in data_list: for data_dict in data_list:
w.writerow([_ for _ in data_dict.values()]) 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): def arrange_frequencies(length, start, stop):
"""Create an array of frequencies """Create an array of frequencies
:param length: number of elements :param length: number of elements
:param start: Start frequency in THz :param star: Start frequency in THz
:param stop: Stop frequency in THz :param stop: Stop frequency in THz
:type length: integer :type length: integer
:type start: float :type start: float
:type stop: 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 :rtype: numpy.ndarray
""" """
return np.linspace(start, stop, length) return np.linspace(start, stop, length)
def h():
"""
Returns plank's constant in J*s
"""
return constants.h
def lin2db(value): 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) return 10 * log10(value)
def db2lin(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) return 10**(value / 10)
def round2float(number, step): def round2float(number, step):
step = round(step, 1) step = round(step, 1)
if step >= 0.01: if step >= 0.01:
@@ -116,28 +110,19 @@ def round2float(number, step):
number = round(number, 2) number = round(number, 2)
return number return number
wavelength2freq = constants.lambda2nu wavelength2freq = constants.lambda2nu
freq2wavelength = constants.nu2lambda freq2wavelength = constants.nu2lambda
def freq2wavelength(value): def freq2wavelength(value):
""" Converts frequency units to wavelength units. """ Converts frequency units to wavelength units.
>>> round(freq2wavelength(191.35e12) * 1e9, 3)
1566.723
>>> round(freq2wavelength(196.1e12) * 1e9, 3)
1528.773
""" """
return constants.c / value return c() / value
def snr_sum(snr, bw, snr_added, bw_added=12.5e9): def snr_sum(snr, bw, snr_added, bw_added=12.5e9):
snr_added = snr_added - lin2db(bw/bw_added) snr_added = snr_added - lin2db(bw/bw_added)
snr = -lin2db(db2lin(-snr)+db2lin(-snr_added)) snr = -lin2db(db2lin(-snr)+db2lin(-snr_added))
return snr return snr
def deltawl2deltaf(delta_wl, wavelength): def deltawl2deltaf(delta_wl, wavelength):
""" deltawl2deltaf(delta_wl, wavelength): """ deltawl2deltaf(delta_wl, wavelength):
delta_wl is BW in wavelength units delta_wl is BW in wavelength units
@@ -199,7 +184,6 @@ def rrc(ffs, baud_rate, alpha):
hf[p_inds] = 1 hf[p_inds] = 1
return sqrt(hf) return sqrt(hf)
def merge_amplifier_restrictions(dict1, dict2): def merge_amplifier_restrictions(dict1, dict2):
"""Updates contents of dicts recursively """Updates contents of dicts recursively
@@ -222,7 +206,6 @@ def merge_amplifier_restrictions(dict1, dict2):
copy_dict1[key] = dict2[key] copy_dict1[key] = dict2[key]
return copy_dict1 return copy_dict1
def silent_remove(this_list, elem): def silent_remove(this_list, elem):
"""Remove matching elements from a list without raising ValueError """Remove matching elements from a list without raising ValueError
@@ -240,59 +223,3 @@ def silent_remove(this_list, elem):
except ValueError: except ValueError:
pass pass
return this_list 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')

Some files were not shown because too many files have changed in this diff Show More