Skip to content

Commit e3a88c9

Browse files
committed
Merge dev branch and preserve return fix
2 parents df5fd80 + f21bb89 commit e3a88c9

12 files changed

Lines changed: 283 additions & 71 deletions

File tree

.github/workflows/tests.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches: [main, dev]
6+
pull_request:
7+
branches: [main, dev]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
python-version: ['3.9', '3.10', '3.11', '3.12']
15+
16+
steps:
17+
- name: Checkout repository
18+
uses: actions/checkout@v4
19+
20+
- name: Set up Python ${{ matrix.python-version }}
21+
uses: actions/setup-python@v5
22+
with:
23+
python-version: ${{ matrix.python-version }}
24+
25+
- name: Install dependencies
26+
run: |
27+
python -m pip install --upgrade pip
28+
pip install -r requirements.txt
29+
pip install -r requirements-dev.txt
30+
pip install pyzmq
31+
32+
- name: Run tests
33+
run: pytest -v

.gitignore

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,21 @@
1-
**/.DS_Store
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*.class
5+
*.so
6+
.Python
7+
venv/
8+
env/
9+
ENV/
10+
11+
# IDE
12+
.vscode/
13+
.idea/
14+
15+
# Testing
16+
.pytest_cache/
17+
htmlcov/
18+
.coverage
19+
20+
# Concore specific
21+
concorekill.bat

concore.py

Lines changed: 67 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import time
2+
import logging
23
import os
34
from ast import literal_eval
45
import sys
56
import re
6-
import zmq # Added for ZeroMQ
7+
import zmq
8+
import numpy as np
9+
logging.basicConfig(
10+
level=logging.INFO,
11+
format='%(levelname)s - %(message)s'
12+
) # Added for ZeroMQ
713

814
# if windows, create script to kill this process
915
# because batch files don't provide easy way to know pid of last command
@@ -36,10 +42,10 @@ def __init__(self, port_type, address, zmq_socket_type):
3642
# Bind or connect
3743
if self.port_type == "bind":
3844
self.socket.bind(address)
39-
print(f"ZMQ Port bound to {address}")
45+
logging.info(f"ZMQ Port bound to {address}")
4046
else:
4147
self.socket.connect(address)
42-
print(f"ZMQ Port connected to {address}")
48+
logging.info(f"ZMQ Port connected to {address}")
4349

4450
def send_json_with_retry(self, message):
4551
"""Send JSON message with retries if timeout occurs."""
@@ -48,9 +54,9 @@ def send_json_with_retry(self, message):
4854
self.socket.send_json(message)
4955
return
5056
except zmq.Again:
51-
print(f"Send timeout (attempt {attempt + 1}/5)")
57+
logging.warning(f"Send timeout (attempt {attempt + 1}/5)")
5258
time.sleep(0.5)
53-
print("Failed to send after retries.")
59+
logging.error("Failed to send after retries.")
5460
return
5561

5662
def recv_json_with_retry(self):
@@ -59,9 +65,9 @@ def recv_json_with_retry(self):
5965
try:
6066
return self.socket.recv_json()
6167
except zmq.Again:
62-
print(f"Receive timeout (attempt {attempt + 1}/5)")
68+
logging.warning(f"Receive timeout (attempt {attempt + 1}/5)")
6369
time.sleep(0.5)
64-
print("Failed to receive after retries.")
70+
logging.error("Failed to receive after retries.")
6571
return None
6672

6773
# Global ZeroMQ ports registry
@@ -76,30 +82,48 @@ def init_zmq_port(port_name, port_type, address, socket_type_str):
7682
socket_type_str (str): String representation of ZMQ socket type (e.g., "REQ", "REP", "PUB", "SUB").
7783
"""
7884
if port_name in zmq_ports:
79-
print(f"ZMQ Port {port_name} already initialized.")
85+
logging.info(f"ZMQ Port {port_name} already initialized.")
8086
return # Avoid reinitialization
8187

8288
try:
8389
# Map socket type string to actual ZMQ constant (e.g., zmq.REQ, zmq.REP)
8490
zmq_socket_type = getattr(zmq, socket_type_str.upper())
8591
zmq_ports[port_name] = ZeroMQPort(port_type, address, zmq_socket_type)
86-
print(f"Initialized ZMQ port: {port_name} ({socket_type_str}) on {address}")
92+
logging.info(f"Initialized ZMQ port: {port_name} ({socket_type_str}) on {address}")
8793
except AttributeError:
88-
print(f"Error: Invalid ZMQ socket type string '{socket_type_str}'.")
94+
logging.error(f"Error: Invalid ZMQ socket type string '{socket_type_str}'.")
8995
except zmq.error.ZMQError as e:
90-
print(f"Error initializing ZMQ port {port_name} on {address}: {e}")
96+
logging.error(f"Error initializing ZMQ port {port_name} on {address}: {e}")
9197
except Exception as e:
92-
print(f"An unexpected error occurred during ZMQ port initialization for {port_name}: {e}")
98+
logging.error(f"An unexpected error occurred during ZMQ port initialization for {port_name}: {e}")
9399

94100
def terminate_zmq():
95101
for port in zmq_ports.values():
96102
try:
97103
port.socket.close()
98104
port.context.term()
99105
except Exception as e:
100-
print(f"Error while terminating ZMQ port {port.address}: {e}")
106+
logging.error(f"Error while terminating ZMQ port {port.address}: {e}")
101107
# --- ZeroMQ Integration End ---
102108

109+
110+
# NumPy Type Conversion Helper
111+
def convert_numpy_to_python(obj):
112+
#Recursively convert numpy types to native Python types.
113+
#This is necessary because literal_eval cannot parse numpy representations
114+
#like np.float64(1.0), but can parse native Python types like 1.0.
115+
if isinstance(obj, np.generic):
116+
# Convert numpy scalar types to Python native types
117+
return obj.item()
118+
elif isinstance(obj, list):
119+
return [convert_numpy_to_python(item) for item in obj]
120+
elif isinstance(obj, tuple):
121+
return tuple(convert_numpy_to_python(item) for item in obj)
122+
elif isinstance(obj, dict):
123+
return {key: convert_numpy_to_python(value) for key, value in obj.items()}
124+
else:
125+
return obj
126+
103127
# ===================================================================
104128
# File & Parameter Handling
105129
# ===================================================================
@@ -125,13 +149,15 @@ def safe_literal_eval(filename, defaultValue):
125149
inpath = "./in" #must be rel path for local
126150
outpath = "./out"
127151
simtime = 0
152+
concore_params_file = os.path.join(inpath + "1", "concore.params")
153+
concore_maxtime_file = os.path.join(inpath + "1", "concore.maxtime")
128154

129155
#9/21/22
130156
# ===================================================================
131157
# Parameter Parsing
132158
# ===================================================================
133159
try:
134-
sparams_path = os.path.join(inpath + "1", "concore.params")
160+
sparams_path = concore_params_file
135161
if os.path.exists(sparams_path):
136162
with open(sparams_path, "r") as f:
137163
sparams = f.read()
@@ -142,13 +168,13 @@ def safe_literal_eval(filename, defaultValue):
142168

143169
# Convert key=value;key2=value2 to Python dict format
144170
if sparams != '{' and not (sparams.startswith('{') and sparams.endswith('}')): # Check if it needs conversion
145-
print("converting sparams: "+sparams)
171+
logging.debug("converting sparams: "+sparams)
146172
sparams = "{'"+re.sub(';',",'",re.sub('=',"':",re.sub(' ','',sparams)))+"}"
147-
print("converted sparams: " + sparams)
173+
logging.debug("converted sparams: " + sparams)
148174
try:
149175
params = literal_eval(sparams)
150176
except Exception as e:
151-
print(f"bad params content: {sparams}, error: {e}")
177+
logging.warning(f"bad params content: {sparams}, error: {e}")
152178
params = dict()
153179
else:
154180
params = dict()
@@ -171,8 +197,7 @@ def tryparam(n, i):
171197
def default_maxtime(default):
172198
"""Read maximum simulation time from file or use default."""
173199
global maxtime
174-
maxtime_path = os.path.join(inpath + "1", "concore.maxtime")
175-
maxtime = safe_literal_eval(maxtime_path, default)
200+
maxtime = safe_literal_eval(concore_maxtime_file, default)
176201

177202
default_maxtime(100)
178203

@@ -206,30 +231,31 @@ def read(port_identifier, name, initstr_val):
206231
message = zmq_p.recv_json_with_retry()
207232
return message
208233
except zmq.error.ZMQError as e:
209-
print(f"ZMQ read error on port {port_identifier} (name: {name}): {e}. Returning default.")
234+
logging.error(f"ZMQ read error on port {port_identifier} (name: {name}): {e}. Returning default.")
210235
return default_return_val
211236
except Exception as e:
212-
print(f"Unexpected error during ZMQ read on port {port_identifier} (name: {name}): {e}. Returning default.")
237+
logging.error(f"Unexpected error during ZMQ read on port {port_identifier} (name: {name}): {e}. Returning default.")
213238
return default_return_val
214239

215240
# Case 2: File-based port
216241
try:
217242
file_port_num = int(port_identifier)
218243
except ValueError:
219-
print(f"Error: Invalid port identifier '{port_identifier}' for file operation. Must be integer or ZMQ name.")
244+
logging.error(f"Error: Invalid port identifier '{port_identifier}' for file operation. Must be integer or ZMQ name.")
220245
return default_return_val
221246

222247
time.sleep(delay)
223-
file_path = os.path.join(inpath+str(file_port_num), name)
248+
file_path = os.path.join(inpath + str(file_port_num), name)
224249
ins = ""
225250

226251
try:
227252
with open(file_path, "r") as infile:
228253
ins = infile.read()
229254
except FileNotFoundError:
230255
ins = str(initstr_val)
256+
s += ins # Update s to break unchanged() loop
231257
except Exception as e:
232-
print(f"Error reading {file_path}: {e}. Using default value.")
258+
logging.error(f"Error reading {file_path}: {e}. Using default value.")
233259
return default_return_val
234260

235261
# Retry logic if file is empty
@@ -241,12 +267,12 @@ def read(port_identifier, name, initstr_val):
241267
with open(file_path, "r") as infile:
242268
ins = infile.read()
243269
except Exception as e:
244-
print(f"Retry {attempts + 1}: Error reading {file_path} - {e}")
270+
logging.warning(f"Retry {attempts + 1}: Error reading {file_path} - {e}")
245271
attempts += 1
246272
retrycount += 1
247273

248274
if len(ins) == 0:
249-
print(f"Max retries reached for {file_path}, using default value.")
275+
logging.error(f"Max retries reached for {file_path}, using default value.")
250276
return default_return_val
251277

252278
s += ins
@@ -260,10 +286,10 @@ def read(port_identifier, name, initstr_val):
260286
simtime = max(simtime, current_simtime_from_file)
261287
return inval[1:]
262288
else:
263-
print(f"Warning: Unexpected data format in {file_path}: {ins}. Returning raw content or default.")
289+
logging.warning(f"Warning: Unexpected data format in {file_path}: {ins}. Returning raw content or default.")
264290
return inval
265291
except Exception as e:
266-
print(f"Error parsing content from {file_path} ('{ins}'): {e}. Returning default.")
292+
logging.error(f"Error parsing content from {file_path} ('{ins}'): {e}. Returning default.")
267293
return default_return_val
268294

269295

@@ -280,39 +306,38 @@ def write(port_identifier, name, val, delta=0):
280306
try:
281307
zmq_p.send_json_with_retry(val)
282308
except zmq.error.ZMQError as e:
283-
print(f"ZMQ write error on port {port_identifier} (name: {name}): {e}")
309+
logging.error(f"ZMQ write error on port {port_identifier} (name: {name}): {e}")
284310
except Exception as e:
285-
print(f"Unexpected error during ZMQ write on port {port_identifier} (name: {name}): {e}")
311+
logging.error(f"Unexpected error during ZMQ write on port {port_identifier} (name: {name}): {e}")
286312
return
287313

288314
# Case 2: File-based port
289315
try:
290-
if isinstance(port_identifier, str) and port_identifier in zmq_ports:
291-
file_path = os.path.join("../"+port_identifier, name)
292-
else:
293-
file_port_num = int(port_identifier)
294-
file_path = os.path.join(outpath+str(file_port_num), name)
316+
file_port_num = int(port_identifier)
317+
file_path = os.path.join(outpath + str(file_port_num), name)
295318
except ValueError:
296-
print(f"Error: Invalid port identifier '{port_identifier}' for file operation. Must be integer or ZMQ name.")
319+
logging.error(f"Error: Invalid port identifier '{port_identifier}' for file operation. Must be integer or ZMQ name.")
297320
return
298321

299322
# File writing rules
300323
if isinstance(val, str):
301324
time.sleep(2 * delay) # string writes wait longer
302325
elif not isinstance(val, list):
303-
print(f"File write to {file_path} must have list or str value, got {type(val)}")
326+
logging.error(f"File write to {file_path} must have list or str value, got {type(val)}")
304327
return
305328

306329
try:
307330
with open(file_path, "w") as outfile:
308331
if isinstance(val, list):
309-
data_to_write = [simtime + delta] + val
332+
# Convert numpy types to native Python types
333+
val_converted = convert_numpy_to_python(val)
334+
data_to_write = [simtime + delta] + val_converted
310335
outfile.write(str(data_to_write))
311336
simtime += delta
312337
else:
313338
outfile.write(val)
314339
except Exception as e:
315-
print(f"Error writing to {file_path}: {e}")
340+
logging.error(f"Error writing to {file_path}: {e}")
316341

317342
def initval(simtime_val_str):
318343
"""
@@ -328,12 +353,12 @@ def initval(simtime_val_str):
328353
simtime = first_element
329354
return val[1:]
330355
else:
331-
print(f"Error: First element in initval string '{simtime_val_str}' is not a number. Using data part as is or empty.")
356+
logging.error(f"Error: First element in initval string '{simtime_val_str}' is not a number. Using data part as is or empty.")
332357
return val[1:] if len(val) > 1 else []
333358
else:
334-
print(f"Error: initval string '{simtime_val_str}' is not a list or is empty. Returning empty list.")
359+
logging.error(f"Error: initval string '{simtime_val_str}' is not a list or is empty. Returning empty list.")
335360
return []
336361

337362
except Exception as e:
338-
print(f"Error parsing simtime_val_str '{simtime_val_str}': {e}. Returning empty list.")
363+
logging.error(f"Error parsing simtime_val_str '{simtime_val_str}': {e}. Returning empty list.")
339364
return []

concoredocker.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@ def safe_literal_eval(filename, defaultValue):
2121
inpath = os.path.abspath("/in")
2222
outpath = os.path.abspath("/out")
2323
simtime = 0
24+
concore_params_file = os.path.join(inpath, "1", "concore.params")
25+
concore_maxtime_file = os.path.join(inpath, "1", "concore.maxtime")
2426

2527
#9/21/22
2628
try:
27-
sparams = open(inpath+"1/concore.params").read()
29+
sparams = open(concore_params_file).read()
2830
if sparams[0] == '"': #windows keeps "" need to remove
2931
sparams = sparams[1:]
3032
sparams = sparams[0:sparams.find('"')]
@@ -46,7 +48,7 @@ def tryparam(n, i):
4648
#9/12/21
4749
def default_maxtime(default):
4850
global maxtime
49-
maxtime = safe_literal_eval(os.path.join(inpath, "1", "concore.maxtime"), default)
51+
maxtime = safe_literal_eval(concore_maxtime_file, default)
5052

5153
default_maxtime(100)
5254

@@ -62,7 +64,7 @@ def read(port, name, initstr):
6264
global s, simtime, retrycount
6365
max_retries=5
6466
time.sleep(delay)
65-
file_path = os.path.join(inpath+str(port), name)
67+
file_path = os.path.join(inpath, str(port), name)
6668

6769
try:
6870
with open(file_path, "r") as infile:
@@ -101,7 +103,7 @@ def read(port, name, initstr):
101103

102104
def write(port, name, val, delta=0):
103105
global simtime
104-
file_path = os.path.join(outpath+str(port), name)
106+
file_path = os.path.join(outpath, str(port), name)
105107

106108
if isinstance(val, str):
107109
time.sleep(2 * delay)

0 commit comments

Comments
 (0)