blob: bb2a7bcc06712a2d7aefa7e39dc274f2f4a56f82 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.commons.rng.examples.stress;
import org.apache.commons.rng.UniformRandomProvider;
import org.apache.commons.rng.core.source64.RandomLongSource;
import org.apache.commons.rng.simple.RandomSource;
import picocli.CommandLine.Command;
import picocli.CommandLine.Mixin;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Formatter;
import java.util.List;
import java.util.concurrent.Callable;
/**
* Specification for the "output" command.
*
* <p>This command creates a named random generator and outputs data in a specified format.</p>
*/
@Command(name = "output",
description = {"Output data from a named random data generator."})
class OutputCommand implements Callable<Void> {
/** The new line characters. */
private static final String NEW_LINE = System.lineSeparator();
/** Character '['. */
private static final char LEFT_SQUARE_BRACKET = '[';
/** Character ']'. */
private static final char RIGHT_SQUARE_BRACKET = ']';
/** Lookup table for binary representation of bytes. */
private static final String[] BIT_REP = {
"0000", "0001", "0010", "0011",
"0100", "0101", "0110", "0111",
"1000", "1001", "1010", "1011",
"1100", "1101", "1110", "1111",
};
/** The standard options. */
@Mixin
private StandardOptions reusableOptions;
/** The random source. */
@Parameters(index = "0",
description = "The random source.")
private RandomSource randomSource;
/** The executable arguments. */
@Parameters(index = "1..*",
description = "The arguments to pass to the constructor.",
paramLabel = "<argument>")
private List<String> arguments = new ArrayList<>();
/** The file output prefix. */
@Option(names = {"-o", "--out"},
description = "The output file (default: stdout).")
private File fileOutput;
/** The output format. */
@Option(names = {"-f", "--format"},
description = {"Output format (default: ${DEFAULT-VALUE}).",
"Valid values: ${COMPLETION-CANDIDATES}."})
private OutputCommand.OutputFormat outputFormat = OutputFormat.DIEHARDER;
/** The random seed. */
@Option(names = {"-s", "--seed"},
description = {"The 64-bit number random seed (default: auto)."})
private Long seed;
/** The random seed as a byte[]. */
@Option(names = {"-x", "--hex-seed"},
description = {"The hex-encoded random seed.",
"Seed conversion for multi-byte primitives use little-endian format.",
"Over-rides the --seed parameter."})
private String byteSeed;
/** The count of numbers to output. */
@Option(names = {"-n", "--count"},
description = {"The count of numbers to output.",
"Use negative for an unlimited stream."})
private long count = 10;
/** The size of the byte buffer for the binary data. */
@Option(names = {"--buffer-size"},
description = {"Byte-buffer size for binary data (default: ${DEFAULT-VALUE}).",
"When outputing binary data the count parameter controls the " +
"number of buffers written."})
private int bufferSize = 8192;
/** The output byte order of the binary data. */
@Option(names = {"-b", "--byte-order"},
description = {"Byte-order of the output data (default: ${DEFAULT-VALUE}).",
"Uses the Java default of big-endian. This may not match the platform byte-order.",
"Valid values: BIG_ENDIAN, LITTLE_ENDIAN."})
private ByteOrder byteOrder = ByteOrder.BIG_ENDIAN;
/** The output byte order of the binary data. */
@Option(names = {"-r", "--reverse-bits"},
description = {"Reverse the bits in the data (default: ${DEFAULT-VALUE})."})
private boolean reverseBits;
/** Flag to use 64-bit long output. */
@Option(names = {"--raw64"},
description = {"Use 64-bit output (default is 32-bit).",
"This is ignored if not a native 64-bit generator.",
"Set to true sets the source64 mode to LONG."})
private boolean raw64;
/** Output mode for 64-bit long output. */
@Option(names = {"--source64"},
description = {"Output mode for 64-bit generators (default: ${DEFAULT-VALUE}).",
"This is ignored if not a native 64-bit generator.",
"In 32-bit mode the output uses a combination of upper and " +
"lower bits of the 64-bit value.",
"Valid values: ${COMPLETION-CANDIDATES}."})
private Source64Mode source64 = RNGUtils.getSource64Default();
/**
* The output mode for existing files.
*/
enum OutputFormat {
/** Binary output. */
BINARY,
/** Use the Dieharder text format. */
DIEHARDER,
/** Output the bits in a text format. */
BITS,
}
/**
* Validates the command arguments, creates the generator and outputs numbers.
*/
@Override
public Void call() {
LogUtils.setLogLevel(reusableOptions.logLevel);
final Object objectSeed = createSeed();
UniformRandomProvider rng = createRNG(objectSeed);
// raw64 flag overrides the source64 mode
if (raw64) {
source64 = Source64Mode.LONG;
}
if (source64 == Source64Mode.LONG && !(rng instanceof RandomLongSource)) {
throw new ApplicationException("Not a 64-bit RNG: " + rng);
}
// Upper or lower bits from 64-bit generators must be created first.
// Note this does not test source64 != Source64Mode.LONG as the full long
// output split into hi-lo or lo-hi is supported by the RngDataOutput.
if (rng instanceof RandomLongSource &&
(source64 == Source64Mode.HI || source64 == Source64Mode.LO || source64 == Source64Mode.INT)) {
rng = RNGUtils.createIntProvider((UniformRandomProvider & RandomLongSource) rng, source64);
}
if (reverseBits) {
rng = RNGUtils.createReverseBitsProvider(rng);
}
// -------
// Note: Manipulation of the byte order for the platform is done during output
// for the binary format. Otherwise do it in Java.
// -------
if (outputFormat != OutputFormat.BINARY) {
rng = toOutputFormat(rng);
}
try (OutputStream out = createOutputStream()) {
switch (outputFormat) {
case BINARY:
writeBinaryData(rng, out);
break;
case DIEHARDER:
writeDieharder(rng, out);
break;
case BITS:
writeBitData(rng, out);
break;
default:
throw new ApplicationException("Unknown output format: " + outputFormat);
}
} catch (IOException ex) {
throw new ApplicationException("IO error: " + ex.getMessage(), ex);
}
return null;
}
/**
* Creates the seed.
*
* @return the seed
*/
private Object createSeed() {
if (byteSeed != null) {
try {
return Hex.decodeHex(byteSeed);
} catch (IllegalArgumentException ex) {
throw new ApplicationException("Invalid hex seed: " + ex.getMessage(), ex);
}
}
if (seed != null) {
return seed;
}
// Let the factory constructor create the native seed.
return null;
}
/**
* Creates the seed.
*
* @return the seed
*/
private String createSeedString() {
if (byteSeed != null) {
return byteSeed;
}
if (seed != null) {
return seed.toString();
}
return "auto";
}
/**
* Creates the RNG.
*
* @param objectSeed Seed.
* @return the uniform random provider
* @throws ApplicationException If the RNG cannot be created
*/
private UniformRandomProvider createRNG(Object objectSeed) {
if (randomSource == null) {
throw new ApplicationException("Random source is null");
}
final ArrayList<Object> data = new ArrayList<>();
// Note: The list command outputs arguments as an array bracketed by [ and ]
// Strip these for convenience.
stripArrayFormatting(arguments);
for (final String argument : arguments) {
data.add(RNGUtils.parseArgument(argument));
}
try {
return randomSource.create(objectSeed, data.toArray());
} catch (IllegalStateException | IllegalArgumentException ex) {
throw new ApplicationException("Failed to create RNG: " + randomSource + ". " + ex.getMessage(), ex);
}
}
/**
* Strip leading bracket from the first argument, trailing bracket from the last
* argument, and any trailing commas from any argument.
*
* <p>This is used to remove the array formatting used by the list command.
*
* @param arguments the arguments
*/
private static void stripArrayFormatting(List<String> arguments) {
final int size = arguments.size();
if (size > 1) {
// These will not be empty as they were created from command-line args.
final String first = arguments.get(0);
if (first.charAt(0) == LEFT_SQUARE_BRACKET) {
arguments.set(0, first.substring(1));
}
final String last = arguments.get(size - 1);
if (last.charAt(last.length() - 1) == RIGHT_SQUARE_BRACKET) {
arguments.set(size - 1, last.substring(0, last.length() - 1));
}
}
for (int i = 0; i < size; i++) {
final String argument = arguments.get(i);
if (argument.endsWith(",")) {
arguments.set(i, argument.substring(0, argument.length() - 1));
}
}
}
/**
* Convert the native RNG to the requested output format. This will convert a 64-bit
* generator to a 32-bit generator unless the 64-bit mode is active. It then optionally
* reverses the byte order of the output.
*
* @param rng The random generator.
* @return the uniform random provider
*/
private UniformRandomProvider toOutputFormat(UniformRandomProvider rng) {
UniformRandomProvider convertedRng = rng;
if (rng instanceof RandomLongSource && source64 != Source64Mode.LONG) {
// Convert to 32-bit generator
convertedRng = RNGUtils.createIntProvider((UniformRandomProvider & RandomLongSource) rng, source64);
}
if (byteOrder == ByteOrder.LITTLE_ENDIAN) {
convertedRng = RNGUtils.createReverseBytesProvider(convertedRng);
}
return convertedRng;
}
/**
* Creates the output stream. This will not be buffered.
*
* @return the output stream
*/
private OutputStream createOutputStream() {
if (fileOutput != null) {
try {
return Files.newOutputStream(fileOutput.toPath());
} catch (IOException ex) {
throw new ApplicationException("Failed to create output: " + fileOutput, ex);
}
}
return new FilterOutputStream(System.out) {
@Override
public void close() {
// Do not close stdout
}
};
}
/**
* Check the count is positive, otherwise create an error message for the provided format.
*
* @param count The count of numbers to output.
* @param format The format.
* @throws ApplicationException If the count is not positive.
*/
private static void checkCount(long count,
OutputFormat format) {
if (count <= 0) {
throw new ApplicationException(format + " format requires a positive count: " + count);
}
}
/**
* Write int data to the specified output using the dieharder text format.
*
* @param rng The random generator.
* @param out The output.
* @throws IOException Signals that an I/O exception has occurred.
* @throws ApplicationException If the count is not positive.
*/
private void writeDieharder(final UniformRandomProvider rng,
final OutputStream out) throws IOException {
checkCount(count, OutputFormat.DIEHARDER);
// Use dieharder output, e.g.
//#==================================================================
//# generator mt19937 seed = 1
//#==================================================================
//type: d
//count: 1
//numbit: 32
//1791095845
try (BufferedWriter output = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))) {
writeHeaderLine(output);
output.write("# generator ");
output.write(rng.toString());
output.write(" seed = ");
output.write(createSeedString());
output.write(NEW_LINE);
writeHeaderLine(output);
output.write("type: d");
output.write(NEW_LINE);
output.write("count: ");
output.write(Long.toString(count));
output.write(NEW_LINE);
output.write("numbit: 32");
output.write(NEW_LINE);
for (long c = 0; c < count; c++) {
// Unsigned integers
final String text = Long.toString(rng.nextInt() & 0xffffffffL);
// Left pad with spaces
for (int i = 10 - text.length(); i > 0; i--) {
output.write(' ');
}
output.write(text);
output.write(NEW_LINE);
}
}
}
/**
* Write a header line to the output.
*
* @param output the output
* @throws IOException Signals that an I/O exception has occurred.
*/
private static void writeHeaderLine(Writer output) throws IOException {
output.write("#==================================================================");
output.write(NEW_LINE);
}
/**
* Write raw binary data to the output.
*
* @param rng The random generator.
* @param out The output.
* @throws IOException Signals that an I/O exception has occurred.
*/
private void writeBinaryData(final UniformRandomProvider rng,
final OutputStream out) throws IOException {
// If count is not positive use max value.
// This is effectively unlimited: program must be killed.
final long limit = (count < 1) ? Long.MAX_VALUE : count;
try (RngDataOutput data = RNGUtils.createDataOutput(rng, source64, out, bufferSize, byteOrder)) {
for (long c = 0; c < limit; c++) {
data.write(rng);
}
}
}
/**
* Write binary bit data to the specified file.
*
* @param rng The random generator.
* @param out The output.
* @throws IOException Signals that an I/O exception has occurred.
* @throws ApplicationException If the count is not positive.
*/
private void writeBitData(final UniformRandomProvider rng,
final OutputStream out) throws IOException {
checkCount(count, OutputFormat.BITS);
final boolean asLong = rng instanceof RandomLongSource;
try (BufferedWriter output = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))) {
for (long c = 0; c < count; c++) {
if (asLong) {
writeLong(output, rng.nextLong());
} else {
writeInt(output, rng.nextInt());
}
}
}
}
/**
* Write an {@code long} value to the the output. The native Java value will be
* written to the writer on a single line using: a binary string representation
* of the bytes; the unsigned integer; and the signed integer.
*
* <pre>
* 10011010 01010011 01011010 11100100 01000111 00010000 01000011 11000101 11120331841399178181 -7326412232310373435
* </pre>
*
* @param out The output.
* @param value The value.
* @throws IOException Signals that an I/O exception has occurred.
*/
@SuppressWarnings("resource")
static void writeLong(Writer out,
long value) throws IOException {
// Write out as 8 bytes with spaces between them, high byte first.
writeByte(out, (int)(value >>> 56) & 0xff);
out.write(' ');
writeByte(out, (int)(value >>> 48) & 0xff);
out.write(' ');
writeByte(out, (int)(value >>> 40) & 0xff);
out.write(' ');
writeByte(out, (int)(value >>> 32) & 0xff);
out.write(' ');
writeByte(out, (int)(value >>> 24) & 0xff);
out.write(' ');
writeByte(out, (int)(value >>> 16) & 0xff);
out.write(' ');
writeByte(out, (int)(value >>> 8) & 0xff);
out.write(' ');
writeByte(out, (int)(value >>> 0) & 0xff);
// Write the unsigned and signed int value
new Formatter(out).format(" %20s %20d%n", Long.toUnsignedString(value), value);
}
/**
* Write an {@code int} value to the the output. The native Java value will be
* written to the writer on a single line using: a binary string representation
* of the bytes; the unsigned integer; and the signed integer.
*
* <pre>
* 11001101 00100011 01101111 01110000 3441651568 -853315728
* </pre>
*
* @param out The output.
* @param value The value.
* @throws IOException Signals that an I/O exception has occurred.
*/
@SuppressWarnings("resource")
static void writeInt(Writer out,
int value) throws IOException {
// Write out as 4 bytes with spaces between them, high byte first.
writeByte(out, (value >>> 24) & 0xff);
out.write(' ');
writeByte(out, (value >>> 16) & 0xff);
out.write(' ');
writeByte(out, (value >>> 8) & 0xff);
out.write(' ');
writeByte(out, (value >>> 0) & 0xff);
// Write the unsigned and signed int value
new Formatter(out).format(" %10d %11d%n", value & 0xffffffffL, value);
}
/**
* Write the lower 8 bits of an {@code int} value to the buffered writer using a
* binary string representation. This is left-filled with zeros if applicable.
*
* <pre>
* 11001101
* </pre>
*
* @param out The output.
* @param value The value.
* @throws IOException Signals that an I/O exception has occurred.
*/
private static void writeByte(Writer out,
int value) throws IOException {
// This matches the functionality of:
// data.write(String.format("%8s", Integer.toBinaryString(value & 0xff)).replace(' ', '0'))
out.write(BIT_REP[value >>> 4]);
out.write(BIT_REP[value & 0x0F]);
}
}