Skip to content

Commit 6942936

Browse files
authored
Merge pull request #79 from marshallmcdonnell/add-docker
Add docker
2 parents 3f266f8 + a6c3a36 commit 6942936

File tree

15 files changed

+535
-8
lines changed

15 files changed

+535
-8
lines changed

.github/workflows/docs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ on: [push, pull_request, delete]
99
jobs:
1010
docs:
1111
runs-on: ubuntu-latest
12+
if: github.repository == 'usnistgov/AFL-agent'
1213
steps:
1314
- name: Free Disk Space (Ubuntu)
1415
uses: jlumbroso/free-disk-space@v1.3.1
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: Publish to GitHub Container Registry
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*'
7+
workflow_dispatch: {}
8+
9+
jobs:
10+
build-and-push-agent:
11+
runs-on: ubuntu-latest
12+
13+
permissions:
14+
contents: read
15+
packages: write
16+
17+
steps:
18+
- name: Checkout
19+
uses: actions/checkout@v4
20+
21+
- name: Set lowercase repository name
22+
run: echo "REPO_LOWER=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
23+
24+
- name: Set up Docker Buildx
25+
uses: docker/setup-buildx-action@v3
26+
27+
- name: Log in to GitHub Container Registry
28+
uses: docker/login-action@v3
29+
with:
30+
registry: ghcr.io
31+
username: ${{ github.actor }}
32+
password: ${{ secrets.GITHUB_TOKEN }}
33+
34+
- name: Build and push Agent image
35+
uses: docker/build-push-action@v5
36+
with:
37+
context: .
38+
file: ./docker/Dockerfile.agent
39+
push: true
40+
tags: |
41+
ghcr.io/${{ env.REPO_LOWER }}/agent:latest
42+
ghcr.io/${{ env.REPO_LOWER }}/agent:${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || github.sha }}
43+
ghcr.io/${{ env.REPO_LOWER }}/agent:${{ github.sha }}
44+
cache-from: type=registry,ref=ghcr.io/${{ env.REPO_LOWER }}/agent:buildcache
45+
cache-to: type=registry,ref=ghcr.io/${{ env.REPO_LOWER }}/agent:buildcache,mode=max

.github/workflows/publish_pypi.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ permissions:
1111
jobs:
1212
build-and-publish:
1313
runs-on: ubuntu-latest
14+
if: github.repository == 'usnistgov/AFL-agent'
1415
steps:
1516
- uses: actions/checkout@v4
1617
- uses: actions/setup-python@v5
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
name: Integration Tests
2+
3+
# TODO: remove the add-docker branch before merging to main
4+
on:
5+
push:
6+
branches: [ main, add-docker ]
7+
pull_request:
8+
branches: [ main, add-docker ]
9+
workflow_dispatch: {}
10+
11+
jobs:
12+
integration:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Checkout
16+
uses: actions/checkout@v4
17+
18+
- name: Build and start stack
19+
run: |
20+
docker compose up -d --build
21+
- name: Wait for services to become healthy
22+
run: |
23+
set -euo pipefail
24+
tiled_id=$(docker compose ps -q tiled)
25+
agent_id=$(docker compose ps -q agent)
26+
for i in $(seq 1 30); do
27+
th=$(docker inspect --format '{{.State.Health.Status}}' "$tiled_id") || true
28+
ah=$(docker inspect --format '{{.State.Health.Status}}' "$agent_id") || true
29+
if [ "$th" = "healthy" ] && [ "$ah" = "healthy" ]; then
30+
echo "Services healthy"; exit 0
31+
fi
32+
sleep 2
33+
done
34+
echo "Services did not become healthy" >&2
35+
docker compose ps
36+
exit 1
37+
38+
- name: Smoke test Tiled API
39+
run: |
40+
set -euo pipefail
41+
code=$(curl -s -o /dev/null -w "%{http_code}" -H "X-Tiled-Api-Key: devkey" http://localhost:8000/api/v1/metadata/)
42+
if [ "$code" != "200" ] && [ "$code" != "401" ]; then
43+
echo "Unexpected Tiled status: $code" >&2
44+
exit 1
45+
fi
46+
47+
- name: Smoke test Agent driver status
48+
run: |
49+
set -euo pipefail
50+
code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:5003/driver_status)
51+
body=$(curl -s http://localhost:5003/driver_status)
52+
if [ "$code" != "200" ]; then
53+
echo "Unexpected agent status: $code" >&2
54+
exit 1
55+
fi
56+
echo "$body" | grep -q "mock_mode"
57+
58+
- name: Teardown
59+
if: always()
60+
run: docker compose down -v

.github/workflows/tests.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
name: Run Tests
22

3+
# TODO: remove the add-docker branch before merging to main
34
on:
45
push:
5-
branches: [ 'main' ]
6+
branches: [ 'main', 'add-docker' ]
67
pull_request:
78
branches: [ '*' ]
89

@@ -29,7 +30,7 @@ jobs:
2930
- name: Install dependencies
3031
run: |
3132
python -m pip install --upgrade pip
32-
pip install -e ".[dev]"
33+
pip install -e ".[dev,mlmodels]"
3334
3435
- name: Run tests
3536
run: |

.vscode/settings.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
{
2+
"python.defaultInterpreterPath": "/Users/tbm/software/AFL-dev/.venv/bin/python",
23
"python.testing.unittestEnabled": false,
3-
"python.testing.pytestEnabled": true
4+
"python.testing.pytestEnabled": true,
5+
"python-envs.pythonProjects": [
6+
{
7+
"path": "",
8+
"envManager": "ms-python.python:venv",
9+
"packageManager": "ms-python.python:pip"
10+
}
11+
]
412
}

AFL/double_agent/AgentDriver.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,8 @@ def __init__(
261261

262262
def status(self):
263263
status = []
264+
if 'mock_mode' in self.config:
265+
status.append(f'mock_mode: {self.config["mock_mode"]}')
264266
if self.input:
265267
status.append(f'Input Dims: {self.input.sizes}')
266268
if self.pipeline:

AFL/double_agent/TreeHierarchy.py

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
#Package Tree Hierarchy
2+
#Author Graham Roberts
3+
#email: grahamrobertsw@gmail.com
4+
#V0.1.0 -- A very early version of the code which contains the basic structure of the tree hierarchy, as well as some functionality for reading and writing from various json formats.
5+
6+
import numpy as np
7+
from sklearn.svm import SVC
8+
from sklearn.kernel_ridge import KernelRidge as KRR
9+
import json
10+
from joblib import dump, load
11+
import joblib
12+
from io import BytesIO
13+
import base64
14+
15+
#TreeHierarchy
16+
# allows construction of custom hierarchical classifiers
17+
# these allow for multiclass classification with indepenently tuned classifiers at each branch
18+
# this is a recursie structure
19+
class TreeHierarchy:
20+
21+
def _init_(self):
22+
self.left = None
23+
self.right = None
24+
self.content = None
25+
self.terminal = None
26+
self.classA = None
27+
self.classB = None
28+
#self.__dict__ = self.vars()
29+
30+
31+
def vars(self):
32+
outdict = {}
33+
for k in self.dir():
34+
v = getattr(self, k, None)
35+
if v is not None:
36+
if isinstance(v, TreeHierarchy):
37+
outdict[k] = vars(v)
38+
elif isinsstance(v, np.ndarray):
39+
outdict[k] = list(v)
40+
elif isinstance(v, SVC):
41+
tempdict = {}
42+
for k2, v2 in v.__dict__:
43+
if isinstance(v2, np.ndarray):
44+
tempdict[k2] = list(v2)
45+
else:
46+
tempdict[k2] = v2
47+
outdict[k] = tempdict
48+
return(outdict)
49+
# return(None)
50+
51+
def add_content(self, contant, terminal):
52+
self.content = content
53+
self.terminal = terminal
54+
55+
def add_left(self, entity):
56+
self.left = entity
57+
58+
def add_right(self, entity):
59+
self.right = entity
60+
61+
#Fit follows the sklearn fit structure and recursively calls fit on each component tree
62+
def fit(self, X, y):
63+
if not getattr(self, 'terminal', False):
64+
templabels = np.zeros(X.shape[0])
65+
for l in np.unique(self.classB):
66+
for i in range(len(y)):
67+
if y[i] == l:
68+
templabels[i] = 1
69+
self.entity.fit(X, templabels)
70+
ia = np.where(templabels == 0)[0]
71+
ib = np.where(templabels == 1)[0]
72+
self.left.fit(X[ia], y[ia])
73+
self.right.fit(X[ib], y[ib])
74+
return
75+
76+
77+
def predict(self, X):
78+
if X.shape[0] == 0:
79+
y = np.zeros(0)
80+
elif not getattr(self, 'terminal', False):
81+
inds = np.arange(X.shape[0])
82+
temp_y = self.entity.predict(X)
83+
ia = np.where(np.logical_not(temp_y))[0]
84+
ib = np.where(temp_y)[0]
85+
return_y = np.empty(X.shape[0], dtype=object)
86+
y_a = self.left.predict(X[ia])
87+
y_b = self.right.predict(X[ib])
88+
for v in range(ia.shape[0]):
89+
vi = ia[v]
90+
return_y[vi] = y_a[v]
91+
for v in range(ib.shape[0]):
92+
vi = ib[v]
93+
return_y[vi] = y_b[v]
94+
y = return_y
95+
else:
96+
y = np.array([self.terminal] * X.shape[0])
97+
return(y)
98+
99+
#Structure from json takes as input a json structure and constructs the tree based on that structure
100+
def structure_from_json(self, J):
101+
if 'class' in J.keys():
102+
self.terminal = J['class']
103+
else:
104+
if 'jobfile' in J.keys():
105+
self.entity = load(J['jobfile'])
106+
print('Loading %s'%(J['jobfile']))
107+
elif 'classifier' in J.keys():
108+
print(J['classifier'])
109+
self.entity = classifier_from_json(J['classifier'])
110+
print(self.entity)
111+
else:
112+
self.entity = None
113+
self.left = TreeHierarchy()
114+
self.left.structure_from_json(J['left'])
115+
self.right = TreeHierarchy()
116+
self.right.structure_from_json(J['right'])
117+
self.classA = J['classA']
118+
self.classB = J['classB']
119+
120+
#Pass to json.dumps as an encoder class
121+
#decontructs the tree, component SVCs KRRs and npArrays into primitives as well.
122+
class TreeEncoder(json.JSONEncoder):
123+
124+
125+
def default(self, obj):
126+
"""If input object is an ndarray it will be converted into a dict
127+
holding dtype, shape and the data, base64 encoded.
128+
"""
129+
if isinstance(obj, np.ndarray):
130+
if obj.flags['C_CONTIGUOUS']:
131+
obj_data = obj.data
132+
else:
133+
cont_obj = np.ascontiguousarray(obj)
134+
assert(cont_obj.flags['C_CONTIGUOUS'])
135+
obj_data = cont_obj.data
136+
data_b64 = base64.b64encode(obj_data)
137+
return dict(__ndarray__=data_b64.decode('utf-8'),
138+
dtype=str(obj.dtype),
139+
shape=obj.shape)
140+
elif isinstance(obj, SVC):
141+
return(obj.__dict__)
142+
elif isinstance(obj, KRR):
143+
return(obj.__dict__)
144+
elif isinstance(obj, TreeHierarchy):
145+
return(obj.__dict__)
146+
else:
147+
# Let the base class default method raise the TypeError
148+
super().default(obj)
149+
150+
def json_decoder(dct):
151+
"""Decodes a previously encoded TreeHierarchy, numpy ndarray with proper shape and dtype, SVC, or KRR.
152+
153+
:param dct: (dict) json encoded ndarray
154+
:return: (ndarray) if input was an encoded ndarray
155+
"""
156+
if isinstance(dct, dict) and '__ndarray__' in dct:
157+
data = base64.b64decode(dct['__ndarray__'].encode('utf-8'))
158+
return np.frombuffer(data, dct['dtype']).reshape(dct['shape'])
159+
elif isinstance(dct, dict) and 'support_vectors_' in dct:
160+
obj = SVC()
161+
for (k, v) in dct.items():
162+
setattr(obj, k, json_decoder(v))
163+
return(obj)
164+
elif isinstance(dct, dict) and 'alpha' in dct:
165+
obj = KRR()
166+
for (k, v) in dct.items():
167+
setattr(obj, k, json_decoder(v))
168+
return(obj)
169+
elif isinstance(dct, dict) and ("classA" in dct or "terminal" in dct):
170+
obj = TreeHierarchy()
171+
for (k,v) in dct.items():
172+
setattr(obj, k, json_decoder(v))
173+
return(obj)
174+
elif isinstance(dct, dict):
175+
newdct = {k: json_decoder(v) for (k,v) in dct.items()}
176+
return(newdct)
177+
return dct
178+
179+
###
180+
### def to_dict(self):
181+
### if getattr(self, "terminal", False):
182+
### return({"class": self.terminal})
183+
### else:
184+
### return({#"classifier": self.entity,
185+
### "classLeft": self.classA,
186+
### "classRight": self.classB,
187+
### "left":self.left.to_dict(),
188+
### "right": self.right.to_dict()})
189+
###
190+
###
191+
### def toJSON(self):
192+
### bc = BytesIO()
193+
### joblib.dump(self,bc)
194+
### return(json.dumps({"tree":str(bc.getvalue())}))
195+
###
196+
### def to_json(self, fn):
197+
### with open(fn, "w") as f:
198+
### json.dump(self.to_dict(), f)
199+
### return
200+
201+
202+
###def classifier_from_json(J):
203+
### if J['type'] in ['svc', 'SVC']:
204+
### print("SVC")
205+
### K = J['kernel']
206+
### classifier = SVC(C = J['C'],
207+
### gamma = J['gamma'],
208+
### kernel = K['type'],
209+
### degree = K['degree'] if K['type'] == 'polynomial' else 1,
210+
### coef0 = J['coeff0'] if 'coeff0' in J.keys() else J['coef0'])
211+
### print(classifier)
212+
### else:
213+
### classifier = None
214+
### return(classifier)

AFL/double_agent/TreePipeline.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from io import BytesIO
1010
import xarray as xr
1111
import json
12-
import TreeHierarchy as te
12+
import AFL.double_agent.TreeHierarchy as te
1313
from sklearn.metrics import classification_report as cr
1414
from sklearn.metrics import root_mean_squared_error as RMSE
1515
from sklearn.metrics import mean_absolute_error as MAE

0 commit comments

Comments
 (0)