olindgre 2ece01e1d7 Make powertrain-build not overlap with pybuild in site-packages
Change-Id: I7b59f3f04f0f787d35db0b9389f295bf1ad24f56
2024-09-17 10:25:04 +02:00

501 lines
17 KiB
Python

# Copyright 2024 Volvo Car Corporation
# Licensed under Apache 2.0.
# -*- coding: utf-8 -*-
"""Python module used for abstracting an application that should be interfacing others."""
from abc import abstractmethod
from ruamel.yaml import YAML
from powertrain_build.lib import logger
LOGGER = logger.create_logger('base')
def filter_signals(signals, domain):
""" Take a list of signals and remove all domains belonging to a domain
If the signal is part of the domain, it is not part of the resulting list
Arguments:
signals (list(Signal)): signals to filter
domain (Domain): domain that the signals should not be part of
"""
filtered_signals = []
for signal in signals:
if signal.name not in domain.signals:
filtered_signals.append(signal)
return filtered_signals
class MultipleProducersError(Exception):
"""Error when setting a producer and there already exists one"""
def __init__(self, signal, old_producer, new_producer):
"""Set error message
Args:
signal (Signal): Signal object
old_producer (BaseApplication): Producer already registered
new_producer (BaseApplication): Producer attempted to be registered
"""
super().__init__()
self.message = (f"{signal.name}:"
f" Attempting to set producer {new_producer}"
f" when {old_producer} is already set")
class BaseApplication:
"""Base application to build other adapters on"""
name = str()
_signals = None
node = str() # Used to calculate interface
read_strategies = {
"Always",
"OnChanged",
"OnUpdated"
}
def __repr__(self):
"""String representation for logging and debugging
Returns:
repr (string): Name of the application, the number of insignals and outsignals
"""
return f"<{self.name}" \
f" insignals:{len(self.insignals)}" \
f" outsignals:{len(self.outsignals)}>"
def parse_signals(self):
"""API interface to read all signals in any child object"""
self._get_signals()
@property
def insignals(self):
""" Insignals to the raster.
Calculated as all read ports - all written ports
Returns:
signals (list): List of Signal objects.
"""
if self._insignals is None:
self._get_signals()
return [self._signals[port] for port in self._insignals - self._outsignals]
@property
def outsignals(self):
""" All outports.
Since we might consume some of the signals that should also be sent elsewhere,
we do not remove internally consumed signals.
Returns:
signals (list): List of Signal objects.
"""
if self._outsignals is None:
self._get_signals()
return [self._signals[port] for port in self._outsignals]
@property
def signals(self):
"""API interface property in any child object
All cached signals.
If no cache exists, reads all signals and save to cache.
Returns:
signals (list): Signal objects
"""
if self._signals is None:
self.parse_signals()
return self._signals.values()
@abstractmethod
def _get_signals(self):
"""Stub to implement in child object"""
@abstractmethod
def get_signal_properties(self, signal):
"""Stub to implement in child object
Ideally, this should be moved to the signal.
Currently, getting the properties depends on how we read and define the signals.
"""
@abstractmethod
def parse_definition(self, definition):
"""Stub for parsing a defintion after the object has been initialized.
Raises NotImplementedError if called without being implemented.
Args:
definition: Definition of the Application. Type depends on the application.
"""
raise NotImplementedError('This is a stub')
class Signal:
"""Signal object
The signal should behave the same way independently of where we define it.
"""
def __repr__(self):
"""String representation for logging and debugging
Returns:
repr (string): Name of the application, the number of insignals and outsignals
"""
return (f"<{self.name} in {self.applications}"
f" producer:{self.producer}"
f" consumers:{self.consumers}>")
def __init__(self, name, application):
"""Define base properties of the signal object
The application object is used to read properties of a signal.
TODO: Do this when we define the signal and add properties known in other
systems when we encounter them.
Args:
name (string): Signal name
application (BaseApplication): Application defining the signal
"""
self.name = name
self.applications = {} # Add applications to a dict to prevent duplicates
if application is not None:
self.applications[application.name] = application
self._consumers = set()
self._producer = None
def add_application(self, application):
"""Add an application to find properties from
Args:
application (BaseApplication): Application to read properties from
"""
if application.name in self.applications:
return
self.applications[application.name] = application
@property
def consumers(self):
"""Get all consumers of a signal
Returns:
consumers (set): All consumers of a signal
"""
if isinstance(self._consumers, set):
return self._consumers
return set()
@consumers.setter
def consumers(self, consumers):
"""Set consumers of a signal
If the consumers is a list or set, iterate over each consumer
Otherwise, add the consumer to the set of consumers
Args:
consumers (list/set/string): consumer(s) of a signal
"""
if isinstance(consumers, (list, set)):
for consumer in consumers:
self._consumers.add(consumer)
else:
self._consumers.add(consumers)
@property
def producer(self):
"""Get the producer of a signal
Since we have some strange signals with multiple producers,
such as counters for dep, this returns a set.
Returns:
producer (set): Producer(s) of a signal
"""
if isinstance(self._producer, set):
return self._producer
return set()
@producer.setter
def producer(self, producer):
"""Set producer of a signal
Args:
producer (string/set): Name of the producer
"""
if isinstance(producer, set):
self._producer = producer
else:
self._producer = {producer}
def set_producer(self, producer):
"""Set producer of a signal
If there already is a registered producer of the signal,
raise MultipleProducersError
This can be expected and force_producer can be called to override this.
That must be explicit in each instance.
Args:
producer (string): Name of the producer
application (BaseApplication): Application defining the signal. Optional
"""
if isinstance(producer, set):
if self._producer is not None and producer - self._producer:
raise MultipleProducersError(self, self._producer, producer)
self.producer = producer
else:
if self._producer is not None \
and isinstance(producer, str) \
and producer not in self._producer:
raise MultipleProducersError(self, self._producer, producer)
self.producer = {producer}
def force_producer(self, producer):
"""Forcefully update add producers of a signal
This is needed since we have some signals that are written by multiple model
Args:
producers (string): Producer of a signal
application (BaseApplication): Application defining the signal. Optional
"""
self._producer.add(producer)
@property
def properties(self):
"""Properties of a signal
Currently not homogenized.
Therefore we read the properties from the application that defined the signal.
Returns:
properties (dict): properties of a signal
"""
properties = {}
for application in self.applications.values():
LOGGER.debug('Getting properties for %s from %s', self.name, application.name)
application_properties = application.get_signal_properties(self)
LOGGER.debug(application_properties)
for key, value in application_properties.items():
LOGGER.debug('Looking at %s: %s', key, value)
if key in properties and value != properties[key]:
LOGGER.debug('Signal %s already has %s with value %s, ignoring %s from %s',
self.name, key, properties[key], value, application.name)
continue
properties[key] = value
return properties
class Interface:
"""Interface between two objects"""
def __repr__(self):
"""String representation for logging and debugging
Returns:
repr (string): Name of the interface, and the length of received and transmitted signals
"""
return (f"<{self.name}"
f" a->b:{len(self.get_directional_signals(self.current, self.corresponding))}"
f" b->a:{len(self.get_directional_signals(self.corresponding, self.current))}>")
def debug(self):
"""Debug an interface object to stdout"""
LOGGER.info('name: %s', self.name)
for signal in self.get_directional_signals(self.current, self.corresponding):
LOGGER.info('insignal: %s', signal)
for signal in self.get_directional_signals(self.corresponding, self.current):
LOGGER.info('outsignal: %s', signal)
def __init__(self, current, corresponding):
"""Create the interface object
Args:
current (BaseApplication): Primary object of an interface
corresponding (BaseApplication): Secondary object of an interface
"""
self.name = current.name + '_' + corresponding.name
self.current = current
self.corresponding = corresponding
@staticmethod
def get_directional_signals(producer, consumer):
"""Get signals going from producer to consumer
Args:
producer (BaseApplication): producer of the signals
consumer (BaseApplication): consumer of the signals
Returns:
signals (list): Signals sent from producer and received in consumer
"""
outsignals = {signal.name: signal for signal in producer.outsignals}
signals = []
for signal in consumer.insignals:
if signal.name in outsignals:
signal.set_producer(outsignals[signal.name].producer)
signal.add_application(producer)
signal.add_application(consumer)
signals.append(signal)
return signals
def get_produced_signals(self, producer_name):
"""Get signals going from producer to consumer
This function can be used if you are lacking some objects
Args:
consumer_name (string): name of the consumer of the signals
Returns:
signals (list): Signals sent from producer and received in consumer
"""
if producer_name == self.current.name:
consumer = self.corresponding
producer = self.current
elif producer_name == self.corresponding.name:
consumer = self.current
producer = self.corresponding
else:
LOGGER.error('%s not in [%s, %s]',
producer_name,
self.current.name,
self.corresponding.name)
return self.get_directional_signals(producer, consumer)
def get_consumed_signals(self, consumer_name):
"""Get signals going from producer to consumer
This function can be used if you are lacking some objects
Args:
consumer_name (string): name of the consumer of the signals
Returns:
signals (list): Signals sent from producer and received in consumer
"""
if consumer_name == self.current.name:
consumer = self.current
producer = self.corresponding
elif consumer_name == self.corresponding.name:
consumer = self.corresponding
producer = self.current
else:
LOGGER.error('%s not in [%s, %s]',
consumer_name,
self.current.name,
self.corresponding.name)
return self.get_directional_signals(producer, consumer)
class Domain:
"""Domain with interacting interfaces"""
def __repr__(self):
"""String representation for logging and debugging
Returns:
repr (string): Name of the domain, and all clients for that domain
"""
return f"<{self.name}: {self.clients}>"
def __init__(self):
"""Initialize the object"""
self.name = ''
self.signals = {}
self.clients = set()
self._clients = {}
def set_name(self, name):
"""Set the name of the domain
Args:
name (string): Name of the domain
"""
self.name = name
def add_interface(self, interface):
"""Add an interface to a domain
Args:
interface (Interface): Interface object
"""
self._process_interface(interface)
def _process_interface(self, interface):
"""Process interface to add signals to the domain
Args:
interface (Interface): Interface object
"""
for signal in interface.get_directional_signals(interface.current, interface.corresponding):
self._process_signal(signal.name, interface.current, interface.corresponding)
for signal in interface.get_directional_signals(interface.corresponding, interface.current):
self._process_signal(signal.name, interface.corresponding, interface.current)
def _process_signal(self, signal_name, producer, consumer):
"""Process signal to add to the domain
Args:
signal_name (string): Name of the signal
consumer (BaseApplication): Consumer application of the signal
producer (BaseApplication): Producer application of the signal
"""
if signal_name not in self.signals:
self.signals[signal_name] = Signal(signal_name, producer)
self.signals[signal_name].consumers = consumer.name
self.signals[signal_name].add_application(producer)
self.signals[signal_name].add_application(consumer)
signal = self.signals[signal_name]
if producer.name not in self.clients:
self.clients.add(producer.name)
self._clients[producer.name] = {'producer': [], 'consumer': []}
self._clients[producer.name]['producer'].append(signal)
if consumer.name not in self.clients:
self.clients.add(producer.name)
self._clients[producer.name] = {'producer': [], 'consumer': []}
self._clients[producer.name]['consumer'].append(signal)
signal.consumers = consumer.name
try:
signal.producer = producer.name
except MultipleProducersError as mpe:
LOGGER.debug(mpe.message)
def create_groups(self):
"""Create groups of signals going from each producer
Returns:
signal_groups (dict): Signal groups
"""
signal_groups = {}
for signal in self.signals.values():
# Producer is always a set, to handle pass-through signals
for producer in signal.producer - set(signal_groups.keys()):
signal_groups[producer] = []
for producer in signal.producer:
signal_groups[producer].append(signal)
return signal_groups
def create_selective_groups(self, a_names, b_names):
"""Create groups for the a_list communicating with the b_names
Returns:
signal_groups (dict): Signal groups
"""
signal_groups = {name: {'consumer': [], 'producer': []} for name in a_names}
for signal in self.signals.values():
for producer in set(signal.producer):
if producer in a_names and signal.consumers & b_names:
signal_groups[producer]['producer'].append(signal)
for consumer in signal.consumers:
if consumer in a_names and set(signal.producer) & a_names:
signal_groups[consumer]['consumer'].append(signal)
return signal_groups
@staticmethod
def to_yaml(spec, output):
"""Writes spec to yaml file
Args:
spec (dict): data for the yaml
output (Path): file to write to
"""
with open(output, 'w', encoding="utf-8") as yaml_file:
yaml = YAML()
yaml.dump(spec, yaml_file)