diff --git a/CHANGELOG.md b/CHANGELOG.md index 0454a71a86..887f2b0f64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added * AutomataLib now supports JPMS modules. Many of the artifacts now provide a `module-info` descriptor with the exception of `automata-brics`, `automata-modelchecking-m3c`, and `automata-jung-visualizer` which do not have modular dependencies and only provide an `Automatic-Module-Name` in their respective `MANIFEST.MF`s. As a consequence of this, the distribution artifacts (for Maven-less environments) also only provide an `Automatic-Module-Name`. Note that while this is a Java 9+ feature, AutomataLib still supports Java 8 byte code for the remaining class files. +* Added `TabakovVardiRandomAutomata` that allows for creating Tabakov-Vardi random automata, in particular, NFAs (thanks to [John Nicol](https://github.com/jn1z)). ### Changed diff --git a/util/src/main/java/net/automatalib/util/automaton/minimizer/paigetarjan/PaigeTarjanMinimization.java b/util/src/main/java/net/automatalib/util/automaton/minimizer/paigetarjan/PaigeTarjanMinimization.java index 330f971172..f2cd9d0682 100644 --- a/util/src/main/java/net/automatalib/util/automaton/minimizer/paigetarjan/PaigeTarjanMinimization.java +++ b/util/src/main/java/net/automatalib/util/automaton/minimizer/paigetarjan/PaigeTarjanMinimization.java @@ -155,7 +155,7 @@ public static CompactMealy minimizeMealy(MealyMachine m /** * Minimizes the given automaton depending on the given partitioning function. The {@code sinkClassification} is * used to describe the signature of the sink state ("successor" of undefined transitions) and may introduce a new, - * on-thy-fly equivalence class if it doesn't match a signature of any existing state. See the {@link + * on-the-fly equivalence class if it doesn't match a signature of any existing state. See the {@link * StateSignature} class for creating signatures for existing states. * * @param automaton diff --git a/util/src/main/java/net/automatalib/util/automaton/random/RandomICAutomatonGenerator.java b/util/src/main/java/net/automatalib/util/automaton/random/RandomICAutomatonGenerator.java index a3612dcd25..eef698ae8c 100644 --- a/util/src/main/java/net/automatalib/util/automaton/random/RandomICAutomatonGenerator.java +++ b/util/src/main/java/net/automatalib/util/automaton/random/RandomICAutomatonGenerator.java @@ -236,7 +236,7 @@ public RandomICAutomatonGenerator withTransitionProperties(Collectionnot be minized. + * will not be minimized. * * @param numStates * the number of states of the resulting automaton diff --git a/util/src/main/java/net/automatalib/util/automaton/random/TabakovVardiRandomAutomata.java b/util/src/main/java/net/automatalib/util/automaton/random/TabakovVardiRandomAutomata.java new file mode 100644 index 0000000000..f531e5e50c --- /dev/null +++ b/util/src/main/java/net/automatalib/util/automaton/random/TabakovVardiRandomAutomata.java @@ -0,0 +1,151 @@ +/* Copyright (C) 2013-2024 TU Dortmund University + * This file is part of AutomataLib, http://www.automatalib.net/. + * + * 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 net.automatalib.util.automaton.random; + +import java.util.Random; + +import net.automatalib.alphabet.Alphabet; +import net.automatalib.automaton.concept.StateIDs; +import net.automatalib.automaton.fsa.MutableNFA; +import net.automatalib.automaton.fsa.NFA; +import net.automatalib.automaton.fsa.impl.CompactNFA; +import net.automatalib.common.util.random.RandomUtil; + +/** + * A generator for random {@link NFA}s as described in the paper + * Experimental Evaluation of Classical Automata Constructions + * by Deian Tabakov and Moshe Y Vardi. + */ +public final class TabakovVardiRandomAutomata { + + private TabakovVardiRandomAutomata() { + // prevent instantiation + } + + /** + * Generates a random {@link NFA} with the given size, transition density, and acceptance density. + * + * @param r + * random instance + * @param size + * number of states + * @param td + * transition density, in [0,size] + * @param ad + * acceptance density, in (0,1]. 0.5 is the usual value + * @param alphabet + * the input symbols to consider when determining successors + * @param + * input symbol type + * + * @return a random NFA, not necessarily connected + */ + public static CompactNFA generateNFA(Random r, int size, float td, float ad, Alphabet alphabet) { + return generateNFA(r, size, Math.round(td * size), Math.round(ad * size), alphabet); + } + + /** + * Generates a random {@link NFA} with the given size, number of edges (per letter), number of accepting states. + * + * @param r + * random instance + * @param size + * number of states + * @param edgeNum + * number of edges (per input) + * @param acceptNum + * number of accepting states (at least one) + * @param alphabet + * the input symbols to consider when determining successors + * @param + * input symbol type + * + * @return a random NFA, not necessarily connected + */ + public static CompactNFA generateNFA(Random r, int size, int edgeNum, int acceptNum, Alphabet alphabet) { + return generateNFA(r, size, edgeNum, acceptNum, alphabet, new CompactNFA<>(alphabet)); + } + + /** + * Generates a random NFA with the given size, number of edges (per letter), and number of accepting states, written + * to the given {@link MutableNFA}. Note that the output automaton must be empty. + * + * @param r + * random instance + * @param size + * number of states + * @param edgeNum + * number of edges (per input) + * @param acceptNum + * number of accepting states (at least one) + * @param alphabet + * the input symbols to consider when determining successors + * @param out + * the (mutable) automaton to write the random structure to + * @param + * state type + * @param + * input symbol type + * @param + * automaton type + * + * @return the {@code out} parameter after the contents have been written to it + */ + public static > A generateNFA(Random r, + int size, + int edgeNum, + int acceptNum, + Alphabet alphabet, + A out) { + assert acceptNum > 0 && acceptNum <= size; + assert edgeNum >= 0 && edgeNum <= size * size; + + initNFA(size, out); + final StateIDs stateIDs = out.stateIDs(); + + // Set final states other than the initial state. + // We want exactly acceptNum-1 of them, from the elements [1,size). + // This works even if acceptNum == 1. + int[] finalStates = RandomUtil.distinctIntegers(r, acceptNum - 1, 1, size); + for (int f : finalStates) { + out.setAccepting(stateIDs.getState(f), true); + } + + // For each letter, add edgeNum transitions. + for (I a : alphabet) { + for (int edgeIndex : RandomUtil.distinctIntegers(r, edgeNum, size * size)) { + out.addTransition(stateIDs.getState(edgeIndex / size), a, stateIDs.getState(edgeIndex % size)); + } + } + + return out; + } + + // Helper method to generate NFA with initial accepting state. + private static > void initNFA(int size, A nfa) { + assert nfa.size() == 0; + + // per the paper, the first state is always initial and accepting + final S init = nfa.addInitialState(true); + nfa.setAccepting(init, true); + + // Create remaining states + for (int i = 1; i < size; i++) { + nfa.addState(false); + } + } +} + diff --git a/util/src/test/java/net/automatalib/util/automaton/random/TabakovVardiRandomAutomataTest.java b/util/src/test/java/net/automatalib/util/automaton/random/TabakovVardiRandomAutomataTest.java new file mode 100644 index 0000000000..cc8e17ad0f --- /dev/null +++ b/util/src/test/java/net/automatalib/util/automaton/random/TabakovVardiRandomAutomataTest.java @@ -0,0 +1,74 @@ +/* Copyright (C) 2013-2024 TU Dortmund University + * This file is part of AutomataLib, http://www.automatalib.net/. + * + * 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 net.automatalib.util.automaton.random; + +import java.util.Random; +import java.util.Set; + +import net.automatalib.alphabet.Alphabet; +import net.automatalib.alphabet.impl.Alphabets; +import net.automatalib.automaton.fsa.impl.CompactNFA; +import org.testng.Assert; +import org.testng.annotations.Test; + +class TabakovVardiRandomAutomataTest { + @Test + void testGenerateNFA() { + Random r = new Random(42); + int size = 4; + float td = 1.25f; // exactly 5 transitions per letter + float ad = 0.5f; // exactly 2 accepting states + Alphabet alphabet = Alphabets.integers(0, 1); + CompactNFA compactNFA = TabakovVardiRandomAutomata.generateNFA(r, size, td, ad, alphabet); + Assert.assertEquals(compactNFA.size(), size); + Assert.assertEquals(compactNFA.getInitialStates().size(), 1); + for (int s : compactNFA.getStates()) { + if (s == 0 || s == 3) { + // 2 accepting states + Assert.assertTrue(compactNFA.isAccepting(s)); + } else { + Assert.assertFalse(compactNFA.isAccepting(s)); + } + } + // 5 transitions for 0 + Assert.assertEquals(compactNFA.getTransitions(0, 0), Set.of(0, 3)); + Assert.assertTrue(compactNFA.getTransitions(1, 0).isEmpty()); + Assert.assertEquals(compactNFA.getTransitions(2, 0), Set.of(1, 2)); + Assert.assertEquals(compactNFA.getTransitions(3, 0), Set.of(3)); + + // 5 transitions for 1 + Assert.assertEquals(compactNFA.getTransitions(0, 1), Set.of(3)); + Assert.assertEquals(compactNFA.getTransitions(1, 1), Set.of(1)); + Assert.assertEquals(compactNFA.getTransitions(2, 1), Set.of(0)); + Assert.assertEquals(compactNFA.getTransitions(3, 1), Set.of(1, 2)); + } + + @Test + void testGenerateNFAEdgeCase() { + Random r = new Random(42); + int size = 2; + float td = 0f; // 0 transitions + float ad = 0.5f; // exactly 1 accepting state + Alphabet alphabet = Alphabets.integers(0, 1); + CompactNFA compactNFA = TabakovVardiRandomAutomata.generateNFA(r, size, td, ad, alphabet); + Assert.assertEquals(compactNFA.size(), size); + Assert.assertEquals(compactNFA.getInitialStates().size(), 1); + Assert.assertTrue(compactNFA.isAccepting(0)); + Assert.assertFalse(compactNFA.isAccepting(1)); + Assert.assertTrue(compactNFA.getTransitions(0).isEmpty()); + Assert.assertTrue(compactNFA.getTransitions(1).isEmpty()); + } +}