generated from ssec-jhu/base-template
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
300342d
commit 3a9ff5f
Showing
6 changed files
with
287 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,44 @@ | ||
import asyncio | ||
from fastapi import FastAPI | ||
from fastapi.responses import RedirectResponse | ||
|
||
from evolver.device import Evolver, EvolverConfig | ||
from .. import __project__, __version__ | ||
|
||
|
||
app = FastAPI() | ||
evolver = Evolver() | ||
|
||
|
||
@app.get("/") | ||
async def root(): | ||
return RedirectResponse(url='/docs') | ||
async def describe_evolver(): | ||
return { | ||
'config': evolver.config, | ||
'state': evolver.get_state(), | ||
'last_read': evolver.last_read, | ||
} | ||
|
||
|
||
@app.post("/") | ||
async def update_evolver(config: EvolverConfig): | ||
evolver.update_config(config) | ||
|
||
|
||
@app.get("/healthz") | ||
async def healthz(): | ||
return {"message": f"Running '{__project__}' ver: '{__version__}'"} | ||
|
||
|
||
async def evolver_async_loop(): | ||
while True: | ||
evolver.loop_once() | ||
await asyncio.sleep(evolver.config.interval) | ||
|
||
|
||
@app.on_event('startup') | ||
async def start_evolver_loop(): | ||
asyncio.create_task(evolver_async_loop()) | ||
|
||
|
||
if __name__ == '__main__': | ||
import uvicorn | ||
uvicorn.run(app, host="127.0.0.1", port=8000, log_level="info") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import time | ||
from pydantic import BaseModel | ||
from evolver.util import load_class_fqcn | ||
from evolver.hardware import SensorDriver, EffectorDriver | ||
|
||
|
||
class AdapterDescriptor(BaseModel): | ||
driver: str | ||
config: dict = {} | ||
|
||
|
||
class HardwareDriverDescriptor(AdapterDescriptor): | ||
calibrator: AdapterDescriptor = None | ||
|
||
|
||
class EvolverConfig(BaseModel): | ||
vials: list = list(range(16)) | ||
hardware: dict[str, HardwareDriverDescriptor] = {} | ||
adapters: list[AdapterDescriptor] = [] | ||
serial: AdapterDescriptor = None | ||
enable_react: bool = True | ||
enable_commit: bool = True | ||
interval: int = 20 | ||
|
||
|
||
class Evolver: | ||
hardware = {} | ||
adapters = [] | ||
last_read = {} | ||
|
||
def __init__(self, config: EvolverConfig = EvolverConfig()): | ||
self.update_config(config) | ||
|
||
def update_config(self, config: EvolverConfig): | ||
self.config = config | ||
for name, driver in config.hardware.items(): | ||
self.setup_driver(name, driver) | ||
for name in list(self.hardware.keys()): | ||
if name not in config.hardware.keys(): | ||
del(self.hardware[name]) | ||
del(self.last_read[name]) | ||
self.adapters = [] | ||
for adapter in config.adapters: | ||
self.setup_adapter(adapter) | ||
self.setup_serial(config.serial) | ||
|
||
def setup_driver(self, name, driver_config: HardwareDriverDescriptor): | ||
driver_class = load_class_fqcn(driver_config.driver) | ||
config = driver_class.Config.model_validate(driver_config.config) | ||
calibrator = None | ||
if driver_config.calibrator is not None: | ||
calibrator_class = load_class_fqcn(driver_config.calibrator.driver) | ||
calibrator_config = calibrator_class.Config.model_validate(driver_config.calibrator.config) | ||
calibrator = calibrator_class(calibrator_config) | ||
self.hardware[name] = driver_class(self, config, calibrator) | ||
|
||
def setup_adapter(self, adapter): | ||
cls = load_class_fqcn(adapter.driver) | ||
conf = cls.Config.model_validate(adapter.config) | ||
self.adapters.append(cls(self, conf)) | ||
|
||
def setup_serial(self, serial): | ||
pass | ||
|
||
def get_hardware(self, name): | ||
return self.hardware[name] | ||
|
||
@property | ||
def sensors(self): | ||
return {k: v for k,v in self.hardware.items() if isinstance(v, SensorDriver)} | ||
|
||
@property | ||
def effectors(self): | ||
return {k: v for k,v in self.hardware.items() if isinstance(v, EffectorDriver)} | ||
|
||
@property | ||
def calibration_status(self): | ||
return {name: device.calibrator.status for name,device in self.hardware.items()} | ||
|
||
def read_state(self): | ||
for name, device in self.sensors.items(): | ||
device.read() | ||
self.last_read[name] = time.time() | ||
|
||
def get_state(self): | ||
return {name: device.get() for name,device in self.sensors.items()} | ||
|
||
def evaluate_adapters(self): | ||
for adapter in self.adapters: | ||
adapter.react() | ||
|
||
def commit_proposals(self): | ||
for device in self.effectors.values(): | ||
device.commit() | ||
|
||
def loop_once(self): | ||
self.read_state() | ||
# for any hardware awaiting calibration, call calibration update method here | ||
if self.config.enable_react: | ||
self.evaluate_adapters() | ||
if self.config.enable_commit: | ||
self.commit_proposals() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
from abc import ABC, abstractmethod | ||
from copy import copy | ||
from pydantic import BaseModel | ||
|
||
|
||
class VialConfigBaseModel(BaseModel): | ||
vials: list[int] | None = None | ||
|
||
|
||
class VialBaseModel(BaseModel): | ||
vial: int | ||
|
||
|
||
class BaseCalibrator(ABC): | ||
class Config(BaseModel): | ||
calibfile: str = None | ||
|
||
def __init__(self, config: Config = Config()): | ||
self.config = config | ||
|
||
@property | ||
@abstractmethod | ||
def status(self): | ||
pass | ||
|
||
|
||
class NoOpCalibrator(BaseCalibrator): | ||
@property | ||
def status(self): | ||
return True | ||
|
||
|
||
class HardwareDriver(ABC): | ||
class Config(BaseModel): | ||
pass | ||
calibrator = NoOpCalibrator() | ||
|
||
def __init__(self, evolver, config = None, calibrator = None): | ||
self.evolver = evolver | ||
self.reconfigure(config or self.Config()) | ||
if calibrator: | ||
self.calibrator = calibrator | ||
|
||
def reconfigure(self, config): | ||
self.config = config | ||
|
||
|
||
class VialHardwareDriver(HardwareDriver): | ||
def reconfigure(self, config): | ||
super().reconfigure(config) | ||
if config.vials is None: | ||
config.vials = self.evolver.config.vials | ||
else: | ||
if not set(config.vials).issubset(self.evolver.all_vials): | ||
raise ValueError('invalid vials found in config') | ||
|
||
|
||
class SensorDriver(VialHardwareDriver): | ||
class Config(VialConfigBaseModel): | ||
pass | ||
class Output(VialBaseModel): | ||
raw: int | ||
value: float | ||
outputs: dict[int, Output] = {} | ||
|
||
def get(self) -> list[Output]: | ||
return self.outputs | ||
|
||
@abstractmethod | ||
def read(self): | ||
pass | ||
|
||
|
||
class EffectorDriver(VialHardwareDriver): | ||
class Config(VialConfigBaseModel): | ||
pass | ||
class Input(VialBaseModel): | ||
value: float | ||
proposal: dict[int, Input] = {} | ||
committed: dict[int, Input] = {} | ||
|
||
def set(self, input: Input): | ||
self.proposal[input.vial] = input | ||
|
||
@abstractmethod | ||
def commit(self): | ||
pass | ||
|
||
|
||
class NoOpSensorDriver(SensorDriver): | ||
class Config(VialConfigBaseModel): | ||
echo_raw: int = 1 | ||
echo_val: int = 2 | ||
|
||
def read(self): | ||
self.outputs = { | ||
i: self.Output(vial=i, raw=self.config.echo_raw, value=self.config.echo_val) | ||
for i in self.config.vials | ||
} | ||
|
||
def get(self): | ||
return self.outputs | ||
|
||
|
||
class NoOpEffectorDriver(EffectorDriver): | ||
def commit(self): | ||
self.comitted = copy(self.proposal) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import pytest | ||
from evolver.device import Evolver, EvolverConfig | ||
from evolver.hardware import NoOpSensorDriver | ||
|
||
|
||
@pytest.fixture | ||
def conf_with_driver(): | ||
return { | ||
'vials': [0,1,2,3], | ||
'hardware': { | ||
'testsensor': {'driver': 'evolver.hardware.NoOpSensorDriver', 'config': {}}, | ||
'testeffector': {'driver': 'evolver.hardware.NoOpEffectorDriver', 'config': {}}, | ||
} | ||
} | ||
|
||
|
||
@pytest.fixture | ||
def demo_evolver(conf_with_driver): | ||
return Evolver(EvolverConfig.model_validate(conf_with_driver)) | ||
|
||
|
||
def test_evolver_instantiate_with_default_config(): | ||
conf = EvolverConfig() | ||
evolver = Evolver() | ||
evolver.update_config(conf) | ||
Evolver(conf) | ||
|
||
|
||
def test_evolver_with_driver(demo_evolver): | ||
assert isinstance(demo_evolver.hardware['testsensor'], NoOpSensorDriver) | ||
|
||
|
||
@pytest.mark.parametrize('method', ['read_state', 'loop_once']) | ||
def test_evolver_read_and_get_state(demo_evolver, method): | ||
state = demo_evolver.get_state() | ||
assert state['testsensor'] == {} | ||
getattr(demo_evolver, method)() | ||
state = demo_evolver.get_state() | ||
for vial in demo_evolver.config.vials: | ||
assert state['testsensor'][vial] == NoOpSensorDriver.Output(vial=vial, raw=1, value=2) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters