Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Tabakov-Vardi random automata generator #69

Merged
merged 9 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/* 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.fsa.impl.CompactNFA;
import net.automatalib.common.util.random.RandomUtil;

public final class TabakovVardiRandomAutomata {
private TabakovVardiRandomAutomata() {
// prevent instantiation
}

/**
* Generate random NFA using Tabakov and Vardi's approach, described in the paper
* <a href="https://doi.org/10.1007/11591191_28">Experimental Evaluation of Classical Automata Constructions</a>
* by Deian Tabakov and Moshe Y. Vardi.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you maybe add this description to the class level so that the description is directly available at the overview page (like here)? You can then use a more generic description for the method (like the second generateNFAmethod). However, be careful with dots as the javadoc tool typically interprets them as the end of the description. So "Moshe Y. Vardi." probably needs to be encoded as Moshe Y&nbsp;Vardi.. You can check the result with mvn site.

*
* @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
* alphabet
* @return
* a random NFA, not necessarily connected
*/
public static CompactNFA<Integer> generateNFA(
Random r, int size, float td, float ad, Alphabet<Integer> alphabet) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you maybe generalize this to Alphabet<I>? You can use Alphabet#getSymbol/Alphabet#getSymbolIndex if you need to map between symbols and their respective indices. Maybe it could also be useful to add a third method that allows you to specify a MutableNFA object to write to so that you don't force CompactNFAs (like in RandomAutomata#randomDeterministic).

Copy link
Contributor Author

@jn1z jn1z Jan 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, easy to generalize to Alphabet<I> !

I'm not sure I understand the distinction with a MutableNFA change, or how to implement it (I tend to get lost in abstraction layers). So if you're okay with it, I'll leave that part like it is.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I can push the changes myself afterwards.

return generateNFA(r, size, Math.round(td * size), Math.round(ad * size), alphabet);
}

/**
* Generate random NFA, with fixed number of accept states and edges (per letter).
*
* @param r
* random instance
* @param size
* number of states
* @param edgeNum
* number of edges (per letter)
* @param acceptNum
* number of accepting states (at least one)
* @param alphabet
* alphabet
* @return
* a random NFA, not necessarily connected
*/
public static CompactNFA<Integer> generateNFA(
Random r, int size, int edgeNum, int acceptNum, Alphabet<Integer> alphabet) {
assert acceptNum > 0 && acceptNum <= size;
assert edgeNum >= 0 && edgeNum <= size*size;

CompactNFA<Integer> nfa = basicNFA(size, alphabet);

// 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) {
nfa.setAccepting(f, true);
}

// For each letter, add edgeNum transitions.
for (int a: alphabet) {
for (int edgeIndex: RandomUtil.distinctIntegers(r, edgeNum, size*size)) {
nfa.addTransition(edgeIndex / size, a, edgeIndex % size);
}
}

return nfa;
}

// Helper method to generate NFA with initial accepting state.
private static CompactNFA<Integer> basicNFA(int size, Alphabet<Integer> alphabet) {
CompactNFA<Integer> basicNFA = new CompactNFA<>(alphabet);

// Create states
for (int i = 0; i < size; i++) {
basicNFA.addState(false);
}
// per the paper, the first state is always initial and accepting
basicNFA.setInitial(0, true);
basicNFA.setAccepting(0, true);
return basicNFA;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/* 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.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<Integer> alphabet = Alphabets.integers(0, 1);
CompactNFA<Integer> compactNFA = TabakovVardiRandomAutomata.generateNFA(r, size, td, ad, alphabet);
Assert.assertEquals(size, compactNFA.size());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parameters of the assertEquals methods are ordered as (actual, expected). So it would be cleaner to flip the arguments for more meaningful error messages (also in the second test case).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Apparently I've gotten that wrong for years.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just FYI -- surprisingly, TestNG and JUnit use different parameter orders.
https://stackoverflow.com/questions/26102865/assertequals-what-is-actual-and-what-is-expected

Assert.assertEquals(1, compactNFA.getInitialStates().size());
for (int s : compactNFA.getStates()) {
if (s == 0 || s == 3) {
// 2 accepting states
Assert.assertTrue(compactNFA.isAccepting(s));
} else {
Assert.assertFalse(compactNFA.isAccepting(s));
}
}
StringBuilder sb = new StringBuilder();
for (int s : compactNFA.getStates()) {
sb.append(compactNFA.getTransitions(s, 0));
}
// 5 transitions for 0
Assert.assertEquals("[0, 3][][1, 2][3]", sb.toString());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a fan of String-based comparisons. Could you please compare the original Set<Integer>s instead? TestNG has assertEquals methods for sets and you can use Java 11 in the tests, so the Set.of methods are available.


sb = new StringBuilder();
for (int s : compactNFA.getStates()) {
sb.append(compactNFA.getTransitions(s, 1));
}
// 5 transitions for 1
Assert.assertEquals("[3][1][0][1, 2]", sb.toString());
}

@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<Integer> alphabet = Alphabets.integers(0, 1);
CompactNFA<Integer> compactNFA = TabakovVardiRandomAutomata.generateNFA(r, size, td, ad, alphabet);
Assert.assertEquals(size, compactNFA.size());
Assert.assertEquals(1, compactNFA.getInitialStates().size());
Assert.assertTrue(compactNFA.isAccepting(0));
Assert.assertFalse(compactNFA.isAccepting(1));
Assert.assertEquals(0, compactNFA.getTransitions(0).size());
Assert.assertEquals(0, compactNFA.getTransitions(1).size());
}
}