/******************************************************************************* * Copyright 2015 Analog Devices, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ********************************************************************************/ package com.analog.lyric.dimple.schedulers; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import com.analog.lyric.dimple.model.core.FactorGraph; import com.analog.lyric.dimple.model.core.IFactorGraphChild; import com.analog.lyric.dimple.model.core.INode; import com.analog.lyric.dimple.model.core.Port; import com.analog.lyric.dimple.model.factors.Factor; import com.analog.lyric.dimple.model.variables.Variable; import com.analog.lyric.dimple.model.variables.VariableBlock; import com.analog.lyric.dimple.options.BPOptions; import com.analog.lyric.dimple.schedulers.schedule.FixedSchedule; import com.analog.lyric.dimple.schedulers.schedule.ScheduleValidationException; import com.analog.lyric.dimple.schedulers.scheduleEntry.BlockScheduleEntry; import com.analog.lyric.dimple.schedulers.scheduleEntry.EdgeScheduleEntry; import com.analog.lyric.dimple.schedulers.scheduleEntry.IBlockUpdater; import com.analog.lyric.dimple.schedulers.scheduleEntry.IScheduleEntry; import com.analog.lyric.dimple.schedulers.scheduleEntry.NodeScheduleEntry; import com.analog.lyric.dimple.schedulers.scheduleEntry.SubgraphScheduleEntry; import com.analog.lyric.dimple.solvers.gibbs.GibbsOptions; import com.analog.lyric.dimple.solvers.interfaces.ISolverFactorGraph; import com.analog.lyric.util.misc.Internal; import com.google.common.base.Predicate; import com.google.common.collect.Iterables; /** * A schedule for producing a fixed custom schedule for a given graph. * <p> * @since 0.08 * @author Christopher Barber */ public class CustomScheduler extends SchedulerBase implements IGibbsScheduler { private static final long serialVersionUID = 1L; /*------- * State */ private final ArrayList<IScheduleEntry> _entries = new ArrayList<>(); private FactorGraph _graph; private final @Nullable SchedulerOptionKey _schedulerKey; /*-------------- * Construction */ /** * Construct a new custom scheduler for given graph and scheduler type. * <p> * @param graph is the graph for which the custom schedule will be generated. * @param schedulerType identifies what type of schedules this will produce * and will be returned by {@link #applicableSchedulerOptions()}. * The schedule type may constrain what types of entries may be added. For * instance, Gibbs schedules may not contain edge or factor update entries. * @since 0.08 */ public CustomScheduler(FactorGraph graph, SchedulerOptionKey schedulerType) { _graph = graph; _schedulerKey = schedulerType; } /** * @category internal */ @Deprecated @Internal public CustomScheduler(FactorGraph graph) { _graph = graph; _schedulerKey = null; } CustomScheduler(CustomScheduler other, Map<Object,Object> old2new, boolean copyToRoot) { _graph = (FactorGraph) old2new.get(other.getGraph()); _schedulerKey = other._schedulerKey; _entries.ensureCapacity(other._entries.size()); for (IScheduleEntry entry : other._entries) { IScheduleEntry entryCopy = entry.copy(old2new, copyToRoot); if (entryCopy != null) { _entries.add(entryCopy); } } } /*---------------------- * IOptionValue methods */ @Override public boolean isMutable() { return true; } /*-------------------- * IScheduler methods */ /** * {@inheritDoc} * <p> * If {@link #declaredSchedulerType()} is not null, that will be returned. Otherwise this * will return a list containing both {@link BPOptions#scheduler} and {@link GibbsOptions#scheduler}. */ @Override public List<SchedulerOptionKey> applicableSchedulerOptions() { SchedulerOptionKey schedulerKey = _schedulerKey; if (schedulerKey != null) { return Collections.singletonList(_schedulerKey); } else { return Arrays.asList(BPOptions.scheduler, GibbsOptions.scheduler); } } @Override public IScheduler copy(Map<Object, Object> old2NewMap, boolean copyToRoot) { return new CustomScheduler(this, old2NewMap, copyToRoot); } /** * {@inheritDoc} * <p> * Returns fixed schedule specified by this custom scheduler (created through * the various add* methods). * <p> * @throws ScheduleValidationException if {@code graph} is not the same as the * {@linkplain #getGraph graph} for which this scheduler was constructed. */ @Override public FixedSchedule createSchedule(FactorGraph graph) { validateForGraph(graph); return new FixedSchedule(this, graph, _entries); } @Override public FixedSchedule createSchedule(ISolverFactorGraph solverGraph) { return createSchedule(solverGraph.getModelObject()); } @Override public boolean isCustomScheduler() { return true; } /** * {@inheritDoc} * <p> * CustomerSchedulers are only valid for use on the {@link #getGraph() graph} for which they were created. */ @Override public void validateForGraph(FactorGraph graph) { if (graph != _graph) { throw new ScheduleValidationException("This scheduler instance can only be used with graph '%s'", _graph); } } /*------------------------- * IGibbsScheduler methods */ @Deprecated @Override public void addBlockScheduleEntry(BlockScheduleEntry blockScheduleEntry) { addBlockWithReplacement(blockScheduleEntry.getBlockUpdater(), blockScheduleEntry.getBlock()); } @Override public void addBlockWithReplacement(IBlockUpdater blockUpdater, final VariableBlock block) { Iterables.removeIf(_entries, new Predicate<IScheduleEntry>() { @NonNullByDefault(false) @Override public boolean apply(IScheduleEntry entry) { switch (entry.type()) { case NODE: return block.contains(((NodeScheduleEntry)entry).getNode()); case EDGE: return block.contains(((EdgeScheduleEntry)entry).getNode()); default: return false; } } }); addBlock(blockUpdater, block); } /*------------------------- * CustomScheduler methods */ /** * Adds all entries to the custom schedule in order. * <p> * Note that the deprecated {@code SubScheduleEntry} type is not supported by this method. * <p> * @since 0.08 */ public void addAll(Iterable<? extends IScheduleEntry> entries) { if (entries instanceof Collection) { _entries.ensureCapacity(((Collection<?>)entries).size() + _entries.size()); } for (IScheduleEntry entry : entries) { addEntry(entry); } } /** * Adds a block schedule entry using given updater and variable block. * @since 0.08 */ public void addBlock(IBlockUpdater blockUpdater, VariableBlock block) { addEntry(new BlockScheduleEntry(blockUpdater, block)); } /** * Adds a block schedule entry using given updater and variable block defined from specified variables. * <p> * This will define a {@link VariableBlock} for the given {@code variables} on the {@linkplain #getGraph() * graph} associated with this scheduler. * <p> * @since 0.08 * @see #addBlock(IBlockUpdater, VariableBlock) */ public void addBlock(IBlockUpdater blockUpdater, Variable ... variables) { addBlock(blockUpdater, _graph.addVariableBlock(variables)); } /** * Adds an edge schedule entry for the given port. * <p> * @param edge a {@link Port} object describing the originating node and index of outgoing edge. * @since 0.08 */ public void addEdge(Port edge) { addEdge(edge.getNode(), edge.getSiblingNumber()); } /** * Adds edge schedule entries for the given ports. * <p> * @since 0.08 * @see #addEdge(Port) */ public void addEdges(Port ... edges) { for (Port port : edges) addEdge(port); } /** * Adds edge schedule entry for given node and {@code siblingNumber} of outgoing edge. * @since 0.08 */ public void addEdge(INode node, int siblingNumber) { addEntry(new EdgeScheduleEntry(node, siblingNumber)); } /** * Adds edge update from {@code source} to {@code target} nodes. * <p> * If there is more than one edge between {@code source} and {@code target}, then * this will produce an update for the lowest numbered edge from the perspective of * {@code source}. * <p> * @param source either a variable or factor * @param target a factor or variable that is connected to {@code source}. * @since 0.08 * @see #addEdge(INode, int) */ public void addEdge(INode source, INode target) { addEdge(source, source.findSibling(target)); } /** * Add node update for given {@code factor}. * @since 0.08 */ public void addFactor(Factor factor) { addEntry(new NodeScheduleEntry(factor)); } /** * Add node update for {@code factors} in given order. * @since 0.08 */ public void addFactors(Factor ... factors) { for (Factor factor : factors) addFactor(factor); } /** * Add node update for given {@code node}. * <p> * If {@code node} is a {@link FactorGraph}, a subgraph schedule entry will be added, otherwise * a node update entry will be added. * @since 0.08 */ public void addNode(INode node) { addEntry(node instanceof FactorGraph ? new SubgraphScheduleEntry((FactorGraph)node) : new NodeScheduleEntry(node)); } /** * Adds node updates for {@code nodes} in given order. * @since 0.08 * @see #addNode(INode) */ public void addNodes(INode ... nodes) { for (INode node : nodes) { addNode(node); } } /** * Adds edge entries following path through specified nodes. * <p> * Creates edge update entries starting from first node and terminating with last node. * <p> * @param nodes each node must not be separated from the previous node by more than one other node * and there must be only one possible path between nodes. * * @since 0.08 */ public void addPath(INode ... nodes) { addPath(Arrays.asList(nodes)); } /** * Adds edge entries following path through specified nodes. * <p> * Creates edge update entries starting from first node and terminating with last node. * <p> * @param nodes each node must not be separated from the previous node by more than one other node * and there must be only one possible path between nodes. * * @since 0.08 */ public void addPath(List<INode> nodes) { // TODO - extend this method to support nodes that are farther than two edges apart if there is a // unique path. if (nodes.size() < 2) { throw new ScheduleValidationException("addPath requires at least two nodes"); } final Iterator<INode> iter = nodes.iterator(); for (INode from = iter.next(), to = null; iter.hasNext() && (to = iter.next()) != null; from = to) { int toi = from.findSibling(to); if (toi >= 0) { if (from.findSibling(to, toi + 1) >= 0) { throw new ScheduleValidationException("There is not a unique path from %s to %s", from, to); } addEdge(from, to); } else { // Not connected directly. See if there is a common sibling. Set<INode> commonSiblings = new HashSet<>(from.getSiblings()); commonSiblings.retainAll(to.getSiblings()); switch (commonSiblings.size()) { case 0: throw new ScheduleValidationException( "Nodes %s and %s are not adjacent and don't share common sibling", from, to); case 1: INode middle = Iterables.getOnlyElement(commonSiblings); addEdge(from, middle); addEdge(middle, to); break; default: throw new ScheduleValidationException( "Nodes %s and %s are connected by more than one path", from, to); } } } } /** * Adds node update for given {@code variable}. * @param variable * @since 0.08 */ public void addVariable(Variable variable) { addEntry(new NodeScheduleEntry(variable)); } /** * Adds node update for {@code variables} in given order. * @since 0.08 * @see #addVariable(Variable) */ public void addVariables(Variable ... variables) { int size = _entries.size(); try { for (Variable var : variables) { addVariable(var); } size = _entries.size(); } finally { // If an exception is thrown, remove any added entries. for (int i = _entries.size(); --i>=size;) { _entries.remove(i); } } } /** * Adds a subgraph schedule entry for given subgraph. * <p> * @param graph must be a subgraph of {@linkplain #getGraph() graph} associated with scheduler. * @since 0.08 */ public void addSubgraph(FactorGraph graph) { addEntry(new SubgraphScheduleEntry(graph)); } /** * The scheduler type that was declared when this scheduler was constructed. * <p> * This should only be null if custom scheduler was created using deprecated * {@link FactorGraph#setSchedule} interface or through the MATLAB interface. * <p> * @since 0.08 * @see #CustomScheduler(FactorGraph, SchedulerOptionKey) * @see #applicableSchedulerOptions() */ public @Nullable SchedulerOptionKey declaredSchedulerType() { return _schedulerKey; } /** * The graph for which the scheduler was constructed. * <p> * The scheduler will not be able to be used on graphs other than this one and any entries * added to the custom schedule must be for objects that are in this graph or its subgraphs. * @since 0.08 * @see #CustomScheduler(FactorGraph, SchedulerOptionKey) */ public FactorGraph getGraph() { return _graph; } /*----------------- * Private methods */ /** * Validates entry, infers scheduler key if necessary, and adds to list. */ @SuppressWarnings("deprecation") // for SUBSCHEDULE private void addEntry(IScheduleEntry entry) { switch (entry.type()) { case EDGE: { EdgeScheduleEntry edgeEntry = (EdgeScheduleEntry)entry; INode node = edgeEntry.getNode(); assertInGraph(node); if (_schedulerKey == GibbsOptions.scheduler) { throw new ScheduleValidationException("Cannot use edge entry with Gibbs schedule"); } break; } case NODE: { NodeScheduleEntry nodeEntry = (NodeScheduleEntry)entry; INode node = nodeEntry.getNode(); if (!node.isVariable() && _schedulerKey == GibbsOptions.scheduler) { throw new ScheduleValidationException("Cannot use factor node entry with Gibbs schedule"); } assertInGraph(node); break; } case SUBGRAPH: { SubgraphScheduleEntry graphEntry = (SubgraphScheduleEntry)entry; FactorGraph subgraph = graphEntry.getSubgraph(); assertInGraph(subgraph); break; } case SUBSCHEDULE: throw new ScheduleValidationException("Cannot add SubScheduleEntry to CustomScheduler"); case VARIABLE_BLOCK: { BlockScheduleEntry blockEntry = (BlockScheduleEntry)entry; assertInGraph(blockEntry.getBlock()); break; } case CUSTOM: break; } _entries.add(entry); } /** * Verifies that child is in graph tree rooted at {@link _graph} or is a boundary * variable of the graph. */ private void assertInGraph(IFactorGraphChild child) { final FactorGraph parent = child.getParentGraph(); if (_graph != parent && !_graph.isAncestorOf(child.getContainingGraph()) && !(child instanceof Variable && _graph.isBoundaryVariable((Variable)child))) { throw new ScheduleValidationException("Cannot add entry containing %s because it is not in root graph %s", child, _graph); } } }