causality inference done
This commit is contained in:
@@ -37,16 +37,49 @@ def infer_causality(component: dict[str, Any], toplevel: bool = True) -> dict[st
|
||||
|
||||
id_list = build_id_list(component)
|
||||
|
||||
# Check fixed causality
|
||||
for connection in graph.get("connections", []):
|
||||
if isinstance(connection, dict) and connection.get("type") == "power":
|
||||
source, target = get_ports_from_bond(connection, id_list)
|
||||
log.info(source)
|
||||
log.info(target)
|
||||
if source.get('causality', 'indifferent') == 'fixed effort out':
|
||||
connection['causality'] = 'target'
|
||||
elif source.get('causality', 'indifferent') == 'fixed flow out':
|
||||
connection['causality'] = 'source'
|
||||
# Fixed causalities
|
||||
for block in graph.get("blocks", []):
|
||||
for port in block.get('interface', {}).get('ports', []):
|
||||
if port.get('type', '') != 'power':
|
||||
continue
|
||||
if port.get('causality', 'indifferent') == 'fixed effort out':
|
||||
propagate_from_port(block, port, 'effort out', graph, id_list)
|
||||
elif port.get('causality', 'indifferent') == 'fixed flow out':
|
||||
propagate_from_port(block, port, 'flow out', graph, id_list)
|
||||
|
||||
# Preferred causalities
|
||||
for block in graph.get("blocks", []):
|
||||
for port in block.get('interface', {}).get('ports', []):
|
||||
if port.get('type', '') != 'power':
|
||||
continue
|
||||
if is_port_fully_assigned(block, port, graph):
|
||||
continue
|
||||
if port.get('causality', 'indifferent') == 'preferred effort out':
|
||||
propagate_from_port(block, port, 'effort out', graph, id_list)
|
||||
elif port.get('causality', 'indifferent') == 'preferred flow out':
|
||||
propagate_from_port(block, port, 'flow out', graph, id_list)
|
||||
|
||||
|
||||
|
||||
# Soft choices
|
||||
for block in graph.get("blocks", []):
|
||||
for port in block.get('interface', {}).get('ports', []):
|
||||
if port.get('type', '') != 'power':
|
||||
continue
|
||||
if is_port_fully_assigned(block, port, graph):
|
||||
continue
|
||||
if port.get('causality', 'indifferent') == 'likes effort out':
|
||||
propagate_from_port(block, port, 'effort out', graph, id_list)
|
||||
elif port.get('causality', 'indifferent') == 'likes flow out':
|
||||
propagate_from_port(block, port, 'flow out', graph, id_list)
|
||||
elif port.get('causality', 'indifferent') in ['indifferent', 'single flow in', 'single effort in']:
|
||||
# Force an arbitrary assignment on the first unassigned bond connected to this port
|
||||
propagate_from_port(block, port, 'effort out', graph, id_list)
|
||||
|
||||
# Last check
|
||||
for con in graph.get('connections', []):
|
||||
if con.get('type') == 'power' and con.get('causality', 'none') == 'none':
|
||||
raise ValueError("System under-constrained: unresolved causal loops or disconnected elements remain")
|
||||
|
||||
for block in graph.get("blocks", []):
|
||||
if isinstance(block, dict):
|
||||
@@ -104,31 +137,167 @@ def build_id_list(graph: dict[str, Any]) -> dict[str, Any]:
|
||||
visit(graph)
|
||||
return id_list
|
||||
|
||||
def get_ports_from_bond(bond: dict[str, Any], id_list: dict[str, Any]) -> tuple[dict[str,Any], dict[str,Any]]:
|
||||
source = bond.get('source', None)
|
||||
target = bond.get('target', None)
|
||||
if source is None or target is None:
|
||||
raise ValueError("Source and Target of a power bond cannot be None")
|
||||
|
||||
source_component = id_list.get(source.get('block'), None)
|
||||
target_component = id_list.get(target.get('block'), None)
|
||||
if source_component is None or target_component is None:
|
||||
raise ValueError("Source or Target blocks not found")
|
||||
|
||||
def _find_port(ports: list[dict[str, Any]], port: str) -> dict[str, Any] | None:
|
||||
for pi in ports:
|
||||
if pi.get('id', '') == port:
|
||||
return pi
|
||||
return None
|
||||
def is_port_fully_assigned(block: dict[str, Any], port: dict[str, Any], graph: dict[str, Any]) -> bool:
|
||||
block_id = block.get('id')
|
||||
port_id = port.get('id')
|
||||
for connection in graph.get('connections', []):
|
||||
if connection.get('type') != 'power':
|
||||
continue
|
||||
source = connection.get('source', {})
|
||||
target = connection.get('target', {})
|
||||
if ((source.get('block') == block_id and source.get('port') == port_id)
|
||||
or (target.get('block') == block_id and target.get('port') == port_id)):
|
||||
if connection.get('causality', 'none') == 'none':
|
||||
return False
|
||||
return True
|
||||
|
||||
source_port = _find_port(source_component.get('interface', {}).get('ports', []), source.get('port'))
|
||||
target_port = _find_port(target_component.get('interface', {}).get('ports', []), target.get('port'))
|
||||
if source_port is None or target_port is None:
|
||||
raise ValueError("Source or Target port not found")
|
||||
def propagate_from_port(block: dict[str, Any], port: dict[str, Any], desired_causality: str, graph: dict[str, Any], id_list: dict[str, Any]) -> None:
|
||||
# find all connection on this specific port
|
||||
attached_connections = []
|
||||
for con in graph.get('connections', []):
|
||||
if con.get('type') == 'power':
|
||||
if con.get('source', {}).get('block') == block.get('id') and con.get('source', {}).get('port') == port.get('id'):
|
||||
attached_connections.append(con)
|
||||
elif con.get('target', {}).get('block') == block.get('id') and con.get('target', {}).get('port') == port.get('id'):
|
||||
attached_connections.append(con)
|
||||
if not attached_connections:
|
||||
# unconnected port
|
||||
return
|
||||
|
||||
if source_port.get('type') != 'power':
|
||||
raise ValueError("Source port is not a power port")
|
||||
if target_port.get('type') != 'power':
|
||||
raise ValueError("Target port is not a power port")
|
||||
# find first unassigned connection on this port
|
||||
target_connection = None
|
||||
for con in attached_connections:
|
||||
if con.get('causality', None) == 'none':
|
||||
target_connection = con
|
||||
break
|
||||
if target_connection is None:
|
||||
# Nothing left to resolve
|
||||
return
|
||||
|
||||
# Update
|
||||
if target_connection.get('source').get('block') == block.get('id'):
|
||||
target_connection['causality'] = 'target' if (desired_causality == 'effort out') else 'source'
|
||||
else:
|
||||
target_connection['causality'] = 'source' if (desired_causality == 'effort out') else 'target'
|
||||
|
||||
propagate_to_neighbor_from_connection(block, target_connection, graph, id_list)
|
||||
|
||||
def evaluate_junction_constraints(block: dict[str, Any], port: dict[str, Any], graph: dict[str, Any], id_list: dict[str, Any]) -> None:
|
||||
# find all connection on this specific port
|
||||
attached_connections = []
|
||||
for con in graph.get('connections', []):
|
||||
if con.get('type') == 'power':
|
||||
if con.get('source', {}).get('block') == block.get('id') and con.get('source', {}).get('port') == port.get('id'):
|
||||
attached_connections.append(con)
|
||||
elif con.get('target', {}).get('block') == block.get('id') and con.get('target', {}).get('port') == port.get('id'):
|
||||
attached_connections.append(con)
|
||||
if not attached_connections:
|
||||
# unconnected port
|
||||
return
|
||||
|
||||
return source_port, target_port
|
||||
efforts_in = 0
|
||||
efforts_out = 0
|
||||
unnassigned_conns = []
|
||||
# Count current states relative to the junction port
|
||||
for con in attached_connections:
|
||||
if con.get('causality', 'none') == 'none':
|
||||
unnassigned_conns.append(con)
|
||||
continue
|
||||
|
||||
is_source = (con.get('source').get('block') == block.get('id'))
|
||||
if (is_source and con.get('causality', 'none')=='source') or (not is_source and con.get('causality', 'none')=='target'):
|
||||
efforts_in += 1
|
||||
else:
|
||||
efforts_out += 1
|
||||
|
||||
# Single effort in
|
||||
if port.get('causality', 'indifferent') == 'single effort in':
|
||||
# ERROR CHECK: Multiple sources/blocks trying to claim effort control on a 0-junction
|
||||
if efforts_in>1:
|
||||
raise ValueError(f"Critical causality conflict: Multiple blocks are dictating effort to 0 junction: {block.get('name')}")
|
||||
|
||||
# 1 effort is coming in so all other ports must be effort out
|
||||
if efforts_in == 1 and len(unnassigned_conns) > 0:
|
||||
for con in unnassigned_conns:
|
||||
desired = 'target' if (con.get('source').get('block')==block.get('id')) else 'source'
|
||||
con['causality'] = desired
|
||||
propagate_to_neighbor_from_connection(block, con, graph, id_list)
|
||||
|
||||
# No effort is coming in yet and one left so must be effort in
|
||||
if efforts_in == 0 and len(unnassigned_conns) == 1:
|
||||
con = unnassigned_conns[0]
|
||||
desired = 'source' if (con.get('source').get('block')==block.get('id')) else 'target'
|
||||
con['causality'] = desired
|
||||
propagate_to_neighbor_from_connection(block, con, graph, id_list)
|
||||
|
||||
# Single flow in
|
||||
elif port.get('causality', 'indifferent') == 'single flow in':
|
||||
# ERROR CHECK: Multiple sources/blocks trying to claim flow control on a 1-junction
|
||||
if efforts_out>1:
|
||||
raise ValueError(f"Critical causality conflict: Multiple blocks are dictating flow to 1 junction: {block.get('name')}")
|
||||
|
||||
# 1 flow is coming in so all other ports must be flow out
|
||||
if efforts_out == 1 and len(unnassigned_conns) > 0:
|
||||
for con in unnassigned_conns:
|
||||
desired = 'source' if (con.get('source').get('block')==block.get('id')) else 'target'
|
||||
con['causality'] = desired
|
||||
propagate_to_neighbor_from_connection(block, con, graph, id_list)
|
||||
|
||||
# No flow is coming in yet and one left so must be flow in
|
||||
if efforts_out == 0 and len(unnassigned_conns) == 1:
|
||||
con = unnassigned_conns[0]
|
||||
desired = 'target' if (con.get('source').get('block')==block.get('id')) else 'source'
|
||||
con['causality'] = desired
|
||||
propagate_to_neighbor_from_connection(block, con, graph, id_list)
|
||||
|
||||
def propagate_to_neighbor_from_connection(block: dict[str, Any], connection: dict[str, Any], graph: dict[str, Any], id_list: dict[str, Any]) -> None:
|
||||
# Get the neighbor block and port on the other side of the bond
|
||||
neighbor_id = connection.get('target').get('block') if connection.get('source').get('block') == block.get('id') else connection.get('source').get('block')
|
||||
neighbor_port_id = connection.get('target').get('port') if connection.get('source').get('block') == block.get('id') else connection.get('source').get('port')
|
||||
neighbor_block = id_list.get(neighbor_id, None)
|
||||
if neighbor_block is None:
|
||||
raise ValueError(f"No other side of the bond found on connection {connection.get('id')}")
|
||||
neighbor_ports = neighbor_block.get('interface').get('ports', [])
|
||||
neighbor_port = None
|
||||
for p in neighbor_ports:
|
||||
if p.get('id') == neighbor_port_id:
|
||||
neighbor_port = p
|
||||
break
|
||||
if neighbor_port is None:
|
||||
raise ValueError(f"No other side of the bond found on connection {connection.get('id')}")
|
||||
|
||||
# Trigger evaluation on the neighbor
|
||||
if neighbor_port.get('causality', 'indifferent') in ['single effort in', 'single flow in']:
|
||||
evaluate_junction_constraints(neighbor_block, neighbor_port, graph, id_list)
|
||||
else:
|
||||
verify_component_compatibility(neighbor_block, neighbor_port, graph)
|
||||
|
||||
def verify_component_compatibility(block: dict[str, Any], port: dict[str, Any], graph: dict[str, Any]) -> None:
|
||||
# Find the connection we are evaluating
|
||||
conn = None
|
||||
for c in graph.get('connections', []):
|
||||
if (c.get('source').get('block') == block.get('id') and c.get('source').get('port') == port.get('id')) or (c.get('target').get('block') == block.get('id') and c.get('target').get('port') == port.get('id')):
|
||||
if c.get('causality', 'none') != 'none':
|
||||
conn = c
|
||||
break
|
||||
|
||||
if conn is None:
|
||||
return
|
||||
|
||||
# Figure out what causality was pushed onto this port from the outside world
|
||||
state = conn.get('causality', 'none')
|
||||
is_source = conn.get('source', {}).get('block') == block.get('id')
|
||||
effort_out = (is_source and state == 'target') or (not is_source and state == 'source')
|
||||
port_causality = port.get('causality', 'indifferent')
|
||||
|
||||
if port_causality == 'fixed effort out' and not effort_out:
|
||||
raise ValueError(f"Critical source conflict: Fixed effort source '{block.get('name')}' forced into an input state.")
|
||||
if port_causality == 'fixed flow out' and effort_out:
|
||||
raise ValueError(f"Critical source conflict: Fixed flow source '{block.get('name')}' forced into an input state.")
|
||||
|
||||
if port_causality == 'preferred effort out' and not effort_out:
|
||||
conn['causality'] = f'warn_{state}'
|
||||
# log.warning(f"Storage block '{block.get('name')}' was forced into derivative causality (dependent state).")
|
||||
if port_causality == 'preferred flow out' and effort_out:
|
||||
conn['causality'] = f'warn_{state}'
|
||||
# log.warning(f"Storage block '{block.get('name')}' was forced into derivative causality (dependent state).")
|
||||
|
||||
Reference in New Issue
Block a user