Merge branch 'feature/CO2_expert' into 'master'
CO2 Expert App See merge request caimira/caimira!424
This commit is contained in:
commit
f80dff1d74
25 changed files with 1104 additions and 197 deletions
|
|
@ -124,12 +124,12 @@ auth-service-image_builder:
|
|||
DOCKER_CONTEXT_DIRECTORY: app-config/auth-service
|
||||
|
||||
|
||||
caimira-webservice-image_builder:
|
||||
calculator-app-image_builder:
|
||||
extends:
|
||||
- .image_builder
|
||||
variables:
|
||||
IMAGE_NAME: caimira-webservice
|
||||
DOCKERFILE_DIRECTORY: app-config/caimira-webservice
|
||||
IMAGE_NAME: calculator-app
|
||||
DOCKERFILE_DIRECTORY: app-config/calculator-app
|
||||
DOCKER_CONTEXT_DIRECTORY: ""
|
||||
|
||||
|
||||
|
|
@ -159,11 +159,11 @@ link_auth-service_with_gitlab_registry:
|
|||
variables:
|
||||
IMAGE_NAME: auth-service
|
||||
|
||||
link_caimira-webservice_with_gitlab_registry:
|
||||
link_calculator-app_with_gitlab_registry:
|
||||
extends:
|
||||
- .link_docker_images_with_gitlab_registry
|
||||
variables:
|
||||
IMAGE_NAME: caimira-webservice
|
||||
IMAGE_NAME: calculator-app
|
||||
|
||||
link_calculator_with_gitlab_registry:
|
||||
extends:
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ pytest ./caimira
|
|||
|
||||
```
|
||||
s2i build file://$(pwd) --copy --keep-symlinks --context-dir ./app-config/nginx/ centos/nginx-112-centos7 caimira-nginx-app
|
||||
docker build . -f ./app-config/caimira-webservice/Dockerfile -t caimira-webservice
|
||||
docker build . -f ./app-config/calculator-app/Dockerfile -t calculator-app
|
||||
docker build ./app-config/auth-service -t auth-service
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ COPY ./app-config/caimira-public-docker-image/run_caimira.sh /opt/caimira/start.
|
|||
# In the best case this will be a no-op.
|
||||
RUN cd /opt/caimira/src/ && /opt/caimira/app/bin/pip install -r /opt/caimira/src/requirements.txt
|
||||
RUN /opt/caimira/app/bin/jupyter trust /opt/caimira/src/caimira/apps/expert/*.ipynb
|
||||
RUN /opt/caimira/app/bin/jupyter trust /opt/caimira/src/caimira/apps/expert_co2/*.ipynb
|
||||
COPY ./app-config/caimira-public-docker-image/nginx.conf /opt/caimira/nginx.conf
|
||||
|
||||
EXPOSE 8080
|
||||
|
|
|
|||
|
|
@ -50,6 +50,12 @@ http {
|
|||
rewrite ^/expert-app$ /voila-server/ last;
|
||||
rewrite ^/(files/static)/(.*)$ /voila-server/voila/$1/$2 last;
|
||||
|
||||
location /co2-voila-server/ {
|
||||
proxy_pass http://localhost:8083/co2-voila-server/;
|
||||
}
|
||||
rewrite ^/co2-app$ /voila-server/ last;
|
||||
rewrite ^/(files/static)/(.*)$ /voila-server/voila/$1/$2 last;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:8081;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,5 +12,10 @@ cd /opt/caimira/src/caimira
|
|||
--Voila.tornado_settings 'allow_origin=*' \
|
||||
>> /var/log/expert-app.log 2>&1 &
|
||||
|
||||
/opt/caimira/app/bin/python -m voila /opt/caimira/src/caimira/apps/expert_co2/caimira.ipynb \
|
||||
--port=8083 --no-browser --base_url=/co2-voila-server/ \
|
||||
--Voila.tornado_settings 'allow_origin=*' \
|
||||
>> /var/log/co2-app.log 2>&1 &
|
||||
|
||||
# Run the calculator in the foreground.
|
||||
/opt/caimira/app/bin/python -m caimira.apps.calculator --port 8081 --no-debug
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ FROM registry.cern.ch/docker.io/condaforge/mambaforge as conda
|
|||
RUN mamba create --yes -p /opt/app python=3.9
|
||||
COPY . /opt/app-source
|
||||
RUN cd /opt/app-source && conda run -p /opt/app python -m pip install -r ./requirements.txt .[app]
|
||||
COPY app-config/caimira-webservice/app.sh /opt/app/bin/caimira-app.sh
|
||||
COPY app-config/calculator-app/app.sh /opt/app/bin/calculator-app.sh
|
||||
RUN cd /opt/app \
|
||||
&& find -name '*.a' -delete \
|
||||
&& rm -rf /opt/app/conda-meta \
|
||||
|
|
@ -32,5 +32,5 @@ WORKDIR /scratch
|
|||
RUN CAIMIRA_INIT_FILE=$(/opt/app/bin/python -c "import caimira; print(caimira.__file__)") \
|
||||
&& ln -s $(dirname ${CAIMIRA_INIT_FILE}) /scratch/caimira
|
||||
CMD [ \
|
||||
"caimira-app.sh" \
|
||||
"calculator-app.sh" \
|
||||
]
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/bash
|
||||
|
||||
if [[ "$APP_NAME" == "caimira-webservice" ]]; then
|
||||
if [[ "$APP_NAME" == "calculator-app" ]]; then
|
||||
args=("$@")
|
||||
if [ "$DEBUG" != "true" ] && [[ ! "${args[@]}" =~ "--no-debug" ]]; then
|
||||
args+=("--no-debug")
|
||||
|
|
@ -26,6 +26,9 @@ if [[ "$APP_NAME" == "caimira-webservice" ]]; then
|
|||
elif [[ "$APP_NAME" == "caimira-voila" ]]; then
|
||||
echo "Starting the voila service"
|
||||
voila caimira/apps/expert/ --port=8080 --no-browser --base_url=/voila-server/ --tornado_settings 'allow_origin=*'
|
||||
elif [[ "$APP_NAME" == "caimira-co2-voila" ]]; then
|
||||
echo "Starting the CO2 voila service"
|
||||
voila caimira/apps/expert_co2/ --port=8080 --no-browser --base_url=/co2-voila-server/ --tornado_settings 'allow_origin=*'
|
||||
else
|
||||
echo "No APP_NAME specified"
|
||||
exit 1
|
||||
|
|
@ -1,26 +1,32 @@
|
|||
version: "3.8"
|
||||
services:
|
||||
caimira-app:
|
||||
image: caimira-webservice
|
||||
expert-app:
|
||||
image: calculator-app
|
||||
environment:
|
||||
- APP_NAME=caimira-voila
|
||||
user: ${CURRENT_UID:?"Please run as follows 'CURRENT_UID=$(id -u):$(id -g) docker-compose up'"}
|
||||
|
||||
caimira-webservice:
|
||||
image: caimira-webservice
|
||||
expert-co2-app:
|
||||
image: calculator-app
|
||||
environment:
|
||||
- APP_NAME=caimira-co2-voila
|
||||
user: ${CURRENT_UID:?"Please run as follows 'CURRENT_UID=$(id -u):$(id -g) docker-compose up'"}
|
||||
|
||||
calculator-app:
|
||||
image: calculator-app
|
||||
environment:
|
||||
- COOKIE_SECRET
|
||||
- APP_NAME=caimira-webservice
|
||||
- APP_NAME=calculator-app
|
||||
- APPLICATION_ROOT=/
|
||||
- CAIMIRA_CALCULATOR_PREFIX=/calculator-cern
|
||||
- CAIMIRA_THEME=caimira/apps/templates/cern
|
||||
user: ${CURRENT_UID}
|
||||
|
||||
caimira-calculator-open:
|
||||
image: caimira-webservice
|
||||
calculator-open-app:
|
||||
image: calculator-app
|
||||
environment:
|
||||
- COOKIE_SECRET
|
||||
- APP_NAME=caimira-webservice
|
||||
- APP_NAME=calculator-app
|
||||
- APPLICATION_ROOT=/
|
||||
- CAIMIRA_CALCULATOR_PREFIX=/calculator-open
|
||||
user: ${CURRENT_UID}
|
||||
|
|
@ -40,11 +46,13 @@ services:
|
|||
ports:
|
||||
- "8080:8080"
|
||||
depends_on:
|
||||
caimira-webservice:
|
||||
calculator-app:
|
||||
condition: service_started
|
||||
caimira-calculator-open:
|
||||
calculator-open-app:
|
||||
condition: service_started
|
||||
caimira-app:
|
||||
expert-app:
|
||||
condition: service_started
|
||||
expert-co2-app:
|
||||
condition: service_started
|
||||
auth-service:
|
||||
condition: service_started
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ http {
|
|||
# Pass the request on to the webservice. Most likely the URI won't
|
||||
# exist so we get a 404 from that service instead (good as the 404
|
||||
# pages are consistent).
|
||||
proxy_pass http://caimira-webservice:8080/$request_uri;
|
||||
proxy_pass http://calculator-app:8080/$request_uri;
|
||||
}
|
||||
|
||||
location /voila-server/ {
|
||||
|
|
@ -81,9 +81,9 @@ http {
|
|||
error_page 401 = @error401;
|
||||
error_page 404 = @proxy_404_error_handler;
|
||||
|
||||
# caimira-app is the name of the voila server in each of docker-compose,
|
||||
# expert-app is the name of the voila server in each of docker-compose,
|
||||
# caimira-test.web.cern.ch and caimira.web.cern.ch.
|
||||
proxy_pass http://caimira-app:8080/voila-server/;
|
||||
proxy_pass http://expert-app:8080/voila-server/;
|
||||
}
|
||||
rewrite ^/expert-app$ /voila-server/voila/render/caimira.ipynb last;
|
||||
rewrite ^/(files/static)/(.*)$ /voila-server/voila/$1/$2 last;
|
||||
|
|
@ -93,9 +93,28 @@ http {
|
|||
absolute_redirect off;
|
||||
rewrite ^/voila/(.*)$ /voila-server/voila/$1 redirect;
|
||||
|
||||
location /co2-voila-server/ {
|
||||
proxy_intercept_errors on;
|
||||
|
||||
# Anything under voila-server or co2-app is authenticated.
|
||||
auth_request /auth/probe;
|
||||
error_page 401 = @error401;
|
||||
error_page 404 = @proxy_404_error_handler;
|
||||
|
||||
# expert-co2-app is the name of the voila server in each of docker-compose,
|
||||
# caimira-test.web.cern.ch and caimira.web.cern.ch.
|
||||
proxy_pass http://expert-co2-app:8080/co2-voila-server/;
|
||||
}
|
||||
rewrite ^/co2-app$ /co2-voila-server/voila/render/caimira.ipynb last;
|
||||
rewrite ^/(files/static)/(.*)$ /co2-voila-server/voila/$1/$2 last;
|
||||
|
||||
# Before implementing the nginx router we could access /voila/render/caimira.ipynb.
|
||||
# Redirect this (and all other) URLs to the new scheme.
|
||||
rewrite ^/voila/(.*)$ /co2-voila-server/voila/$1 redirect;
|
||||
|
||||
location / {
|
||||
# By default we have no authentication.
|
||||
proxy_pass http://caimira-webservice:8080;
|
||||
proxy_pass http://calculator-app:8080;
|
||||
}
|
||||
|
||||
location /calculator {
|
||||
|
|
@ -107,14 +126,14 @@ http {
|
|||
auth_request /auth/probe;
|
||||
error_page 401 = @error401;
|
||||
|
||||
# caimira-webservice is the name of the tornado server (for the calculator)
|
||||
# calculator-app is the name of the tornado server (for the calculator)
|
||||
# in each of docker-compose, caimira-test.web.cern.ch and caimira.web.cern.ch.
|
||||
proxy_pass http://caimira-webservice:8080/calculator-cern;
|
||||
proxy_pass http://calculator-app:8080/calculator-cern;
|
||||
}
|
||||
|
||||
location /calculator-open {
|
||||
# Public open calculator
|
||||
proxy_pass http://caimira-calculator-open:8080/calculator-open;
|
||||
proxy_pass http://calculator-open-app:8080/calculator-open;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,21 +72,21 @@
|
|||
apiVersion: apps.openshift.io/v1
|
||||
kind: DeploymentConfig
|
||||
metadata:
|
||||
name: caimira-app
|
||||
labels: {app: caimira-app}
|
||||
name: expert-app
|
||||
labels: {app: expert-app}
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: caimira-app
|
||||
app: expert-app
|
||||
spec:
|
||||
containers:
|
||||
- name: caimira-webservice
|
||||
- name: calculator-app
|
||||
env:
|
||||
- name: APP_NAME
|
||||
value: caimira-voila
|
||||
image: '${PROJECT_NAME}/caimira-webservice'
|
||||
image: '${PROJECT_NAME}/calculator-app'
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
protocol: TCP
|
||||
|
|
@ -113,17 +113,74 @@
|
|||
type: Rolling
|
||||
test: false
|
||||
selector:
|
||||
app: caimira-app
|
||||
app: expert-app
|
||||
triggers:
|
||||
- type: ConfigChange
|
||||
- type: ImageChange
|
||||
imageChangeParams:
|
||||
automatic: true
|
||||
containerNames:
|
||||
- caimira-webservice
|
||||
- calculator-app
|
||||
from:
|
||||
kind: ImageStreamTag
|
||||
name: 'caimira-webservice:latest'
|
||||
name: 'calculator-app:latest'
|
||||
namespace: ${PROJECT_NAME}
|
||||
-
|
||||
apiVersion: apps.openshift.io/v1
|
||||
kind: DeploymentConfig
|
||||
metadata:
|
||||
name: expert-co2-app
|
||||
labels: {app: expert-co2-app}
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: expert-co2-app
|
||||
spec:
|
||||
containers:
|
||||
- name: calculator-app
|
||||
env:
|
||||
- name: APP_NAME
|
||||
value: caimira-co2-voila
|
||||
image: '${PROJECT_NAME}/calculator-app'
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
protocol: TCP
|
||||
imagePullPolicy: Always
|
||||
resources:
|
||||
limits: { cpu: '1', memory: 1Gi }
|
||||
requests: { cpu: 1m, memory: 512Mi }
|
||||
terminationMessagePath: /dev/termination-log
|
||||
terminationMessagePolicy: File
|
||||
dnsPolicy: ClusterFirst
|
||||
restartPolicy: Always
|
||||
schedulerName: default-scheduler
|
||||
securityContext: { }
|
||||
terminationGracePeriodSeconds: 30
|
||||
strategy:
|
||||
activeDeadlineSeconds: 21600
|
||||
resources: { }
|
||||
rollingParams:
|
||||
intervalSeconds: 1
|
||||
maxSurge: 25%
|
||||
maxUnavailable: 25%
|
||||
timeoutSeconds: 600
|
||||
updatePeriodSeconds: 1
|
||||
type: Rolling
|
||||
test: false
|
||||
selector:
|
||||
app: expert-co2-app
|
||||
triggers:
|
||||
- type: ConfigChange
|
||||
- type: ImageChange
|
||||
imageChangeParams:
|
||||
automatic: true
|
||||
containerNames:
|
||||
- calculator-app
|
||||
from:
|
||||
kind: ImageStreamTag
|
||||
name: 'calculator-app:latest'
|
||||
namespace: ${PROJECT_NAME}
|
||||
-
|
||||
apiVersion: apps.openshift.io/v1
|
||||
|
|
@ -182,19 +239,19 @@
|
|||
apiVersion: apps.openshift.io/v1
|
||||
kind: DeploymentConfig
|
||||
metadata:
|
||||
name: caimira-webservice
|
||||
name: calculator-app
|
||||
labels:
|
||||
image: caimira-webservice
|
||||
app: caimira-webservice
|
||||
image: calculator-app
|
||||
app: calculator-app
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: caimira-webservice
|
||||
app: calculator-app
|
||||
spec:
|
||||
containers:
|
||||
- name: caimira-webservice
|
||||
- name: calculator-app
|
||||
env:
|
||||
- name: COOKIE_SECRET
|
||||
valueFrom:
|
||||
|
|
@ -204,7 +261,7 @@
|
|||
- name: REPORT_PARALLELISM
|
||||
value: '3'
|
||||
- name: APP_NAME
|
||||
value: caimira-webservice
|
||||
value: calculator-app
|
||||
- name: APPLICATION_ROOT
|
||||
value: /
|
||||
- name: CAIMIRA_CALCULATOR_PREFIX
|
||||
|
|
@ -226,7 +283,7 @@
|
|||
secretKeyRef:
|
||||
key: ARVE_API_KEY
|
||||
name: arve-api
|
||||
image: '${PROJECT_NAME}/caimira-webservice'
|
||||
image: '${PROJECT_NAME}/calculator-app'
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
protocol: TCP
|
||||
|
|
@ -267,43 +324,43 @@
|
|||
type: Rolling
|
||||
test: false
|
||||
selector:
|
||||
app: caimira-webservice
|
||||
app: calculator-app
|
||||
triggers:
|
||||
- type: ImageChange
|
||||
imageChangeParams:
|
||||
automatic: true
|
||||
containerNames:
|
||||
- caimira-webservice
|
||||
- calculator-app
|
||||
from:
|
||||
kind: ImageStreamTag
|
||||
name: 'caimira-webservice:latest'
|
||||
name: 'calculator-app:latest'
|
||||
namespace: ${PROJECT_NAME}
|
||||
- type: ConfigChange
|
||||
-
|
||||
apiVersion: apps.openshift.io/v1
|
||||
kind: DeploymentConfig
|
||||
metadata:
|
||||
name: caimira-calculator-open
|
||||
name: calculator-open-app
|
||||
labels:
|
||||
image: caimira-webservice
|
||||
app: caimira-calculator-open
|
||||
image: calculator-app
|
||||
app: calculator-open-app
|
||||
spec:
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: caimira-calculator-open
|
||||
app: calculator-open-app
|
||||
spec:
|
||||
containers:
|
||||
- name: caimira-calculator-open
|
||||
- name: calculator-open-app
|
||||
env:
|
||||
- name: APP_NAME
|
||||
value: caimira-webservice
|
||||
value: calculator-app
|
||||
- name: APPLICATION_ROOT
|
||||
value: /
|
||||
- name: CAIMIRA_CALCULATOR_PREFIX
|
||||
value: /calculator-open
|
||||
image: '${PROJECT_NAME}/caimira-webservice'
|
||||
image: '${PROJECT_NAME}/calculator-app'
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
protocol: TCP
|
||||
|
|
@ -334,17 +391,17 @@
|
|||
type: Rolling
|
||||
test: false
|
||||
selector:
|
||||
app: caimira-calculator-open
|
||||
app: calculator-open-app
|
||||
triggers:
|
||||
- type: ConfigChange
|
||||
- type: ImageChange
|
||||
imageChangeParams:
|
||||
automatic: true
|
||||
containerNames:
|
||||
- caimira-calculator-open
|
||||
- calculator-open-app
|
||||
from:
|
||||
kind: ImageStreamTag
|
||||
name: 'caimira-webservice:latest'
|
||||
name: 'calculator-app:latest'
|
||||
namespace: ${PROJECT_NAME}
|
||||
- type: ConfigChange
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
kind: ImageStream
|
||||
apiVersion: image.openshift.io/v1
|
||||
metadata:
|
||||
name: caimira-webservice
|
||||
name: calculator-app
|
||||
spec:
|
||||
lookupPolicy:
|
||||
local: False
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@
|
|||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app: caimira-app
|
||||
name: caimira-app
|
||||
app: expert-app
|
||||
name: expert-app
|
||||
spec:
|
||||
ports:
|
||||
- name: 8080-tcp
|
||||
|
|
@ -41,7 +41,24 @@
|
|||
protocol: TCP
|
||||
targetPort: 8080
|
||||
selector:
|
||||
deploymentconfig: caimira-app
|
||||
deploymentconfig: expert-app
|
||||
sessionAffinity: 'None'
|
||||
type: 'ClusterIP'
|
||||
-
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app: expert-co2-app
|
||||
name: expert-co2-app
|
||||
spec:
|
||||
ports:
|
||||
- name: 8080-tcp
|
||||
port: 8080
|
||||
protocol: TCP
|
||||
targetPort: 8080
|
||||
selector:
|
||||
deploymentconfig: expert-co2-app
|
||||
sessionAffinity: 'None'
|
||||
type: 'ClusterIP'
|
||||
-
|
||||
|
|
@ -66,8 +83,8 @@
|
|||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app: caimira-webservice
|
||||
name: caimira-webservice
|
||||
app: calculator-app
|
||||
name: calculator-app
|
||||
spec:
|
||||
ports:
|
||||
- name: 8080-tcp
|
||||
|
|
@ -75,7 +92,7 @@
|
|||
protocol: TCP
|
||||
targetPort: 8080
|
||||
selector:
|
||||
deploymentconfig: caimira-webservice
|
||||
deploymentconfig: calculator-app
|
||||
sessionAffinity: 'None'
|
||||
type: 'ClusterIP'
|
||||
-
|
||||
|
|
@ -83,8 +100,8 @@
|
|||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app: caimira-calculator-open
|
||||
name: caimira-calculator-open
|
||||
app: calculator-open-app
|
||||
name: calculator-open-app
|
||||
spec:
|
||||
ports:
|
||||
- name: 8080-tcp
|
||||
|
|
@ -92,6 +109,6 @@
|
|||
protocol: TCP
|
||||
targetPort: 8080
|
||||
selector:
|
||||
deploymentconfig: caimira-calculator-open
|
||||
deploymentconfig: calculator-open-app
|
||||
sessionAffinity: 'None'
|
||||
type: 'ClusterIP'
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from .expert import ExpertApplication
|
||||
from .expert_co2 import CO2Application
|
||||
|
||||
|
||||
__all__ = ['ExpertApplication']
|
||||
__all__ = ['ExpertApplication', 'CO2Application']
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ from .user import AuthenticatedUser, AnonymousUser
|
|||
# calculator version. If the calculator needs to make breaking changes (e.g. change
|
||||
# form attributes) then it can also increase its MAJOR version without needing to
|
||||
# increase the overall CAiMIRA version (found at ``caimira.__version__``).
|
||||
__version__ = "4.6"
|
||||
__version__ = "4.7"
|
||||
|
||||
|
||||
class BaseRequestHandler(RequestHandler):
|
||||
|
|
|
|||
|
|
@ -214,7 +214,7 @@ class ExposureModelResult(View):
|
|||
self.html_output.value = '<br>\n'.join(lines)
|
||||
|
||||
|
||||
class ExposureComparissonResult(View):
|
||||
class ExposureComparisonResult(View):
|
||||
def __init__(self):
|
||||
self.figure = matplotlib.figure.Figure(figsize=(9, 6))
|
||||
ipympl_canvas(self.figure)
|
||||
|
|
@ -255,6 +255,7 @@ class ExposureComparissonResult(View):
|
|||
def update_plot(self, exp_models: typing.Tuple[models.ExposureModel, ...], labels: typing.Tuple[str, ...]):
|
||||
[line.remove() for line in self.ax.lines]
|
||||
[line.remove() for line in self.ax2.lines]
|
||||
|
||||
start, finish = models_start_end(exp_models)
|
||||
colors=['blue', 'red', 'orange', 'yellow', 'pink', 'purple', 'green', 'brown', 'black' ]
|
||||
ts = np.linspace(start, finish, num=250)
|
||||
|
|
@ -275,7 +276,10 @@ class ExposureComparissonResult(View):
|
|||
cumulative_top = max([max(cumulative_dose) for cumulative_dose in cumulative_doses])
|
||||
self.ax2.set_ylim(bottom=0., top=cumulative_top)
|
||||
|
||||
self.ax.legend()
|
||||
handles, labels = self.figure.gca().get_legend_handles_labels()
|
||||
by_label = dict(zip(labels, handles))
|
||||
self.ax.legend(by_label.values(), by_label.keys())
|
||||
|
||||
self.figure.canvas.draw()
|
||||
|
||||
|
||||
|
|
@ -879,6 +883,9 @@ class CAIMIRAStateBuilder(state.StateBuilder):
|
|||
# Note: The methods in this class must correspond to the *type* of the data classes.
|
||||
# For example, build_type__VentilationBase is called when dealing with ConcentrationModel
|
||||
# types as it has a ventilation: _VentilationBase field.
|
||||
|
||||
def __init__(self, selected_ventilation: str):
|
||||
self.selected_ventilation = selected_ventilation
|
||||
|
||||
def build_type_Mask(self, _: dataclasses.Field):
|
||||
return state.DataclassStatePredefined(
|
||||
|
|
@ -897,6 +904,7 @@ class CAIMIRAStateBuilder(state.StateBuilder):
|
|||
'HEPAFilter': self.build_generic(models.HEPAFilter),
|
||||
|
||||
},
|
||||
base_type=self.selected_ventilation,
|
||||
state_builder=self,
|
||||
)
|
||||
#Initialise the "Hinged window" state
|
||||
|
|
@ -933,14 +941,16 @@ class ExpertApplication(Controller):
|
|||
self._model_scenarios: typing.List[ScenarioType] = []
|
||||
self._active_scenario = 0
|
||||
self.multi_model_view = MultiModelView(self)
|
||||
self.comparison_view = ExposureComparissonResult()
|
||||
self.comparison_view = ExposureComparisonResult()
|
||||
self.current_scenario_figure = ExposureModelResult()
|
||||
self._results_tab = widgets.Tab(children=(
|
||||
self.current_scenario_figure.widget,
|
||||
self.comparison_view.widget,
|
||||
# self._debug_output,
|
||||
))
|
||||
self._results_tab.titles = ['Current scenario', 'Scenario comparison', "Debug"]
|
||||
for i, title in enumerate(['Current scenario', 'Scenario comparison', "Debug"]):
|
||||
self._results_tab.set_title(i, title)
|
||||
|
||||
self.widget = widgets.HBox(
|
||||
children=(
|
||||
self.multi_model_view.widget,
|
||||
|
|
@ -949,10 +959,10 @@ class ExpertApplication(Controller):
|
|||
)
|
||||
self.add_scenario('Scenario 1')
|
||||
|
||||
def build_new_model(self) -> state.DataclassInstanceState[models.ExposureModel]:
|
||||
def build_new_model(self, vent: str) -> state.DataclassInstanceState[models.ExposureModel]:
|
||||
default_model = state.DataclassInstanceState(
|
||||
models.ExposureModel,
|
||||
state_builder=CAIMIRAStateBuilder(),
|
||||
state_builder=CAIMIRAStateBuilder(selected_ventilation=vent),
|
||||
)
|
||||
default_model.dcs_update_from(baseline_model)
|
||||
# For the time-being, we have to initialise the select states. Careful
|
||||
|
|
@ -961,9 +971,13 @@ class ExpertApplication(Controller):
|
|||
return default_model
|
||||
|
||||
def add_scenario(self, name, copy_from_model: typing.Optional[state.DataclassInstanceState] = None):
|
||||
model = self.build_new_model()
|
||||
if copy_from_model is not None:
|
||||
model = self.build_new_model(vent=copy_from_model.concentration_model.ventilation._selected)
|
||||
model.dcs_update_from(copy_from_model.dcs_instance())
|
||||
else:
|
||||
model = self.build_new_model(vent='Natural') # Default
|
||||
model.dcs_update_from(baseline_model)
|
||||
|
||||
self._model_scenarios.append((name, model))
|
||||
self._active_scenario = len(self._model_scenarios) - 1
|
||||
model.dcs_observe(self.notify_model_values_changed)
|
||||
|
|
@ -1036,7 +1050,7 @@ class MultiModelView(View):
|
|||
self.add_tab(scenario_name, model)
|
||||
model_scenario_ids.append(id(model))
|
||||
tab_index = self._tab_model_ids.index(id(model))
|
||||
self.widget.titles = [scenario_name for (scenario_name, _) in model_scenarios]
|
||||
self.widget.set_title(tab_index, scenario_name)
|
||||
|
||||
# Any remaining model_scenario_ids are no longer needed, so remove
|
||||
# their tabs.
|
||||
|
|
@ -1067,8 +1081,7 @@ class MultiModelView(View):
|
|||
self._tab_model_ids.pop(tab_index)
|
||||
self._tab_widgets.pop(tab_index)
|
||||
self._tab_model_views.pop(tab_index)
|
||||
if self._active_tab_index >= tab_index:
|
||||
self._active_tab_index = max(0, self._active_tab_index - 1)
|
||||
|
||||
self.update_tab_widget()
|
||||
|
||||
def update_tab_widget(self):
|
||||
|
|
|
|||
|
|
@ -12,14 +12,30 @@
|
|||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"execution_count": 1,
|
||||
"metadata": {
|
||||
"pycharm": {
|
||||
"name": "#%%\n"
|
||||
},
|
||||
"scrolled": false
|
||||
},
|
||||
"outputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"application/vnd.jupyter.widget-view+json": {
|
||||
"model_id": "e8c4e2146d4847d5a1443781f2018483",
|
||||
"version_major": 2,
|
||||
"version_minor": 0
|
||||
},
|
||||
"text/plain": [
|
||||
"HBox(children=(Tab(children=(VBox(children=(VBox(children=(Button(button_style='success', description='Duplica…"
|
||||
]
|
||||
},
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import caimira.apps\n",
|
||||
"\n",
|
||||
|
|
@ -30,7 +46,7 @@
|
|||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"display_name": "caimira",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
|
|
@ -44,7 +60,12 @@
|
|||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.6.12"
|
||||
"version": "3.9.6"
|
||||
},
|
||||
"vscode": {
|
||||
"interpreter": {
|
||||
"hash": "c77495895472738765eb97c8f848f37a4e60c741d594ab92dd40b6b8f4cac818"
|
||||
}
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
|
|
|||
787
caimira/apps/expert_co2.py
Normal file
787
caimira/apps/expert_co2.py
Normal file
|
|
@ -0,0 +1,787 @@
|
|||
import dataclasses
|
||||
import ipywidgets as widgets
|
||||
import typing
|
||||
import numpy as np
|
||||
|
||||
from caimira import data, models, state
|
||||
import matplotlib
|
||||
import matplotlib.figure
|
||||
import matplotlib.lines as mlines
|
||||
import matplotlib.patches as patches
|
||||
from .expert import collapsible, ipympl_canvas, WidgetGroup, CAIMIRAStateBuilder
|
||||
|
||||
|
||||
baseline_model = models.CO2ConcentrationModel(
|
||||
room=models.Room(volume=120, humidity=0.5, inside_temp=models.PiecewiseConstant((0., 24.), (293.15,))),
|
||||
ventilation=models.HVACMechanical(active=models.PeriodicInterval(period=120, duration=120), q_air_mech=500),
|
||||
CO2_emitters=models.Population(
|
||||
number=10,
|
||||
presence=models.SpecificInterval(((8., 12.), (13., 17.))),
|
||||
mask=models.Mask.types['No mask'],
|
||||
activity=models.Activity.types['Seated'],
|
||||
host_immunity=0.,
|
||||
),
|
||||
CO2_atmosphere_concentration=440.44,
|
||||
CO2_fraction_exhaled=0.042,
|
||||
)
|
||||
|
||||
|
||||
class Controller:
|
||||
"""
|
||||
The singleton thing which is the top-level Application.
|
||||
|
||||
It is responsible for owning the Model data and the Views, and
|
||||
orchestrating event messages to each if the Model/View change.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
ScenarioType = typing.Tuple[str, state.DataclassState]
|
||||
|
||||
|
||||
class View:
|
||||
"""
|
||||
A thing which exposes a ``.widget`` attribute which is a view on some
|
||||
data. This view is essentially a complex combination of widgets, along with
|
||||
some event handling capabilities, which may or may not be sent back up to
|
||||
the underlying controller.
|
||||
|
||||
We strive hard to keep "Model" data out of the View (and try to avoid
|
||||
storing it at all on the View itself), instead relying on being able
|
||||
to notify, and receive notifications, of important events from the Controller.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ExposureModelResult(View):
|
||||
def __init__(self):
|
||||
self.figure = matplotlib.figure.Figure(figsize=(9, 6))
|
||||
ipympl_canvas(self.figure)
|
||||
self.html_output = widgets.HTML()
|
||||
self.ax = self.initialize_axes()
|
||||
self.concentration_line = None
|
||||
self.concentration_area = None
|
||||
|
||||
@property
|
||||
def widget(self):
|
||||
return widgets.VBox([
|
||||
self.html_output,
|
||||
self.figure.canvas,
|
||||
])
|
||||
|
||||
def initialize_axes(self) -> matplotlib.figure.Axes:
|
||||
ax = self.figure.add_subplot(1, 1, 1)
|
||||
ax.spines['right'].set_visible(False)
|
||||
ax.spines['top'].set_visible(False)
|
||||
ax.set_xlabel('Time (hours)')
|
||||
ax.set_ylabel('CO₂ concentration (ppm)')
|
||||
ax.set_title('CO₂ Concentration')
|
||||
|
||||
return ax
|
||||
|
||||
def update(self, model: models.CO2ConcentrationModel):
|
||||
self.update_plot(model)
|
||||
|
||||
def update_plot(self, model: models.CO2ConcentrationModel):
|
||||
resolution = 600
|
||||
ts = np.linspace(sorted(model.CO2_emitters.presence.transition_times())[0],
|
||||
sorted(model.CO2_emitters.presence.transition_times())[-1], resolution)
|
||||
concentration = [model.concentration(t) for t in ts]
|
||||
|
||||
if self.concentration_line is None:
|
||||
[self.concentration_line] = self.ax.plot(ts, concentration, color='#3530fe')
|
||||
|
||||
else:
|
||||
self.ax.ignore_existing_data_limits = False
|
||||
self.concentration_line.set_data(ts, concentration)
|
||||
|
||||
if self.concentration_area is None:
|
||||
self.concentration_area = self.ax.fill_between(x = ts, y1=0, y2=concentration, color="#96cbff",
|
||||
where = ((model.CO2_emitters.presence.boundaries()[0][0] < ts) & (ts < model.CO2_emitters.presence.boundaries()[0][1]) |
|
||||
(model.CO2_emitters.presence.boundaries()[1][0] < ts) & (ts < model.CO2_emitters.presence.boundaries()[1][1])))
|
||||
|
||||
else:
|
||||
self.concentration_area.remove()
|
||||
self.concentration_area = self.ax.fill_between(x = ts, y1=0, y2=concentration, color="#96cbff",
|
||||
where = ((model.CO2_emitters.presence.boundaries()[0][0] < ts) & (ts < model.CO2_emitters.presence.boundaries()[0][1]) |
|
||||
(model.CO2_emitters.presence.boundaries()[1][0] < ts) & (ts < model.CO2_emitters.presence.boundaries()[1][1])))
|
||||
|
||||
concentration_top = max(np.array(concentration))
|
||||
self.ax.set_ylim(bottom=model.CO2_atmosphere_concentration * 0.9, top=concentration_top*1.1)
|
||||
self.ax.set_xlim(left = min(model.CO2_emitters.presence.boundaries()[0])*0.95,
|
||||
right = max(model.CO2_emitters.presence.boundaries()[1])*1.05)
|
||||
|
||||
figure_legends = [mlines.Line2D([], [], color='#3530fe', markersize=15, label='CO₂ concentration'),
|
||||
mlines.Line2D([], [], color='salmon', markersize=15, label='Insufficient level', linestyle='--'),
|
||||
mlines.Line2D([], [], color='limegreen', markersize=15, label='Acceptable level', linestyle='--'),
|
||||
patches.Patch(edgecolor="#96cbff", facecolor='#96cbff', label='Presence of person(s)')]
|
||||
self.ax.legend(handles=figure_legends)
|
||||
if 1500 < concentration_top:
|
||||
self.ax.set_ylim(top=concentration_top*1.1)
|
||||
else:
|
||||
self.ax.set_ylim(top=1550)
|
||||
self.ax.hlines([800, 1500], xmin=min(model.CO2_emitters.presence.boundaries()[0])*0.95, xmax=max(model.CO2_emitters.presence.boundaries()[1])*1.05, colors=['limegreen', 'salmon'], linestyles='dashed')
|
||||
self.figure.canvas.draw()
|
||||
|
||||
|
||||
class ExposureComparisonResult(View):
|
||||
def __init__(self):
|
||||
self.figure = matplotlib.figure.Figure(figsize=(9, 6))
|
||||
ipympl_canvas(self.figure)
|
||||
self.html_output = widgets.HTML()
|
||||
self.ax = self.initialize_axes()
|
||||
|
||||
@property
|
||||
def widget(self):
|
||||
# Workaround to a bug with ipymlp, which doesn't work well with tabs
|
||||
# unless the widget is wrapped in a container (it is seen on all tabs otherwise!).
|
||||
return widgets.HBox([self.figure.canvas])
|
||||
|
||||
def initialize_axes(self) -> matplotlib.figure.Axes:
|
||||
ax = self.figure.add_subplot(1, 1, 1)
|
||||
ax.spines[['right', 'top']].set_visible(False)
|
||||
|
||||
ax.set_xlabel('Time (hours)')
|
||||
ax.set_ylabel('CO₂ concentration (ppm)')
|
||||
ax.set_title('CO₂ Concentration')
|
||||
|
||||
return ax
|
||||
|
||||
def scenarios_updated(self, scenarios: typing.Sequence[ScenarioType], _):
|
||||
updated_labels, updated_models = zip(*scenarios)
|
||||
CO2_models = tuple(
|
||||
model.dcs_instance() for model in updated_models
|
||||
)
|
||||
self.update_plot(CO2_models, updated_labels)
|
||||
|
||||
def update_plot(self, CO2_models: typing.Tuple[models.CO2ConcentrationModel, ...], labels: typing.Tuple[str, ...]):
|
||||
self.ax.cla()
|
||||
|
||||
start, finish = models_start_end(CO2_models)
|
||||
colors=['blue', 'red', 'orange', 'yellow', 'pink', 'purple', 'green', 'brown', 'black' ]
|
||||
ts = np.linspace(start, finish, num=250)
|
||||
|
||||
concentrations = [[conc_model.concentration(t) for t in ts] for conc_model in CO2_models]
|
||||
for label, concentration, color in zip(labels, concentrations, colors):
|
||||
self.ax.plot(ts, concentration, label=label, color=color)
|
||||
|
||||
concentration_top = max([max(np.array(concentration)) for concentration in concentrations])
|
||||
concentration_min = min([model.CO2_atmosphere_concentration for model in CO2_models])
|
||||
|
||||
self.ax.set_ylim(bottom=concentration_min * 0.9, top=concentration_top*1.1)
|
||||
self.ax.set_xlim(left = start*0.95,
|
||||
right = finish*1.05)
|
||||
if 1500 < concentration_top:
|
||||
self.ax.set_ylim(top=concentration_top*1.1)
|
||||
else:
|
||||
self.ax.set_ylim(top=1550)
|
||||
self.ax.hlines([800, 1500], xmin=start*0.95, xmax=finish*1.05, colors=['limegreen', 'salmon'], linestyles='dashed')
|
||||
|
||||
self.ax.legend()
|
||||
self.figure.canvas.draw_idle()
|
||||
self.figure.canvas.flush_events()
|
||||
|
||||
|
||||
class CO2Application(Controller):
|
||||
def __init__(self) -> None:
|
||||
# self._debug_output = widgets.Output()
|
||||
|
||||
#: A list of scenario name and ModelState instances. This is intended to be
|
||||
#: mutated. Any mutation should notify the appropriate Views for handling.
|
||||
self._model_scenarios: typing.List[ScenarioType] = []
|
||||
self._active_scenario = 0
|
||||
self.multi_model_view = MultiModelView(self)
|
||||
self.comparison_view = ExposureComparisonResult()
|
||||
self.current_scenario_figure = ExposureModelResult()
|
||||
self._results_tab = widgets.Tab(children=(
|
||||
self.current_scenario_figure.widget,
|
||||
self.comparison_view.widget,
|
||||
# self._debug_output,
|
||||
))
|
||||
for i, title in enumerate(['Current scenario', 'Scenario comparison', "Debug"]):
|
||||
self._results_tab.set_title(i, title)
|
||||
|
||||
self.widget = widgets.HBox(
|
||||
children=(
|
||||
self.multi_model_view.widget,
|
||||
self._results_tab,
|
||||
),
|
||||
)
|
||||
self.add_scenario('Scenario 1')
|
||||
|
||||
def build_new_model(self, vent: str) -> state.DataclassInstanceState[models.CO2ConcentrationModel]:
|
||||
new_model = state.DataclassInstanceState(
|
||||
models.CO2ConcentrationModel,
|
||||
state_builder=CAIMIRACO2StateBuilder(selected_ventilation=vent)
|
||||
)
|
||||
return new_model
|
||||
|
||||
def add_scenario(self, name, copy_from_model: typing.Optional[state.DataclassInstanceState] = None):
|
||||
if copy_from_model is not None:
|
||||
model = self.build_new_model(vent=copy_from_model.ventilation._selected)
|
||||
model.dcs_update_from(copy_from_model.dcs_instance())
|
||||
else:
|
||||
model = self.build_new_model(vent='HVACMechanical') # Default
|
||||
model.dcs_update_from(baseline_model)
|
||||
|
||||
self._model_scenarios.append((name, model))
|
||||
self._active_scenario = len(self._model_scenarios) - 1
|
||||
model.dcs_observe(self.notify_model_values_changed)
|
||||
self.notify_scenarios_changed()
|
||||
|
||||
def _find_model_id(self, model_id):
|
||||
for index, (name, model) in enumerate(list(self._model_scenarios)):
|
||||
if id(model) == model_id:
|
||||
return index, name, model
|
||||
else:
|
||||
raise ValueError("Model not found")
|
||||
|
||||
def rename_scenario(self, model_id, new_name):
|
||||
index, _, model = self._find_model_id(model_id)
|
||||
self._model_scenarios[index] = (new_name, model)
|
||||
self.notify_scenarios_changed()
|
||||
|
||||
def remove_scenario(self, model_id):
|
||||
index, _, model = self._find_model_id(model_id)
|
||||
self._model_scenarios.pop(index)
|
||||
self.multi_model_view.remove_tab(index)
|
||||
self._active_scenario = index - 1
|
||||
|
||||
model.dcs_observe(self.notify_model_values_changed)
|
||||
self.notify_scenarios_changed()
|
||||
|
||||
def set_active_scenario(self, model_id):
|
||||
index, _, model = self._find_model_id(model_id)
|
||||
self._active_scenario = index
|
||||
self.notify_scenarios_changed()
|
||||
self.notify_model_values_changed()
|
||||
|
||||
def notify_scenarios_changed(self):
|
||||
"""
|
||||
Occurs when the set of scenarios has been modified, but not if the values of the scenario has changed.
|
||||
|
||||
"""
|
||||
self.multi_model_view.scenarios_updated(self._model_scenarios, self._active_scenario)
|
||||
self.comparison_view.scenarios_updated(self._model_scenarios, self._active_scenario)
|
||||
|
||||
def notify_model_values_changed(self):
|
||||
"""
|
||||
Occurs when *any* value in *any* of the scenarios has been modified.
|
||||
"""
|
||||
self.comparison_view.scenarios_updated(self._model_scenarios, self._active_scenario)
|
||||
self.current_scenario_figure.update(self._model_scenarios[self._active_scenario][1].dcs_instance())
|
||||
|
||||
|
||||
class ModelWidgets(View):
|
||||
def __init__(self, model_state: state.DataclassState):
|
||||
#: The widgets that this view produces (inputs and outputs together)
|
||||
self.widget = widgets.VBox([])
|
||||
self.construct_widgets(model_state)
|
||||
|
||||
def construct_widgets(self, model_state: state.DataclassState):
|
||||
# Build the input widgets.
|
||||
self._build_widget(model_state)
|
||||
|
||||
def _build_widget(self, node):
|
||||
self.widget.children += (self._build_room(node.room),)
|
||||
self.widget.children += (self._build_population(node.CO2_emitters, node.ventilation),)
|
||||
self.widget.children += (self._build_atmospheric_concentration(node),)
|
||||
self.widget.children += (self._build_ventilation(node.ventilation, node.CO2_emitters),)
|
||||
|
||||
def _build_atmospheric_concentration(self, node):
|
||||
return collapsible([widgets.VBox([
|
||||
self._build_co2_concentration(node),
|
||||
])], title="Carbon Dioxide")
|
||||
|
||||
def _build_population(self, node, ventilation_node):
|
||||
return collapsible([widgets.VBox([
|
||||
self._build_population_number(node),
|
||||
self._build_activity(node.activity),
|
||||
self._build_population_presence(node.presence, ventilation_node)
|
||||
])], title="Population")
|
||||
|
||||
def _build_co2_concentration(self, node):
|
||||
concentration = widgets.IntSlider(value=node.CO2_atmosphere_concentration, min=300, max=1000, step=10)
|
||||
|
||||
def on_atmospheric_concentration_change(change):
|
||||
node.CO2_atmosphere_concentration = change['new']
|
||||
|
||||
concentration.observe(on_atmospheric_concentration_change, names=['value'])
|
||||
|
||||
return widgets.HBox([widgets.Label('Atmospheric Concentration (ppm) '), concentration], layout=widgets.Layout(justify_content='space-between'))
|
||||
|
||||
def _build_room(self,node):
|
||||
room_volume = widgets.IntSlider(value=node.volume, min=5, max=200, step=5)
|
||||
humidity = widgets.FloatSlider(value = node.humidity*100, min=20, max=80, step=5)
|
||||
inside_temp = widgets.IntSlider(value=node.inside_temp.values[0]-273.15, min=15., max=25.)
|
||||
|
||||
def on_volume_change(change):
|
||||
node.volume = change['new']
|
||||
|
||||
def on_humidity_change(change):
|
||||
node.humidity = change['new']/100
|
||||
|
||||
def on_insidetemp_change(change):
|
||||
node.inside_temp.values = (change['new']+273.15,)
|
||||
|
||||
room_volume.observe(on_volume_change, names=['value'])
|
||||
humidity.observe(on_humidity_change, names=['value'])
|
||||
inside_temp.observe(on_insidetemp_change, names=['value'])
|
||||
|
||||
widget = collapsible(
|
||||
[ widgets.VBox([
|
||||
widgets.HBox([widgets.Label('Room volume (m³)'), room_volume],
|
||||
layout=widgets.Layout(width='100%', justify_content='space-between')),
|
||||
widgets.HBox([widgets.Label('Inside temperature (℃)'), inside_temp],
|
||||
layout=widgets.Layout(width='100%', justify_content='space-between')),
|
||||
widgets.HBox([widgets.Label('Indoor relative humidity (%)'), humidity],
|
||||
layout=widgets.Layout(width='100%', justify_content='space-between')),])
|
||||
], title="Specification of workspace"
|
||||
)
|
||||
|
||||
return widget
|
||||
|
||||
def _build_activity(self, node):
|
||||
activity = node.dcs_instance()
|
||||
for name, activity_ in models.Activity.types.items():
|
||||
if activity == activity_:
|
||||
break
|
||||
activity = widgets.Dropdown(options=list(models.Activity.types.keys()), value=name)
|
||||
|
||||
def on_activity_change(change):
|
||||
act = models.Activity.types[change['new']]
|
||||
node.dcs_update_from(act)
|
||||
activity.observe(on_activity_change, names=['value'])
|
||||
|
||||
return widgets.HBox([widgets.Label("Activity"), activity], layout=widgets.Layout(justify_content='space-between'))
|
||||
|
||||
def _build_population_number(self, node):
|
||||
number = widgets.IntSlider(value=node.number, min=1, max=200, step=1)
|
||||
|
||||
def on_population_number_change(change):
|
||||
node.number = change['new']
|
||||
|
||||
number.observe(on_population_number_change, names=['value'])
|
||||
|
||||
return widgets.HBox([widgets.Label('Number of people in the room '), number], layout=widgets.Layout(justify_content='space-between'))
|
||||
|
||||
def _build_population_presence(self, node, ventilation_node):
|
||||
presence_start = widgets.FloatRangeSlider(value = node.present_times[0], min = 8., max=13., step=0.1)
|
||||
presence_finish = widgets.FloatRangeSlider(value = node.present_times[1], min = 13., max=18., step=0.1)
|
||||
|
||||
def on_presence_start_change(change):
|
||||
ventilation_node.active.start = change['new'][0] - ventilation_node.active.duration / 60
|
||||
node.present_times = (change['new'], presence_finish.value)
|
||||
|
||||
def on_presence_finish_change(change):
|
||||
node.present_times = (presence_start.value, change['new'])
|
||||
|
||||
presence_start.observe(on_presence_start_change, names=['value'])
|
||||
presence_finish.observe(on_presence_finish_change, names=['value'])
|
||||
|
||||
return widgets.HBox([widgets.Label('Population presence'), presence_start, presence_finish], layout = widgets.Layout(justify_content='space-between'))
|
||||
|
||||
def present(self):
|
||||
return self.widget
|
||||
|
||||
def _build_ventilation(
|
||||
self,
|
||||
node: typing.Union[
|
||||
state.DataclassStateNamed[models.Ventilation],
|
||||
state.DataclassStateNamed[models.MultipleVentilation],
|
||||
],
|
||||
emitters_node: models.Population,
|
||||
) -> widgets.Widget:
|
||||
ventilation_widgets = {
|
||||
'HVACMechanical': self._build_mechanical(node),
|
||||
'Sliding window': self._build_window(node, emitters_node),
|
||||
'No ventilation': self._build_no_ventilation(node._states['No ventilation']),
|
||||
}
|
||||
|
||||
keys=[("Mechanical", "HVACMechanical"), ("Natural", "Sliding window"), ("No ventilation", "No ventilation")]
|
||||
|
||||
for name, widget in ventilation_widgets.items():
|
||||
widget.layout.visible = False
|
||||
|
||||
ventilation_w = widgets.Dropdown(
|
||||
options=keys,
|
||||
)
|
||||
|
||||
def toggle_ventilation(value):
|
||||
for name, widget in ventilation_widgets.items():
|
||||
widget.layout.visible = False
|
||||
widget.layout.display = 'none'
|
||||
|
||||
node.dcs_select(value)
|
||||
|
||||
widget = ventilation_widgets[value]
|
||||
widget.layout.visible = True
|
||||
widget.layout.display = 'flex'
|
||||
|
||||
ventilation_w.observe(lambda event: toggle_ventilation(event['new']), 'value')
|
||||
toggle_ventilation(ventilation_w.value)
|
||||
|
||||
w = collapsible(
|
||||
([widgets.HBox([widgets.Label('Ventilation type'), ventilation_w], layout=widgets.Layout(justify_content='space-between'))])
|
||||
+ list(ventilation_widgets.values()),
|
||||
title='Ventilation scheme',
|
||||
)
|
||||
return w
|
||||
|
||||
def _build_month(self, node) -> WidgetGroup:
|
||||
|
||||
month_choice = widgets.Select(options=list(data.GenevaTemperatures.keys()), value='Jan')
|
||||
|
||||
def on_month_change(change):
|
||||
node.outside_temp = data.GenevaTemperatures[change['new']]
|
||||
month_choice.observe(on_month_change, names=['value'])
|
||||
|
||||
return WidgetGroup(
|
||||
(
|
||||
(widgets.Label("Month"), month_choice),
|
||||
),
|
||||
)
|
||||
|
||||
def _build_outsidetemp(self, node) -> WidgetGroup:
|
||||
outside_temp = widgets.IntSlider(value=10, min=-10, max=30)
|
||||
|
||||
def on_outsidetemp_change(change):
|
||||
node.values = (change['new'] + 273.15, )
|
||||
|
||||
outside_temp.observe(on_outsidetemp_change, names=['value'])
|
||||
auto_width = widgets.Layout(width='auto')
|
||||
return WidgetGroup(
|
||||
(
|
||||
(
|
||||
widgets.Label('Outside temperature (℃)', layout=auto_width,),
|
||||
outside_temp,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
def _build_hinged_window(self, node):
|
||||
hinged_window = widgets.FloatSlider(value=node.window_width, min=0.1, max=2, step=0.1)
|
||||
|
||||
def on_hinged_window_change(change):
|
||||
node.window_width = change['new']
|
||||
|
||||
hinged_window.observe(on_hinged_window_change, names=['value'])
|
||||
|
||||
return widgets.HBox([widgets.Label('Window width (meters) '), hinged_window], layout=widgets.Layout(justify_content='space-between', width='100%'))
|
||||
|
||||
def _build_sliding_window(self, node):
|
||||
return widgets.HBox([])
|
||||
|
||||
def _build_window(self, node, emitters_node) -> WidgetGroup:
|
||||
window_widgets = {
|
||||
'Sliding window': self._build_sliding_window(node._states['Sliding window']),
|
||||
'Hinged window': self._build_hinged_window(node._states['Hinged window']),
|
||||
}
|
||||
|
||||
for name, widget in window_widgets.items():
|
||||
widget.layout.visible = False
|
||||
widget.layout.display = 'none'
|
||||
|
||||
window_w = widgets.RadioButtons(
|
||||
options= list(zip(['Sliding window', 'Hinged window'], window_widgets.keys())),
|
||||
button_style='info',
|
||||
layout=widgets.Layout(height='auto', width='auto'),
|
||||
)
|
||||
|
||||
def toggle_window(value):
|
||||
for name, widget in window_widgets.items():
|
||||
widget.layout.visible = False
|
||||
widget.layout.display = 'none'
|
||||
|
||||
node.dcs_select(value)
|
||||
|
||||
widget = window_widgets[value]
|
||||
widget.layout.visible = True
|
||||
widget.layout.display = 'flex'
|
||||
|
||||
window_w.observe(lambda event: toggle_window(event['new']), 'value')
|
||||
toggle_window(window_w.value)
|
||||
|
||||
number_of_windows= widgets.IntText(value= 1, min= 0, max= 5, step=1)
|
||||
frequency = widgets.IntSlider(value=node.active.period, min=0, max=120)
|
||||
duration = widgets.IntSlider(value=node.active.duration, min=0, max=frequency.value)
|
||||
opening_length = widgets.FloatSlider(value=node.opening_length, min=0, max=3, step=0.1)
|
||||
window_height = widgets.FloatSlider(value=node.window_height, min=0, max=3, step=0.1)
|
||||
|
||||
def on_value_change(change):
|
||||
node.number_of_windows = change['new']
|
||||
|
||||
def on_period_change(change):
|
||||
node.active.period = change['new']
|
||||
duration.max = change['new']
|
||||
|
||||
def on_duration_change(change):
|
||||
node.active.start = emitters_node.presence.present_times[0][0] - change['new'] / 60
|
||||
node.active.duration = change['new']
|
||||
|
||||
def on_opening_length_change(change):
|
||||
node.opening_length = change['new']
|
||||
|
||||
def on_window_height_change(change):
|
||||
node.window_height = change['new']
|
||||
|
||||
number_of_windows.observe(on_value_change, names=['value'])
|
||||
frequency.observe(on_period_change, names=['value'])
|
||||
duration.observe(on_duration_change, names=['value'])
|
||||
opening_length.observe(on_opening_length_change, names=['value'])
|
||||
window_height.observe(on_window_height_change, names=['value'])
|
||||
|
||||
outsidetemp_widgets = {
|
||||
'Fixed': self._build_outsidetemp(node.outside_temp),
|
||||
'Meteorological data': self._build_month(node),
|
||||
}
|
||||
|
||||
outsidetemp_w = widgets.Dropdown(
|
||||
options=outsidetemp_widgets.keys(),
|
||||
)
|
||||
|
||||
def toggle_outsidetemp(value):
|
||||
for name, widget_group in outsidetemp_widgets.items():
|
||||
widget_group.set_visible(False)
|
||||
|
||||
widget_group = outsidetemp_widgets[value]
|
||||
widget_group.set_visible(True)
|
||||
|
||||
outsidetemp_w.observe(lambda event: toggle_outsidetemp(event['new']), 'value')
|
||||
toggle_outsidetemp(outsidetemp_w.value)
|
||||
|
||||
auto_width = widgets.Layout(width='auto', justify_content='space-between')
|
||||
result = WidgetGroup(
|
||||
(
|
||||
(
|
||||
widgets.Label('Number of windows ', layout=auto_width),
|
||||
number_of_windows,
|
||||
),
|
||||
(
|
||||
widgets.Label('Opening distance (meters)', layout=auto_width),
|
||||
opening_length,
|
||||
),
|
||||
(
|
||||
widgets.Label('Window height (meters)', layout=auto_width),
|
||||
window_height,
|
||||
),
|
||||
(
|
||||
widgets.Label('Frequency (minutes)', layout=auto_width),
|
||||
frequency,
|
||||
),
|
||||
(
|
||||
widgets.Label('Duration (minutes)', layout=auto_width),
|
||||
duration,
|
||||
),
|
||||
(
|
||||
widgets.Label('Outside temperature scheme', layout=auto_width),
|
||||
outsidetemp_w,
|
||||
),
|
||||
),
|
||||
)
|
||||
for sub_group in outsidetemp_widgets.values():
|
||||
result.add_pairs(sub_group.pairs())
|
||||
return widgets.VBox([window_w, widgets.HBox(list(window_widgets.values())), result.build()])
|
||||
|
||||
def _build_q_air_mech(self, node):
|
||||
q_air_mech = widgets.FloatSlider(value=node.q_air_mech, min=0, max=5000, step=25)
|
||||
|
||||
def on_q_air_mech_change(change):
|
||||
node.q_air_mech = change['new']
|
||||
|
||||
q_air_mech.observe(on_q_air_mech_change, names=['value'])
|
||||
|
||||
return widgets.HBox([q_air_mech, widgets.Label('m³/h')])
|
||||
|
||||
def _build_ach(self, node):
|
||||
air_exch = widgets.IntSlider(value=node.air_exch, min=0, max=20, step=1)
|
||||
|
||||
def on_air_exch_change(change):
|
||||
node.air_exch = change['new']
|
||||
|
||||
air_exch.observe(on_air_exch_change, names=['value'])
|
||||
|
||||
return widgets.HBox([air_exch, widgets.Label('h⁻¹')])
|
||||
|
||||
def _build_mechanical(self, node):
|
||||
mechanical_widgets = {
|
||||
'HVACMechanical': self._build_q_air_mech(node._states['HVACMechanical']),
|
||||
'AirChange': self._build_ach(node._states['AirChange']),
|
||||
}
|
||||
|
||||
for name, widget in mechanical_widgets.items():
|
||||
widget.layout.visible = False
|
||||
|
||||
mechanival_w = widgets.RadioButtons(
|
||||
options=list(zip(['Air supply flow rate (m³/h)', 'Air changes per hour (h⁻¹)'], mechanical_widgets.keys())),
|
||||
button_style='info',
|
||||
)
|
||||
|
||||
def toggle_mechanical(value):
|
||||
for name, widget in mechanical_widgets.items():
|
||||
widget.layout.visible = False
|
||||
widget.layout.display = 'none'
|
||||
|
||||
node.dcs_select(value)
|
||||
widget = mechanical_widgets[value]
|
||||
widget.layout.visible = True
|
||||
widget.layout.display = 'flex'
|
||||
|
||||
mechanival_w.observe(lambda event: toggle_mechanical(event['new']), 'value')
|
||||
toggle_mechanical(mechanival_w.value)
|
||||
|
||||
return widgets.VBox([mechanival_w, widgets.HBox(list(mechanical_widgets.values()))])
|
||||
|
||||
def _build_no_ventilation(self, node):
|
||||
return widgets.HBox([])
|
||||
|
||||
|
||||
class MultiModelView(View):
|
||||
def __init__(self, controller: CO2Application):
|
||||
self._controller = controller
|
||||
self.widget = widgets.Tab()
|
||||
self.widget.observe(self._on_tab_change, 'selected_index')
|
||||
self._tab_model_ids: typing.List[int] = []
|
||||
self._tab_widgets: typing.List[widgets.Widget] = []
|
||||
self._tab_model_views: typing.List[ModelWidgets] = []
|
||||
|
||||
def scenarios_updated(
|
||||
self,
|
||||
model_scenarios: typing.Sequence[ScenarioType],
|
||||
active_scenario_index: int
|
||||
):
|
||||
"""
|
||||
Called when a scenario is added/removed/renamed etc.
|
||||
|
||||
Note: Not called when the model state is modified.
|
||||
|
||||
"""
|
||||
model_scenario_ids = []
|
||||
for i, (scenario_name, model) in enumerate(model_scenarios):
|
||||
if id(model) not in self._tab_model_ids:
|
||||
self.add_tab(scenario_name, model)
|
||||
model_scenario_ids.append(id(model))
|
||||
tab_index = self._tab_model_ids.index(id(model))
|
||||
self.widget.set_title(tab_index, scenario_name)
|
||||
|
||||
# Any remaining model_scenario_ids are no longer needed, so remove
|
||||
# their tabs.
|
||||
for tab_index, tab_scenario_id in enumerate(self._tab_model_ids[:]):
|
||||
if tab_scenario_id not in model_scenario_ids:
|
||||
self.remove_tab(tab_index)
|
||||
|
||||
assert self._tab_model_ids == model_scenario_ids
|
||||
|
||||
self.widget.selected_index = active_scenario_index
|
||||
|
||||
|
||||
def add_tab(self, name, model):
|
||||
self._tab_model_views.append(ModelWidgets(model))
|
||||
self._tab_model_ids.append(id(model))
|
||||
tab_idx = len(self._tab_model_ids) - 1
|
||||
tab_widget = widgets.VBox(
|
||||
children=(
|
||||
self._build_settings_menu(name, model),
|
||||
self._tab_model_views[tab_idx].widget,
|
||||
)
|
||||
)
|
||||
self._tab_widgets.append(tab_widget)
|
||||
self.update_tab_widget()
|
||||
|
||||
def remove_tab(self, tab_index):
|
||||
assert 0 <= tab_index < len(self._tab_model_ids)
|
||||
assert len(self._tab_model_ids) > 1
|
||||
self._tab_model_ids.pop(tab_index)
|
||||
self._tab_widgets.pop(tab_index)
|
||||
self._tab_model_views.pop(tab_index)
|
||||
|
||||
self.update_tab_widget()
|
||||
|
||||
def update_tab_widget(self):
|
||||
self.widget.children = tuple(self._tab_widgets)
|
||||
|
||||
def _on_tab_change(self, change):
|
||||
self._controller.set_active_scenario(
|
||||
self._tab_model_ids[change['new']]
|
||||
)
|
||||
|
||||
def _build_settings_menu(self, name, model):
|
||||
delete_button = widgets.Button(description='Delete Scenario', button_style='danger')
|
||||
rename_text_field = widgets.Text(description='Rename Scenario:', value=name,
|
||||
style={'description_width': 'auto'})
|
||||
duplicate_button = widgets.Button(description='Replicate Scenario', button_style='success')
|
||||
model_id = id(model)
|
||||
|
||||
def on_delete_click(b):
|
||||
self._controller.remove_scenario(model_id)
|
||||
|
||||
def on_rename_text_field(change):
|
||||
self._controller.rename_scenario(model_id, new_name=change['new'])
|
||||
|
||||
def on_duplicate_click(b):
|
||||
tab_index = self._tab_model_ids.index(model_id)
|
||||
name = self.widget.get_title(tab_index)
|
||||
self._controller.add_scenario(f'{name} (copy)', model)
|
||||
|
||||
delete_button.on_click(on_delete_click)
|
||||
duplicate_button.on_click(on_duplicate_click)
|
||||
rename_text_field.observe(on_rename_text_field, 'value')
|
||||
|
||||
buttons_w_delete = widgets.HBox(children=(duplicate_button, delete_button))
|
||||
buttons = duplicate_button if len(self._tab_model_ids) < 2 else buttons_w_delete
|
||||
|
||||
return widgets.VBox(children=(buttons, rename_text_field))
|
||||
|
||||
|
||||
class CAIMIRACO2StateBuilder(CAIMIRAStateBuilder):
|
||||
|
||||
def __init__(self, selected_ventilation: str):
|
||||
self.selected_ventilation = selected_ventilation
|
||||
|
||||
def build_type__VentilationBase(self, _: dataclasses.Field):
|
||||
s: state.DataclassStateNamed = state.DataclassStateNamed(
|
||||
states={
|
||||
'HVACMechanical': self.build_generic(models.HVACMechanical),
|
||||
'Sliding window': self.build_generic(models.WindowOpening),
|
||||
'No ventilation': self.build_generic(models.AirChange),
|
||||
'AirChange': self.build_generic(models.AirChange),
|
||||
'Hinged window': self.build_generic(models.WindowOpening),
|
||||
},
|
||||
base_type=self.selected_ventilation,
|
||||
state_builder=self,
|
||||
)
|
||||
s._states['HVACMechanical'].dcs_update_from(baseline_model.ventilation)
|
||||
#Initialise the "Sliding window" state
|
||||
s._states['Sliding window'].dcs_update_from(
|
||||
models.SlidingWindow(active=models.PeriodicInterval(period=120, duration=15, start=8-(15/60)),
|
||||
outside_temp=models.PiecewiseConstant((0,24.), (283.15,)),
|
||||
window_height=1.6, opening_length=0.6,
|
||||
),
|
||||
)
|
||||
#Initialise the "Hinged window" state
|
||||
s._states['Hinged window'].dcs_update_from(
|
||||
models.HingedWindow(active=models.PeriodicInterval(period=120, duration=15, start=8-(15/60)),
|
||||
outside_temp=models.PiecewiseConstant((0,24.), (283.15,)),
|
||||
window_height=1.6, opening_length=0.6,
|
||||
window_width=10.
|
||||
),
|
||||
)
|
||||
s._states['AirChange'].dcs_update_from(
|
||||
models.AirChange(models.PeriodicInterval(period=24*60, duration=24*60), 10.)
|
||||
)
|
||||
# Initialize the "No ventilation" state
|
||||
s._states['No ventilation'].dcs_update_from(
|
||||
models.AirChange(active=models.PeriodicInterval(period=60, duration=60), air_exch=0.)
|
||||
)
|
||||
return s
|
||||
|
||||
def models_start_end(models: typing.Sequence[models.CO2ConcentrationModel]) -> typing.Tuple[float, float]:
|
||||
"""
|
||||
Returns the earliest start and latest end time of a collection of v objects
|
||||
|
||||
"""
|
||||
emitters_start = min(model.CO2_emitters.presence.boundaries()[0][0] for model in models)
|
||||
emitters_finish = min(model.CO2_emitters.presence.boundaries()[-1][1] for model in models)
|
||||
return emitters_start, emitters_finish
|
||||
69
caimira/apps/expert_co2/caimira.ipynb
Normal file
69
caimira/apps/expert_co2/caimira.ipynb
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"attachments": {},
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"<div style=\"text-align: center;\" align=\"center\" >\n",
|
||||
"<img src=\"./files/static/images/header_image.png\" style=\"height:5em\"></img></div>\n",
|
||||
"<p style=\"text-align: center;\">\n",
|
||||
"Please see the <a href=\"https://caimira.web.cern.ch/\">CAiMIRA homepage</a> for details on the methodology, assumptions and limitations of CAiMIRA.</p>"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"application/vnd.jupyter.widget-view+json": {
|
||||
"model_id": "888611458e044cd3b7cda7dfcf1fdbe4",
|
||||
"version_major": 2,
|
||||
"version_minor": 0
|
||||
},
|
||||
"text/plain": [
|
||||
"HBox(children=(Tab(children=(VBox(children=(VBox(children=(Button(button_style='success', description='Duplica…"
|
||||
]
|
||||
},
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import caimira.apps\n",
|
||||
"\n",
|
||||
"app = caimira.apps.CO2Application()\n",
|
||||
"app.widget"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3.9.6 64-bit ('caimira')",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.9.6"
|
||||
},
|
||||
"vscode": {
|
||||
"interpreter": {
|
||||
"hash": "c77495895472738765eb97c8f848f37a4e60c741d594ab92dd40b6b8f4cac818"
|
||||
}
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
BIN
caimira/apps/expert_co2/static/images/header_image.png
Normal file
BIN
caimira/apps/expert_co2/static/images/header_image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
|
|
@ -32,7 +32,7 @@
|
|||
<div class="d-flex flex-row" >
|
||||
<div class="pr-3"><a href="{{ get_calculator_url() }}" role="button" class="btn btn-lg btn-outline-primary"><div class="d-flex d-row"><i class="icon-calculator"></i><span class="pl-2">Calculator</div></a></div>
|
||||
<br>
|
||||
<div class="expert_app_button"><a href="/expert-app" role="button" class="btn btn-lg btn-outline-secondary"><div class="d-flex d-row"><i class="icon-expert"></i><span class="pl-2">Expert (beta)</div></a></div>
|
||||
<div class="expert_app_button"><a href="/expert-app" role="button" class="btn btn-lg btn-outline-secondary"><div class="d-flex d-row"><i class="icon-expert"></i><span class="pl-2">Expert app</div></a></div>
|
||||
</div>
|
||||
<br>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -45,7 +45,8 @@
|
|||
<ul class="dropdown-menu dropwown-navbar-colors" style="min-width: 12rem;" aria-labelledby="navbarDropdown">
|
||||
<li><a href="{{ get_calculator_url() }}" class="{{ "header-navbar nav-link active" if "calculator/" == active_page else "header-navbar nav-link" }}">Calculator</a></li>
|
||||
<li><div class="d-flex"><span class="d-flex align-self-center submenu-division"></span><a href="{{ get_calculator_url() }}/user-guide" class="{{ "header-navbar nav-link active" if "user-guide" in active_page else "header-navbar nav-link" }}">User Guide</a></div></li>
|
||||
<li><a href="/expert-app" class="{{ "header-navbar nav-link active" if "/expert-app" == active_page else "header-navbar nav-link" }}">Expert app (beta)</a></li>
|
||||
<li><a href="/expert-app" class="{{ "header-navbar nav-link active" if "/expert-app" == active_page else "header-navbar nav-link" }}">Expert app</a></li>
|
||||
<li><a href="/co2-app" class="{{ "header-navbar nav-link active" if "/co2-app" == active_page else "header-navbar nav-link" }}">CO₂ Simulator</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1009,7 +1009,7 @@ class _ConcentrationModelBase:
|
|||
return self.min_background_concentration()/self.normalization_factor()
|
||||
V = self.room.volume
|
||||
RR = self.removal_rate(time)
|
||||
|
||||
|
||||
return (1. / (RR * V) + self.min_background_concentration()/
|
||||
self.normalization_factor())
|
||||
|
||||
|
|
@ -1210,7 +1210,9 @@ class CO2ConcentrationModel(_ConcentrationModelBase):
|
|||
return self.CO2_emitters
|
||||
|
||||
def removal_rate(self, time: float) -> _VectorisedFloat:
|
||||
return self.ventilation.air_exchange(self.room, time)
|
||||
# Setting minimum air exchange rate to 1e-6 to avoid divisions by
|
||||
# zero when computing the CO2 concentration.
|
||||
return np.maximum(1e-6,self.ventilation.air_exchange(self.room, time))
|
||||
|
||||
def min_background_concentration(self) -> _VectorisedFloat:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -319,18 +319,18 @@ class DataclassStateNamed(DataclassState[Datamodel_T]):
|
|||
"""
|
||||
def __init__(self,
|
||||
states: typing.Dict[str, DataclassState[Datamodel_T]],
|
||||
base_type: str,
|
||||
**kwargs
|
||||
):
|
||||
# TODO: This is effectively a container type. We shouldn't use the standard constructor for this.
|
||||
enabled = list(states.keys())[0]
|
||||
t = states[enabled]
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
with self._object_setattr():
|
||||
self._states = states.copy()
|
||||
self._selected: str = None # type: ignore
|
||||
# Pick the first choice until we know otherwise.
|
||||
self.dcs_select(enabled)
|
||||
|
||||
self.dcs_select(base_type)
|
||||
|
||||
def __getattr__(self, name):
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -167,11 +167,14 @@ def test_DCS_predefined():
|
|||
def test_DCS_named():
|
||||
opt1 = DCSimpleSubclass('a', 1, 3.14)
|
||||
opt2 = DCAnother(4.2)
|
||||
s = state.DataclassStateNamed({
|
||||
s = state.DataclassStateNamed(
|
||||
states={
|
||||
# Entirely different types possible.
|
||||
'option 1': state.DataclassInstanceState(DCSimple),
|
||||
'option 2': state.DataclassInstanceState(DCAnother),
|
||||
})
|
||||
},
|
||||
base_type='option 1'
|
||||
)
|
||||
assert s._selected == 'option 1'
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
|
|
|
|||
|
|
@ -1,105 +0,0 @@
|
|||
anyio==3.6.2
|
||||
appnope==0.1.3
|
||||
argon2-cffi==21.3.0
|
||||
argon2-cffi-bindings==21.2.0
|
||||
asttokens==2.2.1
|
||||
attrs==22.2.0
|
||||
Babel==2.11.0
|
||||
backcall==0.2.0
|
||||
beautifulsoup4==4.11.2
|
||||
bleach==6.0.0
|
||||
certifi==2022.12.7
|
||||
cffi==1.15.1
|
||||
charset-normalizer==3.0.1
|
||||
cloudpickle==2.2.1
|
||||
comm==0.1.2
|
||||
contourpy==1.0.7
|
||||
cycler==0.11.0
|
||||
debugpy==1.6.6
|
||||
decorator==5.1.1
|
||||
defusedxml==0.7.1
|
||||
entrypoints==0.4
|
||||
executing==1.2.0
|
||||
fastjsonschema==2.16.2
|
||||
fonttools==4.38.0
|
||||
h3==3.7.6
|
||||
idna==3.4
|
||||
importlib-metadata==6.0.0
|
||||
importlib-resources==5.12.0
|
||||
ipykernel==6.21.2
|
||||
ipympl==0.9.3
|
||||
ipython==8.10.0
|
||||
ipython-genutils==0.2.0
|
||||
ipywidgets==7.7.3
|
||||
jedi==0.18.2
|
||||
Jinja2==3.1.2
|
||||
joblib==1.2.0
|
||||
json5==0.9.11
|
||||
jsonschema==4.17.3
|
||||
jupyter-client==7.4.1
|
||||
jupyter-core==5.2.0
|
||||
jupyter-server==1.23.6
|
||||
jupyterlab-pygments==0.2.2
|
||||
jupyterlab-server==2.19.0
|
||||
jupyterlab-widgets==1.1.2
|
||||
kiwisolver==1.4.4
|
||||
loky==3.3.0
|
||||
MarkupSafe==2.1.2
|
||||
matplotlib==3.7.0
|
||||
matplotlib-inline==0.1.6
|
||||
memoization==0.4.0
|
||||
mistune==2.0.5
|
||||
nbclassic==0.5.2
|
||||
nbclient==0.7.2
|
||||
nbconvert==7.2.9
|
||||
nbformat==5.7.3
|
||||
nest-asyncio==1.5.6
|
||||
notebook==6.5.2
|
||||
notebook-shim==0.2.2
|
||||
numpy==1.24.2
|
||||
packaging==23.0
|
||||
pandas==1.5.3
|
||||
pandocfilters==1.5.0
|
||||
parso==0.8.3
|
||||
pexpect==4.8.0
|
||||
pickleshare==0.7.5
|
||||
Pillow==9.4.0
|
||||
platformdirs==3.0.0
|
||||
prometheus-client==0.16.0
|
||||
prompt-toolkit==3.0.37
|
||||
psutil==5.9.4
|
||||
ptyprocess==0.7.0
|
||||
pure-eval==0.2.2
|
||||
py==1.11.0
|
||||
pycparser==2.21
|
||||
Pygments==2.14.0
|
||||
pyparsing==3.0.9
|
||||
pyrsistent==0.19.3
|
||||
python-dateutil==2.8.2
|
||||
pytz==2022.7.1
|
||||
pyzmq==25.0.0
|
||||
requests==2.28.2
|
||||
retry==0.9.2
|
||||
scikit-learn==1.2.1
|
||||
scipy==1.10.1
|
||||
Send2Trash==1.8.0
|
||||
six==1.16.0
|
||||
sniffio==1.3.0
|
||||
soupsieve==2.4
|
||||
stack-data==0.6.2
|
||||
terminado==0.17.1
|
||||
threadpoolctl==3.1.0
|
||||
timezonefinder==6.1.9
|
||||
tinycss2==1.2.1
|
||||
tornado==6.2
|
||||
traitlets==5.9.0
|
||||
types-retry==0.9.9.2
|
||||
urllib3==1.26.14
|
||||
voila==0.4.0
|
||||
wcwidth==0.2.6
|
||||
webencodings==0.5.1
|
||||
websocket-client==1.5.1
|
||||
websockets==10.4
|
||||
wheel==0.36.2
|
||||
widgetsnbextension==3.6.2
|
||||
zipp==3.14.0
|
||||
Loading…
Reference in a new issue