| /* |
| * 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]); |
| } |
| } |