From 59407260b4d0ce4b2b1b39ccb7f853c28ebb6b00 Mon Sep 17 00:00:00 2001 From: Logan Drescher Date: Tue, 14 Jan 2025 12:58:43 -0500 Subject: [PATCH 01/17] Updated Syntax to ITextWriter --- .../java/cbit/vcell/publish/ITextWriter.java | 3626 ++++++++--------- 1 file changed, 1808 insertions(+), 1818 deletions(-) diff --git a/vcell-core/src/main/java/cbit/vcell/publish/ITextWriter.java b/vcell-core/src/main/java/cbit/vcell/publish/ITextWriter.java index 46cacb1855..b6bd174341 100644 --- a/vcell-core/src/main/java/cbit/vcell/publish/ITextWriter.java +++ b/vcell-core/src/main/java/cbit/vcell/publish/ITextWriter.java @@ -9,6 +9,7 @@ */ package cbit.vcell.publish; + import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics2D; @@ -134,41 +135,42 @@ import cbit.vcell.units.VCUnitDefinition; /** -This is the root class that handles publishing of models in the Virtual Cell. It supports the publishing of BioModels, MathModels, -and Geometries. This class should receive the object to be published, an output stream, a page format, as well as the -publishing preferences object. The same ITextWriter instance can be reused to publish different models with different preferences. - + * This is the root class that handles publishing of models in the Virtual Cell. It supports the publishing of BioModels, MathModels, + * and Geometries. This class should receive the object to be published, an output stream, a page format, as well as the + * publishing preferences object. The same ITextWriter instance can be reused to publish different models with different preferences. + *

* Creation date: (4/18/2003 2:09:05 PM) + * * @author: John Wagner & Rashad Badrawi -*/ - + */ + public abstract class ITextWriter { - private final static Logger lg = LogManager.getLogger(ITextWriter.class); - - public static final String PDF_WRITER = "PDF_WRITER"; - public static final String HTM_WRITER = "HTML_WRITER"; - protected static int DEF_IMAGE_WIDTH = 400; - protected static int DEF_IMAGE_HEIGHT = 400; - protected static final int DEF_GEOM_WIDTH = 150; - protected static final int DEF_GEOM_HEIGHT = 150; - + private final static Logger lg = LogManager.getLogger(ITextWriter.class); - //image resolution settings, for saving individual reaction and structure images. - public static final String HIGH_RESOLUTION = "high resolution"; //default_scale*2.5 - public static final String MEDIUM_RESOLUTION = "medium resolution"; //default_scale*1.5 - public static final String LOW_RESOLUTION = "low resolution"; //default_scale - - private static int DEF_FONT_SIZE = 9; - private static int DEF_HEADER_FONT_SIZE = 11; - private Font fieldFont = null; - private Font fieldBold = null; - - protected Document document; - + public static final String PDF_WRITER = "PDF_WRITER"; + public static final String HTM_WRITER = "HTML_WRITER"; + protected static int DEF_IMAGE_WIDTH = 400; + protected static int DEF_IMAGE_HEIGHT = 400; + protected static final int DEF_GEOM_WIDTH = 150; + protected static final int DEF_GEOM_HEIGHT = 150; -/** - * Comment - */ + + //image resolution settings, for saving individual reaction and structure images. + public static final String HIGH_RESOLUTION = "high resolution"; //default_scale*2.5 + public static final String MEDIUM_RESOLUTION = "medium resolution"; //default_scale*1.5 + public static final String LOW_RESOLUTION = "low resolution"; //default_scale + + private static final int DEF_FONT_SIZE = 9; + private static final int DEF_HEADER_FONT_SIZE = 11; + private Font fieldFont = null; + private Font fieldBold = null; + + protected Document document; + + + /** + * Comment + */ /* private void createRegionImageIcon() throws Exception{ final int DISPLAY_DIM_MAX = 256; @@ -337,100 +339,112 @@ public abstract class ITextWriter { } } */ -protected ITextWriter() { - super(); -} - -//helper method. - private void addImage(Section container, ByteArrayOutputStream bos) throws Exception { - com.lowagie.text.Image image = com.lowagie.text.Image.getInstance(Toolkit.getDefaultToolkit().createImage(bos.toByteArray()), null); - //com.lowagie.text.Image structImage = com.lowagie.text.Image.getInstance(bos.toByteArray()); - //Gif structImage = new Gif(bos.toByteArray()); - //setNewPage(container, image); - image.setBackgroundColor(java.awt.Color.white); //? - Table imageTable = getTable(1,100, 2, 0, 0); - Cell imageCell = new Cell(); - imageCell.setLeading(0); - imageCell.add(image); - imageTable.addCell(imageCell); - imageTable.setTableFitsPage(true); - imageTable.setCellsFitPage(true); - container.add(imageTable); + protected ITextWriter() { + super(); + } + + //helper method. + private void addImage(Section container, ByteArrayOutputStream bos) throws Exception { + com.lowagie.text.Image image = com.lowagie.text.Image.getInstance(Toolkit.getDefaultToolkit().createImage(bos.toByteArray()), null); + //com.lowagie.text.Image structImage = com.lowagie.text.Image.getInstance(bos.toByteArray()); + //Gif structImage = new Gif(bos.toByteArray()); + //setNewPage(container, image); + image.setBackgroundColor(java.awt.Color.white); //? + Table imageTable = getTable(1, 100, 2, 0, 0); + Cell imageCell = new Cell(); + imageCell.setLeading(0); + imageCell.add(image); + imageTable.addCell(imageCell); + imageTable.setTableFitsPage(true); + imageTable.setCellsFitPage(true); + container.add(imageTable); /* com.lowagie.text.pdf.PdfPTable imageTable = new com.lowagie.text.pdf.PdfPTable(1); imageTable.setTotalWidth(image.width()); com.lowagie.text.pdf.PdfPCell imageCell = new com.lowagie.text.pdf.PdfPCell(image); imageCell.setBorderWidth(1); */ - } + } -protected Cell createCell(String text, Font font) throws DocumentException { - - return createCell(text, font, 1, 1, Element.ALIGN_LEFT, false); -} + protected Cell createCell(String text, Font font) throws DocumentException { + return createCell(text, font, 1, 1, Element.ALIGN_LEFT, false); + } -protected Cell createCell(String text, Font font, int colspan) throws DocumentException { - return createCell(text, font, colspan, 1, Element.ALIGN_LEFT, false); -} + protected Cell createCell(String text, Font font, int colspan) throws DocumentException { + return createCell(text, font, colspan, 1, Element.ALIGN_LEFT, false); + } - protected Cell createCell(String text, Font font, int colspan, int borderWidth, int alignment, boolean isHeader) throws DocumentException { - Cell cell = new Cell(new Paragraph(text, font)); - cell.setBorderWidth(borderWidth); - cell.setHorizontalAlignment(alignment); - if (colspan > 1) { - cell.setColspan(colspan); - } - if (isHeader) { - cell.setHeader(true); - } - return(cell); - } + protected Cell createCell(String text, Font font, int colspan, int borderWidth, int alignment, boolean isHeader) throws DocumentException { + Cell cell = new Cell(new Paragraph(text, font)); + cell.setBorderWidth(borderWidth); + cell.setHorizontalAlignment(alignment); + if (colspan > 1) { + cell.setColspan(colspan); + } + if (isHeader) { + cell.setHeader(true); + } + return (cell); + } -public abstract DocWriter createDocWriter(FileOutputStream fileOutputStream) throws DocumentException; -protected Cell createHeaderCell(String text, Font font, int colspan) throws DocumentException { - - return createCell(text, font, colspan, 1, Element.ALIGN_LEFT, true); -} + public abstract DocWriter createDocWriter(FileOutputStream fileOutputStream) throws DocumentException; + + protected Cell createHeaderCell(String text, Font font, int colspan) throws DocumentException { + + return createCell(text, font, colspan, 1, Element.ALIGN_LEFT, true); + } + + public static BufferedImage generateDocReactionsImage(Model model, Integer width) throws Exception { - public static BufferedImage generateDocReactionsImage(Model model,Integer width) throws Exception { - // if (model == null || !isValidResolutionSetting(resolution)) { // throw new IllegalArgumentException("Invalid parameters for generating reactions image for model: " + model.getName()); // } - ReactionCartoon rcartoon = new ReactionCartoonFull(); - rcartoon.setModel(model); - StructureSuite structureSuite = new AllStructureSuite(new Model.Owner() { - @Override - public Model getModel() { - return model; - } - }); - rcartoon.setStructureSuite(structureSuite); - rcartoon.refreshAll(); - //dummy settings to get the real dimensions. - BufferedImage dummyBufferedImage = new BufferedImage(DEF_IMAGE_WIDTH, DEF_IMAGE_HEIGHT, BufferedImage.TYPE_3BYTE_BGR); - Graphics2D dummyGraphics = (Graphics2D)dummyBufferedImage.getGraphics(); - Dimension prefDim = rcartoon.getPreferedCanvasSize(dummyGraphics); - dummyGraphics.dispose(); + ReactionCartoon rcartoon = new ReactionCartoonFull(); + rcartoon.setModel(model); + StructureSuite structureSuite = new AllStructureSuite(() -> model); + rcartoon.setStructureSuite(structureSuite); + rcartoon.refreshAll(); + //dummy settings to get the real dimensions. + BufferedImage dummyBufferedImage = new BufferedImage(DEF_IMAGE_WIDTH, DEF_IMAGE_HEIGHT, BufferedImage.TYPE_3BYTE_BGR); + Graphics2D dummyGraphics = (Graphics2D) dummyBufferedImage.getGraphics(); + Dimension prefDim = rcartoon.getPreferedCanvasSize(dummyGraphics); + dummyGraphics.dispose(); // double width = prefDim.getWidth(); // double height = prefDim.getHeight(); - double widthHeightRatio = (double)prefDim.getWidth()/(double)prefDim.getHeight(); - if(width == null){ - width = ITextWriter.DEF_IMAGE_WIDTH; - } - int height = (int)((double)width/widthHeightRatio); - + double widthHeightRatio = prefDim.getWidth() / prefDim.getHeight(); + if (width == null) { + width = ITextWriter.DEF_IMAGE_WIDTH; + } + Dimension newDim = ITextWriter.getNewDimensions(width, widthHeightRatio); + + rcartoon.getResizeManager().setZoomPercent((int) (100 * width / prefDim.getWidth())); + + BufferedImage bufferedImage = new BufferedImage(newDim.width, newDim.height, BufferedImage.TYPE_3BYTE_BGR); + Graphics2D g = (Graphics2D) bufferedImage.getGraphics(); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + GraphContainerLayoutReactions containerLayout = new GraphContainerLayoutReactions(); + containerLayout.layout(rcartoon, g, prefDim); + + rcartoon.paint(g); + g.dispose(); + return bufferedImage; + } + + private static Dimension getNewDimensions(Integer width, double widthHeightRatio) { + int height = (int) ((double) width / widthHeightRatio); + // int MAX_IMAGE_HEIGHT = 532; // if (width < ITextWriter.DEF_IMAGE_WIDTH) { // width = ITextWriter.DEF_IMAGE_WIDTH; -// } +// } // height= height * width/prefDim.getWidth(); // if (height < ITextWriter.DEF_IMAGE_HEIGHT) { // height = ITextWriter.DEF_IMAGE_HEIGHT; @@ -438,38 +452,24 @@ public Model getModel() { // height = MAX_IMAGE_HEIGHT; // } // width= width * height/prefDim.getHeight(); - Dimension newDim = new Dimension((int)width,(int)height); - - rcartoon.getResizeManager().setZoomPercent((int)(100*width/prefDim.getWidth())); - - BufferedImage bufferedImage = new BufferedImage(newDim.width,newDim.height, BufferedImage.TYPE_3BYTE_BGR); - Graphics2D g = (Graphics2D)bufferedImage.getGraphics(); - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - GraphContainerLayoutReactions containerLayout = new GraphContainerLayoutReactions(); - containerLayout.layout(rcartoon, g, prefDim); - - rcartoon.paint(g); - g.dispose(); - return bufferedImage; + return new Dimension(width, height); } - - public static ByteArrayOutputStream encodeJPEG(BufferedImage bufferedImage) throws Exception{ - ImageWriter imageWriter = ImageIO.getImageWritersBySuffix("jpeg").next(); - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - ImageOutputStream imageOutputStream = ImageIO.createImageOutputStream(byteArrayOutputStream); - imageWriter.setOutput(imageOutputStream); - ImageWriteParam imageWriteParam = imageWriter.getDefaultWriteParam(); - imageWriteParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); - imageWriteParam.setCompressionQuality(1.0f); // quality 0(very compressed, lossy) -> 1.0(less compressed,loss-less) - IIOImage iioImage = new IIOImage(bufferedImage, null, null); - imageWriter.write(null, iioImage, imageWriteParam); - imageOutputStream.close(); - imageWriter.dispose(); - return byteArrayOutputStream; - } + public static ByteArrayOutputStream encodeJPEG(BufferedImage bufferedImage) throws Exception { + ImageWriter imageWriter = ImageIO.getImageWritersBySuffix("jpeg").next(); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + ImageOutputStream imageOutputStream = ImageIO.createImageOutputStream(byteArrayOutputStream); + imageWriter.setOutput(imageOutputStream); + ImageWriteParam imageWriteParam = imageWriter.getDefaultWriteParam(); + imageWriteParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + imageWriteParam.setCompressionQuality(1.0f); // quality 0(very compressed, lossy) -> 1.0(less compressed,loss-less) + IIOImage iioImage = new IIOImage(bufferedImage, null, null); + imageWriter.write(null, iioImage, imageWriteParam); + imageOutputStream.close(); + imageWriter.dispose(); + return byteArrayOutputStream; + } //pretty similar to its static counterpart /* @@ -518,187 +518,192 @@ protected ByteArrayOutputStream generateDocStructureImage(Model model, String re } */ - protected ByteArrayOutputStream generateGeometryImage(Geometry geom) throws Exception{ + protected ByteArrayOutputStream generateGeometryImage(Geometry geom) throws Exception { + + GeometrySpec geomSpec = geom.getGeometrySpec(); + IndexColorModel icm = DisplayAdapterService.getHandleColorMap(); + geom.precomputeAll(new GeometryThumbnailImageFactoryAWT()); + VCImage geomImage = geomSpec.getSampledImage().getCurrentValue(); + if (geomImage == null) { + throw new Exception("generateGeometryImage error : No Image"); + } + int x = geomImage.getNumX(); + int y = geomImage.getNumY(); + int z = geomImage.getNumZ(); + + BufferedImage bufferedImage = null; + WritableRaster pixelWR = null; + Image adjImage; + BufferedImage newBufferedImage = null; + + if (geom.getDimension() > 0 && geom.getDimension() < 3) { + bufferedImage = new BufferedImage(x, y, BufferedImage.TYPE_BYTE_INDEXED, icm); + pixelWR = bufferedImage.getRaster(); + for (int i = 0; i < x; i++) { + for (int j = 0; j < y; j++) { + pixelWR.setSample(i, j, 0, geomImage.getPixel(i, j, 0)); + } + + } + // Adjust the image width and height + // retaining the aspect ratio. Start by adjusting the height, then adjust width to maintain aspect ratio. + + double scaleFactor = 1.0; + if (x * scaleFactor > DEF_GEOM_WIDTH) { + scaleFactor = ((double) DEF_GEOM_WIDTH) / x; + } + if (y * scaleFactor > DEF_GEOM_HEIGHT) { + scaleFactor = ((double) DEF_GEOM_HEIGHT) / y; + } + int adjX = (int) Math.ceil(x * scaleFactor); + int adjY = (int) Math.ceil(y * scaleFactor); + + adjImage = bufferedImage.getScaledInstance(adjX, adjY, BufferedImage.SCALE_REPLICATE); + newBufferedImage = new BufferedImage(adjX, adjY, BufferedImage.TYPE_BYTE_INDEXED, icm); + newBufferedImage.getGraphics().drawImage(adjImage, 0, 0, null); + } else if (geom.getDimension() == 3) { + WritableRaster smallPixelWR = null; + int[] cmap = new int[256]; + final int DISPLAY_DIM_MAX = 256; + + try { + // int RGB interpretation as follows: + // int bits(32): (alpha)31-24,(red)23-16,(green)15-8,(blue)7-0 + // for alpha: 0-most transparent(see-through), 255-most opaque(solid) + + //Reset colormap (grayscale) + for (int i = 0; i < cmap.length; i += 1) { + int iv = 0x000000FF & i; + cmap[i] = 0xFF << 24 | iv << 16 | iv << 8 | i; + } + //stretch cmap grays + if (geomImage != null && geomImage.getPixelClasses().length < 32) { + for (int i = 0; i < geomImage.getPixelClasses().length; i += 1) { + int stretchIndex = 0xFF & geomImage.getPixelClasses()[i].getPixel(); + int newI = 32 + (i * ((256 - 32) / geomImage.getPixelClasses().length)); + cmap[stretchIndex] = 0xFF << 24 | newI << 16 | newI << 8 | newI; + } + } + //Set grid color + cmap[cmap.length - 1] = 0xFFFFFFFF; //white + + //Initialize image data + int xSide = 0; + int ySide = 0; + if (pixelWR == null) { + double side = Math.sqrt(x * y * z); + xSide = (int) Math.round(side / (double) x); + if (xSide == 0) { + xSide = 1; + } + if (xSide > z) { + xSide = z; + } + ySide = (int) Math.ceil((double) z / (double) xSide); + if (ySide == 0) { + ySide = 1; + } + if (ySide > z) { + ySide = z; + } + pixelWR = icm.createCompatibleWritableRaster(xSide * x, ySide * y); + byte[] sib = geomImage.getPixels(); + + //write the image to buffer + int ystride = x; + int zstride = x * y; + for (int row = 0; row < ySide; row += 1) { + for (int col = 0; col < xSide; col += 1) { + int xoffset = col * x; + int yoffset = (row * y); + int zoffset = (col + (row * xSide)) * zstride; + if (zoffset >= sib.length) { + for (int xi = 0; xi < x; xi += 1) { + for (int yi = 0; yi < y; yi += 1) { + pixelWR.setSample(xi + xoffset, yi + yoffset, 0, cmap.length - 1); + } + } + } else { + for (int xi = 0; xi < x; xi += 1) { + for (int yi = 0; yi < y; yi += 1) { + pixelWR.setSample(xi + xoffset, yi + yoffset, 0, 0xFF & sib[xi + (ystride * yi) + zoffset]); + } + } + } + } + } + + // scale if necessary + double displayScale = 1.0; + if (pixelWR.getWidth() < DISPLAY_DIM_MAX || pixelWR.getHeight() < DISPLAY_DIM_MAX) { + displayScale = Math.min((DISPLAY_DIM_MAX / pixelWR.getWidth()), (DISPLAY_DIM_MAX / pixelWR.getHeight())); + if (displayScale == 0) { + displayScale = 1; + } + } + if ((displayScale == 1) && (pixelWR.getWidth() > DISPLAY_DIM_MAX || pixelWR.getHeight() > DISPLAY_DIM_MAX)) { + displayScale = Math.max((pixelWR.getWidth() / DISPLAY_DIM_MAX), (pixelWR.getHeight() / DISPLAY_DIM_MAX)); + //displayScale = Math.min(((double)DISPLAY_DIM_MAX/(double)pixelWR.getWidth()),((double)DISPLAY_DIM_MAX/(double)pixelWR.getHeight())); + if (displayScale == 0) { + displayScale = 1; + } + displayScale = 1.0 / displayScale; + } + if (displayScale != 1) { + java.awt.geom.AffineTransform at = new java.awt.geom.AffineTransform(); + at.setToScale(displayScale, displayScale); + java.awt.image.AffineTransformOp ato = new java.awt.image.AffineTransformOp(at, java.awt.image.AffineTransformOp.TYPE_NEAREST_NEIGHBOR); + smallPixelWR = ato.createCompatibleDestRaster(pixelWR); + ato.filter(pixelWR, smallPixelWR); + } + } + + //Create display image, re-use image data and colormap + // draw labels and grid + if (pixelWR != null) { + bufferedImage = new java.awt.image.BufferedImage(icm, smallPixelWR, false, null); + + if (xSide > 0 || ySide > 0) { + float gridXBlockLen = ((float) (bufferedImage.getWidth()) / xSide); + float gridYBlockLen = ((float) (bufferedImage.getHeight()) / ySide); + + java.awt.Graphics g = bufferedImage.getGraphics(); + g.setColor(java.awt.Color.white); + // horiz lines + for (int row = 0; row < ySide; row += 1) { + if (row > 0) { + g.drawLine(0, (int) (row * gridYBlockLen), bufferedImage.getWidth(), (int) (row * gridYBlockLen)); + } + } + // vert lines + for (int col = 0; col < xSide; col += 1) { + if (col > 0) { + g.drawLine((int) (col * gridXBlockLen), 0, (int) (col * gridXBlockLen), bufferedImage.getHeight()); + } + } + // z markers + if (xSide > 1 || ySide > 1) { + for (int row = 0; row < xSide; row += 1) { + for (int col = 0; col < ySide; col += 1) { + g.drawString("" + (1 + row + (col * xSide)), (int) (row * gridXBlockLen) + 3, (int) (col * gridYBlockLen) + 12); + } + } + } + } + } + } catch (Throwable e) { + throw new Exception("CreateGeometryImageIcon error\n" + (e.getMessage() != null ? e.getMessage() : e.getClass().getName())); + } + + // Adjust the image width and height + adjImage = bufferedImage.getScaledInstance(smallPixelWR.getWidth(), smallPixelWR.getHeight(), BufferedImage.SCALE_REPLICATE); + newBufferedImage = new BufferedImage(smallPixelWR.getWidth(), smallPixelWR.getHeight(), BufferedImage.TYPE_BYTE_INDEXED, icm); + newBufferedImage.getGraphics().drawImage(adjImage, 0, 0, null); + } - GeometrySpec geomSpec = geom.getGeometrySpec(); - IndexColorModel icm = DisplayAdapterService.getHandleColorMap(); - geom.precomputeAll(new GeometryThumbnailImageFactoryAWT()); - VCImage geomImage = geomSpec.getSampledImage().getCurrentValue(); - if(geomImage == null){ - throw new Exception("generateGeometryImage error : No Image"); - } - int x = geomImage.getNumX(); - int y = geomImage.getNumY(); - int z = geomImage.getNumZ(); - - BufferedImage bufferedImage = null; - WritableRaster pixelWR = null; - Image adjImage = null; - BufferedImage newBufferedImage = null; - - if (geom.getDimension() > 0 && geom.getDimension() < 3) { - bufferedImage = new BufferedImage(x, y, BufferedImage.TYPE_BYTE_INDEXED, icm); - pixelWR = bufferedImage.getRaster(); - for (int i = 0; i < x; i++){ - for (int j = 0; j < y; j++){ - pixelWR.setSample(i , j, 0, geomImage.getPixel(i, j, 0)); - } - - } - // Adjust the image width and height - // retaining the aspect ratio. Start by adjusting the height, then adjust width to maintain aspect ratio. - - double scaleFactor = 1.0; - if (x * scaleFactor > DEF_GEOM_WIDTH) { - scaleFactor = ((double) DEF_GEOM_WIDTH) / x; - } - if (y * scaleFactor > DEF_GEOM_HEIGHT) { - scaleFactor = ((double) DEF_GEOM_HEIGHT) / y; - } - int adjX = (int)Math.ceil(x*scaleFactor); - int adjY = (int)Math.ceil(y*scaleFactor); - - adjImage = bufferedImage.getScaledInstance(adjX, adjY, BufferedImage.SCALE_REPLICATE); - newBufferedImage = new BufferedImage(adjX, adjY, BufferedImage.TYPE_BYTE_INDEXED, icm); - newBufferedImage.getGraphics().drawImage(adjImage, 0, 0, null); - } else if (geom.getDimension() == 3) { - WritableRaster smallPixelWR = null; - int[] cmap = new int[256]; - final int DISPLAY_DIM_MAX = 256; - - try{ - // int RGB interpretation as follows: - // int bits(32): (alpha)31-24,(red)23-16,(green)15-8,(blue)7-0 - // for alpha: 0-most transparent(see-through), 255-most opaque(solid) - - //Reset colormap (grayscale) - for(int i = 0; i < cmap.length; i += 1){ - int iv = (int)(0x000000FF&i); - cmap[i] = 0xFF<<24 | iv<<16 | iv<<8 | i; - } - //stretch cmap grays - if(geomImage != null && geomImage.getPixelClasses().length < 32){ - for(int i=0;i< geomImage.getPixelClasses().length;i+= 1){ - int stretchIndex = (int)(0xFF&geomImage.getPixelClasses()[i].getPixel()); - int newI = 32+(i*((256-32)/geomImage.getPixelClasses().length)); - cmap[stretchIndex] = 0xFF<<24 | newI<<16 | newI<<8 | newI; - } - } - //Set grid color - cmap[cmap.length-1] = 0xFFFFFFFF; //white - - //Initialize image data - int xSide = 0; - int ySide = 0; - if(pixelWR == null){ - VCImage sampledImage = geomImage; - double side = Math.sqrt(x*y*z); - xSide = (int)Math.round(side/(double)x); - if(xSide == 0){xSide = 1;} - if(xSide > z){ - xSide = z; - } - ySide = (int)Math.ceil((double)z/(double)xSide); - if(ySide == 0){ySide = 1;} - if(ySide > z){ - ySide = z; - } - pixelWR = icm.createCompatibleWritableRaster(xSide*x,ySide*y); - byte[] sib = sampledImage.getPixels(); - - //write the image to buffer - int ystride = x; - int zstride = x*y; - for(int row = 0; row < ySide; row += 1){ - for(int col = 0; col < xSide; col += 1){ - int xoffset = col*x; - int yoffset = (row*y); - int zoffset = (col+(row*xSide))*zstride; - if(zoffset >= sib.length){ - for(int xi = 0; xi < x; xi += 1){ - for(int yi = 0; yi < y; yi += 1){ - pixelWR.setSample(xi + xoffset, yi + yoffset, 0, cmap.length-1); - } - } - }else{ - for(int xi = 0; xi < x; xi += 1){ - for(int yi = 0; yi < y; yi += 1){ - pixelWR.setSample(xi + xoffset, yi + yoffset,0,(int)(0xFF&sib[xi + (ystride*yi) + zoffset])); - } - } - } - } - } - - // scale if necessary - double displayScale = 1.0; - if(pixelWR.getWidth() < DISPLAY_DIM_MAX || pixelWR.getHeight() < DISPLAY_DIM_MAX){ - displayScale = (int)Math.min((DISPLAY_DIM_MAX/pixelWR.getWidth()),(DISPLAY_DIM_MAX/pixelWR.getHeight())); - if(displayScale == 0){displayScale = 1;} - } - if((displayScale == 1) && (pixelWR.getWidth() > DISPLAY_DIM_MAX || pixelWR.getHeight() > DISPLAY_DIM_MAX)){ - displayScale = Math.max((pixelWR.getWidth()/DISPLAY_DIM_MAX),(pixelWR.getHeight()/DISPLAY_DIM_MAX)); - //displayScale = Math.min(((double)DISPLAY_DIM_MAX/(double)pixelWR.getWidth()),((double)DISPLAY_DIM_MAX/(double)pixelWR.getHeight())); - if(displayScale == 0) {displayScale = 1;} - displayScale = 1.0/displayScale; - } - if(displayScale != 1){ - java.awt.geom.AffineTransform at = new java.awt.geom.AffineTransform(); - at.setToScale(displayScale, displayScale); - java.awt.image.AffineTransformOp ato = new java.awt.image.AffineTransformOp(at,java.awt.image.AffineTransformOp.TYPE_NEAREST_NEIGHBOR); - smallPixelWR = ato.createCompatibleDestRaster(pixelWR); - ato.filter(pixelWR, smallPixelWR); - } - } - - //Create display image, re-use image data and colormap - // draw labels and grid - if(pixelWR != null){ - bufferedImage = new java.awt.image.BufferedImage(icm,smallPixelWR,false,null); - - if(xSide > 0 || ySide > 0){ - float gridXBlockLen = ((float)(bufferedImage.getWidth())/xSide); - float gridYBlockLen = ((float)(bufferedImage.getHeight())/ySide); - - java.awt.Graphics g = bufferedImage.getGraphics(); - g.setColor(java.awt.Color.white); - // horiz lines - for(int row=0;row < ySide;row+= 1){ - if(row > 0){ - g.drawLine(0,(int)(row*gridYBlockLen),bufferedImage.getWidth(),(int)(row*gridYBlockLen)); - } - } - // vert lines - for(int col=0;col 0){ - g.drawLine((int)(col*gridXBlockLen),0,(int)(col*gridXBlockLen),bufferedImage.getHeight()); - } - } - // z markers - if(xSide > 1 || ySide > 1){ - for(int row=0;row < xSide;row+= 1){ - for(int col=0;col 0) { - Chapter simContextsChapter = new Chapter("Applications For " + name, chapterNum++); - if (introSection == null) { - introSection = simContextsChapter.addSection("General Info", simContextsChapter.getNumberDepth() + 1); - String freeTextAnnotation = bioModel.getVCMetaData().getFreeTextAnnotation(bioModel); - writeMetadata(introSection, name, freeTextAnnotation, userName, "BioModel"); - } - for (int i = 0; i < simContexts.length; i++) { - writeSimulationContext(simContextsChapter, simContexts[i], preferences); - } - document.add(simContextsChapter); - } else { - System.err.println("Bad Request: No applications to publish for Biomodel: " + bioModel.getName()); - } - } - document.close(); -} - protected void writeEquation(Section container, Equation eq) throws DocumentException { - - if (eq instanceof FilamentRegionEquation) { - writeFilamentRegionEquation(container, (FilamentRegionEquation)eq); - } else if (eq instanceof MembraneRegionEquation) { - writeMemRegionEquation(container, (MembraneRegionEquation)eq); - } else if (eq instanceof OdeEquation) { - writeOdeEquation(container, (OdeEquation)eq); - } else if (eq instanceof PdeEquation) { - writePdeEquation(container, (PdeEquation)eq); - } else if (eq instanceof VolumeRegionEquation) { - writeVolumeRegionEquation(container, (VolumeRegionEquation)eq); - } - } + public void writeBioModel(BioModel bioModel, FileOutputStream fos, PageFormat pageFormat, PublishPreferences preferences) throws Exception { + if (bioModel == null || fos == null || pageFormat == null || preferences == null) { + throw new IllegalArgumentException("One or more null params while publishing BioModel."); + } + createDocument(pageFormat); + createDocWriter(fos); + // Add metadata before you open the document... + String name = bioModel.getName().trim(); + String userName = "Unknown"; + if (bioModel.getVersion() != null) { + userName = bioModel.getVersion().getOwner().getName(); + } + document.addTitle(name + "[owned by " + userName + "]"); + document.addCreator("Virtual Cell"); + document.addCreationDate(); + //writeWatermark(document, pageFormat); + writeHeaderFooter("BioModel: " + name); + document.open(); + // + Section introSection = null; + int chapterNum = 1; + if (preferences.includePhysio()) { + Chapter physioChapter = new Chapter("Physiology For " + name, chapterNum++); + introSection = physioChapter.addSection("General Info", physioChapter.getNumberDepth() + 1); + String freeTextAnnotation = bioModel.getVCMetaData().getFreeTextAnnotation(bioModel); + writeMetadata(introSection, name, freeTextAnnotation, userName, "BioModel"); + writeModel(physioChapter, bioModel.getModel()); + document.add(physioChapter); + } + if (preferences.includeApp()) { + SimulationContext[] simContexts = bioModel.getSimulationContexts(); + if (simContexts.length > 0) { + Chapter simContextsChapter = new Chapter("Applications For " + name, chapterNum++); + if (introSection == null) { + introSection = simContextsChapter.addSection("General Info", simContextsChapter.getNumberDepth() + 1); + String freeTextAnnotation = bioModel.getVCMetaData().getFreeTextAnnotation(bioModel); + writeMetadata(introSection, name, freeTextAnnotation, userName, "BioModel"); + } + for (SimulationContext simContext : simContexts) { + writeSimulationContext(simContextsChapter, simContext, preferences); + } + document.add(simContextsChapter); + } else { + lg.error("Bad Request: No applications to publish for Biomodel: {}", bioModel.getName()); + } + } + document.close(); + } + + protected void writeEquation(Section container, Equation eq) throws DocumentException { + + if (eq instanceof FilamentRegionEquation) { + writeFilamentRegionEquation(container, (FilamentRegionEquation) eq); + } else if (eq instanceof MembraneRegionEquation) { + writeMemRegionEquation(container, (MembraneRegionEquation) eq); + } else if (eq instanceof OdeEquation) { + writeOdeEquation(container, (OdeEquation) eq); + } else if (eq instanceof PdeEquation) { + writePdeEquation(container, (PdeEquation) eq); + } else if (eq instanceof VolumeRegionEquation) { + writeVolumeRegionEquation(container, (VolumeRegionEquation) eq); + } + } - protected void writeFastSystem(Section container, FastSystem fs) throws DocumentException { - - Table eqTable = getTable(2, 100, 1, 2, 2); - eqTable.addCell(createCell(VCML.FastSystem, - getBold(DEF_HEADER_FONT_SIZE), 2, 1, Element.ALIGN_CENTER, true)); - eqTable.endHeaders(); - Enumeration enum_fi = fs.getFastInvariants(); - while (enum_fi.hasMoreElements()){ - FastInvariant fi = enum_fi.nextElement(); - eqTable.addCell(createCell(VCML.FastInvariant, getFont())); - eqTable.addCell(createCell(fi.getFunction().infix(), getFont())); - } - Enumeration enum_fr = fs.getFastRates(); - while (enum_fr.hasMoreElements()){ - FastRate fr = enum_fr.nextElement(); - eqTable.addCell(createCell(VCML.FastRate, getFont())); - eqTable.addCell(createCell(fr.getFunction().infix(), getFont())); - } - - container.add(eqTable); - } + protected void writeFastSystem(Section container, FastSystem fs) throws DocumentException { - protected void writeFilamentRegionEquation(Section container, FilamentRegionEquation eq) throws DocumentException { + Table eqTable = getTable(2, 100, 1, 2, 2); + eqTable.addCell(createCell(VCML.FastSystem, + getBold(DEF_HEADER_FONT_SIZE), 2, 1, Element.ALIGN_CENTER, true)); + eqTable.endHeaders(); + Enumeration enum_fi = fs.getFastInvariants(); + while (enum_fi.hasMoreElements()) { + FastInvariant fi = enum_fi.nextElement(); + eqTable.addCell(createCell(VCML.FastInvariant, getFont())); + eqTable.addCell(createCell(fi.getFunction().infix(), getFont())); + } + Enumeration enum_fr = fs.getFastRates(); + while (enum_fr.hasMoreElements()) { + FastRate fr = enum_fr.nextElement(); + eqTable.addCell(createCell(VCML.FastRate, getFont())); + eqTable.addCell(createCell(fr.getFunction().infix(), getFont())); + } - Table eqTable = getTable(2, 100, 1, 2, 2); - eqTable.addCell(createCell(VCML.FilamentRegionEquation + " " + eq.getVariable().getName(), - getBold(DEF_HEADER_FONT_SIZE), 2, 1, Element.ALIGN_CENTER, true)); - eqTable.endHeaders(); - String exp = "0.0"; - eqTable.addCell(createCell(VCML.FilamentRate, getFont())); - if (eq.getFilamentRateExpression() != null) { - exp = eq.getFilamentRateExpression().infix(); - } - eqTable.addCell(createCell(exp, getFont())); - if (eq.getInitialExpression() != null) { - eqTable.addCell(createCell(VCML.Initial, getFont())); - eqTable.addCell(createCell(eq.getInitialExpression().infix(), getFont())); - } - int solutionType = eq.getSolutionType(); - switch (solutionType) { - case Equation.UNKNOWN_SOLUTION:{ - if (eq.getInitialExpression() == null) { - eqTable.addCell(createCell(VCML.Initial, getFont())); - eqTable.addCell(createCell("0.0", getFont())); - } - break; - } - case Equation.EXACT_SOLUTION:{ - eqTable.addCell(createCell(VCML.Exact, getFont())); - eqTable.addCell(createCell(eq.getExactSolution().infix(), getFont())); - break; - } - } - - container.add(eqTable); - } + container.add(eqTable); + } -//Section used can be a chapter or a section of one, based on the document type. - protected void writeGeom(Section container, Geometry geom, GeometryContext geomCont) throws Exception { - - try { - Section geomSection = container.addSection("Geometry: " + geom.getName(), container.getNumberDepth() + 1); - if (geom.getDimension() == 0) { - Paragraph p = new Paragraph(new Phrase("Non spatial geometry.")); - p.setAlignment(Paragraph.ALIGN_CENTER); - geomSection.add(p); - return; - } - ByteArrayOutputStream bos = generateGeometryImage(geom); - com.lowagie.text.Image geomImage = com.lowagie.text.Image.getInstance(java.awt.Toolkit.getDefaultToolkit().createImage(bos.toByteArray()), null); - geomImage.setAlignment(Table.ALIGN_LEFT); - geomSection.add(geomImage); - //addImage(geomSection, bos); - Table geomTable = getTable(2, 50, 1, 2, 2); - geomTable.setAlignment(Table.ALIGN_LEFT); - Extent extent = geom.getExtent(); - String extentStr = "(" + extent.getX() + ", " + extent.getY() + ", " + extent.getZ() + ")"; - Origin origin = geom.getOrigin(); - String originStr = "(" + origin.getX() + ", " + origin.getY() + ", " + origin.getZ() + ")"; - geomTable.addCell(createCell("Size", getFont())); - geomTable.addCell(createCell(extentStr, getFont())); - geomTable.addCell(createCell("Origin", getFont())); - geomTable.addCell(createCell(originStr, getFont())); - geomSection.add(geomTable); - } catch (Exception e) { - lg.error("Unable to add geometry image to report.", e); - } - } + protected void writeFilamentRegionEquation(Section container, FilamentRegionEquation eq) throws DocumentException { + Table eqTable = getTable(2, 100, 1, 2, 2); + eqTable.addCell(createCell(VCML.FilamentRegionEquation + " " + eq.getVariable().getName(), + getBold(DEF_HEADER_FONT_SIZE), 2, 1, Element.ALIGN_CENTER, true)); + eqTable.endHeaders(); + String exp = "0.0"; + eqTable.addCell(createCell(VCML.FilamentRate, getFont())); + if (eq.getFilamentRateExpression() != null) { + exp = eq.getFilamentRateExpression().infix(); + } + eqTable.addCell(createCell(exp, getFont())); + if (eq.getInitialExpression() != null) { + eqTable.addCell(createCell(VCML.Initial, getFont())); + eqTable.addCell(createCell(eq.getInitialExpression().infix(), getFont())); + } + int solutionType = eq.getSolutionType(); + switch (solutionType) { + case Equation.UNKNOWN_SOLUTION: { + if (eq.getInitialExpression() == null) { + eqTable.addCell(createCell(VCML.Initial, getFont())); + eqTable.addCell(createCell("0.0", getFont())); + } + break; + } + case Equation.EXACT_SOLUTION: { + eqTable.addCell(createCell(VCML.Exact, getFont())); + eqTable.addCell(createCell(eq.getExactSolution().infix(), getFont())); + break; + } + } - public void writeGeometry(Geometry geom, FileOutputStream fos, PageFormat format) throws Exception { + container.add(eqTable); + } + + + //Section used can be a chapter or a section of one, based on the document type. + protected void writeGeom(Section container, Geometry geom, GeometryContext geomCont) { + + try { + Section geomSection = container.addSection("Geometry: " + geom.getName(), container.getNumberDepth() + 1); + if (geom.getDimension() == 0) { + Paragraph p = new Paragraph(new Phrase("Non spatial geometry.")); + p.setAlignment(Paragraph.ALIGN_CENTER); + geomSection.add(p); + return; + } + ByteArrayOutputStream bos = generateGeometryImage(geom); + com.lowagie.text.Image geomImage = com.lowagie.text.Image.getInstance(java.awt.Toolkit.getDefaultToolkit().createImage(bos.toByteArray()), null); + geomImage.setAlignment(Table.ALIGN_LEFT); + geomSection.add(geomImage); + //addImage(geomSection, bos); + Table geomTable = getTable(2, 50, 1, 2, 2); + geomTable.setAlignment(Table.ALIGN_LEFT); + Extent extent = geom.getExtent(); + String extentStr = "(" + extent.getX() + ", " + extent.getY() + ", " + extent.getZ() + ")"; + Origin origin = geom.getOrigin(); + String originStr = "(" + origin.getX() + ", " + origin.getY() + ", " + origin.getZ() + ")"; + geomTable.addCell(createCell("Size", getFont())); + geomTable.addCell(createCell(extentStr, getFont())); + geomTable.addCell(createCell("Origin", getFont())); + geomTable.addCell(createCell(originStr, getFont())); + geomSection.add(geomTable); + } catch (Exception e) { + lg.error("Unable to add geometry image to report.", e); + } + } - writeGeometry(geom, fos, format, PublishPreferences.DEFAULT_GEOM_PREF); - } + public void writeGeometry(Geometry geom, FileOutputStream fos, PageFormat format) throws Exception { - //for now, the preferences for a geometry is a dummy. - public void writeGeometry(Geometry geom, FileOutputStream fos, PageFormat pageFormat, PublishPreferences preferences) throws Exception { + writeGeometry(geom, fos, format, PublishPreferences.DEFAULT_GEOM_PREF); + } - if (geom == null || fos == null || pageFormat == null || preferences == null) { - throw new IllegalArgumentException("One or more null params while publishing Geometry."); - } - try { - createDocument(pageFormat); - createDocWriter(fos); - //Add metadata before you open the document... - String name = geom.getName().trim(); - String userName = "Unknown"; - if (geom.getVersion() != null) { - userName = geom.getVersion().getOwner().getName(); - } - document.addTitle(name + "[owned by " + userName + "]"); - document.addCreator("Virtual Cell"); - document.addCreationDate(); - //writeWatermark(document, pageFormat); - writeHeaderFooter("Geometry: " + name); - document.open(); - // - Section introSection = null; - int chapterNum = 1; - Chapter geomChapter = new Chapter("Geometry", chapterNum++); - introSection = geomChapter.addSection("General Info", geomChapter.getNumberDepth() + 1); - writeMetadata(introSection, name, geom.getDescription(), userName, "Geometry"); - Section geomSection = geomChapter.addSection("Geometry", geomChapter.getNumberDepth() + 1); //title? - writeGeom(geomSection, geom, null); - document.add(geomChapter); - document.close(); - } catch (DocumentException e) { - lg.error("Unable to publish BioModel.", e); - throw e; - } - } + //for now, the preferences for a geometry is a dummy. + public void writeGeometry(Geometry geom, FileOutputStream fos, PageFormat pageFormat, PublishPreferences preferences) throws Exception { -/** - * Default is no header or footer... - */ -protected void writeHeaderFooter(String headerStr) throws DocumentException { + if (geom == null || fos == null || pageFormat == null || preferences == null) { + throw new IllegalArgumentException("One or more null params while publishing Geometry."); + } + try { + createDocument(pageFormat); + createDocWriter(fos); + //Add metadata before you open the document... + String name = geom.getName().trim(); + String userName = "Unknown"; + if (geom.getVersion() != null) { + userName = geom.getVersion().getOwner().getName(); + } + document.addTitle(name + "[owned by " + userName + "]"); + document.addCreator("Virtual Cell"); + document.addCreationDate(); + //writeWatermark(document, pageFormat); + writeHeaderFooter("Geometry: " + name); + document.open(); + // + Section introSection; + int chapterNum = 1; + Chapter geomChapter = new Chapter("Geometry", chapterNum++); + introSection = geomChapter.addSection("General Info", geomChapter.getNumberDepth() + 1); + writeMetadata(introSection, name, geom.getDescription(), userName, "Geometry"); + Section geomSection = geomChapter.addSection("Geometry", geomChapter.getNumberDepth() + 1); //title? + writeGeom(geomSection, geom, null); + document.add(geomChapter); + document.close(); + } catch (DocumentException e) { + lg.error("Unable to publish BioModel.", e); + throw e; + } + } -} + /** + * Default is no header or footer... + */ + protected void writeHeaderFooter(String headerStr) throws DocumentException { - protected void writeJumpCondition(Section container, JumpCondition eq) throws DocumentException { + } - Table eqTable = getTable(2, 100, 1, 2, 2); - eqTable.addCell(createCell(VCML.JumpCondition + " " + eq.getVariable().getName(), - getBold(DEF_HEADER_FONT_SIZE), 2, 1, Element.ALIGN_CENTER, true)); - eqTable.endHeaders(); - String exp = "0.0"; - eqTable.addCell(createCell(VCML.InFlux, getFont())); - if (eq.getInFluxExpression() != null) { - exp = eq.getInFluxExpression().infix(); - } - eqTable.addCell(createCell(exp, getFont())); - exp = "0.0"; - eqTable.addCell(createCell(VCML.OutFlux, getFont())); - if (eq.getOutFluxExpression() != null) { - exp = eq.getOutFluxExpression().infix(); - } - eqTable.addCell(createCell(exp, getFont())); - - container.add(eqTable); - } + protected void writeJumpCondition(Section container, JumpCondition eq) throws DocumentException { - protected void writeKineticsParams(Section reactionSection, ReactionStep rs) throws DocumentException { - - Kinetics.KineticsParameter kineticsParameters[] = rs.getKinetics().getKineticsParameters(); - Table paramTable = null; - int widths [] = {1, 4, 3, 2}; - if (kineticsParameters.length > 0) { - paramTable = getTable(4, 100, 1, 3, 3); - paramTable.addCell(createCell("Kinetics Parameters", getBold(DEF_HEADER_FONT_SIZE), 4, 1, Element.ALIGN_CENTER, true)); - paramTable.addCell(createHeaderCell("Name", getBold(), 1)); - paramTable.addCell(createHeaderCell("Expression", getBold(), 1)); - paramTable.addCell(createHeaderCell("Role", getBold(), 1)); - paramTable.addCell(createHeaderCell("Unit", getBold(), 1)); - paramTable.endHeaders(); - for (int k = 0; k < kineticsParameters.length; k++) { - String name = kineticsParameters[k].getName(); - Expression expression = kineticsParameters[k].getExpression(); - String role = rs.getKinetics().getDefaultParameterDesc(kineticsParameters[k].getRole()); - VCUnitDefinition unit = kineticsParameters[k].getUnitDefinition(); - paramTable.addCell(createCell(name, getFont())); - paramTable.addCell(createCell((expression == null ? "": expression.infix()), getFont())); - paramTable.addCell(createCell(role, getFont())); - paramTable.addCell(createCell((unit == null ? "": unit.getSymbolUnicode()), getFont())); //dimensionless will show as '1'. - } - } - if (paramTable != null) { - paramTable.setWidths(widths); - reactionSection.add(paramTable); - } - } + Table eqTable = getTable(2, 100, 1, 2, 2); + eqTable.addCell(createCell(VCML.JumpCondition + " " + eq.getVariable().getName(), + getBold(DEF_HEADER_FONT_SIZE), 2, 1, Element.ALIGN_CENTER, true)); + eqTable.endHeaders(); + String exp = "0.0"; + eqTable.addCell(createCell(VCML.InFlux, getFont())); + if (eq.getInFluxExpression() != null) { + exp = eq.getInFluxExpression().infix(); + } + eqTable.addCell(createCell(exp, getFont())); + exp = "0.0"; + eqTable.addCell(createCell(VCML.OutFlux, getFont())); + if (eq.getOutFluxExpression() != null) { + exp = eq.getOutFluxExpression().infix(); + } + eqTable.addCell(createCell(exp, getFont())); + + container.add(eqTable); + } + + + protected void writeKineticsParams(Section reactionSection, ReactionStep rs) throws DocumentException { + + Kinetics.KineticsParameter[] kineticsParameters = rs.getKinetics().getKineticsParameters(); + Table paramTable = null; + int[] widths = {1, 4, 3, 2}; + if (kineticsParameters.length > 0) { + paramTable = getTable(4, 100, 1, 3, 3); + paramTable.addCell(createCell("Kinetics Parameters", getBold(DEF_HEADER_FONT_SIZE), 4, 1, Element.ALIGN_CENTER, true)); + paramTable.addCell(createHeaderCell("Name", getBold(), 1)); + paramTable.addCell(createHeaderCell("Expression", getBold(), 1)); + paramTable.addCell(createHeaderCell("Role", getBold(), 1)); + paramTable.addCell(createHeaderCell("Unit", getBold(), 1)); + paramTable.endHeaders(); + for (Kinetics.KineticsParameter kineticsParameter : kineticsParameters) { + String name = kineticsParameter.getName(); + Expression expression = kineticsParameter.getExpression(); + String role = rs.getKinetics().getDefaultParameterDesc(kineticsParameter.getRole()); + VCUnitDefinition unit = kineticsParameter.getUnitDefinition(); + paramTable.addCell(createCell(name, getFont())); + paramTable.addCell(createCell((expression == null ? "" : expression.infix()), getFont())); + paramTable.addCell(createCell(role, getFont())); + paramTable.addCell(createCell((unit == null ? "" : unit.getSymbolUnicode()), getFont())); //dimensionless will show as '1'. + } + } + if (paramTable != null) { + paramTable.setWidths(widths); + reactionSection.add(paramTable); + } + } -//container can be a chapter or a section of a chapter. + //container can be a chapter or a section of a chapter. //MathDescription.description ignored. //currently not used. - protected void writeMathDescAsImages(Section container, MathDescription mathDesc) throws DocumentException { + protected void writeMathDescAsImages(Section container, MathDescription mathDesc) throws DocumentException { - if (mathDesc == null) { - return; - } - Section mathDescSection = container.addSection("Math Description: " + mathDesc.getName(), container.getDepth() + 1); - Section mathDescSubSection = null; - Expression expArray [] = null; - BufferedImage dummy = new BufferedImage(500, 50, BufferedImage.TYPE_3BYTE_BGR); - int scale = 1; - int viewableWidth = (int)(document.getPageSize().getWidth() - document.leftMargin() - document.rightMargin()); - //add Constants - Enumeration constantsList = mathDesc.getConstants(); - while (constantsList.hasMoreElements()) { - Constant constant = constantsList.nextElement(); - Expression exp = constant.getExpression(); - try { - expArray = new Expression[] { Expression.assign(new Expression(constant.getName()), exp.flatten()) }; - } catch(ExpressionException ee) { - lg.error("Unable to process constant " + constant.getName() + " for publishing", ee); - continue; - } - try { - Dimension dim = ExpressionCanvas.getExpressionImageSize(expArray, (Graphics2D)dummy.getGraphics()); - BufferedImage bufferedImage = new BufferedImage((int)dim.getWidth()*scale, (int)dim.getHeight()*scale, BufferedImage.TYPE_3BYTE_BGR); - ExpressionCanvas.getExpressionAsImage(expArray, bufferedImage, scale); - com.lowagie.text.Image expImage = com.lowagie.text.Image.getInstance(bufferedImage, null); - expImage.setAlignment(com.lowagie.text.Image.ALIGN_LEFT); - if (mathDescSubSection == null) { - mathDescSubSection = mathDescSection.addSection("Constants", mathDescSection.getDepth() + 1); - } - if (viewableWidth < Math.floor(expImage.getScaledWidth())) { - expImage.scaleToFit(viewableWidth, expImage.getPlainHeight()); - System.out.println("Constant After scaling: " + expImage.getScaledWidth()); - } - mathDescSubSection.add(expImage); - } catch (Exception e) { - lg.error("Unable to add structure mapping image to report.", e); - } - } - mathDescSubSection = null; - //add functions - Enumeration functionsList = mathDesc.getFunctions(); - while (functionsList.hasMoreElements()) { - Function function = functionsList.nextElement(); - Expression exp = function.getExpression(); - try { - expArray = new Expression[] { Expression.assign(new Expression(function.getName()), exp.flatten()) }; - } catch(ExpressionException ee) { - lg.error("Unable to process function " + function.getName() + " for publishing", ee); - continue; - } - try { - Dimension dim = ExpressionCanvas.getExpressionImageSize(expArray, (Graphics2D)dummy.getGraphics()); - BufferedImage bufferedImage = new BufferedImage((int)dim.getWidth()*scale, (int)dim.getHeight()*scale, BufferedImage.TYPE_3BYTE_BGR); - ExpressionCanvas.getExpressionAsImage(expArray, bufferedImage, scale); - com.lowagie.text.Image expImage = com.lowagie.text.Image.getInstance(bufferedImage, null); - expImage.setAlignment(com.lowagie.text.Image.ALIGN_LEFT); - if (mathDescSubSection == null) { - mathDescSubSection = mathDescSection.addSection("Functions", mathDescSection.getDepth() + 1); - } - if (viewableWidth < Math.floor(expImage.getScaledWidth())) { - expImage.scaleToFit(viewableWidth, expImage.getHeight()); - System.out.println("Function After scaling: " + expImage.getScaledWidth()); - } - mathDescSubSection.add(expImage); - } catch (Exception e) { - lg.error("Unable to add structure mapping image to report.", e); - } - } - writeSubDomainsEquationsAsImages(mathDescSection, mathDesc); - } + if (mathDesc == null) { + return; + } + Section mathDescSection = container.addSection("Math Description: " + mathDesc.getName(), container.getDepth() + 1); + Section mathDescSubSection = null; + Expression[] expArray; + BufferedImage dummy = new BufferedImage(500, 50, BufferedImage.TYPE_3BYTE_BGR); + int scale = 1; + int viewableWidth = (int) (document.getPageSize().getWidth() - document.leftMargin() - document.rightMargin()); + //add Constants + Enumeration constantsList = mathDesc.getConstants(); + while (constantsList.hasMoreElements()) { + Constant constant = constantsList.nextElement(); + Expression exp = constant.getExpression(); + try { + expArray = new Expression[]{Expression.assign(new Expression(constant.getName()), exp.flatten())}; + } catch (ExpressionException ee) { + lg.error("Unable to process constant " + constant.getName() + " for publishing", ee); + continue; + } + try { + Dimension dim = ExpressionCanvas.getExpressionImageSize(expArray, (Graphics2D) dummy.getGraphics()); + BufferedImage bufferedImage = new BufferedImage((int) dim.getWidth() * scale, (int) dim.getHeight() * scale, BufferedImage.TYPE_3BYTE_BGR); + ExpressionCanvas.getExpressionAsImage(expArray, bufferedImage, scale); + com.lowagie.text.Image expImage = com.lowagie.text.Image.getInstance(bufferedImage, null); + expImage.setAlignment(com.lowagie.text.Image.ALIGN_LEFT); + if (mathDescSubSection == null) { + mathDescSubSection = mathDescSection.addSection("Constants", mathDescSection.getDepth() + 1); + } + if (viewableWidth < Math.floor(expImage.getScaledWidth())) { + expImage.scaleToFit(viewableWidth, expImage.getPlainHeight()); + lg.debug("Constant After scaling: {}", expImage.getScaledWidth()); + } + mathDescSubSection.add(expImage); + } catch (Exception e) { + lg.error("Unable to add structure mapping image to report.", e); + } + } + mathDescSubSection = null; + //add functions + Enumeration functionsList = mathDesc.getFunctions(); + while (functionsList.hasMoreElements()) { + Function function = functionsList.nextElement(); + Expression exp = function.getExpression(); + try { + expArray = new Expression[]{Expression.assign(new Expression(function.getName()), exp.flatten())}; + } catch (ExpressionException ee) { + lg.error("Unable to process function " + function.getName() + " for publishing", ee); + continue; + } + try { + Dimension dim = ExpressionCanvas.getExpressionImageSize(expArray, (Graphics2D) dummy.getGraphics()); + BufferedImage bufferedImage = new BufferedImage((int) dim.getWidth() * scale, (int) dim.getHeight() * scale, BufferedImage.TYPE_3BYTE_BGR); + ExpressionCanvas.getExpressionAsImage(expArray, bufferedImage, scale); + com.lowagie.text.Image expImage = com.lowagie.text.Image.getInstance(bufferedImage, null); + expImage.setAlignment(com.lowagie.text.Image.ALIGN_LEFT); + if (mathDescSubSection == null) { + mathDescSubSection = mathDescSection.addSection("Functions", mathDescSection.getDepth() + 1); + } + if (viewableWidth < Math.floor(expImage.getScaledWidth())) { + expImage.scaleToFit(viewableWidth, expImage.getHeight()); + lg.debug("Function After scaling: {}", expImage.getScaledWidth()); + } + mathDescSubSection.add(expImage); + } catch (Exception e) { + lg.error("Unable to add structure mapping image to report.", e); + } + } + writeSubDomainsEquationsAsImages(mathDescSection, mathDesc); + } - //container can be a chapter or a section of a chapter. - //MathDescription.description ignored. - protected void writeMathDescAsText(Section container, MathDescription mathDesc) throws DocumentException { + //container can be a chapter or a section of a chapter. + //MathDescription.description ignored. + protected void writeMathDescAsText(Section container, MathDescription mathDesc) throws DocumentException { - if (mathDesc == null) { - return; - } - Section mathDescSection = container.addSection("Math Description: " + mathDesc.getName(), container.getDepth() + 1); - Section mathDescSubSection = null; - Table expTable = null; - int widths [] = {2, 8}; - //add Constants - Enumeration constantsList = mathDesc.getConstants(); - while (constantsList.hasMoreElements()) { - Constant constant = constantsList.nextElement(); - Expression exp = constant.getExpression(); - if (expTable == null) { - expTable = getTable(2, 100, 1, 2, 2); - expTable.addCell(createHeaderCell("Constant Name", getBold(), 1)); - expTable.addCell(createHeaderCell("Expression", getBold(), 1)); - expTable.setWidths(widths); - expTable.endHeaders(); - } - //widths[0] = Math.max(constant.getName().length(), widths[0]); - //widths[1] = Math.max(exp.infix().length(), widths[1]); - expTable.addCell(createCell(constant.getName(), getFont())); - expTable.addCell(createCell(exp.infix(), getFont())); - } - //expTable.setWidths(widths); breaks the contents of the cell, also, widths[1] = widths[1]/widths[0], widths[0] = 1 - if (expTable != null) { - mathDescSubSection = mathDescSection.addSection("Constants", mathDescSection.getDepth() + 1); - mathDescSubSection.add(expTable); - expTable = null; - } - mathDescSubSection = null; - //add functions - Enumeration functionsList = mathDesc.getFunctions(); - while (functionsList.hasMoreElements()) { - Function function = functionsList.nextElement(); - Expression exp = function.getExpression(); - if (expTable == null) { - expTable = getTable(2, 100, 1, 2, 2); - expTable.addCell(createHeaderCell("Function Name", getBold(), 1)); - expTable.addCell(createHeaderCell("Expression", getBold(), 1)); - expTable.endHeaders(); - expTable.setWidths(widths); - } - expTable.addCell(createCell(function.getName(), getFont())); - expTable.addCell(createCell(exp.infix(), getFont())); - } - if (expTable != null) { - mathDescSubSection = mathDescSection.addSection("Functions", mathDescSection.getDepth() + 1); - mathDescSubSection.add(expTable); - } - writeSubDomainsEquationsAsText(mathDescSection, mathDesc); - } + if (mathDesc == null) { + return; + } + Section mathDescSection = container.addSection("Math Description: " + mathDesc.getName(), container.getDepth() + 1); + Section mathDescSubSection; + Table expTable = null; + int[] widths = {2, 8}; + //add Constants + Enumeration constantsList = mathDesc.getConstants(); + while (constantsList.hasMoreElements()) { + Constant constant = constantsList.nextElement(); + Expression exp = constant.getExpression(); + if (expTable == null) { + expTable = getTable(2, 100, 1, 2, 2); + expTable.addCell(createHeaderCell("Constant Name", getBold(), 1)); + expTable.addCell(createHeaderCell("Expression", getBold(), 1)); + expTable.setWidths(widths); + expTable.endHeaders(); + } + //widths[0] = Math.max(constant.getName().length(), widths[0]); + //widths[1] = Math.max(exp.infix().length(), widths[1]); + expTable.addCell(createCell(constant.getName(), getFont())); + expTable.addCell(createCell(exp.infix(), getFont())); + } + //expTable.setWidths(widths); breaks the contents of the cell, also, widths[1] = widths[1]/widths[0], widths[0] = 1 + if (expTable != null) { + mathDescSubSection = mathDescSection.addSection("Constants", mathDescSection.getDepth() + 1); + mathDescSubSection.add(expTable); + expTable = null; + } + mathDescSubSection = null; + //add functions + Enumeration functionsList = mathDesc.getFunctions(); + while (functionsList.hasMoreElements()) { + Function function = functionsList.nextElement(); + Expression exp = function.getExpression(); + if (expTable == null) { + expTable = getTable(2, 100, 1, 2, 2); + expTable.addCell(createHeaderCell("Function Name", getBold(), 1)); + expTable.addCell(createHeaderCell("Expression", getBold(), 1)); + expTable.endHeaders(); + expTable.setWidths(widths); + } + expTable.addCell(createCell(function.getName(), getFont())); + expTable.addCell(createCell(exp.infix(), getFont())); + } + if (expTable != null) { + mathDescSubSection = mathDescSection.addSection("Functions", mathDescSection.getDepth() + 1); + mathDescSubSection.add(expTable); + } + writeSubDomainsEquationsAsText(mathDescSection, mathDesc); + } - public void writeMathModel(MathModel mathModel, FileOutputStream fos, PageFormat pageFormat) throws Exception { + public void writeMathModel(MathModel mathModel, FileOutputStream fos, PageFormat pageFormat) throws Exception { - writeMathModel(mathModel, fos, pageFormat, PublishPreferences.DEFAULT_MATH_PREF); - } + writeMathModel(mathModel, fos, pageFormat, PublishPreferences.DEFAULT_MATH_PREF); + } - public void writeMathModel(MathModel mathModel, FileOutputStream fos, PageFormat pageFormat, PublishPreferences preferences) throws Exception { + public void writeMathModel(MathModel mathModel, FileOutputStream fos, PageFormat pageFormat, PublishPreferences preferences) throws Exception { - if (mathModel == null || fos == null || pageFormat == null || preferences == null) { - throw new IllegalArgumentException("One or more null params while publishing MathModel."); - } - try { - createDocument(pageFormat); - createDocWriter(fos); - // Add metadata before you open the document... - String name = mathModel.getName().trim(); - String userName = "Unknown"; - if (mathModel.getVersion() != null) { - userName = mathModel.getVersion().getOwner().getName(); - } - document.addTitle(name + "[owned by " + userName + "]"); - document.addCreator("Virtual Cell"); - document.addCreationDate(); - //writeWatermark(document, pageFormat); - writeHeaderFooter("MathModel: " + name); - document.open(); - // - int chapterNum = 1; - Section introSection = null; - if (preferences.includeMathDesc()) { - MathDescription mathDesc = mathModel.getMathDescription(); - Chapter mathDescChapter = new Chapter("Math Description", chapterNum++); - introSection = mathDescChapter.addSection("General Info", mathDescChapter.getNumberDepth() + 1); - writeMetadata(introSection, name, mathModel.getDescription(), userName, "MathModel"); - writeMathDescAsText(mathDescChapter, mathDesc); - document.add(mathDescChapter); - } - if (preferences.includeSim()) { //unlike biomodels, simulations are chapters, not chapter sections. - Simulation [] sims = mathModel.getSimulations(); - if (sims != null) { - Chapter simChapter = new Chapter("Simulations", chapterNum++); - if (introSection == null) { - introSection = simChapter.addSection("General Info", simChapter.getNumberDepth() + 1); - writeMetadata(introSection, name, mathModel.getDescription(), userName, "MathModel"); - } - for (int i = 0; i < sims.length; i++) { - writeSimulation(simChapter, sims[i]); - } - document.add(simChapter); - } else { - System.err.println("Bad Request: No simulations to publish for Mathmodel: " + name); - } - } - if (preferences.includeGeom()) { //unlike biomodels, geometry is a chapter, not a chapter section. - Geometry geom = mathModel.getMathDescription().getGeometry(); - if (geom != null) { - Chapter geomChapter = new Chapter("Geometry", chapterNum++); - if (introSection == null) { - introSection = geomChapter.addSection("General Info", geomChapter.getNumberDepth() + 1); - writeMetadata(introSection, name, mathModel.getDescription(), userName, "MathModel"); - } - writeGeom(geomChapter, geom, null); - document.add(geomChapter); - } else { - System.err.println("Bad Request: No geometry to publish for Mathmodel: " + name); - } - } - document.close(); - } catch (DocumentException e) { - lg.error("Unable to publish MathModel.", e); - throw e; - } - } + if (mathModel == null || fos == null || pageFormat == null || preferences == null) { + throw new IllegalArgumentException("One or more null params while publishing MathModel."); + } + try { + createDocument(pageFormat); + createDocWriter(fos); + // Add metadata before you open the document... + String name = mathModel.getName().trim(); + String userName = "Unknown"; + if (mathModel.getVersion() != null) { + userName = mathModel.getVersion().getOwner().getName(); + } + document.addTitle(name + "[owned by " + userName + "]"); + document.addCreator("Virtual Cell"); + document.addCreationDate(); + //writeWatermark(document, pageFormat); + writeHeaderFooter("MathModel: " + name); + document.open(); + // + int chapterNum = 1; + Section introSection = null; + if (preferences.includeMathDesc()) { + MathDescription mathDesc = mathModel.getMathDescription(); + Chapter mathDescChapter = new Chapter("Math Description", chapterNum++); + introSection = mathDescChapter.addSection("General Info", mathDescChapter.getNumberDepth() + 1); + writeMetadata(introSection, name, mathModel.getDescription(), userName, "MathModel"); + writeMathDescAsText(mathDescChapter, mathDesc); + document.add(mathDescChapter); + } + if (preferences.includeSim()) { //unlike biomodels, simulations are chapters, not chapter sections. + Simulation[] sims = mathModel.getSimulations(); + if (sims != null) { + Chapter simChapter = new Chapter("Simulations", chapterNum++); + if (introSection == null) { + introSection = simChapter.addSection("General Info", simChapter.getNumberDepth() + 1); + writeMetadata(introSection, name, mathModel.getDescription(), userName, "MathModel"); + } + for (Simulation sim : sims) { + writeSimulation(simChapter, sim); + } + document.add(simChapter); + } else { + lg.error("Bad Request: No simulations to publish for Mathmodel: {}", name); + } + } + if (preferences.includeGeom()) { //unlike biomodels, geometry is a chapter, not a chapter section. + Geometry geom = mathModel.getMathDescription().getGeometry(); + if (geom != null) { + Chapter geomChapter = new Chapter("Geometry", chapterNum++); + if (introSection == null) { + introSection = geomChapter.addSection("General Info", geomChapter.getNumberDepth() + 1); + writeMetadata(introSection, name, mathModel.getDescription(), userName, "MathModel"); + } + writeGeom(geomChapter, geom, null); + document.add(geomChapter); + } else { + lg.error("Bad Request: No geometry to publish for Mathmodel: {}", name); + } + } + document.close(); + } catch (DocumentException e) { + lg.error("Unable to publish MathModel.", e); + throw e; + } + } - protected void writeMembraneMapping(Section simContextSection, SimulationContext simContext) throws DocumentException { + protected void writeMembraneMapping(Section simContextSection, SimulationContext simContext) throws DocumentException { - GeometryContext geoContext = simContext.getGeometryContext(); - if (geoContext == null) { - return; - } - Section memMapSection = null; - Table memMapTable = null; - StructureMapping structMappings [] = geoContext.getStructureMappings(); - for (int i = 0; i < structMappings.length; i++) { - MembraneMapping memMapping = null; - if (structMappings[i] instanceof FeatureMapping) { - continue; - } else { - memMapping = (MembraneMapping)structMappings[i]; - } - String structName = memMapping.getStructure().getName(); - String initVoltage = ""; - Expression tempExp = memMapping.getInitialVoltageParameter().getExpression(); - VCUnitDefinition tempUnit = memMapping.getInitialVoltageParameter().getUnitDefinition(); - if (tempExp != null) { - initVoltage = tempExp.infix(); - if (tempUnit != null) { - initVoltage += " " + tempUnit.getSymbolUnicode(); - } - } - String spCap = ""; - tempExp = memMapping.getSpecificCapacitanceParameter().getExpression(); - tempUnit = memMapping.getSpecificCapacitanceParameter().getUnitDefinition(); - if (tempExp != null) { - spCap = tempExp.infix(); - if (tempUnit != null) { - spCap += " " + tempUnit.getSymbolUnicode(); - } - } - if (memMapTable == null) { - memMapTable = getTable(4, 100, 1, 3, 3); - memMapTable.addCell(createCell("Electrical Mapping - Membrane Potential", getBold(DEF_HEADER_FONT_SIZE), 4, 1, Element.ALIGN_CENTER, true)); - memMapTable.addCell(createHeaderCell("Membrane", getBold(), 1)); - memMapTable.addCell(createHeaderCell("Calculate V (T/F)", getBold(), 1)); - memMapTable.addCell(createHeaderCell("V initial", getBold(), 1)); - memMapTable.addCell(createHeaderCell("Specific Capacitance", getBold(), 1)); - memMapTable.endHeaders(); - } - memMapTable.addCell(createCell(structName, getFont())); - memMapTable.addCell(createCell((memMapping.getCalculateVoltage() ? " T ": " F "), getFont())); - memMapTable.addCell(createCell(initVoltage, getFont())); - memMapTable.addCell(createCell(spCap, getFont())); - } - if (memMapTable != null) { - memMapSection = simContextSection.addSection("Membrane Mapping For " + simContext.getName(), simContextSection.getNumberDepth() + 1); - memMapSection.add(memMapTable); - } - int[] widths = {1, 1, 1, 5, 8}; - Table electTable = null; - ElectricalStimulus[] electricalStimuli = simContext.getElectricalStimuli(); - for (int j = 0; j < electricalStimuli.length; j++) { - if (j == 0) { - electTable = getTable(5, 100, 1, 3, 3); - electTable.addCell(createCell("Electrical Mapping - Electrical Stimulus", getBold(DEF_HEADER_FONT_SIZE), 5, 1, Element.ALIGN_CENTER, true)); - electTable.addCell(createHeaderCell("Stimulus Name", getBold(), 1)); - electTable.addCell(createHeaderCell("Current Name", getBold(), 1)); - electTable.addCell(createHeaderCell("Clamp Type", getBold(), 1)); - electTable.addCell(createHeaderCell("Voltage/Current Density", getBold(), 1)); - electTable.addCell(createHeaderCell("Clamp Device", getBold(), 1)); - electTable.endHeaders(); - } - String stimName = electricalStimuli[j].getName(); - String currName = ""; - String clampType = "", expStr = ""; - Expression tempExp = null; - VCUnitDefinition tempUnit = null; - if (electricalStimuli[j] instanceof CurrentDensityClampStimulus) { - CurrentDensityClampStimulus stimulus = (CurrentDensityClampStimulus) electricalStimuli[j]; - LocalParameter currentDensityParameter = stimulus.getCurrentDensityParameter(); - tempExp = currentDensityParameter.getExpression(); - tempUnit = currentDensityParameter.getUnitDefinition(); - clampType = "Current Density (deprecated)"; - } else if (electricalStimuli[j] instanceof TotalCurrentClampStimulus) { - TotalCurrentClampStimulus stimulus = (TotalCurrentClampStimulus) electricalStimuli[j]; - LocalParameter totalCurrentParameter = stimulus.getCurrentParameter(); - tempExp = totalCurrentParameter.getExpression(); - tempUnit = totalCurrentParameter.getUnitDefinition(); - clampType = "Current"; - } else if (electricalStimuli[j] instanceof VoltageClampStimulus) { - VoltageClampStimulus stimulus = (VoltageClampStimulus) electricalStimuli[j]; - Parameter voltageParameter = stimulus.getVoltageParameter(); - tempExp = voltageParameter.getExpression(); - tempUnit = voltageParameter.getUnitDefinition(); - clampType = "Voltage"; - } - if (tempExp != null) { - expStr = tempExp.infix(); - if (tempUnit != null) { - expStr += " " + tempUnit.getSymbolUnicode(); - } - } - electTable.addCell(createCell(stimName, getFont())); - electTable.addCell(createCell(currName, getFont())); - electTable.addCell(createCell(clampType, getFont())); - electTable.addCell(createCell(expStr, getFont())); - //add electrode info - Electrode electrode = electricalStimuli[j].getElectrode(); - if (electrode == null) { - electTable.addCell(createCell("N/A", getFont())); - } else { - Coordinate c = electrode.getPosition(); - String location = c.getX() + ", " + c.getY() + ", " + c.getZ(); - String featureName = electrode.getFeature().getName(); - electTable.addCell(createCell("(" + location + ") in " + featureName, getFont())); - } - } - if (electTable != null) { - if (memMapSection == null) { - memMapSection = simContextSection.addSection("Membrane Mapping For " + simContext.getName(), 1); - } - electTable.setWidths(widths); - memMapSection.add(electTable); - } - //add temperature - Table tempTable = getTable(1, 75, 1, 3, 3); - tempTable.setAlignment(Table.ALIGN_LEFT); - tempTable.addCell(createCell("Temperature: " + simContext.getTemperatureKelvin() + " K", getFont())); - if (memMapSection != null) { - memMapSection.add(tempTable); - } - } + GeometryContext geoContext = simContext.getGeometryContext(); + if (geoContext == null) { + return; + } + Section memMapSection = null; + Table memMapTable = null; + StructureMapping[] structMappings = geoContext.getStructureMappings(); + for (StructureMapping structMapping : structMappings) { + MembraneMapping memMapping; + if (structMapping instanceof FeatureMapping) { + continue; + } else { + memMapping = (MembraneMapping) structMapping; + } + String structName = memMapping.getStructure().getName(); + String initVoltage = ""; + Expression tempExp = memMapping.getInitialVoltageParameter().getExpression(); + VCUnitDefinition tempUnit = memMapping.getInitialVoltageParameter().getUnitDefinition(); + if (tempExp != null) { + initVoltage = tempExp.infix(); + if (tempUnit != null) { + initVoltage += " " + tempUnit.getSymbolUnicode(); + } + } + String spCap = ""; + tempExp = memMapping.getSpecificCapacitanceParameter().getExpression(); + tempUnit = memMapping.getSpecificCapacitanceParameter().getUnitDefinition(); + if (tempExp != null) { + spCap = tempExp.infix(); + if (tempUnit != null) { + spCap += " " + tempUnit.getSymbolUnicode(); + } + } + if (memMapTable == null) { + memMapTable = getTable(4, 100, 1, 3, 3); + memMapTable.addCell(createCell("Electrical Mapping - Membrane Potential", getBold(DEF_HEADER_FONT_SIZE), 4, 1, Element.ALIGN_CENTER, true)); + memMapTable.addCell(createHeaderCell("Membrane", getBold(), 1)); + memMapTable.addCell(createHeaderCell("Calculate V (T/F)", getBold(), 1)); + memMapTable.addCell(createHeaderCell("V initial", getBold(), 1)); + memMapTable.addCell(createHeaderCell("Specific Capacitance", getBold(), 1)); + memMapTable.endHeaders(); + } + memMapTable.addCell(createCell(structName, getFont())); + memMapTable.addCell(createCell((memMapping.getCalculateVoltage() ? " T " : " F "), getFont())); + memMapTable.addCell(createCell(initVoltage, getFont())); + memMapTable.addCell(createCell(spCap, getFont())); + } + if (memMapTable != null) { + memMapSection = simContextSection.addSection("Membrane Mapping For " + simContext.getName(), simContextSection.getNumberDepth() + 1); + memMapSection.add(memMapTable); + } + int[] widths = {1, 1, 1, 5, 8}; + Table electTable = null; + ElectricalStimulus[] electricalStimuli = simContext.getElectricalStimuli(); + for (int j = 0; j < electricalStimuli.length; j++) { + if (j == 0) { + electTable = getTable(5, 100, 1, 3, 3); + electTable.addCell(createCell("Electrical Mapping - Electrical Stimulus", getBold(DEF_HEADER_FONT_SIZE), 5, 1, Element.ALIGN_CENTER, true)); + electTable.addCell(createHeaderCell("Stimulus Name", getBold(), 1)); + electTable.addCell(createHeaderCell("Current Name", getBold(), 1)); + electTable.addCell(createHeaderCell("Clamp Type", getBold(), 1)); + electTable.addCell(createHeaderCell("Voltage/Current Density", getBold(), 1)); + electTable.addCell(createHeaderCell("Clamp Device", getBold(), 1)); + electTable.endHeaders(); + } + String stimName = electricalStimuli[j].getName(); + String currName = ""; + String clampType = "", expStr = ""; + Expression tempExp = null; + VCUnitDefinition tempUnit = null; + if (electricalStimuli[j] instanceof CurrentDensityClampStimulus stimulus) { + LocalParameter currentDensityParameter = stimulus.getCurrentDensityParameter(); + tempExp = currentDensityParameter.getExpression(); + tempUnit = currentDensityParameter.getUnitDefinition(); + clampType = "Current Density (deprecated)"; + } else if (electricalStimuli[j] instanceof TotalCurrentClampStimulus stimulus) { + LocalParameter totalCurrentParameter = stimulus.getCurrentParameter(); + tempExp = totalCurrentParameter.getExpression(); + tempUnit = totalCurrentParameter.getUnitDefinition(); + clampType = "Current"; + } else if (electricalStimuli[j] instanceof VoltageClampStimulus stimulus) { + Parameter voltageParameter = stimulus.getVoltageParameter(); + tempExp = voltageParameter.getExpression(); + tempUnit = voltageParameter.getUnitDefinition(); + clampType = "Voltage"; + } + if (tempExp != null) { + expStr = tempExp.infix(); + if (tempUnit != null) { + expStr += " " + tempUnit.getSymbolUnicode(); + } + } + electTable.addCell(createCell(stimName, getFont())); + electTable.addCell(createCell(currName, getFont())); + electTable.addCell(createCell(clampType, getFont())); + electTable.addCell(createCell(expStr, getFont())); + //add electrode info + Electrode electrode = electricalStimuli[j].getElectrode(); + if (electrode == null) { + electTable.addCell(createCell("N/A", getFont())); + } else { + Coordinate c = electrode.getPosition(); + String location = c.getX() + ", " + c.getY() + ", " + c.getZ(); + String featureName = electrode.getFeature().getName(); + electTable.addCell(createCell("(" + location + ") in " + featureName, getFont())); + } + } + if (electTable != null) { + if (memMapSection == null) { + memMapSection = simContextSection.addSection("Membrane Mapping For " + simContext.getName(), 1); + } + electTable.setWidths(widths); + memMapSection.add(electTable); + } + //add temperature + Table tempTable = getTable(1, 75, 1, 3, 3); + tempTable.setAlignment(Table.ALIGN_LEFT); + tempTable.addCell(createCell("Temperature: " + simContext.getTemperatureKelvin() + " K", getFont())); + if (memMapSection != null) { + memMapSection.add(tempTable); + } + } - protected void writeMemRegionEquation(Section container, MembraneRegionEquation eq) throws DocumentException { - - Table eqTable = getTable(2, 100, 1, 2, 2); - eqTable.addCell(createCell(VCML.MembraneRegionEquation + " " + eq.getVariable().getName(), - getBold(DEF_HEADER_FONT_SIZE), 2, 1, Element.ALIGN_CENTER, true)); - eqTable.endHeaders(); - String exp = "0.0"; - eqTable.addCell(createCell(VCML.UniformRate, getFont())); - if (eq.getUniformRateExpression() != null) { - exp = eq.getUniformRateExpression().infix(); - } - exp = "0.0"; - eqTable.addCell(createCell(VCML.MembraneRate, getFont())); - if (eq.getMembraneRateExpression() != null) { - exp = eq.getMembraneRateExpression().infix(); - } - eqTable.addCell(createCell(exp, getFont())); - if (eq.getInitialExpression() != null) { - eqTable.addCell(createCell(VCML.Initial, getFont())); - eqTable.addCell(createCell(eq.getInitialExpression().infix(), getFont())); - } - int solutionType = eq.getSolutionType(); - switch (solutionType) { - case Equation.UNKNOWN_SOLUTION:{ - if (eq.getInitialExpression() == null) { - eqTable.addCell(createCell(VCML.Initial, getFont())); - eqTable.addCell(createCell("0.0", getFont())); - } - break; - } - case Equation.EXACT_SOLUTION:{ - eqTable.addCell(createCell(VCML.Exact, getFont())); - eqTable.addCell(createCell(eq.getExactSolution().infix(), getFont())); - break; - } - } - - container.add(eqTable); - } + protected void writeMemRegionEquation(Section container, MembraneRegionEquation eq) throws DocumentException { + Table eqTable = getTable(2, 100, 1, 2, 2); + eqTable.addCell(createCell(VCML.MembraneRegionEquation + " " + eq.getVariable().getName(), + getBold(DEF_HEADER_FONT_SIZE), 2, 1, Element.ALIGN_CENTER, true)); + eqTable.endHeaders(); + String exp; + eqTable.addCell(createCell(VCML.UniformRate, getFont())); + if (eq.getUniformRateExpression() != null) { + exp = eq.getUniformRateExpression().infix(); + } + exp = "0.0"; + eqTable.addCell(createCell(VCML.MembraneRate, getFont())); + if (eq.getMembraneRateExpression() != null) { + exp = eq.getMembraneRateExpression().infix(); + } + eqTable.addCell(createCell(exp, getFont())); + if (eq.getInitialExpression() != null) { + eqTable.addCell(createCell(VCML.Initial, getFont())); + eqTable.addCell(createCell(eq.getInitialExpression().infix(), getFont())); + } + int solutionType = eq.getSolutionType(); + switch (solutionType) { + case Equation.UNKNOWN_SOLUTION: { + if (eq.getInitialExpression() == null) { + eqTable.addCell(createCell(VCML.Initial, getFont())); + eqTable.addCell(createCell("0.0", getFont())); + } + break; + } + case Equation.EXACT_SOLUTION: { + eqTable.addCell(createCell(VCML.Exact, getFont())); + eqTable.addCell(createCell(eq.getExactSolution().infix(), getFont())); + break; + } + } -protected void writeMetadata(Section metaSection, String name, String description, String userName, String type) throws DocumentException { + container.add(eqTable); + } - Table metaTable = getTable(1, 100, 0, 3, 3); - // - if (name != null && name.trim().length() > 0) { - metaTable.addCell(createCell(type + " Name: " + name.trim(), getBold(DEF_HEADER_FONT_SIZE))); - } - if (description != null && description.trim().length() > 0) { - metaTable.addCell(createCell(type + " Description: " + description.trim(), getBold(DEF_HEADER_FONT_SIZE))); - } - if (userName != null) { - metaTable.addCell(createCell("Owner: " + userName, getBold(DEF_HEADER_FONT_SIZE))); - } - // - metaSection.add(metaTable); -} + protected void writeMetadata(Section metaSection, String name, String description, String userName, String type) throws DocumentException { -//model description ignored. -protected void writeModel(Chapter physioChapter, Model model) throws DocumentException { + Table metaTable = getTable(1, 100, 0, 3, 3); + // + if (name != null && !name.trim().isEmpty()) { + metaTable.addCell(createCell(type + " Name: " + name.trim(), getBold(DEF_HEADER_FONT_SIZE))); + } + if (description != null && !description.trim().isEmpty()) { + metaTable.addCell(createCell(type + " Description: " + description.trim(), getBold(DEF_HEADER_FONT_SIZE))); + } + if (userName != null) { + metaTable.addCell(createCell("Owner: " + userName, getBold(DEF_HEADER_FONT_SIZE))); + } + // + metaSection.add(metaTable); + } + + + //model description ignored. + protected void writeModel(Chapter physioChapter, Model model) throws DocumentException { - Section structSection = null; - //add structures image + Section structSection = null; + //add structures image // if (model.getNumStructures() > 0) { // try { // ByteArrayOutputStream bos = generateDocStructureImage(model, ITextWriter.LOW_RESOLUTION); @@ -1592,608 +1586,605 @@ protected void writeModel(Chapter physioChapter, Model model) throws DocumentExc // lg.error(e); // } // } - //write structures - Table structTable = null; - for (int i = 0; i < model.getNumStructures(); i++) { - if (structTable == null) { - structTable = getTable(4, 100, 1, 3, 3); - structTable.addCell(createCell("Structures", getBold(DEF_HEADER_FONT_SIZE), 4, 1, Element.ALIGN_CENTER, true)); - structTable.addCell(createHeaderCell("Name", getFont(), 1)); - structTable.addCell(createHeaderCell("Type", getFont(), 1)); - structTable.addCell(createHeaderCell("Inside", getFont(), 1)); - structTable.addCell(createHeaderCell("Outside", getFont(), 1)); - structTable.endHeaders(); - } - writeStructure(model, model.getStructure(i), structTable); - } - + //write structures + Table structTable = null; + for (int i = 0; i < model.getNumStructures(); i++) { + if (structTable == null) { + structTable = getTable(4, 100, 1, 3, 3); + structTable.addCell(createCell("Structures", getBold(DEF_HEADER_FONT_SIZE), 4, 1, Element.ALIGN_CENTER, true)); + structTable.addCell(createHeaderCell("Name", getFont(), 1)); + structTable.addCell(createHeaderCell("Type", getFont(), 1)); + structTable.addCell(createHeaderCell("Inside", getFont(), 1)); + structTable.addCell(createHeaderCell("Outside", getFont(), 1)); + structTable.endHeaders(); + } + writeStructure(model, model.getStructure(i), structTable); + } + // if (structTable != null) { // structSection.add(structTable); // } - //write reactions - writeReactions(physioChapter, model); -} + //write reactions + writeReactions(physioChapter, model); + } + + protected void writeOdeEquation(Section container, OdeEquation eq) throws DocumentException { + + Table eqTable = getTable(2, 100, 1, 2, 2); + eqTable.addCell(createCell(VCML.OdeEquation + " " + eq.getVariable().getName(), + getBold(DEF_HEADER_FONT_SIZE), 2, 1, Element.ALIGN_CENTER, true)); + eqTable.endHeaders(); + String exp = "0.0"; + eqTable.addCell(createCell(VCML.Rate, getFont())); + if (eq.getRateExpression() != null) { + exp = eq.getRateExpression().infix(); + } + eqTable.addCell(createCell(exp, getFont())); + if (eq.getInitialExpression() != null) { + eqTable.addCell(createCell(VCML.Initial, getFont())); + eqTable.addCell(createCell(eq.getInitialExpression().infix(), getFont())); + } + int solutionType = eq.getSolutionType(); + switch (solutionType) { + case Equation.UNKNOWN_SOLUTION: { + if (eq.getInitialExpression() == null) { + eqTable.addCell(createCell(VCML.Initial, getFont())); + eqTable.addCell(createCell("0.0", getFont())); + } + break; + } + case Equation.EXACT_SOLUTION: { + eqTable.addCell(createCell(VCML.Exact, getFont())); + eqTable.addCell(createCell(eq.getExactSolution().infix(), getFont())); + break; + } + } - protected void writeOdeEquation(Section container, OdeEquation eq) throws DocumentException { + container.add(eqTable); + } - Table eqTable = getTable(2, 100, 1, 2, 2); - eqTable.addCell(createCell(VCML.OdeEquation + " " + eq.getVariable().getName(), - getBold(DEF_HEADER_FONT_SIZE), 2, 1, Element.ALIGN_CENTER, true)); - eqTable.endHeaders(); - String exp = "0.0"; - eqTable.addCell(createCell(VCML.Rate, getFont())); - if (eq.getRateExpression() != null) { - exp = eq.getRateExpression().infix(); - } - eqTable.addCell(createCell(exp, getFont())); - if (eq.getInitialExpression() != null) { - eqTable.addCell(createCell(VCML.Initial, getFont())); - eqTable.addCell(createCell(eq.getInitialExpression().infix(), getFont())); - } - int solutionType = eq.getSolutionType(); - switch (solutionType) { - case Equation.UNKNOWN_SOLUTION:{ - if (eq.getInitialExpression() == null) { - eqTable.addCell(createCell(VCML.Initial, getFont())); - eqTable.addCell(createCell("0.0", getFont())); - } - break; - } - case Equation.EXACT_SOLUTION:{ - eqTable.addCell(createCell(VCML.Exact, getFont())); - eqTable.addCell(createCell(eq.getExactSolution().infix(), getFont())); - break; - } - } - - container.add(eqTable); - } + protected void writePdeEquation(Section container, PdeEquation eq) throws DocumentException { - protected void writePdeEquation(Section container, PdeEquation eq) throws DocumentException { + Table eqTable = getTable(2, 100, 1, 2, 2); + eqTable.addCell(createCell(VCML.PdeEquation + " " + eq.getVariable().getName(), + getBold(DEF_HEADER_FONT_SIZE), 2, 1, Element.ALIGN_CENTER, true)); + eqTable.endHeaders(); + if (eq.getBoundaryXm() != null) { + eqTable.addCell(createCell(VCML.BoundaryXm, getFont())); + eqTable.addCell(createCell(eq.getBoundaryXm().infix(), getFont())); + } + if (eq.getBoundaryXp() != null) { + eqTable.addCell(createCell(VCML.BoundaryXp, getFont())); + eqTable.addCell(createCell(eq.getBoundaryXp().infix(), getFont())); + } + if (eq.getBoundaryYm() != null) { + eqTable.addCell(createCell(VCML.BoundaryYm, getFont())); + eqTable.addCell(createCell(eq.getBoundaryYm().infix(), getFont())); + } + if (eq.getBoundaryYp() != null) { + eqTable.addCell(createCell(VCML.BoundaryYp, getFont())); + eqTable.addCell(createCell(eq.getBoundaryYp().infix(), getFont())); + } + if (eq.getBoundaryZm() != null) { + eqTable.addCell(createCell(VCML.BoundaryZm, getFont())); + eqTable.addCell(createCell(eq.getBoundaryZm().infix(), getFont())); + } + if (eq.getBoundaryZp() != null) { + eqTable.addCell(createCell(VCML.BoundaryZp, getFont())); + eqTable.addCell(createCell(eq.getBoundaryZp().infix(), getFont())); + } + if (eq.getVelocityX() != null) { + eqTable.addCell(createCell(VCML.VelocityX, getFont())); + eqTable.addCell(createCell(eq.getVelocityX().infix(), getFont())); + } + if (eq.getVelocityY() != null) { + eqTable.addCell(createCell(VCML.VelocityY, getFont())); + eqTable.addCell(createCell(eq.getVelocityY().infix(), getFont())); + } + if (eq.getVelocityZ() != null) { + eqTable.addCell(createCell(VCML.VelocityZ, getFont())); + eqTable.addCell(createCell(eq.getVelocityZ().infix(), getFont())); + } + String exp = "0.0"; + if (eq.getRateExpression() != null) { + exp = eq.getRateExpression().infix(); + } + eqTable.addCell(createCell(VCML.Rate, getFont())); + eqTable.addCell(createCell(exp, getFont())); + exp = "0.0"; + if (eq.getDiffusionExpression() != null) { + exp = eq.getDiffusionExpression().infix(); + } + eqTable.addCell(createCell(VCML.Diffusion, getFont())); + eqTable.addCell(createCell(exp, getFont())); + if (eq.getInitialExpression() != null) { + eqTable.addCell(createCell(VCML.Initial, getFont())); + eqTable.addCell(createCell(eq.getInitialExpression().infix(), getFont())); + } + int solutionType = eq.getSolutionType(); + switch (solutionType) { + case Equation.UNKNOWN_SOLUTION: { + if (eq.getInitialExpression() == null) { + eqTable.addCell(createCell(VCML.Initial, getFont())); + eqTable.addCell(createCell("0.0", getFont())); + } + break; + } + case Equation.EXACT_SOLUTION: { + eqTable.addCell(createCell(VCML.Exact, getFont())); + eqTable.addCell(createCell(eq.getExactSolution().infix(), getFont())); + break; + } + } - Table eqTable = getTable(2, 100, 1, 2, 2); - eqTable.addCell(createCell(VCML.PdeEquation + " " + eq.getVariable().getName(), - getBold(DEF_HEADER_FONT_SIZE), 2, 1, Element.ALIGN_CENTER, true)); - eqTable.endHeaders(); - if (eq.getBoundaryXm() != null) { - eqTable.addCell(createCell(VCML.BoundaryXm, getFont())); - eqTable.addCell(createCell(eq.getBoundaryXm().infix(), getFont())); - } - if (eq.getBoundaryXp() != null) { - eqTable.addCell(createCell(VCML.BoundaryXp, getFont())); - eqTable.addCell(createCell(eq.getBoundaryXp().infix(), getFont())); - } - if (eq.getBoundaryYm() != null) { - eqTable.addCell(createCell(VCML.BoundaryYm, getFont())); - eqTable.addCell(createCell(eq.getBoundaryYm().infix(), getFont())); - } - if (eq.getBoundaryYp() != null) { - eqTable.addCell(createCell(VCML.BoundaryYp, getFont())); - eqTable.addCell(createCell(eq.getBoundaryYp().infix(), getFont())); - } - if (eq.getBoundaryZm() != null) { - eqTable.addCell(createCell(VCML.BoundaryZm, getFont())); - eqTable.addCell(createCell(eq.getBoundaryZm().infix(), getFont())); - } - if (eq.getBoundaryZp() != null) { - eqTable.addCell(createCell(VCML.BoundaryZp, getFont())); - eqTable.addCell(createCell(eq.getBoundaryZp().infix(), getFont())); - } - if (eq.getVelocityX() != null) { - eqTable.addCell(createCell(VCML.VelocityX, getFont())); - eqTable.addCell(createCell(eq.getVelocityX().infix(), getFont())); - } - if (eq.getVelocityY() != null) { - eqTable.addCell(createCell(VCML.VelocityY, getFont())); - eqTable.addCell(createCell(eq.getVelocityY().infix(), getFont())); - } - if (eq.getVelocityZ() != null) { - eqTable.addCell(createCell(VCML.VelocityZ, getFont())); - eqTable.addCell(createCell(eq.getVelocityZ().infix(), getFont())); - } - String exp = "0.0"; - if (eq.getRateExpression() != null) { - exp = eq.getRateExpression().infix(); - } - eqTable.addCell(createCell(VCML.Rate, getFont())); - eqTable.addCell(createCell(exp, getFont())); - exp = "0.0"; - if (eq.getDiffusionExpression() != null) { - exp = eq.getDiffusionExpression().infix(); - } - eqTable.addCell(createCell(VCML.Diffusion, getFont())); - eqTable.addCell(createCell(exp, getFont())); - if (eq.getInitialExpression() != null) { - eqTable.addCell(createCell(VCML.Initial, getFont())); - eqTable.addCell(createCell(eq.getInitialExpression().infix(), getFont())); - } - int solutionType = eq.getSolutionType(); - switch (solutionType) { - case Equation.UNKNOWN_SOLUTION:{ - if (eq.getInitialExpression() == null) { - eqTable.addCell(createCell(VCML.Initial, getFont())); - eqTable.addCell(createCell("0.0", getFont())); - } - break; - } - case Equation.EXACT_SOLUTION:{ - eqTable.addCell(createCell(VCML.Exact, getFont())); - eqTable.addCell(createCell(eq.getExactSolution().infix(), getFont())); - break; - } - } - - container.add(eqTable); - } + container.add(eqTable); + } -//ReactionContext - SpeciesContextSpec: ignored boundary conditions. - protected void writeReactionContext(Section simContextSection, SimulationContext simContext) throws DocumentException { + //ReactionContext - SpeciesContextSpec: ignored boundary conditions. + protected void writeReactionContext(Section simContextSection, SimulationContext simContext) throws DocumentException { - ReactionContext rc = simContext.getReactionContext(); - if (rc == null) { - return; - } - Section rcSection = null; - //add reaction specs - ReactionSpec reactionSpecs [] = rc.getReactionSpecs(); - Table reactionSpecTable = null; - for (int i = 0; i < reactionSpecs.length; i++) { - if (i == 0) { - reactionSpecTable = getTable(4, 100, 1, 3, 3); - //reactionSpecTable.setTableFitsPage(true); - reactionSpecTable.addCell(createCell("Reaction Mapping", getBold(DEF_HEADER_FONT_SIZE), 4, 1, Element.ALIGN_CENTER, true)); - reactionSpecTable.addCell(createHeaderCell("Name", getBold(), 1)); - reactionSpecTable.addCell(createHeaderCell("Type", getBold(), 1)); - reactionSpecTable.addCell(createHeaderCell("Enabled (T/F)", getBold(), 1)); - reactionSpecTable.addCell(createHeaderCell("Fast (T/F)", getBold(), 1)); - reactionSpecTable.endHeaders(); - } - String reactionName = reactionSpecs[i].getReactionStep().getName(); - String reactionType = reactionSpecs[i].getReactionStep().getDisplayType(); - reactionSpecTable.addCell(createCell(reactionName, getFont())); - reactionSpecTable.addCell(createCell(reactionType, getFont())); - reactionSpecTable.addCell(createCell((reactionSpecs[i].isExcluded() ? " F ": " T "), getFont())); - reactionSpecTable.addCell(createCell((reactionSpecs[i].isFast() ? " T ": " F "), getFont())); - } - if (reactionSpecTable != null) { - rcSection = simContextSection.addSection("Reaction Mapping For " + simContext.getName(), simContextSection.getNumberDepth() + 1); - rcSection.add(reactionSpecTable); - } - - //add species context specs - SpeciesContextSpec speciesContSpecs [] = rc.getSpeciesContextSpecs(); - Table speciesSpecTable = null; - int widths [] = {2, 2, 4, 4, 1}; - for (int i = 0; i < speciesContSpecs.length; i++) { - if (i == 0) { - speciesSpecTable = getTable(5, 100, 1, 3, 3); - speciesSpecTable.addCell(createCell("Initial Conditions", getBold(DEF_HEADER_FONT_SIZE), 5, 1, Element.ALIGN_CENTER, true)); - speciesSpecTable.addCell(createHeaderCell("Species", getBold(), 1)); - speciesSpecTable.addCell(createHeaderCell("Structure", getBold(), 1)); - speciesSpecTable.addCell(createHeaderCell("Initial Conc.", getBold(), 1)); - speciesSpecTable.addCell(createHeaderCell("Diffusion Const.", getBold(), 1)); - speciesSpecTable.addCell(createHeaderCell("Fixed (T/F)", getBold(), 1)); - speciesSpecTable.endHeaders(); - } - String speciesName = speciesContSpecs[i].getSpeciesContext().getSpecies().getCommonName(); - String structName = speciesContSpecs[i].getSpeciesContext().getStructure().getName(); - String diff = speciesContSpecs[i].getDiffusionParameter().getExpression().infix(); - VCUnitDefinition diffUnit = speciesContSpecs[i].getDiffusionParameter().getUnitDefinition(); - SpeciesContextSpecParameter initParam = speciesContSpecs[i].getInitialConditionParameter(); - String initConc = initParam == null? "" :initParam.getExpression().infix(); - VCUnitDefinition initConcUnit = initParam == null? null : initParam.getUnitDefinition(); - speciesSpecTable.addCell(createCell(speciesName, getFont())); - speciesSpecTable.addCell(createCell(structName, getFont())); - speciesSpecTable.addCell(createCell(initConc + (initConcUnit == null ? "": " " + initConcUnit.getSymbolUnicode()), getFont())); - speciesSpecTable.addCell(createCell(diff + (diffUnit == null ? "": " " + diffUnit.getSymbolUnicode()), getFont())); - speciesSpecTable.addCell(createCell((speciesContSpecs[i].isConstant() ? " T ": " F "), getFont())); - } - if (speciesSpecTable != null) { - if (rcSection == null) { - rcSection = simContextSection.addSection("Reaction Mapping For " + simContext.getName(), simContextSection.getNumberDepth() + 1); - } - speciesSpecTable.setWidths(widths); - rcSection.add(speciesSpecTable); - } - } + ReactionContext rc = simContext.getReactionContext(); + if (rc == null) { + return; + } + Section rcSection = null; + //add reaction specs + ReactionSpec[] reactionSpecs = rc.getReactionSpecs(); + Table reactionSpecTable = null; + for (int i = 0; i < reactionSpecs.length; i++) { + if (i == 0) { + reactionSpecTable = getTable(4, 100, 1, 3, 3); + //reactionSpecTable.setTableFitsPage(true); + reactionSpecTable.addCell(createCell("Reaction Mapping", getBold(DEF_HEADER_FONT_SIZE), 4, 1, Element.ALIGN_CENTER, true)); + reactionSpecTable.addCell(createHeaderCell("Name", getBold(), 1)); + reactionSpecTable.addCell(createHeaderCell("Type", getBold(), 1)); + reactionSpecTable.addCell(createHeaderCell("Enabled (T/F)", getBold(), 1)); + reactionSpecTable.addCell(createHeaderCell("Fast (T/F)", getBold(), 1)); + reactionSpecTable.endHeaders(); + } + String reactionName = reactionSpecs[i].getReactionStep().getName(); + String reactionType = reactionSpecs[i].getReactionStep().getDisplayType(); + reactionSpecTable.addCell(createCell(reactionName, getFont())); + reactionSpecTable.addCell(createCell(reactionType, getFont())); + reactionSpecTable.addCell(createCell((reactionSpecs[i].isExcluded() ? " F " : " T "), getFont())); + reactionSpecTable.addCell(createCell((reactionSpecs[i].isFast() ? " T " : " F "), getFont())); + } + if (reactionSpecTable != null) { + rcSection = simContextSection.addSection("Reaction Mapping For " + simContext.getName(), simContextSection.getNumberDepth() + 1); + rcSection.add(reactionSpecTable); + } + //add species context specs + SpeciesContextSpec[] speciesContSpecs = rc.getSpeciesContextSpecs(); + Table speciesSpecTable = null; + int[] widths = {2, 2, 4, 4, 1}; + for (int i = 0; i < speciesContSpecs.length; i++) { + if (i == 0) { + speciesSpecTable = getTable(5, 100, 1, 3, 3); + speciesSpecTable.addCell(createCell("Initial Conditions", getBold(DEF_HEADER_FONT_SIZE), 5, 1, Element.ALIGN_CENTER, true)); + speciesSpecTable.addCell(createHeaderCell("Species", getBold(), 1)); + speciesSpecTable.addCell(createHeaderCell("Structure", getBold(), 1)); + speciesSpecTable.addCell(createHeaderCell("Initial Conc.", getBold(), 1)); + speciesSpecTable.addCell(createHeaderCell("Diffusion Const.", getBold(), 1)); + speciesSpecTable.addCell(createHeaderCell("Fixed (T/F)", getBold(), 1)); + speciesSpecTable.endHeaders(); + } + String speciesName = speciesContSpecs[i].getSpeciesContext().getSpecies().getCommonName(); + String structName = speciesContSpecs[i].getSpeciesContext().getStructure().getName(); + String diff = speciesContSpecs[i].getDiffusionParameter().getExpression().infix(); + VCUnitDefinition diffUnit = speciesContSpecs[i].getDiffusionParameter().getUnitDefinition(); + SpeciesContextSpecParameter initParam = speciesContSpecs[i].getInitialConditionParameter(); + String initConc = initParam == null ? "" : initParam.getExpression().infix(); + VCUnitDefinition initConcUnit = initParam == null ? null : initParam.getUnitDefinition(); + speciesSpecTable.addCell(createCell(speciesName, getFont())); + speciesSpecTable.addCell(createCell(structName, getFont())); + speciesSpecTable.addCell(createCell(initConc + (initConcUnit == null ? "" : " " + initConcUnit.getSymbolUnicode()), getFont())); + speciesSpecTable.addCell(createCell(diff + (diffUnit == null ? "" : " " + diffUnit.getSymbolUnicode()), getFont())); + speciesSpecTable.addCell(createCell((speciesContSpecs[i].isConstant() ? " T " : " F "), getFont())); + } + if (speciesSpecTable != null) { + if (rcSection == null) { + rcSection = simContextSection.addSection("Reaction Mapping For " + simContext.getName(), simContextSection.getNumberDepth() + 1); + } + speciesSpecTable.setWidths(widths); + rcSection.add(speciesSpecTable); + } + } + + + private Cell getReactionArrowImageCell(boolean bReversible) { + // Create image for arrow(s) + int imageWidth = 150; + int imageHeight = 50; + BufferedImage bufferedImage = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_3BYTE_BGR); + + Graphics2D g = (Graphics2D) bufferedImage.getGraphics(); + g.setClip(0, 0, imageWidth, imageHeight); + g.setColor(Color.white); + g.fillRect(0, 0, imageWidth, imageHeight); + g.setColor(Color.black); + int fontSize = 12; + g.setFont(new java.awt.Font("SansSerif", Font.BOLD, fontSize)); + + // get image for reaction equation arrows + // Draw the arrows on canvas/image + if (bReversible) { + // Forward *AND* Reverse (bi-directional) arrow + java.awt.Polygon arrow = new java.awt.Polygon(new int[]{20, 40, 40, 110, 110, 130, 110, 110, 40, 40}, + new int[]{25, 14, 22, 22, 14, 25, 36, 28, 28, 36}, 10); + g.fill(arrow); + } else { + // Only Forward Arrow + java.awt.Polygon arrow = new java.awt.Polygon(new int[]{20, 110, 110, 130, 110, 110, 20}, + new int[]{22, 22, 14, 25, 36, 28, 28}, 7); + g.fill(arrow); + } - private Cell getReactionArrowImageCell(boolean bReversible) throws DocumentException { - // Create image for arrow(s) - int imageWidth = 150; - int imageHeight = 50; - BufferedImage bufferedImage = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_3BYTE_BGR); - - Graphics2D g = (Graphics2D)bufferedImage.getGraphics(); - g.setClip(0, 0, imageWidth, imageHeight); - g.setColor(Color.white); - g.fillRect(0, 0, imageWidth, imageHeight); - g.setColor(Color.black); - int fontSize = 12; - g.setFont(new java.awt.Font("SansSerif", Font.BOLD, fontSize)); - - // get image for reaction equation arrows - // Draw the arrows on canvas/image - if (bReversible){ - // Forward *AND* Reverse (bi-directional) arrow - java.awt.Polygon arrow = new java.awt.Polygon( new int[] {20, 40, 40, 110, 110, 130, 110, 110, 40, 40}, - new int[] {25, 14, 22, 22, 14, 25, 36, 28, 28, 36}, 10); - g.fill(arrow); - } else { - // Only Forward Arrow - java.awt.Polygon arrow = new java.awt.Polygon( new int[] {20, 110, 110, 130, 110, 110, 20}, - new int[] {22, 22, 14, 25, 36, 28, 28}, 7); - g.fill(arrow); - } - - Cell imageCell = null; - try { - com.lowagie.text.Image rpImage = com.lowagie.text.Image.getInstance(bufferedImage, null); - rpImage.setAlignment(com.lowagie.text.Image.MIDDLE); - imageCell = new Cell(); - imageCell.add(rpImage); - } catch (Exception e) { - lg.error("Unable to add structure mapping image to report.", e); - } + Cell imageCell = null; + try { + com.lowagie.text.Image rpImage = com.lowagie.text.Image.getInstance(bufferedImage, null); + rpImage.setAlignment(com.lowagie.text.Image.MIDDLE); + imageCell = new Cell(); + imageCell.add(rpImage); + } catch (Exception e) { + lg.error("Unable to add structure mapping image to report.", e); + } - return imageCell; - } + return imageCell; + } - -//each reaction has its own table, ordered by the structures. - protected void writeReactions(Chapter physioChapter, Model model) throws DocumentException { - if (model == null) { - return; - } - Paragraph reactionParagraph = new Paragraph(); - reactionParagraph.add(new Chunk("Structures and Reactions Diagram").setLocalDestination(model.getName())); - Section reactionDiagramSection = physioChapter.addSection(reactionParagraph, physioChapter.getNumberDepth() + 1); - try{ - addImage(reactionDiagramSection, encodeJPEG(generateDocReactionsImage(model,null))); - }catch(Exception e){ - lg.error(e.getMessage(), e); - throw new DocumentException(e.getClass().getName()+": "+e.getMessage()); - } + //each reaction has its own table, ordered by the structures. + protected void writeReactions(Chapter physioChapter, Model model) throws DocumentException { - - for (int i = 0; i < model.getNumStructures(); i++) { - ReactionStep[] reactionSteps = model.getReactionSteps(); - ReactionStep rs = null; - Table modifierTable = null; - Table reactionTable = null; - boolean firstTime = true; - Section reactStructSection = null; - for (int j = 0; j < reactionSteps.length; j++) { - if (reactionSteps[j].getStructure() == model.getStructure(i)) { //can also use structureName1.equals(structureName2) - if (firstTime) { - Paragraph linkParagraph = new Paragraph(); - linkParagraph.add(new Chunk("Reaction(s) in " + model.getStructure(i).getName()).setLocalDestination(model.getStructure(i).getName())); - reactStructSection = physioChapter.addSection(linkParagraph, physioChapter.getNumberDepth() + 1); - firstTime = false; - } - rs = reactionSteps[j]; - String type; - if (rs instanceof SimpleReaction) { - type = "Reaction"; - } else { - type = "Flux"; - } - //write Reaction equation as a table + if (model == null) { + return; + } + Paragraph reactionParagraph = new Paragraph(); + reactionParagraph.add(new Chunk("Structures and Reactions Diagram").setLocalDestination(model.getName())); + Section reactionDiagramSection = physioChapter.addSection(reactionParagraph, physioChapter.getNumberDepth() + 1); + try { + addImage(reactionDiagramSection, encodeJPEG(generateDocReactionsImage(model, null))); + } catch (Exception e) { + lg.error(e.getMessage(), e); + throw new DocumentException(e.getClass().getName() + ": " + e.getMessage()); + } - // Get the image arrow cell depending on type of reactionStep : MassAction => double arrow, otherwise, forward arrow - boolean bReversible = false; - if (rs.getKinetics() instanceof MassActionKinetics) { - bReversible = true; - } - Cell arrowImageCell = getReactionArrowImageCell(bReversible); - - // Get reactants and products strings - ReactionCanvas rc = new ReactionCanvas(); - rc.setReactionStep(rs); - ReactionCanvasDisplaySpec rcdSpec = rc.getReactionCanvasDisplaySpec(); - String reactants = rcdSpec.getLeftText(); - String products = rcdSpec.getRightText(); - - // Create table and add cells for reactants, arrow(s) images, products - int widths [] = {8, 1, 8}; - reactionTable = getTable(3, 100, 0, 2, 2); - - // Add reactants as cell - Cell tableCell = createCell(reactants, getBold()); - tableCell.setHorizontalAlignment(Cell.ALIGN_RIGHT); - tableCell.setBorderColor(Color.white); - reactionTable.addCell(tableCell); - // add arrow(s) image as cell - if (arrowImageCell != null) { - arrowImageCell.setHorizontalAlignment(Cell.ALIGN_CENTER); - arrowImageCell.setBorderColor(Color.white); - reactionTable.addCell(arrowImageCell); - } - // add products as cell - tableCell = createCell(products, getBold()); - tableCell.setBorderColor(Color.white); - reactionTable.addCell(tableCell); - - // reactionTable.setBorderColor(Color.white); - reactionTable.setWidths(widths); - - // Identify modifiers, - ReactionParticipant[] rpArr = rs.getReactionParticipants(); - Vector modifiersVector = new Vector(); - for(int k = 0; k < rpArr.length; k += 1){ - if (rpArr[k] instanceof Catalyst) { - modifiersVector.add(rpArr[k]); - } - } - // Write the modifiers in a separate table, if present - if (modifiersVector.size() > 0) { - modifierTable = getTable(1, 50, 0, 1, 1); - modifierTable.addCell(createCell("Modifiers List", getBold(DEF_HEADER_FONT_SIZE), 1, 1, Element.ALIGN_CENTER, true)); - StringBuffer modifierNames = new StringBuffer(); - for (int k = 0; k < modifiersVector.size(); k++) { - modifierNames.append(((Catalyst)modifiersVector.elementAt(k)).getName() + "\n"); - } - modifierTable.addCell(createCell(modifierNames.toString().trim(), getFont())); - modifiersVector.removeAllElements(); - } - - Section reactionSection = reactStructSection.addSection(type + " " + rs.getName(), reactStructSection.getNumberDepth() + 1); - //Annotation - VCMetaData vcMetaData = rs.getModel().getVcMetaData(); - if (vcMetaData.getFreeTextAnnotation(rs) != null) { - Table annotTable = getTable(1, 100, 1, 3, 3); - annotTable.addCell(createCell("Reaction Annotation", getBold(DEF_HEADER_FONT_SIZE), 1, 1, Element.ALIGN_CENTER, true)); - annotTable.addCell(createCell(vcMetaData.getFreeTextAnnotation(rs),getFont())); - reactionSection.add(annotTable); - //reactionSection.add(new Paragraph("\""+rs.getAnnotation()+"\"")); - } - // reaction table - if (reactionTable != null) { - reactionSection.add(reactionTable); - reactionTable = null; // re-set reactionTable - } - if (modifierTable != null) { - reactionSection.add(modifierTable); - modifierTable = null; - } - // Write kinetics parameters, etc. in a table - writeKineticsParams(reactionSection, rs); - } - } - } - } + for (int i = 0; i < model.getNumStructures(); i++) { + ReactionStep[] reactionSteps = model.getReactionSteps(); + ReactionStep rs; + Table modifierTable = null; + Table reactionTable; + boolean firstTime = true; + Section reactStructSection = null; + for (ReactionStep reactionStep : reactionSteps) { + if (reactionStep.getStructure() == model.getStructure(i)) { //can also use structureName1.equals(structureName2) + if (firstTime) { + Paragraph linkParagraph = new Paragraph(); + linkParagraph.add(new Chunk("Reaction(s) in " + model.getStructure(i).getName()).setLocalDestination(model.getStructure(i).getName())); + reactStructSection = physioChapter.addSection(linkParagraph, physioChapter.getNumberDepth() + 1); + firstTime = false; + } + rs = reactionStep; + String type; + if (rs instanceof SimpleReaction) { + type = "Reaction"; + } else { + type = "Flux"; + } + //write Reaction equation as a table + + // Get the image arrow cell depending on type of reactionStep : MassAction => double arrow, otherwise, forward arrow + boolean bReversible = rs.getKinetics() instanceof MassActionKinetics; + Cell arrowImageCell = getReactionArrowImageCell(bReversible); + + // Get reactants and products strings + ReactionCanvas rc = new ReactionCanvas(); + rc.setReactionStep(rs); + ReactionCanvasDisplaySpec rcdSpec = rc.getReactionCanvasDisplaySpec(); + String reactants = rcdSpec.getLeftText(); + String products = rcdSpec.getRightText(); + + // Create table and add cells for reactants, arrow(s) images, products + int[] widths = {8, 1, 8}; + reactionTable = getTable(3, 100, 0, 2, 2); + + // Add reactants as cell + Cell tableCell = createCell(reactants, getBold()); + tableCell.setHorizontalAlignment(Cell.ALIGN_RIGHT); + tableCell.setBorderColor(Color.white); + reactionTable.addCell(tableCell); + // add arrow(s) image as cell + if (arrowImageCell != null) { + arrowImageCell.setHorizontalAlignment(Cell.ALIGN_CENTER); + arrowImageCell.setBorderColor(Color.white); + reactionTable.addCell(arrowImageCell); + } + // add products as cell + tableCell = createCell(products, getBold()); + tableCell.setBorderColor(Color.white); + reactionTable.addCell(tableCell); + + // reactionTable.setBorderColor(Color.white); + reactionTable.setWidths(widths); + + // Identify modifiers, + ReactionParticipant[] rpArr = rs.getReactionParticipants(); + Vector modifiersVector = new Vector<>(); + for (ReactionParticipant reactionParticipant : rpArr) { + if (reactionParticipant instanceof Catalyst) { + modifiersVector.add(reactionParticipant); + } + } + + // Write the modifiers in a separate table, if present + if (!modifiersVector.isEmpty()) { + modifierTable = getTable(1, 50, 0, 1, 1); + modifierTable.addCell(createCell("Modifiers List", getBold(DEF_HEADER_FONT_SIZE), 1, 1, Element.ALIGN_CENTER, true)); + StringBuilder modifierNames = new StringBuilder(); + for (int k = 0; k < modifiersVector.size(); k++) { + modifierNames.append(modifiersVector.elementAt(k).getName()).append("\n"); + } + modifierTable.addCell(createCell(modifierNames.toString().trim(), getFont())); + modifiersVector.removeAllElements(); + } + + Section reactionSection = reactStructSection.addSection(type + " " + rs.getName(), reactStructSection.getNumberDepth() + 1); + //Annotation + VCMetaData vcMetaData = rs.getModel().getVcMetaData(); + if (vcMetaData.getFreeTextAnnotation(rs) != null) { + Table annotTable = getTable(1, 100, 1, 3, 3); + annotTable.addCell(createCell("Reaction Annotation", getBold(DEF_HEADER_FONT_SIZE), 1, 1, Element.ALIGN_CENTER, true)); + annotTable.addCell(createCell(vcMetaData.getFreeTextAnnotation(rs), getFont())); + reactionSection.add(annotTable); + //reactionSection.add(new Paragraph("\""+rs.getAnnotation()+"\"")); + } + // reaction table + if (reactionTable != null) { + reactionSection.add(reactionTable); + reactionTable = null; // re-set reactionTable + } + if (modifierTable != null) { + reactionSection.add(modifierTable); + modifierTable = null; + } + // Write kinetics parameters, etc. in a table + writeKineticsParams(reactionSection, rs); + } + } + } + } -//container can be a chapter or a section of a chapter. - protected void writeSimulation(Section container, Simulation sim) throws DocumentException { + //container can be a chapter or a section of a chapter. + protected void writeSimulation(Section container, Simulation sim) throws DocumentException { - if (sim == null) { - return; - } - Section simSection = container.addSection(sim.getName(), container.getNumberDepth() + 1); - writeMetadata(simSection, sim.getName(), sim.getDescription(), null, "Simulation "); - //add overriden params - Table overParamTable = null; - MathOverrides mo = sim.getMathOverrides(); - if (mo != null) { - String constants [] = mo.getOverridenConstantNames(); - for (int i = 0; i < constants.length; i++) { - String actualStr = "", defStr = ""; - Expression tempExp = mo.getDefaultExpression(constants[i]); - if (tempExp != null) { - defStr = tempExp.infix(); - } - if (mo.isScan(constants[i])) { - actualStr = mo.getConstantArraySpec(constants[i]).toString(); - } else { - tempExp = mo.getActualExpression(constants[i], MathOverrides.ScanIndex.ZERO); - if (tempExp != null) { - actualStr = tempExp.infix(); - } - } - if (overParamTable == null) { - overParamTable = getTable(3, 75, 1, 3, 3); - overParamTable.setAlignment(Table.ALIGN_LEFT); - overParamTable.addCell(createCell("Overriden Parameters", getBold(DEF_HEADER_FONT_SIZE), 3, 1, Element.ALIGN_CENTER, true)); - overParamTable.addCell(createHeaderCell("Name", getBold(), 1)); - overParamTable.addCell(createHeaderCell("Actual Value", getBold(), 1)); - overParamTable.addCell(createHeaderCell("Default Value", getBold(), 1)); - } - overParamTable.addCell(createCell(constants[i], getFont())); - overParamTable.addCell(createCell(actualStr, getFont())); - overParamTable.addCell(createCell(defStr, getFont())); - } - } - if (overParamTable != null) { - simSection.add(overParamTable); - } - //add spatial details - //sim.isSpatial(); - Table meshTable = null; - MeshSpecification mesh = sim.getMeshSpecification(); - if (mesh != null) { - Geometry geom = mesh.getGeometry(); - Extent extent = geom.getExtent(); - String extentStr = "(" + extent.getX() + ", " + extent.getY() + ", " + extent.getZ() + ")"; - ISize meshSize = mesh.getSamplingSize(); - String meshSizeStr = "(" + meshSize.getX() + ", " + meshSize.getY() + ", " + meshSize.getZ() + ")"; - meshTable = getTable(2, 75, 1, 3, 3); - meshTable.setAlignment(Table.ALIGN_LEFT); - meshTable.addCell(createCell("Geometry Setting", getBold(DEF_HEADER_FONT_SIZE), 2, 1, Element.ALIGN_CENTER, true)); - meshTable.addCell(createCell("Geometry Size (um)", getFont())); - meshTable.addCell(createCell(extentStr, getFont())); - meshTable.addCell(createCell("Mesh Size (elements)", getFont())); - meshTable.addCell(createCell(meshSizeStr, getFont())); - } - if (meshTable != null) { - simSection.add(meshTable); - } - //write advanced sim settings - Table simAdvTable = null; - SolverTaskDescription solverDesc = sim.getSolverTaskDescription(); - if (solverDesc != null) { - String solverName = solverDesc.getSolverDescription().getDisplayLabel(); - simAdvTable = getTable(2, 75, 1, 3, 3); - simAdvTable.setAlignment(Table.ALIGN_LEFT); - simAdvTable.addCell(createCell("Advanced Settings", getBold(DEF_HEADER_FONT_SIZE), 2, 1, Element.ALIGN_CENTER, true)); - simAdvTable.addCell(createCell("Solver Name", getFont())); - simAdvTable.addCell(createCell(solverName, getFont())); - simAdvTable.addCell(createCell("Time Bounds - Starting", getFont())); - simAdvTable.addCell(createCell("" + solverDesc.getTimeBounds().getStartingTime(), getFont())); - simAdvTable.addCell(createCell("Time Bounds - Ending", getFont())); - simAdvTable.addCell(createCell("" + solverDesc.getTimeBounds().getEndingTime(), getFont())); - simAdvTable.addCell(createCell("Time Step - Min", getFont())); - simAdvTable.addCell(createCell("" + solverDesc.getTimeStep().getMinimumTimeStep(), getFont())); - simAdvTable.addCell(createCell("Time Step - Default", getFont())); - simAdvTable.addCell(createCell("" + solverDesc.getTimeStep().getDefaultTimeStep(), getFont())); - simAdvTable.addCell(createCell("Time Step - Max", getFont())); - simAdvTable.addCell(createCell("" + solverDesc.getTimeStep().getMaximumTimeStep(), getFont())); - ErrorTolerance et = solverDesc.getErrorTolerance(); - if (et != null) { - simAdvTable.addCell(createCell("Error Tolerance - Absolute", getFont())); - simAdvTable.addCell(createCell("" + et.getAbsoluteErrorTolerance(), getFont())); - simAdvTable.addCell(createCell("Error Tolerance - Relative", getFont())); - simAdvTable.addCell(createCell("" + et.getRelativeErrorTolerance(), getFont())); - } - OutputTimeSpec ots = solverDesc.getOutputTimeSpec(); - if (ots.isDefault()) { - simAdvTable.addCell(createCell("Keep Every", getFont())); - simAdvTable.addCell(createCell("" + ((DefaultOutputTimeSpec)ots).getKeepEvery(), getFont())); - simAdvTable.addCell(createCell("Keep At Most", getFont())); - simAdvTable.addCell(createCell("" + ((DefaultOutputTimeSpec)ots).getKeepAtMost(), getFont())); - } else if (ots.isUniform()) { - simAdvTable.addCell(createCell("Output Time Step", getFont())); - simAdvTable.addCell(createCell("" + ((UniformOutputTimeSpec)ots).getOutputTimeStep(), getFont())); - } else if (ots.isExplicit()) { - simAdvTable.addCell(createCell("Output Time Points", getFont())); - simAdvTable.addCell(createCell("" + ((ExplicitOutputTimeSpec)ots).toCommaSeperatedOneLineOfString(), getFont())); - } - simAdvTable.addCell(createCell("Use Symbolic Jacobian (T/F)", getFont())); - simAdvTable.addCell(createCell((solverDesc.getUseSymbolicJacobian() ? " T ": " F "), getFont())); - Constant sp = solverDesc.getSensitivityParameter(); - if (sp != null) { - simAdvTable.addCell(createCell("Sensitivity Analysis Param", getFont())); - simAdvTable.addCell(createCell(sp.getName(), getFont())); - } - } - if (simAdvTable != null) { - simSection.add(simAdvTable); - } - } + if (sim == null) { + return; + } + Section simSection = container.addSection(sim.getName(), container.getNumberDepth() + 1); + writeMetadata(simSection, sim.getName(), sim.getDescription(), null, "Simulation "); + //add overriden params + Table overParamTable = null; + MathOverrides mo = sim.getMathOverrides(); + if (mo != null) { + String[] constants = mo.getOverridenConstantNames(); + for (String constant : constants) { + String actualStr = "", defStr = ""; + Expression tempExp = mo.getDefaultExpression(constant); + if (tempExp != null) { + defStr = tempExp.infix(); + } + if (mo.isScan(constant)) { + actualStr = mo.getConstantArraySpec(constant).toString(); + } else { + tempExp = mo.getActualExpression(constant, MathOverrides.ScanIndex.ZERO); + if (tempExp != null) { + actualStr = tempExp.infix(); + } + } + if (overParamTable == null) { + overParamTable = getTable(3, 75, 1, 3, 3); + overParamTable.setAlignment(Table.ALIGN_LEFT); + overParamTable.addCell(createCell("Overriden Parameters", getBold(DEF_HEADER_FONT_SIZE), 3, 1, Element.ALIGN_CENTER, true)); + overParamTable.addCell(createHeaderCell("Name", getBold(), 1)); + overParamTable.addCell(createHeaderCell("Actual Value", getBold(), 1)); + overParamTable.addCell(createHeaderCell("Default Value", getBold(), 1)); + } + overParamTable.addCell(createCell(constant, getFont())); + overParamTable.addCell(createCell(actualStr, getFont())); + overParamTable.addCell(createCell(defStr, getFont())); + } + } + if (overParamTable != null) { + simSection.add(overParamTable); + } + //add spatial details + //sim.isSpatial(); + Table meshTable = null; + MeshSpecification mesh = sim.getMeshSpecification(); + if (mesh != null) { + Geometry geom = mesh.getGeometry(); + Extent extent = geom.getExtent(); + String extentStr = "(" + extent.getX() + ", " + extent.getY() + ", " + extent.getZ() + ")"; + ISize meshSize = mesh.getSamplingSize(); + String meshSizeStr = "(" + meshSize.getX() + ", " + meshSize.getY() + ", " + meshSize.getZ() + ")"; + meshTable = getTable(2, 75, 1, 3, 3); + meshTable.setAlignment(Table.ALIGN_LEFT); + meshTable.addCell(createCell("Geometry Setting", getBold(DEF_HEADER_FONT_SIZE), 2, 1, Element.ALIGN_CENTER, true)); + meshTable.addCell(createCell("Geometry Size (um)", getFont())); + meshTable.addCell(createCell(extentStr, getFont())); + meshTable.addCell(createCell("Mesh Size (elements)", getFont())); + meshTable.addCell(createCell(meshSizeStr, getFont())); + } + if (meshTable != null) { + simSection.add(meshTable); + } + //write advanced sim settings + Table simAdvTable = null; + SolverTaskDescription solverDesc = sim.getSolverTaskDescription(); + if (solverDesc != null) { + String solverName = solverDesc.getSolverDescription().getDisplayLabel(); + simAdvTable = getTable(2, 75, 1, 3, 3); + simAdvTable.setAlignment(Table.ALIGN_LEFT); + simAdvTable.addCell(createCell("Advanced Settings", getBold(DEF_HEADER_FONT_SIZE), 2, 1, Element.ALIGN_CENTER, true)); + simAdvTable.addCell(createCell("Solver Name", getFont())); + simAdvTable.addCell(createCell(solverName, getFont())); + simAdvTable.addCell(createCell("Time Bounds - Starting", getFont())); + simAdvTable.addCell(createCell("" + solverDesc.getTimeBounds().getStartingTime(), getFont())); + simAdvTable.addCell(createCell("Time Bounds - Ending", getFont())); + simAdvTable.addCell(createCell("" + solverDesc.getTimeBounds().getEndingTime(), getFont())); + simAdvTable.addCell(createCell("Time Step - Min", getFont())); + simAdvTable.addCell(createCell("" + solverDesc.getTimeStep().getMinimumTimeStep(), getFont())); + simAdvTable.addCell(createCell("Time Step - Default", getFont())); + simAdvTable.addCell(createCell("" + solverDesc.getTimeStep().getDefaultTimeStep(), getFont())); + simAdvTable.addCell(createCell("Time Step - Max", getFont())); + simAdvTable.addCell(createCell("" + solverDesc.getTimeStep().getMaximumTimeStep(), getFont())); + ErrorTolerance et = solverDesc.getErrorTolerance(); + if (et != null) { + simAdvTable.addCell(createCell("Error Tolerance - Absolute", getFont())); + simAdvTable.addCell(createCell("" + et.getAbsoluteErrorTolerance(), getFont())); + simAdvTable.addCell(createCell("Error Tolerance - Relative", getFont())); + simAdvTable.addCell(createCell("" + et.getRelativeErrorTolerance(), getFont())); + } + OutputTimeSpec ots = solverDesc.getOutputTimeSpec(); + if (ots.isDefault()) { + simAdvTable.addCell(createCell("Keep Every", getFont())); + simAdvTable.addCell(createCell("" + ((DefaultOutputTimeSpec) ots).getKeepEvery(), getFont())); + simAdvTable.addCell(createCell("Keep At Most", getFont())); + simAdvTable.addCell(createCell("" + ((DefaultOutputTimeSpec) ots).getKeepAtMost(), getFont())); + } else if (ots.isUniform()) { + simAdvTable.addCell(createCell("Output Time Step", getFont())); + simAdvTable.addCell(createCell("" + ((UniformOutputTimeSpec) ots).getOutputTimeStep(), getFont())); + } else if (ots.isExplicit()) { + simAdvTable.addCell(createCell("Output Time Points", getFont())); + simAdvTable.addCell(createCell(((ExplicitOutputTimeSpec) ots).toCommaSeperatedOneLineOfString(), getFont())); + } + simAdvTable.addCell(createCell("Use Symbolic Jacobian (T/F)", getFont())); + simAdvTable.addCell(createCell((solverDesc.getUseSymbolicJacobian() ? " T " : " F "), getFont())); + Constant sp = solverDesc.getSensitivityParameter(); + if (sp != null) { + simAdvTable.addCell(createCell("Sensitivity Analysis Param", getFont())); + simAdvTable.addCell(createCell(sp.getName(), getFont())); + } + } + if (simAdvTable != null) { + simSection.add(simAdvTable); + } + } -//SimulationContext: ignored the constraints (steady/unsteady). + //SimulationContext: ignored the constraints (steady/unsteady). //Electrical Stimulus: ignored the Ground Electrode, -protected void writeSimulationContext(Chapter simContextsChapter, SimulationContext simContext, PublishPreferences preferences) throws Exception { - - Section simContextSection = simContextsChapter.addSection("Application: " + simContext.getName(), simContextsChapter.getNumberDepth() + 1); - writeMetadata(simContextSection, simContext.getName(), simContext.getDescription(), null, "Application "); - //add geometry context (structure mapping) - writeStructureMapping(simContextSection, simContext); - //add reaction context (Reaction Mapping) - writeReactionContext(simContextSection, simContext); - //add Membrane Mapping & electrical stimuli - writeMembraneMapping(simContextSection, simContext); - // - if (preferences.includeGeom()) { - writeGeom(simContextSection, simContext.getGeometry(), simContext.getGeometryContext()); - } - if (preferences.includeMathDesc()) { - writeMathDescAsText(simContextSection, simContext.getMathDescription()); - //writeMathDescAsImages(simContextSection, simContext.getMathDescription()); - } - if (preferences.includeSim()) { - Section simSection = simContextSection.addSection("Simulation(s)", simContextSection.getDepth() + 1); - Simulation sims [] = simContext.getSimulations(); - for (int i = 0; i < sims.length; i++) { - writeSimulation(simSection, sims[i]); - } - } -} - - -//not used for now... -protected void writeSpecies(Species[] species) throws DocumentException { - - if (species.length > 0) { - Table table = new Table(2); - table.setWidth(100); - table.setBorderWidth(0); //for now... - // - int[] widths = new int[] { 1, 1 }; - // - table.addCell(createHeaderCell("Species", getBold(), 2)); - for(int i = 0; i < species.length/2 + (species.length % 2); i++) { - int n = species.length/2 + (species.length % 2) + i; - table.addCell(createCell(species[i].getCommonName(), getFont())); - widths[0] = Math.max(widths[0], species[i].getCommonName().length()); - if (n < species.length) { - table.addCell(createCell(species[n].getCommonName(), getFont())); - widths[1] = Math.max(widths[1], species[n].getCommonName().length()); - } else { - table.addCell(createCell("", getFont())); - } - } - table.setWidths(widths); - document.add(table); - } -} - - - protected void writeStructure(Model model, Structure struct, Table structTable) throws DocumentException { - - //If this structure has any reactions in it, add its name as a hyperlink to the reactions' list. - if (hasReactions(model, struct)) { - Paragraph linkParagraph = new Paragraph(); - Font linkFont; - try { - BaseFont fontBaseFont = BaseFont.createFont(BaseFont.HELVETICA, BaseFont.CP1252, BaseFont.NOT_EMBEDDED); - linkFont = new Font(fontBaseFont, DEF_FONT_SIZE, Font.NORMAL, new java.awt.Color(0, 0, 255)); - } catch (Exception e) { - linkFont = getFont(); - lg.error(e.getMessage(), e); - } - linkParagraph.add(new Chunk(struct.getName(), linkFont).setLocalGoto(struct.getName())); - Cell structLinkCell = new Cell(linkParagraph); - structLinkCell.setBorderWidth(1); - structLinkCell.setHorizontalAlignment(Element.ALIGN_LEFT); - structTable.addCell(structLinkCell); - } else { - structTable.addCell(createCell(struct.getName(), getFont())); - } - StructureTopology structTopology = model.getStructureTopology(); - if (struct instanceof Membrane) { - structTable.addCell(createCell("Membrane", getFont())); - Feature outsideFeature = structTopology.getOutsideFeature((Membrane)struct); - Feature insideFeature = structTopology.getInsideFeature((Membrane)struct); - structTable.addCell(createCell((insideFeature != null ? insideFeature.getName() : "N/A"), getFont())); - structTable.addCell(createCell((outsideFeature != null ? outsideFeature.getName() : "N/A"), getFont())); - } else { - structTable.addCell(createCell("Feature", getFont())); - String outsideStr = "N/A", insideStr = "N/A"; - Membrane enclosingMem = (Membrane)structTopology.getParentStructure(struct); - if (enclosingMem != null) { - outsideStr = enclosingMem.getName(); - } - //To do: retrieve the 'child' membrane here... - structTable.addCell(createCell(insideStr, getFont())); - structTable.addCell(createCell(outsideStr, getFont())); - } - } + protected void writeSimulationContext(Chapter simContextsChapter, SimulationContext simContext, PublishPreferences preferences) throws Exception { + + Section simContextSection = simContextsChapter.addSection("Application: " + simContext.getName(), simContextsChapter.getNumberDepth() + 1); + writeMetadata(simContextSection, simContext.getName(), simContext.getDescription(), null, "Application "); + //add geometry context (structure mapping) + writeStructureMapping(simContextSection, simContext); + //add reaction context (Reaction Mapping) + writeReactionContext(simContextSection, simContext); + //add Membrane Mapping & electrical stimuli + writeMembraneMapping(simContextSection, simContext); + // + if (preferences.includeGeom()) { + writeGeom(simContextSection, simContext.getGeometry(), simContext.getGeometryContext()); + } + if (preferences.includeMathDesc()) { + writeMathDescAsText(simContextSection, simContext.getMathDescription()); + //writeMathDescAsImages(simContextSection, simContext.getMathDescription()); + } + if (preferences.includeSim()) { + Section simSection = simContextSection.addSection("Simulation(s)", simContextSection.getDepth() + 1); + Simulation[] sims = simContext.getSimulations(); + for (Simulation sim : sims) { + writeSimulation(simSection, sim); + } + } + } + + + //not used for now... + protected void writeSpecies(Species[] species) throws DocumentException { + + if (species.length > 0) { + Table table = new Table(2); + table.setWidth(100); + table.setBorderWidth(0); //for now... + // + int[] widths = new int[]{1, 1}; + // + table.addCell(createHeaderCell("Species", getBold(), 2)); + for (int i = 0; i < species.length / 2 + (species.length % 2); i++) { + int n = species.length / 2 + (species.length % 2) + i; + table.addCell(createCell(species[i].getCommonName(), getFont())); + widths[0] = Math.max(widths[0], species[i].getCommonName().length()); + if (n < species.length) { + table.addCell(createCell(species[n].getCommonName(), getFont())); + widths[1] = Math.max(widths[1], species[n].getCommonName().length()); + } else { + table.addCell(createCell("", getFont())); + } + } + table.setWidths(widths); + document.add(table); + } + } + + + protected void writeStructure(Model model, Structure struct, Table structTable) throws DocumentException { + + //If this structure has any reactions in it, add its name as a hyperlink to the reactions' list. + if (hasReactions(model, struct)) { + Paragraph linkParagraph = new Paragraph(); + Font linkFont; + try { + BaseFont fontBaseFont = BaseFont.createFont(BaseFont.HELVETICA, BaseFont.CP1252, BaseFont.NOT_EMBEDDED); + linkFont = new Font(fontBaseFont, DEF_FONT_SIZE, Font.NORMAL, new java.awt.Color(0, 0, 255)); + } catch (Exception e) { + linkFont = getFont(); + lg.error(e.getMessage(), e); + } + linkParagraph.add(new Chunk(struct.getName(), linkFont).setLocalGoto(struct.getName())); + Cell structLinkCell = new Cell(linkParagraph); + structLinkCell.setBorderWidth(1); + structLinkCell.setHorizontalAlignment(Element.ALIGN_LEFT); + structTable.addCell(structLinkCell); + } else { + structTable.addCell(createCell(struct.getName(), getFont())); + } + StructureTopology structTopology = model.getStructureTopology(); + if (struct instanceof Membrane) { + structTable.addCell(createCell("Membrane", getFont())); + Feature outsideFeature = structTopology.getOutsideFeature((Membrane) struct); + Feature insideFeature = structTopology.getInsideFeature((Membrane) struct); + structTable.addCell(createCell((insideFeature != null ? insideFeature.getName() : "N/A"), getFont())); + structTable.addCell(createCell((outsideFeature != null ? outsideFeature.getName() : "N/A"), getFont())); + } else { + structTable.addCell(createCell("Feature", getFont())); + String outsideStr = "N/A", insideStr = "N/A"; + Membrane enclosingMem = (Membrane) structTopology.getParentStructure(struct); + if (enclosingMem != null) { + outsideStr = enclosingMem.getName(); + } + //To do: retrieve the 'child' membrane here... + structTable.addCell(createCell(insideStr, getFont())); + structTable.addCell(createCell(outsideStr, getFont())); + } + } -//boundary types ignored. - protected void writeStructureMapping(Section simContextSection, SimulationContext sc) throws DocumentException { + //boundary types ignored. + protected void writeStructureMapping(Section simContextSection, SimulationContext sc) throws DocumentException { - GeometryContext gc = sc.getGeometryContext(); - if (gc == null) { - return; - } - Section structMappSection = null; + GeometryContext gc = sc.getGeometryContext(); + if (gc == null) { + return; + } + Section structMappSection = null; /*try { ByteArrayOutputStream bos = generateStructureMappingImage(sc); com.lowagie.text.Image structMapImage = com.lowagie.text.Image.getInstance(Toolkit.getDefaultToolkit().createImage(bos.toByteArray()), null); @@ -2203,117 +2194,116 @@ protected void writeStructureMapping(Section simContextSection, SimulationContex System.err.println("Unable to add structure mapping image to report."); lg.error(e); }*/ - StructureMapping structMap [] = gc.getStructureMappings(); - Table structMapTable = null; - ModelUnitSystem modelUnitSystem = sc.getModel().getUnitSystem(); - for (int i = 0; i < structMap.length; i++) { - if (!(structMap[i] instanceof FeatureMapping)) { - continue; - } - if (i == 0) { - structMapTable = getTable(5, 100, 1, 3, 3); - structMapTable.addCell(createCell("Structure Mapping", getBold(DEF_HEADER_FONT_SIZE), 5, 1, Element.ALIGN_CENTER, true)); - structMapTable.addCell(createHeaderCell("Structure", getBold(), 1)); - structMapTable.addCell(createHeaderCell("Subdomain", getBold(), 1)); - structMapTable.addCell(createHeaderCell("Resolved (T/F)", getBold(), 1)); - structMapTable.addCell(createHeaderCell("Surf/Vol", getBold(), 1)); - structMapTable.addCell(createHeaderCell("VolFract", getBold(), 1)); - structMapTable.endHeaders(); - } - String structName = structMap[i].getStructure().getName(); - SubVolume subVol = (SubVolume)((FeatureMapping)structMap[i]).getGeometryClass(); - String subDomain = ""; - if (subVol != null) { - subDomain = subVol.getName(); - } - boolean isResolved = false; // ((FeatureMapping)structMap[i]).getResolved(); - String surfVolStr = "", volFractStr = ""; - MembraneMapping mm = (MembraneMapping)gc.getStructureMapping(sc.getModel().getStructureTopology().getParentStructure(structMap[i].getStructure())); - if (mm != null) { - StructureMapping.StructureMappingParameter smp = mm.getSurfaceToVolumeParameter(); - if (smp != null) { - Expression tempExp = smp.getExpression(); - VCUnitDefinition tempUnit = smp.getUnitDefinition(); - if (tempExp != null) { - surfVolStr = tempExp.infix(); - if (tempUnit != null && !modelUnitSystem.getInstance_DIMENSIONLESS().compareEqual(tempUnit)) { //no need to add '1' for dimensionless unit - surfVolStr += " " + tempUnit.getSymbolUnicode(); - } - } - } - smp = mm.getVolumeFractionParameter(); - if (smp != null) { - Expression tempExp = smp.getExpression(); - VCUnitDefinition tempUnit = smp.getUnitDefinition(); - if (tempExp != null) { - volFractStr = tempExp.infix(); - if (tempUnit != null && !modelUnitSystem.getInstance_DIMENSIONLESS().compareEqual(tempUnit)) { - volFractStr += " " + tempUnit.getSymbolUnicode(); - } - } - } - } - structMapTable.addCell(createCell(structName, getFont())); - structMapTable.addCell(createCell(subDomain, getFont())); - structMapTable.addCell(createCell((isResolved ? " T ": " F "), getFont())); - structMapTable.addCell(createCell(surfVolStr, getFont())); - structMapTable.addCell(createCell(volFractStr, getFont())); - } - if (structMapTable != null) { - if (structMappSection == null) { - structMappSection = simContextSection.addSection("Structure Mapping For " + sc.getName(), simContextSection.getNumberDepth() + 1); - } - structMappSection.add(structMapTable); - } - } - - -//currently not used. - protected void writeSubDomainsEquationsAsImages(Section mathDescSection, MathDescription mathDesc) { - - Enumeration subDomains = mathDesc.getSubDomains(); - Expression expArray[]; - Section volDomains = mathDescSection.addSection("Volume Domains", mathDescSection.getDepth() + 1); - Section memDomains = mathDescSection.addSection("Membrane Domains", mathDescSection.getDepth() + 1); - int scale = 1, height = 200; //arbitrary - int viewableWidth = (int)(document.getPageSize().getWidth() - document.leftMargin() - document.rightMargin()); - BufferedImage dummy = new BufferedImage(viewableWidth, height, BufferedImage.TYPE_3BYTE_BGR); - while(subDomains.hasMoreElements()) { - SubDomain subDomain = subDomains.nextElement(); - Enumeration equationsList = subDomain.getEquations(); - ArrayList expList = new ArrayList(); - while (equationsList.hasMoreElements()) { - Equation equ = equationsList.nextElement(); - try { - Enumeration enum_equ = equ.getTotalExpressions(); - while (enum_equ.hasMoreElements()){ - Expression exp = new Expression(enum_equ.nextElement()); - expList.add(exp.flatten()); - } - } catch (ExpressionException ee) { - lg.error("Unable to process the equation for subdomain: " + subDomain.getName(), ee); - continue; - } - } - expArray = (Expression [])expList.toArray(new Expression[expList.size()]); - Section tempSection = null; - if (subDomain instanceof CompartmentSubDomain) { - tempSection = volDomains.addSection(subDomain.getName(), volDomains.getDepth() + 1); - } else if (subDomain instanceof MembraneSubDomain) { - tempSection = memDomains.addSection(subDomain.getName(), memDomains.getDepth() + 1); - } - try { - Dimension dim = ExpressionCanvas.getExpressionImageSize(expArray, (Graphics2D)dummy.getGraphics()); - System.out.println("Image dim: " + dim.width + " " + dim.height); - BufferedImage bufferedImage = new BufferedImage((int)dim.getWidth()*scale, (int)dim.getHeight()*scale, BufferedImage.TYPE_3BYTE_BGR); - ExpressionCanvas.getExpressionAsImage(expArray, bufferedImage, scale); - //Table imageTable = null;; - com.lowagie.text.Image expImage = com.lowagie.text.Image.getInstance(bufferedImage, null); - expImage.setAlignment(com.lowagie.text.Image.LEFT); - if (viewableWidth < expImage.getScaledWidth()) { - expImage.scaleToFit(viewableWidth, expImage.getHeight()); - System.out.println("SubDomain expresions After scaling: " + expImage.getScaledWidth()); - } + StructureMapping[] structMap = gc.getStructureMappings(); + Table structMapTable = null; + ModelUnitSystem modelUnitSystem = sc.getModel().getUnitSystem(); + for (int i = 0; i < structMap.length; i++) { + if (!(structMap[i] instanceof FeatureMapping)) { + continue; + } + if (i == 0) { + structMapTable = getTable(5, 100, 1, 3, 3); + structMapTable.addCell(createCell("Structure Mapping", getBold(DEF_HEADER_FONT_SIZE), 5, 1, Element.ALIGN_CENTER, true)); + structMapTable.addCell(createHeaderCell("Structure", getBold(), 1)); + structMapTable.addCell(createHeaderCell("Subdomain", getBold(), 1)); + structMapTable.addCell(createHeaderCell("Resolved (T/F)", getBold(), 1)); + structMapTable.addCell(createHeaderCell("Surf/Vol", getBold(), 1)); + structMapTable.addCell(createHeaderCell("VolFract", getBold(), 1)); + structMapTable.endHeaders(); + } + String structName = structMap[i].getStructure().getName(); + SubVolume subVol = (SubVolume) structMap[i].getGeometryClass(); + String subDomain = ""; + if (subVol != null) { + subDomain = subVol.getName(); + } + boolean isResolved = false; // ((FeatureMapping)structMap[i]).getResolved(); + String surfVolStr = "", volFractStr = ""; + MembraneMapping mm = (MembraneMapping) gc.getStructureMapping(sc.getModel().getStructureTopology().getParentStructure(structMap[i].getStructure())); + if (mm != null) { + StructureMapping.StructureMappingParameter smp = mm.getSurfaceToVolumeParameter(); + if (smp != null) { + Expression tempExp = smp.getExpression(); + VCUnitDefinition tempUnit = smp.getUnitDefinition(); + if (tempExp != null) { + surfVolStr = tempExp.infix(); + if (tempUnit != null && !modelUnitSystem.getInstance_DIMENSIONLESS().compareEqual(tempUnit)) { //no need to add '1' for dimensionless unit + surfVolStr += " " + tempUnit.getSymbolUnicode(); + } + } + } + smp = mm.getVolumeFractionParameter(); + if (smp != null) { + Expression tempExp = smp.getExpression(); + VCUnitDefinition tempUnit = smp.getUnitDefinition(); + if (tempExp != null) { + volFractStr = tempExp.infix(); + if (tempUnit != null && !modelUnitSystem.getInstance_DIMENSIONLESS().compareEqual(tempUnit)) { + volFractStr += " " + tempUnit.getSymbolUnicode(); + } + } + } + } + structMapTable.addCell(createCell(structName, getFont())); + structMapTable.addCell(createCell(subDomain, getFont())); + structMapTable.addCell(createCell((isResolved ? " T " : " F "), getFont())); + structMapTable.addCell(createCell(surfVolStr, getFont())); + structMapTable.addCell(createCell(volFractStr, getFont())); + } + if (structMapTable != null) { + if (structMappSection == null) { + structMappSection = simContextSection.addSection("Structure Mapping For " + sc.getName(), simContextSection.getNumberDepth() + 1); + } + structMappSection.add(structMapTable); + } + } + + + //currently not used. + protected void writeSubDomainsEquationsAsImages(Section mathDescSection, MathDescription mathDesc) { + + Enumeration subDomains = mathDesc.getSubDomains(); + Expression[] expArray; + Section volDomains = mathDescSection.addSection("Volume Domains", mathDescSection.getDepth() + 1); + Section memDomains = mathDescSection.addSection("Membrane Domains", mathDescSection.getDepth() + 1); + int scale = 1, height = 200; //arbitrary + int viewableWidth = (int) (document.getPageSize().getWidth() - document.leftMargin() - document.rightMargin()); + BufferedImage dummy = new BufferedImage(viewableWidth, height, BufferedImage.TYPE_3BYTE_BGR); + while (subDomains.hasMoreElements()) { + SubDomain subDomain = subDomains.nextElement(); + Enumeration equationsList = subDomain.getEquations(); + ArrayList expList = new ArrayList<>(); + while (equationsList.hasMoreElements()) { + Equation equ = equationsList.nextElement(); + try { + Enumeration enum_equ = equ.getTotalExpressions(); + while (enum_equ.hasMoreElements()) { + Expression exp = new Expression(enum_equ.nextElement()); + expList.add(exp.flatten()); + } + } catch (ExpressionException ee) { + lg.error("Unable to process the equation for subdomain: " + subDomain.getName(), ee); + } + } + expArray = expList.toArray(Expression[]::new); + Section tempSection = null; + if (subDomain instanceof CompartmentSubDomain) { + tempSection = volDomains.addSection(subDomain.getName(), volDomains.getDepth() + 1); + } else if (subDomain instanceof MembraneSubDomain) { + tempSection = memDomains.addSection(subDomain.getName(), memDomains.getDepth() + 1); + } + try { + Dimension dim = ExpressionCanvas.getExpressionImageSize(expArray, (Graphics2D) dummy.getGraphics()); + lg.debug("Image dim: {} {}", dim.width, dim.height); + BufferedImage bufferedImage = new BufferedImage((int) dim.getWidth() * scale, (int) dim.getHeight() * scale, BufferedImage.TYPE_3BYTE_BGR); + ExpressionCanvas.getExpressionAsImage(expArray, bufferedImage, scale); + //Table imageTable = null;; + com.lowagie.text.Image expImage = com.lowagie.text.Image.getInstance(bufferedImage, null); + expImage.setAlignment(com.lowagie.text.Image.LEFT); + if (viewableWidth < expImage.getScaledWidth()) { + expImage.scaleToFit(viewableWidth, expImage.getHeight()); + lg.debug("SubDomain expressions After scaling: {}", expImage.getScaledWidth()); + } /*Cell imageCell = new Cell(); imageCell.add(expImage); if (imageTable == null) { @@ -2324,110 +2314,110 @@ protected void writeSubDomainsEquationsAsImages(Section mathDescSection, MathDes imageTable.addCell(imageCell); imageTable.setWidth(100); tempSection.add(imageTable);*/ - tempSection.add(expImage); - } catch (Exception e) { - lg.error("Unable to add subdomain equation image to report.", e); - } - } - if (volDomains.isEmpty()) { - mathDescSection.remove(volDomains); - } - if (memDomains.isEmpty()) { - mathDescSection.remove(memDomains); - } - } - + tempSection.add(expImage); + } catch (Exception e) { + lg.error("Unable to add subdomain equation image to report.", e); + } + } + if (volDomains.isEmpty()) { + mathDescSection.remove(volDomains); + } + if (memDomains.isEmpty()) { + mathDescSection.remove(memDomains); + } + } + + + protected void writeSubDomainsEquationsAsText(Section mathDescSection, MathDescription mathDesc) throws DocumentException { + + Enumeration subDomains = mathDesc.getSubDomains(); + Section volDomains = mathDescSection.addSection("Volume Domains", mathDescSection.getDepth() + 1); + Section memDomains = mathDescSection.addSection("Membrane Domains", mathDescSection.getDepth() + 1); + Section filDomains = mathDescSection.addSection("Filament Domains", mathDescSection.getDepth() + 1); + while (subDomains.hasMoreElements()) { + Section tempSection = null; + SubDomain subDomain = subDomains.nextElement(); + if (subDomain instanceof CompartmentSubDomain) { + tempSection = volDomains.addSection(subDomain.getName(), volDomains.getDepth() + 1); + } else if (subDomain instanceof MembraneSubDomain) { + tempSection = memDomains.addSection(subDomain.getName(), memDomains.getDepth() + 1); + } else if (subDomain instanceof FilamentSubDomain) { + tempSection = filDomains.addSection(subDomain.getName(), filDomains.getDepth() + 1); + } + Enumeration equationsList = subDomain.getEquations(); + while (equationsList.hasMoreElements()) { + Equation equ = equationsList.nextElement(); + writeEquation(tempSection, equ); + } + if (subDomain.getFastSystem() != null) { + writeFastSystem(tempSection, subDomain.getFastSystem()); + } + if (subDomain instanceof MembraneSubDomain) { + Enumeration jcList = ((MembraneSubDomain) subDomain).getJumpConditions(); + while (jcList.hasMoreElements()) { + JumpCondition jc = jcList.nextElement(); + writeJumpCondition(tempSection, jc); + } + } + } + if (volDomains.isEmpty()) { + mathDescSection.remove(volDomains); + } + if (memDomains.isEmpty()) { + mathDescSection.remove(memDomains); + } + if (filDomains.isEmpty()) { + mathDescSection.remove(filDomains); + } + } - protected void writeSubDomainsEquationsAsText(Section mathDescSection, MathDescription mathDesc) throws DocumentException { - - Enumeration subDomains = mathDesc.getSubDomains(); - Section volDomains = mathDescSection.addSection("Volume Domains", mathDescSection.getDepth() + 1); - Section memDomains = mathDescSection.addSection("Membrane Domains", mathDescSection.getDepth() + 1); - Section filDomains = mathDescSection.addSection("Filament Domains", mathDescSection.getDepth() + 1); - while(subDomains.hasMoreElements()) { - Section tempSection = null; - SubDomain subDomain = subDomains.nextElement(); - if (subDomain instanceof CompartmentSubDomain) { - tempSection = volDomains.addSection(subDomain.getName(), volDomains.getDepth() + 1); - } else if (subDomain instanceof MembraneSubDomain) { - tempSection = memDomains.addSection(subDomain.getName(), memDomains.getDepth() + 1); - } else if (subDomain instanceof FilamentSubDomain) { - tempSection = filDomains.addSection(subDomain.getName(), filDomains.getDepth() + 1); - } - Enumeration equationsList = subDomain.getEquations(); - while (equationsList.hasMoreElements()) { - Equation equ = equationsList.nextElement(); - writeEquation(tempSection, equ); - } - if (subDomain.getFastSystem() != null) { - writeFastSystem(tempSection, subDomain.getFastSystem()); - } - if (subDomain instanceof MembraneSubDomain) { - Enumeration jcList = ((MembraneSubDomain)subDomain).getJumpConditions(); - while (jcList.hasMoreElements()) { - JumpCondition jc = jcList.nextElement(); - writeJumpCondition(tempSection, jc); - } - } - } - if (volDomains.isEmpty()) { - mathDescSection.remove(volDomains); - } - if (memDomains.isEmpty()) { - mathDescSection.remove(memDomains); - } - if (filDomains.isEmpty()) { - mathDescSection.remove(filDomains); - } - } + protected void writeVolumeRegionEquation(Section container, VolumeRegionEquation eq) throws DocumentException { - protected void writeVolumeRegionEquation(Section container, VolumeRegionEquation eq) throws DocumentException { + Table eqTable = getTable(2, 100, 1, 2, 2); + eqTable.addCell(createCell(VCML.VolumeRegionEquation + " " + eq.getVariable().getName(), + getBold(DEF_HEADER_FONT_SIZE), 2, 1, Element.ALIGN_CENTER, true)); + eqTable.endHeaders(); + String exp = "0.0"; + eqTable.addCell(createCell(VCML.UniformRate, getFont())); + if (eq.getUniformRateExpression() != null) { + exp = eq.getUniformRateExpression().infix(); + } + eqTable.addCell(createCell(exp, getFont())); + exp = "0.0"; + eqTable.addCell(createCell(VCML.VolumeRate, getFont())); + if (eq.getVolumeRateExpression() != null) { + exp = eq.getVolumeRateExpression().infix(); + } + eqTable.addCell(createCell(exp, getFont())); - Table eqTable = getTable(2, 100, 1, 2, 2); - eqTable.addCell(createCell(VCML.VolumeRegionEquation + " " + eq.getVariable().getName(), - getBold(DEF_HEADER_FONT_SIZE), 2, 1, Element.ALIGN_CENTER, true)); - eqTable.endHeaders(); - String exp = "0.0"; - eqTable.addCell(createCell(VCML.UniformRate, getFont())); - if (eq.getUniformRateExpression() != null) { - exp = eq.getUniformRateExpression().infix(); - } - eqTable.addCell(createCell(exp, getFont())); - exp = "0.0"; - eqTable.addCell(createCell(VCML.VolumeRate, getFont())); - if (eq.getVolumeRateExpression() != null) { - exp = eq.getVolumeRateExpression().infix(); - } - eqTable.addCell(createCell(exp, getFont())); - // exp = "0.0"; // eqTable.addCell(createCell(VCML.MembraneRate, getFont())); // if (eq.getMembraneRateExpression() != null) { // exp = eq.getMembraneRateExpression().infix(); // } // eqTable.addCell(createCell(exp, getFont())); - - if (eq.getInitialExpression() != null) { - eqTable.addCell(createCell(VCML.Initial, getFont())); - eqTable.addCell(createCell(eq.getInitialExpression().infix(), getFont())); - } - int solutionType = eq.getSolutionType(); - switch (solutionType) { - case Equation.UNKNOWN_SOLUTION:{ - if (eq.getInitialExpression() == null) { - eqTable.addCell(createCell(VCML.Initial, getFont())); - eqTable.addCell(createCell("0.0", getFont())); - } - break; - } - case Equation.EXACT_SOLUTION:{ - eqTable.addCell(createCell(VCML.Exact, getFont())); - eqTable.addCell(createCell(eq.getExactSolution().infix(), getFont())); - break; - } - } - - container.add(eqTable); - } + + if (eq.getInitialExpression() != null) { + eqTable.addCell(createCell(VCML.Initial, getFont())); + eqTable.addCell(createCell(eq.getInitialExpression().infix(), getFont())); + } + int solutionType = eq.getSolutionType(); + switch (solutionType) { + case Equation.UNKNOWN_SOLUTION: { + if (eq.getInitialExpression() == null) { + eqTable.addCell(createCell(VCML.Initial, getFont())); + eqTable.addCell(createCell("0.0", getFont())); + } + break; + } + case Equation.EXACT_SOLUTION: { + eqTable.addCell(createCell(VCML.Exact, getFont())); + eqTable.addCell(createCell(eq.getExactSolution().infix(), getFont())); + break; + } + } + + container.add(eqTable); + } } From d9773c216bf249d03cc789c04e483642364746e9 Mon Sep 17 00:00:00 2001 From: Logan Drescher Date: Wed, 15 Jan 2025 14:23:19 -0500 Subject: [PATCH 02/17] Created two examples as tests --- vcell-core/pom.xml | 5 + .../java/cbit/vcell/publish/ITextWriter.java | 34 +++- .../org/vcell/plotting/Results2DLinePlot.java | 67 ++++++++ .../org/vcell/plotting/ResultsLinePlot.java | 29 ++++ .../vcell/plotting/TestJFreeChartLibrary.java | 149 ++++++++++++++++++ .../plotting/plot2d_SimpleSimulation.csv | 3 + 6 files changed, 280 insertions(+), 7 deletions(-) create mode 100644 vcell-core/src/main/java/org/vcell/plotting/Results2DLinePlot.java create mode 100644 vcell-core/src/main/java/org/vcell/plotting/ResultsLinePlot.java create mode 100644 vcell-core/src/test/java/org/vcell/plotting/TestJFreeChartLibrary.java create mode 100644 vcell-core/src/test/resources/org/vcell/plotting/plot2d_SimpleSimulation.csv diff --git a/vcell-core/pom.xml b/vcell-core/pom.xml index c543edcf0b..b00999072a 100644 --- a/vcell-core/pom.xml +++ b/vcell-core/pom.xml @@ -171,6 +171,11 @@ + + org.jfree + jfreechart + 1.5.5 + org.jgrapht jgrapht-core diff --git a/vcell-core/src/main/java/cbit/vcell/publish/ITextWriter.java b/vcell-core/src/main/java/cbit/vcell/publish/ITextWriter.java index b6bd174341..555312bcc1 100644 --- a/vcell-core/src/main/java/cbit/vcell/publish/ITextWriter.java +++ b/vcell-core/src/main/java/cbit/vcell/publish/ITextWriter.java @@ -20,10 +20,10 @@ import java.awt.image.IndexColorModel; import java.awt.image.WritableRaster; import java.awt.print.PageFormat; -import java.io.ByteArrayOutputStream; -import java.io.FileOutputStream; +import java.io.*; import java.util.ArrayList; import java.util.Enumeration; +import java.util.List; import java.util.Vector; import javax.imageio.IIOImage; @@ -905,20 +905,40 @@ protected void createDocument(PageFormat pageFormat) { this.document = new Document(pageSize, (float) marginL, (float) marginR, (float) marginT, (float) marginB); } + public void writePlotImageDocument(String docTitle, FileOutputStream fos, PageFormat pageFormat, BufferedImage... plotImages) throws DocumentException, IOException { + if (plotImages == null) throw new IllegalArgumentException("No plot images provided"); + if (fos == null) throw new IllegalArgumentException("No outputstream for plot-document provided"); + if (pageFormat == null) throw new IllegalArgumentException("No page format provided"); + + this.createDocument(pageFormat); + this.createDocWriter(fos); + this.document.addTitle(docTitle); + this.document.addCreator("Virtual Cell"); + this.document.addCreationDate(); + this.document.open(); + for (BufferedImage plotImage : plotImages){ + com.lowagie.text.Image image = com.lowagie.text.Image.getInstance(plotImage, null); + image.setAlignment(Table.ALIGN_LEFT); + this.document.add(image); + if (this.document.getPageNumber() != plotImages.length) this.document.newPage(); + } + this.document.close(); + } + - public void writeBioModel(BioModel biomodel, FileOutputStream fos, PageFormat pageFormat) throws Exception { + public void writeBioModel(BioModel biomodel, FileOutputStream fos, PageFormat pageFormat) throws DocumentException { writeBioModel(biomodel, fos, pageFormat, PublishPreferences.DEFAULT_BIO_PREF); } - public void writeBioModel(BioModel bioModel, FileOutputStream fos, PageFormat pageFormat, PublishPreferences preferences) throws Exception { + public void writeBioModel(BioModel bioModel, FileOutputStream fos, PageFormat pageFormat, PublishPreferences preferences) throws DocumentException { if (bioModel == null || fos == null || pageFormat == null || preferences == null) { throw new IllegalArgumentException("One or more null params while publishing BioModel."); } - createDocument(pageFormat); - createDocWriter(fos); + this.createDocument(pageFormat); + this.createDocWriter(fos); // Add metadata before you open the document... String name = bioModel.getName().trim(); String userName = "Unknown"; @@ -2079,7 +2099,7 @@ protected void writeSimulation(Section container, Simulation sim) throws Documen //SimulationContext: ignored the constraints (steady/unsteady). //Electrical Stimulus: ignored the Ground Electrode, - protected void writeSimulationContext(Chapter simContextsChapter, SimulationContext simContext, PublishPreferences preferences) throws Exception { + protected void writeSimulationContext(Chapter simContextsChapter, SimulationContext simContext, PublishPreferences preferences) throws DocumentException { Section simContextSection = simContextsChapter.addSection("Application: " + simContext.getName(), simContextsChapter.getNumberDepth() + 1); writeMetadata(simContextSection, simContext.getName(), simContext.getDescription(), null, "Application "); diff --git a/vcell-core/src/main/java/org/vcell/plotting/Results2DLinePlot.java b/vcell-core/src/main/java/org/vcell/plotting/Results2DLinePlot.java new file mode 100644 index 0000000000..c5e0035702 --- /dev/null +++ b/vcell-core/src/main/java/org/vcell/plotting/Results2DLinePlot.java @@ -0,0 +1,67 @@ +package org.vcell.plotting; + +import java.io.File; + +/** + * Stores all relevant info to create a 2D plot, and lazily builds a PDF on request + */ +public class Results2DLinePlot implements ResultsLinePlot { + private String plotTitle; + private double[] xData, yData; + private String xLabel, yLabel; + + public Results2DLinePlot(){ + this.plotTitle = ""; + this.xData = new double[0]; + this.yData = new double[0]; + this.xLabel = ""; + this.yLabel = ""; + } + + @Override + public void setTitle(String newTitle) { + this.plotTitle = newTitle; + } + + @Override + public String getTitle() { + return this.plotTitle; + } + + public void setXData(double[] newXData){ + this.xData = newXData; + } + + public double[] getXData(){ + return this.xData; + } + + public void setYData(double[] newYData){ + this.yData = newYData; + } + + public double[] getYData(){ + return this.yData; + } + + public void setXLabel(String newXLabel){ + this.xLabel = newXLabel; + } + + public String getXLabel(){ + return this.xLabel; + } + + public void setYLabel(String newYLabel){ + this.yLabel = newYLabel; + } + + public String getYLabel(){ + return this.yLabel; + } + + @Override + public void generatePdf(String desiredFileName, File desiredParentDirectory) { + + } +} diff --git a/vcell-core/src/main/java/org/vcell/plotting/ResultsLinePlot.java b/vcell-core/src/main/java/org/vcell/plotting/ResultsLinePlot.java new file mode 100644 index 0000000000..371167d26b --- /dev/null +++ b/vcell-core/src/main/java/org/vcell/plotting/ResultsLinePlot.java @@ -0,0 +1,29 @@ +package org.vcell.plotting; + +import java.io.File; + +/** + * Describes the basic functionality all LinePlots displaying simulation results should have. + */ +public interface ResultsLinePlot { + + /** + * Generates a PDF copy of the current plot state, and exports it as PDF at the desired location. + * @param desiredFileName name of the plot file; needs no file suffix: ".pdf" will be appended. + * @param desiredParentDirectory directory to place the new pdf into. + */ + void generatePdf(String desiredFileName, File desiredParentDirectory); + + /** + * Setter for the title of the plot + * @param newTitle the desired new title as a String + */ + void setTitle(String newTitle); + + /** + * Getter for the title of the plot + * @return the title as a String + */ + String getTitle(); + +} diff --git a/vcell-core/src/test/java/org/vcell/plotting/TestJFreeChartLibrary.java b/vcell-core/src/test/java/org/vcell/plotting/TestJFreeChartLibrary.java new file mode 100644 index 0000000000..5cd41910b0 --- /dev/null +++ b/vcell-core/src/test/java/org/vcell/plotting/TestJFreeChartLibrary.java @@ -0,0 +1,149 @@ +package org.vcell.plotting; + +import cbit.vcell.publish.PDFWriter; +import com.lowagie.text.DocumentException; +import org.jfree.chart.*; +import org.jfree.chart.labels.StandardXYItemLabelGenerator; +import org.jfree.chart.labels.XYItemLabelGenerator; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.category.LineAndShapeRenderer; +import org.jfree.data.*; +import org.jfree.data.xy.*; +import org.jfree.data.category.CategoryDataset; +import org.jfree.data.general.DefaultKeyedValues2DDataset; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.vcell.sbml.BMDB_SBML_Files; +import org.vcell.util.Pair; +import scala.collection.immutable.Page; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.print.PageFormat; +import java.awt.print.Paper; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.List; + + +@Tag("Fast") +public class TestJFreeChartLibrary { + + private static List testData = List.of( + new XYDataItem(0.0, 0.0), + new XYDataItem(0.5, 0.25), + new XYDataItem(1.0, 1.0), + new XYDataItem(1.5, 2.25), + new XYDataItem(2.0, 4.0), + new XYDataItem(2.5, 6.25), + new XYDataItem(3.0, 9.0), + new XYDataItem(3.5, 12.25), + new XYDataItem(4.0, 16.0), + new XYDataItem(4.5, 20.25), + new XYDataItem(5.0, 25.0), + new XYDataItem(5.5, 30.25) + ); + + @Test + public void testDirectChartCreation() throws IOException, DocumentException { + PageFormat pageFormat = TestJFreeChartLibrary.generateAlternatePageFormat(); + + // Build Chart + XYSeries xySeries = new XYSeries("Ka+", true, false); // The "key" field is what to name the curve itself, when shown in a Legend + for (XYDataItem valuePair: TestJFreeChartLibrary.testData){ + xySeries.add(valuePair); + } + XYDataset dataset2D = new XYSeriesCollection(xySeries); + this.generateChart(dataset2D, pageFormat, "Time"); + } + + @Test + public void testDirectChartCreationFromCSV() throws IOException, DocumentException { + PageFormat pageFormat = TestJFreeChartLibrary.generateAlternatePageFormat(); + Pair datasetPair = TestJFreeChartLibrary.csvToXYDataset(); + this.generateChart(datasetPair.two, pageFormat, datasetPair.one); + } + + private void generateChart(XYDataset dataset2D, PageFormat pageFormat, String xAxisLabel) throws IOException, DocumentException { + int AXIS_LABEL_FONT_SIZE = 15; + String yAxisLabel = ""; + JFreeChart chart = ChartFactory.createXYLineChart("Test Sim Results", xAxisLabel, yAxisLabel, dataset2D); + + // Tweak Chart so it looks better + chart.setBorderVisible(true); + XYPlot chartPlot = chart.getXYPlot(); + chartPlot.getDomainAxis().setLabelFont(new Font(xAxisLabel, Font.PLAIN, AXIS_LABEL_FONT_SIZE)); + chartPlot.getRangeAxis().setLabelFont(new Font(yAxisLabel, Font.PLAIN, AXIS_LABEL_FONT_SIZE)); + if (dataset2D.getItemCount(0) <= 10) { // if it's too crowded, having data point labels is bad + for (int i = 0; i < dataset2D.getSeriesCount(); i++){ + //DecimalFormat decimalformat1 = new DecimalFormat("##"); + chartPlot.getRenderer().setSeriesItemLabelGenerator(0, + new StandardXYItemLabelGenerator("({1}, {2})")); + chartPlot.getRenderer().setSeriesItemLabelFont(i, new Font(null, Font.PLAIN, 8)); + chartPlot.getRenderer().setSeriesItemLabelsVisible(i, true); + } + } + + // Prepare for export + PDFWriter pdfWriter = new PDFWriter(); + + File testfile = File.createTempFile("VCell::TestJFreeChartLibrary::testDirectChartCreation::", ".pdf"); + if (!testfile.exists()) throw new IllegalArgumentException("Unable to create the testfile, somehow."); + try (FileOutputStream fos = new FileOutputStream(testfile)){ + BufferedImage bfi = chart.createBufferedImage((int)pageFormat.getImageableWidth(), (int)pageFormat.getImageableHeight()); + pdfWriter.writePlotImageDocument("Test Document", fos, pageFormat, bfi); + } + } + + private static PageFormat generateAlternatePageFormat(){ + java.awt.print.PageFormat pageFormat = java.awt.print.PrinterJob.getPrinterJob().defaultPage(); + Paper alternatePaper = new Paper(); // We want to try and increase the margins + double altOriginX = alternatePaper.getImageableX() / 2, altOriginY = alternatePaper.getImageableY() / 2; + double altWidth = alternatePaper.getWidth() - 2 * altOriginX, altHeight = alternatePaper.getHeight() - 2 * altOriginY; + alternatePaper.setImageableArea(altOriginX, altOriginY, altWidth, altHeight); + pageFormat.setPaper(alternatePaper); + pageFormat.setOrientation(PageFormat.LANDSCAPE); + return pageFormat; + } + + private static Pair csvToXYDataset() throws IOException { + String CSV_DATA_FILE_LOCAL_PATH = "plot2d_SimpleSimulation.csv"; + InputStream inputStream = TestJFreeChartLibrary.class.getResourceAsStream(CSV_DATA_FILE_LOCAL_PATH); + if (inputStream == null) + throw new FileNotFoundException(String.format("can not find `%s`; maybe it moved?", CSV_DATA_FILE_LOCAL_PATH)); + XYSeriesCollection dataset2D = new XYSeriesCollection(); + List linesOfData; + try (BufferedReader br = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))){ + linesOfData = br.lines().toList(); + } + String[] indepDataParts = linesOfData.get(linesOfData.size() - 1).split(","); + List xSeriesData = new ArrayList<>(); + for (int partNum = 3; partNum < indepDataParts.length; partNum++){ // Skip first three; no need for them + xSeriesData.add(Double.parseDouble(indepDataParts[partNum])); + } + + for (int lineNum = 0; lineNum < linesOfData.size() - 1; lineNum++){ // Skip last line; that's our independent-var data + XYSeries series = getXySeries(linesOfData, lineNum, xSeriesData); + dataset2D.addSeries(series); + } + + return new Pair<>(indepDataParts[2], dataset2D); + } + + private static XYSeries getXySeries(List linesOfData, int lineNum, List xSeriesData) { + String[] parts = linesOfData.get(lineNum).split(","); + String ySeriesName = parts[2]; + XYSeries series = new XYSeries(ySeriesName, true); + if (parts.length - 3 != xSeriesData.size()) throw new RuntimeException("Mismatched data length!"); + for (int partNum = 3; partNum < parts.length; partNum++){ // Skip first two; third we've already grabbed + double xData = xSeriesData.get(partNum - 3), yData = Double.parseDouble(parts[partNum]); + series.add(xData, yData); + } + return series; + } +} diff --git a/vcell-core/src/test/resources/org/vcell/plotting/plot2d_SimpleSimulation.csv b/vcell-core/src/test/resources/org/vcell/plotting/plot2d_SimpleSimulation.csv new file mode 100644 index 0000000000..3b22f9b7b9 --- /dev/null +++ b/vcell-core/src/test/resources/org/vcell/plotting/plot2d_SimpleSimulation.csv @@ -0,0 +1,3 @@ +dataGen_tsk_0_0_SpeciesAlpha,dataGen_tsk_0_0_SpeciesAlpha,"SpeciesAlpha",10.0,9.987016241437045,9.974064893924155,9.961145887611409,9.948259137986712,9.935404564900482,9.922582088174698,9.909791627690753,9.89703310346457,9.884306435723548,9.871611544913684,9.858948351676503,9.84631677685886,9.833716741523588,9.821148166930389,9.808610974531152,9.796105085968858,9.78363042308044,9.771186907898187,9.758774462649843,9.746393009757767,9.734042471838068,9.721722771699977,9.709433832345374,9.697175576968265,9.684947928954436,9.67275081188104,9.660584149516083,9.648447865817857,9.636341884934554,9.624266131203779,9.612220529152037,9.60020500349427,9.5882194791334,9.57626388115986,9.564338134851125,9.552442165671241,9.540575899270385,9.528739261484354,9.51693217833411,9.50515457602533,9.493406380947961,9.481687519675752,9.469997918965785,9.458337505758013,9.446706207174815,9.435103950520537,9.423530663281038,9.411986273123237,9.400470707894655,9.388983895622973,9.377525764515575,9.366096242959106,9.354695259519021,9.343322742939135,9.33197862214118,9.320662826224368,9.309375284464934,9.298115926315708,9.286884681405663,9.275681479539482,9.264506250697119,9.253358925033355,9.24223943287737,9.2311477047323,9.220083671274809,9.209047263354652,9.198038411994244,9.187057048388226,9.176103103903042,9.165176510076503,9.15427719861736,9.143405101404882,9.132560150488429,9.121742278087018,9.110951416588913,9.100187498551195,9.08945045669934,9.078740223926804,9.068056733294595,9.057399918030864,9.046769711530478,9.036166047354614,9.025588859230336,9.015038081050182,9.004513646871755,8.99401549091731,8.983543547573333,8.97309775139015,8.962678037081494,8.952284339524121,8.941916593757387,8.931574734982847,8.92125869856385,8.910968420025135,8.900703835052427,8.890464879492036,8.880251489350458,8.87006360079397,8.859901150148236,8.849764073897905,8.839652308686219,8.829565791314613,8.819504458742319,8.809468248085977,8.799457096619237,8.78947094177237,8.779509721131875,8.769573372440092,8.759661833594809,8.749775042648874,8.739912937809814,8.73007545743944,8.720262540053469,8.710474124321134,8.700710149064806,8.690970553259607,8.681255276033031,8.671564256664565,8.661897434585306,8.652254749377585,8.642636140774588,8.633041548659978,8.623470913067525,8.613924174180726,8.60440127233243,8.59490214800447,8.58542674182729,8.575974994579566,8.566546847187851,8.557142240726192,8.547761116415767,8.53840341562452,8.52906907986679,8.519758050802947,8.510470270239031,8.501205680126382,8.491964222561283,8.482745839784592,8.473550474181387,8.464378068280604,8.455228564754677,8.446101906419178,8.436998036232463,8.427916897295313,8.41885843285058,8.409822586282834,8.400809301118,8.391818521023021,8.38285018980549,8.373904251413308,8.364980649934331,8.356079329596021,8.347200234765097,8.338343309947188,8.329508499786483,8.320695749065392,8.311905002704192,8.30313620576069,8.29438930342988,8.28566424104359,8.276960964070152,8.26827941811406,8.259619548915621,8.250981302350624,8.24236462443,8.233769461299485,8.22519575923928,8.21664346466372,8.208112524120937,8.199602884292522,8.191114491993202,8.182647294170495,8.174201237904388,8.165776270407001,8.157372339022261,8.14898939122557,8.140627374623474,8.132286236953345,8.123965926083043,8.1156663900106,8.10738757686389,8.099129434900298,8.090891912506414,8.082674958197696,8.074478520618154,8.066302548540024,8.058146990863456,8.050011796616186,8.041896914953224,8.033802295156534,8.025727886634714,8.017673638922687,8.009639501681377,8.001625424697401,7.9936313578827525,7.985657251274486,7.977703055034411,7.969768719448773,7.96185419492795,7.953959432006138,7.946084381341042,7.9382289937135715,7.930393220027527,7.922577011309298,7.914780318707554,7.907003093492942,7.899245287057777,7.8915068509157456,7.883787736701596,7.876087896170839,7.868407281199446,7.860745843783549,7.853103536039139,7.8454803102017685,7.83787611862625,7.830290913786361,7.822724648274548,7.815177274801626,7.807648746196487,7.800139015405803,7.79264803549373,7.785175759641621,7.777722141147727,7.770287133426908,7.762870690010343,7.755472764545234,7.748093310794525,7.740732282636605,7.733389634065024,7.726065319188205,7.718759292229156,7.711471507525184,7.704201919527612,7.6969504828014905,7.689717152025317,7.68250188199075,7.675304627602329,7.66812534387719,7.660963985944786,7.653820509046608,7.646694868535901,7.639587019877389,7.632496918646995,7.6254245205315625,7.618369781328581,7.6113326569459065,7.6043131034014895,7.597311076823098,7.590326533448041,7.583359429622902,7.576409721803259,7.569477366553414,7.562562320546126,7.555664540562335,7.548783983490892,7.5419206063282935,7.53507436617841,7.528245220252218,7.521433125867534,7.514638040448746,7.507859921526547,7.5010987267376725,7.494354413824633,7.48762694063545,7.480916265123395,7.474222345346724,7.467545139468417,7.460884605755914,7.454240702580859,7.447613388418835,7.441002621849108,7.434408361554365,7.4278305663204565,7.421269195036142,7.414724206692829,7.408195560384319,7.4016832153065515,7.395187130757349,7.388707266136161,7.382243580943814,7.375796034782253,7.369364587354294,7.362949198463371,7.35654982801328,7.350166436007937,7.34379898255112,7.337447427846225,7.331111732196014,7.324791856002369,7.318487759766041,7.312199404086411,7.305926749661234,7.2996697572863996,7.293428387855685,7.287202602360511,7.280992361889699,7.2747976276292246,7.2686183608619785,7.262454522967524,7.256306075421854,7.250172979797151,7.244055197761548,7.2379526910788865,7.231865421608481,7.225793351304875,7.219736442217612,7.21369465649099,7.207667956363828,7.201656304169231,7.195659662334353,7.189677993380163,7.18371125992121,7.177759424665391,7.171822450413716,7.165900300060077,7.159992936591015,7.15410032308549,7.148222422714646,7.142359198741588,7.136510614521148,7.130676633499654,7.124857219214706,7.119052335294946,7.113261945459831,7.107486013519405,7.101724503374075,7.095977379014383,7.090244604520784,7.084526144063418,7.07882196190189,7.073132022385042,7.067456289950733,7.061794729125617,7.056147304524921,7.050513980852222,7.04489472289923,7.039289495545563,7.033698263758533,7.028120992592922,7.0225576471907685,7.0170081927811445,7.011472594679942,7.005950818289655,7.000442829099163,6.994948592683516,6.989468074703719,6.984001240906516,6.978548057124178,6.9731084892742885,6.967682503359531,6.962270065467475,6.9568711417703675,6.951485698524917,6.946113702072085,6.940755118836878,6.935409915328132,6.930078058138308,6.924759513943282,6.919454249502134,6.914162231656944,6.908883427332585,6.903617803536511,6.898365327358557,6.89312596597073,6.887899686627003,6.882686456663115,6.87748624349636,6.87229901462539,6.867124737630006,6.8619633801709625,6.856814909989756,6.8516792949084335,6.846556502829382,6.841446501735136,6.83634925968817,6.8312647448307064,6.82619292538451,6.821133769650693,6.816087246009515,6.811053322920189,6.806031968920676,6.8010231526275,6.796026842735541,6.791043008017847,6.786071617325434,6.781112639587095,6.7761660438092015,6.771231799075515,6.76630987454699,6.761400239461581,6.756502863134054,6.75161771495579,6.746744764394597,6.741883980994519,6.737035334375643,6.732198794233913,6.727374330340936,6.722561912543798,6.717761510764872,6.71297309500163,6.70819663532646,6.703432101886472,6.698679464903317,6.693938694672998,6.689209761565686,6.684492636025533,6.67978728857049,6.67509368979212,6.670411810355414,6.665741620998612,6.661083092533015,6.6564361958428035,6.65180090188486,6.647177181688581,6.6425650063557,6.637964347060107,6.633375175047667,6.628797461636038,6.624231178214498,6.61967629624376,6.6151327872557975,6.610600622853664,6.606079774711316,6.601570214573439,6.597071914255267,6.592584845642409,6.588108980690672,6.583644291425885,6.579190749943725,6.5747483284095445,6.5703169990581936,6.565896734193849,6.561487506189842,6.557089287488482,6.552702050600889,6.548325768106817,6.543960412654488,6.539605956960414,6.535262373809235,6.53092963605354,6.5266077166137055,6.52229658847772,6.517996224701019,6.513706598406315,6.5094276827834285,6.505159451089125,6.5009018766469415,6.496654932847023,6.492418593145958,6.488192831066609,6.48397762019795,6.479772934194899,6.4755787467781545,6.471395031734032,6.467221762914298,6.463058914236011,6.458906459681351,6.454764373297463,6.450632629196296,6.446511201554434,6.442400064612941,6.438299192677198,6.434208560116743,6.430128141365109,6.426057910919664,6.4219978433414555,6.417947913255048,6.413908095348365,6.409878364372531,6.405858695141716,6.401849062532972,6.397849441486084,6.393859807003406,6.389880134149711,6.385910398052029,6.3819505738994975,6.378000636943202,6.374060562496025,6.370130325932487,6.366209902688597,6.3622992682616974,6.35839839821031,6.354507268153984,6.350625853773145,6.346754130808939,6.3428920750630855,6.339039662397724,6.335196868735262,6.331363670058227,6.327540042409115,6.323725961890241,6.319921404663588,6.3161263469506626,6.31234076503234,6.308564635248723,6.304797933998986,6.301040637741235,6.297292722992357,6.293554166327872,6.289824944381787,6.286105033846455,6.2823944114724215,6.2786930540682855,6.2750009385005505,6.271318041693483,6.267644340628966,6.263979812346355,6.260324433942336,6.256678182570783,6.253041035442614,6.249412969825645,6.2457939630444566,6.242183992480244,6.238583035570678,6.234991069809768,6.231408072747715,6.227834021990776,6.2242688952011225,6.220712670096701,6.217165324451091,6.213626836093373,6.210097182907981,6.206576342834573,6.203064293867887,6.199561014057604,6.196066481508216,6.192580674378881,6.189103570883295,6.185635149289547,6.182175387919994,6.178724265151114,6.17528175941338,6.171847849191119,6.168422513022381,6.165005729498803,6.161597477265479,6.15819773502082,6.154806481516426,6.151423695556951,6.148049355999973,6.144683441755858,6.141325931787631,6.137976805110845,6.1346360407934455,6.131303617955646,6.127979515769792,6.124663713460235,6.1213561903031986,6.118056925626653,6.114765898810183,6.11148308928486,6.108208476533115,6.104942040088606,6.101683759536096,6.09843361451132,6.095191584700862,6.091957649842024,6.0887317897227025,6.085513984181262,6.082304213106408,6.0791024564370595,6.075908694162228,6.072722906320888,6.069545073001856,6.066375174343662,6.06321319053443,6.060059101811752,6.056912888462561,6.053774530823015,6.0506440092783675,6.047521304262849,6.044406396259544,6.041299265800266,6.03819989346544,6.0351082598839785,6.032024345733162,6.028948131738517,6.025879598673695,6.022818727360356,6.019765498668043,6.016719893514068,6.013681892863388,6.010651477728489,6.007628629169266,6.0046133282929075,6.001605556253772,5.998605294253274,5.995612523539767,5.992627225408424,5.989649381201122,5.986678972306326,5.983715980158968,5.98076038624034,5.97781217207797,5.974871319245509,5.971937809362618,5.9690116240948505,5.9660927451535395,5.963181154295681,5.960276833323823,5.9573797640859505,5.954489928475371,5.951607308430603,5.948731885935261,5.945863643017946,5.943002561752129,5.940148624256044,5.9373018126925725,5.934462109269132,5.9316294962375675,5.928803955894038,5.925985470578907,5.923174022676633,5.920369594615657,5.917572168868294,5.914781727950625,5.911998254422384,5.909221730886853,5.90645213999075,5.903689464424125,5.9009336869202444,5.898184790255492,5.895442757249253,5.892707570763812,5.889979213704246,5.887257669018314,5.884542919696352,5.881834948771169,5.879133739317937,5.876439274454089,5.87375153733921,5.871070511174935,5.868396179204841,5.865728524714345,5.863067531030597,5.86041318152238,5.857765459599999,5.855124348715183,5.852489832360981,5.849861894071658,5.84724051742259,5.844625686030166,5.842017383551679,5.839415593685231,5.836820300169628,5.834231486784276,5.831649137349083,5.829073235724356,5.826503765810703,5.823940711548927,5.821384056919931,5.818833785944617,5.8162898826837806,5.813752331238019,5.811221115747628,5.808696220392502,5.806177629392037,5.803665327005032,5.801159297529587,5.798659525303011,5.796165994701719,5.793678690141136,5.791197596075602,5.788722696998269,5.786253977441011,5.783791421974323,5.781335015207224,5.778884741787165,5.776440586399928,5.774002533769535,5.771570568658149,5.769144675865979,5.766724840231189,5.764311046629796,5.761903279975584,5.759501525220004,5.757105767352079,5.7547159913983155,5.752332182422606,5.7499543255261365,5.747582405847294,5.745216408561573,5.742856318881483,5.740502122056454,5.73815380337275,5.735811348153371,5.733474741757964,5.731143969582732,5.72881901706034,5.726499869659829,5.7241865128865195,5.721878932281924,5.7195771134236555,5.7172810419253395,5.714990703436522,5.712706083642579,5.71042716826463,5.708153943059446,5.7058863938193625,5.703624506372189,5.701368266581122,5.699117660344656,5.696872673596493,5.69463329230546,5.692399502475416,5.690171290145167,5.687948641388379,5.685731542313491,5.683519979063625,5.681313937816505,5.679113404784364,5.676918366213865,5.67472880838601,5.672544717616054,5.670366080253424,5.668192882681629,5.666025111318176,5.663862752614489,5.661705793055817,5.659554219161157,5.6574080174831645,5.655267174608072,5.6531316771556055,5.651001511778898,5.64887666516441,5.646757124031843,5.644642875134056,5.642533905256989,5.640430201219572,5.6383317498736485,5.636238538103891,5.634150552827719,5.632067780995217,5.629990209589055,5.6279178256244045,5.625850616148859,5.623788568242353,5.62173166901708,5.619679905617414,5.617633265219825,5.615591735032806,5.613555302296787,5.611523954284054,5.609497678298677,5.607476461676425,5.605460291784687,5.6034491560223945,5.601443041819943,5.599441936639111,5.597445827972986,5.595454703345881,5.593468550313262,5.591487356461665,5.5895111094086225,5.5875397968025835,5.585573406322839,5.58361192567944,5.581655342613128,5.579703644895251,5.577756820327694,5.575814856742794,5.573877742003275,5.571945464002163,5.5700180106627135,5.568095369938337,5.566177529812523,5.564264478298765,5.562356203440483,5.560452693310955,5.558553936013236,5.556659919680086,5.554770632473897,5.552886062586618,5.551006198239681,5.549131027683927,5.547260539199535,5.5453947210959464,5.54353356171179,5.541677049414815,5.539825172601813,5.537977919698548,5.536135279159684,5.53429723946871,5.532463789137874,5.530634916708104,5.528810610748942,5.526990859858469,5.525175652663237,5.523364977818195,5.521558824006619,5.519757179940042,5.5179600343581825,5.516167376028875,5.514379193747999,5.512595476339409,5.510816212654865,5.509041391573965,5.50727100200407,5.50550503288024,5.503743473165162,5.501986311849082,5.500233537949737,5.498485140512283,5.496741108609231,5.495001431340376,5.493266097832729,5.49153509724045,5.48980841874478,5.488086051553973,5.486367984903227,5.4846542080546214,5.482944710297045,5.481239480946131,5.479538509344191,5.4778417848601455,5.476149296889463,5.4744610348540865,5.472776988202374,5.471097146409027,5.469421498975029,5.46775003542758,5.466082745320025,5.464419618231798,5.4627606437683465,5.461105811561076,5.45945511126728,5.457808532570076,5.456166065178342,5.454527698826651,5.452893423275209,5.451263228309788,5.449637103741663,5.448015039407552,5.446397025169546,5.444783050915051,5.443173106556724,5.441567182032407,5.439965267305065,5.438367352362728,5.43677342721842,5.435183481910106,5.433597506500621,5.432015491077614,5.4304374257534835,5.428863300665316,5.427293105974826,5.42572683186829,5.42416446855649,5.42260600627465,5.421051435282376,5.419500745863594,5.41795392832649,5.41641097300345,5.414871870250998,5.413336610449736,5.411805184004287,5.4102775813432284,5.408753792919041,5.40723380920804,5.405717620710323,5.404205217949708,5.4026965914736715,5.401191731853293,5.399690629683197,5.398193275581487,5.396699660189698,5.395209774172727,5.3937236082187825,5.392241153039322,5.390762399368995,5.389287337965588,5.387815959609962,5.386348255105997,5.384884215280534,5.383423830983321,5.381967093086951,5.380513992486807,5.379064520101005,5.377618666870339,5.3761764237582215,5.374737781750628,5.373302731856041,5.371871265105395,5.3704433725520175,5.369019045271575,5.367598274362018,5.366181050943522,5.364767366158439,5.363357211171233,5.361950577168431,5.360547455358565,5.359147836972119,5.357751713261475,5.356359075500854,5.354969914986266,5.353584223035453,5.352201990987836,5.35082321020446,5.349447872067941,5.348075967982411,5.346707489373466,5.3453424276881085,5.3439807743947,5.342622520982901,5.341267658963625,5.33991617986898,5.3385680752522156,5.337223336687675,5.335881955770738,5.334543924117768,5.333209233366064,5.331877875173805,5.330549841219997,5.329225123204424,5.3279037128475935,5.326585601890686,5.325270782095505,5.323959245244422,5.322650983140328,5.321345987606579,5.32004425048695,5.318745763645579,5.317450518966919,5.316158508355688,5.314869723736814,5.31358415705539,5.312301800276619,5.311022645385767,5.309746684388113,5.308473909308895,5.3072043121932655,5.305937885106238,5.30467462013264,5.303414509377061,5.302157544963803,5.300903719036836,5.299653023759745,5.298405451315677,5.297160993907303,5.2959196437567595,5.294681393105604,5.293446234214765,5.292214159364496,5.290985160854326,5.28975923100301,5.288536362148481,5.287316546647806,5.286099776877133,5.284886045231649,5.283675344125526,5.282467665991878,5.281263003282713,5.280061348468885,5.278862694040048,5.277667032504607,5.2764743563896745,5.275284658241021,5.274097930623028,5.272914166118644,5.271733357329339,5.270555496875052,5.269380577394153,5.26820859154339,5.267039531997848,5.265873391450902,5.264710162614169,5.2635498382174655,5.26239241100876,5.261237873754127,5.260086219237707,5.258937440261655,5.257791529646099,5.256648480229093,5.2555082848665755,5.254370936432321,5.253236427817898,5.252104751932624,5.250975901703521,5.2498498700752725,5.248726650010176,5.247606234488103,5.246488616506452,5.245373789080109,5.244261745241396,5.243152478040038,5.24204598054311,5.240942245834999,5.2398412670173595,5.238743037209071,5.237647549546193,5.236554797181925,5.23546477328656,5.234377471047445,5.2332928836689385,5.232211004372366,5.231131826395977,5.230055342994906,5.22898154744113,5.227910433023421,5.226841993047312 +dataGen_tsk_0_0_SpeciesGamma,dataGen_tsk_0_0_SpeciesGamma,"SpeciesGamma",2.0,2.012983758562952,2.0259351060758437,2.0388541123885893,2.0517408620132853,2.0645954350995153,2.0774179118252993,2.090208372309244,2.1029668965354267,2.115693564276449,2.128388455086312,2.1410516483234927,2.1536832231411362,2.166283258476408,2.1788518330696065,2.191389025468843,2.2038949140311384,2.2163695769195555,2.2288130921018086,2.2412255373501533,2.2536069902422287,2.265957528161927,2.2782772283000186,2.290566167654622,2.3028244230317303,2.31505207104556,2.3272491881189574,2.339415850483914,2.3515521341821413,2.3636581150654434,2.3757338687962193,2.3877794708479616,2.3997949965057277,2.4117805208665977,2.4237361188401385,2.435661865148874,2.447557834328757,2.459424100729613,2.4712607385156433,2.483067821665888,2.494845423974668,2.506593619052037,2.5183124803242456,2.5300020810342136,2.5416624942419856,2.5532937928251838,2.564896049479462,2.576469336718961,2.5880137268767633,2.5995292921053457,2.6110161043770286,2.6224742354844257,2.6339037570408936,2.645304740480979,2.6566772570608665,2.668021377858821,2.679337173775635,2.690624715535069,2.7018840736842966,2.7131153185943417,2.7243185204605225,2.7354937493028872,2.7466410749666523,2.7577605671226393,2.76885229526771,2.7799163287252022,2.7909527366453606,2.8019615880057707,2.812942951611789,2.8238968960969744,2.834823489923515,2.8457228013826583,2.856594898595136,2.867439849511591,2.8782577219130023,2.8890485834111073,2.899812501448826,2.9105495433006805,2.9212597760732173,2.9319432667054266,2.942600081969159,2.9532302884695456,2.9638339526454107,2.9744111407696905,2.984961918949845,2.9954863531282716,3.005984509082719,3.0164564524266955,3.0269022486098813,3.037321962918537,3.04771566047591,3.0580834062426447,3.068425265017186,3.078741301436184,3.0890315799749004,3.0992961649476096,3.1095351205080015,3.1197485106495813,3.1299363992060703,3.1400988498518054,3.150235926102136,3.1603476913138224,3.1704342086854296,3.1804955412577236,3.1905317519140666,3.200542903380807,3.210529058227675,3.2204902788681697,3.2304266275599534,3.2403381664052375,3.2502249573511723,3.2600870621902334,3.2699245425606076,3.27973745994658,3.2895258756789154,3.2992898509352444,3.309029446740444,3.31874472396702,3.3284357433354863,3.338102565414746,3.347745250622468,3.3573638592254667,3.3669584513400768,3.3765290869325306,3.3860758258193315,3.3955987276676285,3.4050978519955892,3.414573258172772,3.424025005420496,3.4334531528122123,3.4428577592738727,3.452238883584298,3.4615965843755463,3.4709309201332776,3.480241949197121,3.489529729761038,3.4987943198736877,3.5080357774387885,3.51725416021548,3.5264495258186845,3.5356219317194677,3.5447714352453956,3.5538980935808953,3.563001963767611,3.5720831027047613,3.5811415671494933,3.59017741371724,3.5991906988820723,3.608181478977051,3.617149810194581,3.626095748586762,3.635019350065738,3.643920670404047,3.65279976523497,3.661656690052879,3.6704915002135827,3.6793042509346736,3.688094997295872,3.6968637942393716,3.7056106965701816,3.7143357589564703,3.723039035929906,3.731720581885998,3.740380451084437,3.7490186976494333,3.7576353755700564,3.766230538700571,3.774804240760775,3.783356535336334,3.7918874758791166,3.8003971157075296,3.808885508006849,3.817352705829555,3.825798762095661,3.8342237295930466,3.8426276609777856,3.8510106087744767,3.859372625376571,3.8677137630467,3.8760340739170003,3.8843336099894423,3.892612423136154,3.9008705650997437,3.909108087493626,3.9173250418023424,3.9255214793818847,3.933697451460014,3.941853009136582,3.949988203383852,3.958103085046813,3.9661977048435024,3.9742721133653207,3.982326361077347,3.990360498318656,3.9983745753026314,4.00636864211728,4.0143427487255465,4.022296944965622,4.03023128055126,4.038145805072083,4.046040567993895,4.053915618658991,4.061771006286461,4.0696067799725055,4.077422988690735,4.085219681292479,4.092996906507091,4.100754712942256,4.108493149084287,4.116212263298437,4.123912103829194,4.1315927188005865,4.1392541562164835,4.146896463960894,4.154519689798264,4.162123881373783,4.169709086213672,4.177275351725485,4.184822725198407,4.192351253803546,4.19986098459423,4.2073519645063024,4.2148242403584115,4.222277858852306,4.2297128665731245,4.23712930998969,4.2445272354547985,4.251906689205508,4.259267717363428,4.2666103659350085,4.273934680811828,4.281240707770877,4.288528492474849,4.295798080472421,4.303049517198542,4.310282847974716,4.317498118009283,4.324695372397704,4.331874656122843,4.339036014055247,4.346179490953425,4.353305131464132,4.360412980122644,4.367503081353038,4.37457547946847,4.381630218671452,4.388667343054126,4.395686896598543,4.402688923176935,4.409673466551991,4.4166405703771305,4.423590278196774,4.430522633446619,4.4374376794539065,4.444335459437698,4.451216016509141,4.458079393671739,4.464925633821623,4.4717547797478145,4.478566874132499,4.485361959551287,4.4921400784734855,4.49890127326236,4.5056455861754,4.512373059364583,4.519083734876638,4.525777654653309,4.532454860531616,4.539115394244119,4.545759297419174,4.5523866115811975,4.5589973781509245,4.565591638445668,4.572169433679576,4.578730804963891,4.585275793307204,4.591804439615714,4.598316784693481,4.604812869242684,4.611292733863872,4.617756419056219,4.62420396521778,4.630635412645739,4.637050801536662,4.6434501719867525,4.649833563992096,4.656201017448913,4.662552572153808,4.668888267804019,4.675208143997664,4.681512240233991,4.687800595913621,4.694073250338798,4.700330242713632,4.706571612144347,4.7127973976395205,4.719007638110333,4.725202372370807,4.731381639138053,4.737545477032508,4.743693924578178,4.749827020202881,4.755944802238484,4.7620473089211455,4.768134578391551,4.774206648695157,4.78026355778242,4.786305343509042,4.792332043636204,4.798343695830801,4.804340337665679,4.810322006619869,4.816288740078822,4.822240575334641,4.828177549586316,4.834099699939955,4.840007063409017,4.845899676914542,4.851777577285386,4.8576408012584436,4.863489385478884,4.869323366500378,4.875142780785326,4.880947664705086,4.886738054540201,4.892513986480627,4.898275496625957,4.904022620985649,4.909755395479248,4.915473855936614,4.921178038098142,4.92686797761499,4.932543710049299,4.938205270874415,4.943852695475111,4.94948601914781,4.955105277100802,4.960710504454469,4.966301736241499,4.97187900740711,4.9774423528092635,4.9829918072188875,4.98852740532009,4.994049181710377,4.999557170900869,5.0050514073165155,5.010531925296313,5.015998759093516,5.021451942875854,5.0268915107257435,5.032317496640501,5.037729934532557,5.0431288582296645,5.048514301475115,5.053886297927947,5.059244881163154,5.0645900846719,5.069921941861724,5.07524048605675,5.080545750497898,5.0858377683430875,5.091116572667447,5.096382196463521,5.101634672641475,5.106874034029302,5.112100313373029,5.117313543336917,5.122513756503672,5.127700985374642,5.132875262370026,5.1380366198290695,5.143185090010276,5.1483207050915984,5.15344349717065,5.158553498264896,5.163650740311862,5.1687352551693255,5.173807074615522,5.178866230349339,5.1839127539905165,5.188946677079843,5.193968031079356,5.198976847372532,5.203973157264491,5.208956991982185,5.213928382674598,5.218887360412937,5.2238339561908305,5.228768200924517,5.233690125453042,5.238599760538451,5.243497136865978,5.248382285044242,5.253255235605435,5.258116019005513,5.262964665624389,5.267801205766119,5.272625669659096,5.277438087456234,5.28223848923516,5.2870269049984016,5.291803364673572,5.29656789811356,5.301320535096715,5.306061305327034,5.310790238434346,5.315507363974499,5.320212711429542,5.324906310207912,5.329588189644618,5.33425837900142,5.338916907467017,5.343563804157228,5.348199098115172,5.352822818311451,5.357434993644332,5.362035652939925,5.366624824952365,5.371202538363994,5.375768821785534,5.380323703756272,5.3848672127442345,5.389399377146368,5.393920225288717,5.398429785426594,5.402928085744766,5.407415154357624,5.411891019309361,5.416355708574148,5.420809250056307,5.425251671590488,5.429683000941839,5.434103265806184,5.438512493810191,5.442910712511551,5.447297949399144,5.451674231893215,5.456039587345545,5.460394043039619,5.464737626190798,5.469070363946493,5.473392283386327,5.477703411522313,5.482003775299014,5.486293401593718,5.490572317216604,5.494840548910908,5.499098123353091,5.50334506715301,5.507581406854075,5.511807168933424,5.516022379802083,5.520227065805134,5.524421253221878,5.528604968266001,5.532778237085735,5.536941085764022,5.541093540318682,5.54523562670257,5.549367370803737,5.553488798445599,5.557599935387092,5.5617008073228345,5.56579143988329,5.569871858634924,5.573942089080369,5.578002156658577,5.5820520867449845,5.586091904651668,5.590121635627502,5.594141304858317,5.598150937467061,5.602150558513949,5.606140192996627,5.610119865850322,5.614089601948004,5.618049426100535,5.621999363056831,5.625939437504008,5.629869674067546,5.633790097311436,5.637700731738335,5.641601601789723,5.645492731846049,5.649374146226888,5.653245869191094,5.657107924936947,5.660960337602309,5.664803131264771,5.668636329941806,5.672459957590918,5.676274038109792,5.680078595336445,5.68387365304937,5.687659234967692,5.69143536475131,5.6952020660010465,5.6989593622587975,5.702707277007676,5.706445833672161,5.710175055618246,5.713894966153578,5.717605588527611,5.721306945931747,5.724999061499482,5.72868195830655,5.732355659371067,5.736020187653678,5.739675566057697,5.7433218174292495,5.746958964557419,5.750587030174388,5.754206036955576,5.757816007519789,5.761416964429355,5.765008930190265,5.768591927252318,5.772165978009257,5.77573110479891,5.779287329903332,5.7828346755489415,5.78637316390666,5.7899028170920515,5.79342365716546,5.796935706132146,5.800438985942429,5.803933518491817,5.807419325621152,5.810896429116738,5.8143648507104855,5.817824612080039,5.821275734848919,5.824718240586653,5.828152150808914,5.831577486977652,5.834994270501229,5.838402522734554,5.841802264979213,5.845193518483607,5.848576304443082,5.85195064400006,5.855316558244175,5.858674068212402,5.862023194889188,5.865363959206587,5.868696382044387,5.872020484230241,5.875336286539798,5.878643809696834,5.88194307437338,5.88523410118985,5.888516910715173,5.891791523466918,5.895057959911427,5.898316240463937,5.901566385488713,5.904808415299171,5.908042350158009,5.91126821027733,5.91448601581877,5.917695786893625,5.920897543562973,5.924091305837805,5.927277093679145,5.930454926998177,5.933624825656371,5.936786809465603,5.939940898188281,5.943087111537472,5.946225469177018,5.949355990721665,5.952478695737184,5.955593603740489,5.958700734199767,5.961800106534593,5.964891740116054,5.967975654266871,5.971051868261516,5.974120401326338,5.977181272639677,5.9802345013319895,5.983280106485965,5.986318107136645,5.989348522271544,5.992371370830766,5.995386671707125,5.998394443746261,6.001394705746759,6.004387476460266,6.007372774591609,6.010350618798911,6.013321027693707,6.0162840198410645,6.019239613759693,6.022187827922063,6.0251286807545235,6.0280621906374146,6.030988375905182,6.033907254846493,6.036818845704352,6.03972316667621,6.042620235914082,6.045510071524662,6.04839269156943,6.051268114064772,6.054136356982087,6.056997438247904,6.059851375743989,6.06269818730746,6.0655378907309005,6.068370503762465,6.071196044105995,6.074014529421126,6.0768259773233995,6.079630405384376,6.082427831131739,6.085218272049408,6.088001745577649,6.09077826911318,6.0935478600092825,6.096310535575908,6.099066313079788,6.101815209744541,6.10455724275078,6.107292429236221,6.110020786295787,6.112742330981719,6.115457080303681,6.118165051228864,6.120866260682096,6.123560725545944,6.126248462660823,6.128929488825098,6.131603820795192,6.134271475285688,6.136932468969436,6.139586818477653,6.142234540400034,6.14487565128485,6.147510167639052,6.150138105928375,6.1527594825774425,6.155374313969867,6.157982616448354,6.160584406314801,6.163179699830405,6.165768513215757,6.16835086265095,6.170926764275677,6.17349623418933,6.176059288451106,6.178615943080102,6.181166214055416,6.183710117316252,6.186247668762014,6.188778884252405,6.191303779607531,6.193822370607996,6.196334672995001,6.198840702470446,6.201340474697022,6.203834005298314,6.206321309858897,6.208802403924431,6.211277303001764,6.213746022559022,6.21620857802571,6.218664984792809,6.221115258212868,6.223559413600105,6.225997466230498,6.228429431341884,6.230855324134054,6.233275159768844,6.235688953370237,6.238096720024449,6.240498474780029,6.242894232647954,6.245284008601717,6.247667817577427,6.250045674473896,6.252417594152739,6.25478359143846,6.25714368111855,6.259497877943579,6.261846196627283,6.264188651846662,6.266525258242069,6.268856030417301,6.2711809829396925,6.273500130340204,6.275813487113513,6.278121067718109,6.280422886576377,6.282718958074693,6.285009296563511,6.287293916357454,6.289572831735403,6.291846056940587,6.29411360618067,6.296375493627844,6.298631733418911,6.300882339655377,6.30312732640354,6.3053667076945725,6.307600497524617,6.309828709854866,6.312051358611654,6.314268457686542,6.316480020936408,6.318686062183528,6.3208865952156685,6.323081633786168,6.325271191614023,6.327455282383979,6.329633919746609,6.331807117318404,6.3339748886818565,6.336137247385544,6.338294206944216,6.340445780838876,6.342591982516868,6.344732825391961,6.346868322844427,6.3489984882211346,6.351123334835623,6.35324287596819,6.355357124865977,6.357466094743044,6.359569798780461,6.361668250126384,6.363761461896142,6.365849447172314,6.367932219004816,6.370009790410978,6.372082174375628,6.374149383851174,6.37621143175768,6.378268330982952,6.380320094382619,6.3823667347802076,6.3844082649672265,6.386444697703246,6.388476045715979,6.390502321701356,6.392523538323608,6.394539708215346,6.396550843977638,6.39855695818009,6.400558063360922,6.402554172027047,6.404545296654152,6.406531449686771,6.4085126435383675,6.41048889059141,6.412460203197449,6.414426593677194,6.416388074320593,6.418344657386905,6.420296355104782,6.422243179672339,6.4241851432572386,6.426122257996758,6.42805453599787,6.429981989337319,6.4319046300616955,6.4338224701875095,6.435735521701268,6.437643796559549,6.439547306689078,6.441446063986797,6.443340080319947,6.445229367526136,6.447113937413415,6.448993801760352,6.450868972316106,6.4527394608004975,6.454605278904086,6.456466438288243,6.4583229505852175,6.46017482739822,6.462022080301485,6.463864720840349,6.465702760531323,6.467536210862159,6.469365083291929,6.471189389251091,6.4730091401415635,6.474824347336796,6.476635022181838,6.478441175993414,6.480242820059991,6.48203996564185,6.483832623971158,6.485620806252034,6.487404523660624,6.489183787345167,6.490958608426068,6.492728997995963,6.494494967119793,6.496256526834871,6.498013688150951,6.499766462050296,6.50151485948775,6.503258891390802,6.504998568659657,6.506733902167304,6.508464902759583,6.510191581255253,6.51191394844606,6.513632015096806,6.515345791945411,6.517055289702988,6.5187605190539015,6.520461490655842,6.522158215139887,6.52385070311057,6.525538965145946,6.527223011797659,6.528902853591006,6.530578501025004,6.532249964572453,6.5339172546800075,6.535580381768235,6.537239356231686,6.538894188438957,6.540544888732753,6.542191467429957,6.5438339348216905,6.5454723011733815,6.547106576724824,6.548736771690245,6.5503628962583695,6.551984960592481,6.553602974830487,6.555216949084982,6.556826893443309,6.558432817967626,6.560034732694968,6.561632647637305,6.563226572781613,6.564816518089927,6.566402493499412,6.567984508922419,6.569562574246549,6.5711366993347164,6.572706894025207,6.574273168131743,6.575835531443543,6.577393993725383,6.578948564717657,6.580499254136439,6.582046071673543,6.583589026996583,6.585128129749035,6.586663389550297,6.588194815995746,6.589722418656804,6.591246207080992,6.592766190791993,6.59428237928971,6.595794782050325,6.597303408526361,6.5988082681467395,6.600309370316836,6.601806724418545,6.6033003398103345,6.604790225827306,6.60627639178125,6.607758846960711,6.609237600631038,6.6107126620344445,6.612184040390071,6.613651744894036,6.615115784719499,6.616576169016712,6.618032906913082,6.619486007513226,6.620935479899027,6.6223813331296935,6.623823576241811,6.6252622182494045,6.6266972681439915,6.628128734894638,6.629556627448015,6.630980954728458,6.632401725638015,6.6338189490565105,6.635232633841594,6.6366427888288,6.638049422831602,6.639452544641468,6.640852163027914,6.642248286738558,6.643640924499179,6.645030085013767,6.64641577696458,6.647798009012197,6.649176789795573,6.650552127932092,6.651924032017622,6.653292510626567,6.654657572311924,6.656019225605333,6.657377479017132,6.658732341036408,6.660083820131053,6.661431924747817,6.662776663312358,6.664118044229295,6.665456075882265,6.6667907666339685,6.668122124826228,6.669450158780036,6.670774876795609,6.672096287152439,6.673414398109347,6.6747292179045274,6.67604075475561,6.677349016859705,6.678654012393454,6.679955749513083,6.681254236354454,6.682549481033114,6.683841491644345,6.6851302762632185,6.686415842944643,6.687698199723414,6.6889773546142655,6.69025331561192,6.691526090691138,6.692795687806767,6.694062114893795,6.695325379867393,6.696585490622972,6.69784245503623,6.699096280963197,6.700346976240288,6.701594548684356,6.70283900609273,6.704080356243273,6.705318606894429,6.706553765785268,6.707785840635537,6.7090148391457065,6.710240768997023,6.711463637851552,6.712683453352227,6.7139002231228995,6.715113954768384,6.716324655874507,6.717532334008155,6.71873699671732,6.719938651531148,6.721137305959985,6.722332967495426,6.723525643610358,6.724715341759012,6.725902069377005,6.727085833881389,6.728266642670694,6.729444503124981,6.73061942260588,6.731791408456643,6.732960468002185,6.734126608549131,6.735289837385864,6.736450161782567,6.737607588991273,6.738762126245906,6.7399137807623255,6.741062559738378,6.742208470353934,6.74335151977094,6.744491715133457,6.745629063567712,6.746763572182135,6.747895248067409,6.749024098296512,6.75015012992476,6.751273349989857,6.75239376551193,6.7535113834935805,6.754626210919924,6.755738254758636,6.756847521959995,6.757954019456923,6.759057754165034,6.760158732982673,6.761256962790962,6.7623524504538395,6.763445202818108,6.764535226713473,6.765622528952588,6.766707116331094,6.767788995627667,6.768868173604056,6.7699446570051265,6.771018452558903,6.772089566976612,6.773158006952721 +time_tsk_0_0,time_tsk_0_0,"time_tsk_0_0",0.0,0.001,0.002,0.003,0.004,0.005,0.006,0.007,0.008,0.009000000000000001,0.01,0.011,0.012,0.013000000000000001,0.014,0.015,0.016,0.017,0.018000000000000002,0.019,0.02,0.021,0.022,0.023,0.024,0.025,0.026000000000000002,0.027,0.028,0.029,0.03,0.031,0.032,0.033,0.034,0.035,0.036000000000000004,0.037,0.038,0.039,0.04,0.041,0.042,0.043000000000000003,0.044,0.045,0.046,0.047,0.048,0.049,0.05,0.051000000000000004,0.052000000000000005,0.053,0.054,0.055,0.056,0.057,0.058,0.059000000000000004,0.06,0.061,0.062,0.063,0.064,0.065,0.066,0.067,0.068,0.069,0.07,0.07100000000000001,0.07200000000000001,0.073,0.074,0.075,0.076,0.077,0.078,0.079,0.08,0.081,0.082,0.083,0.084,0.085,0.08600000000000001,0.08700000000000001,0.088,0.089,0.09,0.091,0.092,0.093,0.094,0.095,0.096,0.097,0.098,0.099,0.1,0.101,0.10200000000000001,0.10300000000000001,0.10400000000000001,0.105,0.106,0.107,0.108,0.109,0.11,0.111,0.112,0.113,0.114,0.115,0.116,0.117,0.11800000000000001,0.11900000000000001,0.12,0.121,0.122,0.123,0.124,0.125,0.126,0.127,0.128,0.129,0.13,0.131,0.132,0.133,0.134,0.135,0.136,0.137,0.138,0.139,0.14,0.14100000000000001,0.14200000000000002,0.14300000000000002,0.14400000000000002,0.145,0.146,0.147,0.148,0.149,0.15,0.151,0.152,0.153,0.154,0.155,0.156,0.157,0.158,0.159,0.16,0.161,0.162,0.163,0.164,0.165,0.166,0.167,0.168,0.169,0.17,0.171,0.17200000000000001,0.17300000000000001,0.17400000000000002,0.17500000000000002,0.176,0.177,0.178,0.179,0.18,0.181,0.182,0.183,0.184,0.185,0.186,0.187,0.188,0.189,0.19,0.191,0.192,0.193,0.194,0.195,0.196,0.197,0.198,0.199,0.2,0.201,0.202,0.203,0.20400000000000001,0.20500000000000002,0.20600000000000002,0.20700000000000002,0.20800000000000002,0.209,0.21,0.211,0.212,0.213,0.214,0.215,0.216,0.217,0.218,0.219,0.22,0.221,0.222,0.223,0.224,0.225,0.226,0.227,0.228,0.229,0.23,0.231,0.232,0.233,0.234,0.23500000000000001,0.23600000000000002,0.23700000000000002,0.23800000000000002,0.23900000000000002,0.24,0.241,0.242,0.243,0.244,0.245,0.246,0.247,0.248,0.249,0.25,0.251,0.252,0.253,0.254,0.255,0.256,0.257,0.258,0.259,0.26,0.261,0.262,0.263,0.264,0.265,0.266,0.267,0.268,0.269,0.27,0.271,0.272,0.273,0.274,0.275,0.276,0.277,0.278,0.279,0.28,0.281,0.28200000000000003,0.28300000000000003,0.28400000000000003,0.28500000000000003,0.28600000000000003,0.28700000000000003,0.28800000000000003,0.289,0.29,0.291,0.292,0.293,0.294,0.295,0.296,0.297,0.298,0.299,0.3,0.301,0.302,0.303,0.304,0.305,0.306,0.307,0.308,0.309,0.31,0.311,0.312,0.313,0.314,0.315,0.316,0.317,0.318,0.319,0.32,0.321,0.322,0.323,0.324,0.325,0.326,0.327,0.328,0.329,0.33,0.331,0.332,0.333,0.334,0.335,0.336,0.337,0.338,0.339,0.34,0.341,0.342,0.343,0.34400000000000003,0.34500000000000003,0.34600000000000003,0.34700000000000003,0.34800000000000003,0.34900000000000003,0.35000000000000003,0.35100000000000003,0.352,0.353,0.354,0.355,0.356,0.357,0.358,0.359,0.36,0.361,0.362,0.363,0.364,0.365,0.366,0.367,0.368,0.369,0.37,0.371,0.372,0.373,0.374,0.375,0.376,0.377,0.378,0.379,0.38,0.381,0.382,0.383,0.384,0.385,0.386,0.387,0.388,0.389,0.39,0.391,0.392,0.393,0.394,0.395,0.396,0.397,0.398,0.399,0.4,0.401,0.402,0.403,0.404,0.405,0.406,0.40700000000000003,0.40800000000000003,0.40900000000000003,0.41000000000000003,0.41100000000000003,0.41200000000000003,0.41300000000000003,0.41400000000000003,0.41500000000000004,0.41600000000000004,0.417,0.418,0.419,0.42,0.421,0.422,0.423,0.424,0.425,0.426,0.427,0.428,0.429,0.43,0.431,0.432,0.433,0.434,0.435,0.436,0.437,0.438,0.439,0.44,0.441,0.442,0.443,0.444,0.445,0.446,0.447,0.448,0.449,0.45,0.451,0.452,0.453,0.454,0.455,0.456,0.457,0.458,0.459,0.46,0.461,0.462,0.463,0.464,0.465,0.466,0.467,0.468,0.46900000000000003,0.47000000000000003,0.47100000000000003,0.47200000000000003,0.47300000000000003,0.47400000000000003,0.47500000000000003,0.47600000000000003,0.47700000000000004,0.47800000000000004,0.47900000000000004,0.48,0.481,0.482,0.483,0.484,0.485,0.486,0.487,0.488,0.489,0.49,0.491,0.492,0.493,0.494,0.495,0.496,0.497,0.498,0.499,0.5,0.501,0.502,0.503,0.504,0.505,0.506,0.507,0.508,0.509,0.51,0.511,0.512,0.513,0.514,0.515,0.516,0.517,0.518,0.519,0.52,0.521,0.522,0.523,0.524,0.525,0.526,0.527,0.528,0.529,0.53,0.531,0.532,0.533,0.534,0.535,0.536,0.537,0.538,0.539,0.54,0.541,0.542,0.543,0.544,0.545,0.546,0.547,0.548,0.549,0.55,0.551,0.552,0.553,0.554,0.555,0.556,0.557,0.558,0.559,0.56,0.561,0.562,0.5630000000000001,0.5640000000000001,0.5650000000000001,0.5660000000000001,0.5670000000000001,0.5680000000000001,0.5690000000000001,0.5700000000000001,0.5710000000000001,0.5720000000000001,0.5730000000000001,0.5740000000000001,0.5750000000000001,0.5760000000000001,0.577,0.578,0.579,0.58,0.581,0.582,0.583,0.584,0.585,0.586,0.587,0.588,0.589,0.59,0.591,0.592,0.593,0.594,0.595,0.596,0.597,0.598,0.599,0.6,0.601,0.602,0.603,0.604,0.605,0.606,0.607,0.608,0.609,0.61,0.611,0.612,0.613,0.614,0.615,0.616,0.617,0.618,0.619,0.62,0.621,0.622,0.623,0.624,0.625,0.626,0.627,0.628,0.629,0.63,0.631,0.632,0.633,0.634,0.635,0.636,0.637,0.638,0.639,0.64,0.641,0.642,0.643,0.644,0.645,0.646,0.647,0.648,0.649,0.65,0.651,0.652,0.653,0.654,0.655,0.656,0.657,0.658,0.659,0.66,0.661,0.662,0.663,0.664,0.665,0.666,0.667,0.668,0.669,0.67,0.671,0.672,0.673,0.674,0.675,0.676,0.677,0.678,0.679,0.68,0.681,0.682,0.683,0.684,0.685,0.686,0.687,0.6880000000000001,0.6890000000000001,0.6900000000000001,0.6910000000000001,0.6920000000000001,0.6930000000000001,0.6940000000000001,0.6950000000000001,0.6960000000000001,0.6970000000000001,0.6980000000000001,0.6990000000000001,0.7000000000000001,0.7010000000000001,0.7020000000000001,0.7030000000000001,0.704,0.705,0.706,0.707,0.708,0.709,0.71,0.711,0.712,0.713,0.714,0.715,0.716,0.717,0.718,0.719,0.72,0.721,0.722,0.723,0.724,0.725,0.726,0.727,0.728,0.729,0.73,0.731,0.732,0.733,0.734,0.735,0.736,0.737,0.738,0.739,0.74,0.741,0.742,0.743,0.744,0.745,0.746,0.747,0.748,0.749,0.75,0.751,0.752,0.753,0.754,0.755,0.756,0.757,0.758,0.759,0.76,0.761,0.762,0.763,0.764,0.765,0.766,0.767,0.768,0.769,0.77,0.771,0.772,0.773,0.774,0.775,0.776,0.777,0.778,0.779,0.78,0.781,0.782,0.783,0.784,0.785,0.786,0.787,0.788,0.789,0.79,0.791,0.792,0.793,0.794,0.795,0.796,0.797,0.798,0.799,0.8,0.801,0.802,0.803,0.804,0.805,0.806,0.807,0.808,0.809,0.81,0.811,0.812,0.8130000000000001,0.8140000000000001,0.8150000000000001,0.8160000000000001,0.8170000000000001,0.8180000000000001,0.8190000000000001,0.8200000000000001,0.8210000000000001,0.8220000000000001,0.8230000000000001,0.8240000000000001,0.8250000000000001,0.8260000000000001,0.8270000000000001,0.8280000000000001,0.8290000000000001,0.8300000000000001,0.8310000000000001,0.8320000000000001,0.833,0.834,0.835,0.836,0.837,0.838,0.839,0.84,0.841,0.842,0.843,0.844,0.845,0.846,0.847,0.848,0.849,0.85,0.851,0.852,0.853,0.854,0.855,0.856,0.857,0.858,0.859,0.86,0.861,0.862,0.863,0.864,0.865,0.866,0.867,0.868,0.869,0.87,0.871,0.872,0.873,0.874,0.875,0.876,0.877,0.878,0.879,0.88,0.881,0.882,0.883,0.884,0.885,0.886,0.887,0.888,0.889,0.89,0.891,0.892,0.893,0.894,0.895,0.896,0.897,0.898,0.899,0.9,0.901,0.902,0.903,0.904,0.905,0.906,0.907,0.908,0.909,0.91,0.911,0.912,0.913,0.914,0.915,0.916,0.917,0.918,0.919,0.92,0.921,0.922,0.923,0.924,0.925,0.926,0.927,0.928,0.929,0.93,0.931,0.932,0.933,0.934,0.935,0.936,0.937,0.9380000000000001,0.9390000000000001,0.9400000000000001,0.9410000000000001,0.9420000000000001,0.9430000000000001,0.9440000000000001,0.9450000000000001,0.9460000000000001,0.9470000000000001,0.9480000000000001,0.9490000000000001,0.9500000000000001,0.9510000000000001,0.9520000000000001,0.9530000000000001,0.9540000000000001,0.9550000000000001,0.9560000000000001,0.9570000000000001,0.9580000000000001,0.9590000000000001,0.96,0.961,0.962,0.963,0.964,0.965,0.966,0.967,0.968,0.969,0.97,0.971,0.972,0.973,0.974,0.975,0.976,0.977,0.978,0.979,0.98,0.981,0.982,0.983,0.984,0.985,0.986,0.987,0.988,0.989,0.99,0.991,0.992,0.993,0.994,0.995,0.996,0.997,0.998,0.999,1.0 From cac96242849f3c7ca6295e02b15553c92376bc70 Mon Sep 17 00:00:00 2001 From: Logan Drescher Date: Thu, 16 Jan 2025 12:06:37 -0500 Subject: [PATCH 03/17] Adjusting wrapper to new paradigm --- .../org/vcell/plotting/Results2DLinePlot.java | 182 +++++++++++++++--- 1 file changed, 159 insertions(+), 23 deletions(-) diff --git a/vcell-core/src/main/java/org/vcell/plotting/Results2DLinePlot.java b/vcell-core/src/main/java/org/vcell/plotting/Results2DLinePlot.java index c5e0035702..9db00a3cad 100644 --- a/vcell-core/src/main/java/org/vcell/plotting/Results2DLinePlot.java +++ b/vcell-core/src/main/java/org/vcell/plotting/Results2DLinePlot.java @@ -1,21 +1,73 @@ package org.vcell.plotting; +import cbit.vcell.publish.ITextWriter; +import cbit.vcell.publish.PDFWriter; + +import com.lowagie.text.DocumentException; + +import org.jfree.chart.ChartFactory; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.labels.StandardXYItemLabelGenerator; +import org.jfree.chart.plot.XYPlot; +import org.jfree.data.xy.XYDataset; +import org.jfree.data.xy.XYSeries; +import org.jfree.data.xy.XYSeriesCollection; + +import org.vcell.util.Pair; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.print.PageFormat; +import java.awt.print.Paper; import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.*; +import java.util.stream.Stream; /** * Stores all relevant info to create a 2D plot, and lazily builds a PDF on request */ public class Results2DLinePlot implements ResultsLinePlot { + + public static final int AXIS_LABEL_FONT_SIZE = 15; + public static final int MAX_SERIES_DATA_POINT_LABELS = 10; + public static final int SERIES_DATA_POINT_LABEL_FONT_SIZE = 8; + + private static final Logger lg = LogManager.getLogger(Results2DLinePlot.class); + private String plotTitle; - private double[] xData, yData; - private String xLabel, yLabel; + private Pair xData; + private final Map yDataSets; public Results2DLinePlot(){ - this.plotTitle = ""; - this.xData = new double[0]; - this.yData = new double[0]; - this.xLabel = ""; - this.yLabel = ""; + this(""); + } + + public Results2DLinePlot(String plotTitle){ + this(plotTitle, "", new double[0]); + } + + public Results2DLinePlot(String xLabel, Double[] xDataValues){ + this("", xLabel, xDataValues); + } + + public Results2DLinePlot(String xLabel, double[] xDataValues){ + this("", xLabel, xDataValues); + } + + public Results2DLinePlot(String plotTitle, String xLabel, Double[] xDataValues){ + this(plotTitle, xLabel, Stream.of(xDataValues).mapToDouble(Double::doubleValue).toArray()); + } + + public Results2DLinePlot(String plotTitle, String xLabel, double[] xDataValues){ + this.plotTitle = plotTitle; + this.xData = new Pair<>(xLabel, xDataValues); + this.yDataSets = new HashMap<>(); } @Override @@ -28,40 +80,124 @@ public String getTitle() { return this.plotTitle; } + public void setXLabel(String newXLabel){ + this.xData = new Pair<>(newXLabel, this.xData.two); + } + + public void setXData(Double[] newXData){ + this.setXData(Stream.of(newXData).mapToDouble(Double::doubleValue).toArray()); + } + public void setXData(double[] newXData){ - this.xData = newXData; + this.setXData(this.xData.one, newXData); } - public double[] getXData(){ - return this.xData; + public void setXData(String newXLabel, double[] newXData){ + if (this.xData.two.length != newXData.length){ + lg.warn("Changing xData to different length! YData will be purged!"); + this.yDataSets.clear(); + } + this.xData = new Pair<>(newXLabel, newXData); } - public void setYData(double[] newYData){ - this.yData = newYData; + public String getXLabel(){ + return this.xData.one; } - public double[] getYData(){ - return this.yData; + public double[] getXDataValues(){ + return this.xData.two; } - public void setXLabel(String newXLabel){ - this.xLabel = newXLabel; + public Pair getXData(){ + return this.xData; } - public String getXLabel(){ - return this.xLabel; + public void setYData(String yLabel, double[] newYData){ + if (this.xData.two.length != newYData.length){ + lg.error("Error adding dataset `{}`; see exception below", yLabel); + String exceptionMessage = String.format("Can not accept yDataSet: size (%d) does not map with domain size (%d)", newYData.length, this.xData.two.length); + throw new IllegalArgumentException(exceptionMessage); + } + this.yDataSets.put(yLabel, newYData); } - public void setYLabel(String newYLabel){ - this.yLabel = newYLabel; + public int getNumYDataSets(){ + return this.yDataSets.size(); } - public String getYLabel(){ - return this.yLabel; + public double[] getYData(String yLabel){ + if (this.yDataSets.containsKey(yLabel)) return this.yDataSets.get(yLabel); + throw new IllegalArgumentException(String.format("`%s` is not a known dataset in this object", yLabel)); } + public Set getYDataSetLabels(){ + return this.yDataSets.keySet(); + } + + @Override - public void generatePdf(String desiredFileName, File desiredParentDirectory) { + public void generatePdf(String desiredFileName, File desiredParentDirectory) throws ChartCouldNotBeProducedException { + String yAxisLabel = ""; + XYDataset dataset2D = this.generateChartLibraryDataset(); + JFreeChart chart = ChartFactory.createXYLineChart("Test Sim Results", this.xData.one, yAxisLabel, dataset2D); + + // Tweak Chart so it looks better + chart.setBorderVisible(true); + chart.getPlot().setBackgroundPaint(Color.white); + XYPlot chartPlot = chart.getXYPlot(); + chartPlot.getDomainAxis().setLabelFont(new Font(this.xData.one, Font.PLAIN, Results2DLinePlot.AXIS_LABEL_FONT_SIZE)); + chartPlot.getRangeAxis().setLabelFont(new Font(yAxisLabel, Font.PLAIN, Results2DLinePlot.AXIS_LABEL_FONT_SIZE)); + if (this.xData.two.length <= Results2DLinePlot.MAX_SERIES_DATA_POINT_LABELS) { // if it's too crowded, having data point labels is bad + for (int i = 0; i < dataset2D.getSeriesCount(); i++) { + //DecimalFormat decimalformat1 = new DecimalFormat("##"); // Should we ever need it, this object is used in formating the labels. + chartPlot.getRenderer().setSeriesItemLabelGenerator(i, new StandardXYItemLabelGenerator("({1}, {2})")); + chartPlot.getRenderer().setSeriesItemLabelFont(i, new Font(null, Font.PLAIN, Results2DLinePlot.SERIES_DATA_POINT_LABEL_FONT_SIZE)); + chartPlot.getRenderer().setSeriesItemLabelsVisible(i, true); + } + } + + // Prepare for export + PDFWriter pdfWriter = new PDFWriter(); + PageFormat pageFormat = Results2DLinePlot.generateAlternatePageFormat(); + + File testfile = new File(desiredParentDirectory, desiredFileName); + try { + if (testfile.exists() && !testfile.isFile()) throw new IllegalArgumentException("desired PDF already exists, and is not a regular file"); + if (!testfile.exists() && !testfile.createNewFile()) throw new IllegalArgumentException("Unable to create desired PDF; creation itself failed."); + try (FileOutputStream fos = new FileOutputStream(testfile)) { + BufferedImage bfi = chart.createBufferedImage((int) pageFormat.getImageableWidth(), (int) pageFormat.getImageableHeight()); + pdfWriter.writePlotImageDocument("Test Document", fos, pageFormat, bfi); + } catch (DocumentException e) { + lg.error("Error while building PDF; see exception below"); + throw new ChartCouldNotBeProducedException(e); + } + } catch (IOException e){ + lg.error("Error while preparing PDF; see exception below"); + throw new ChartCouldNotBeProducedException(e); + } + } + + private XYDataset generateChartLibraryDataset(){ + XYSeriesCollection dataset2D = new XYSeriesCollection(); + for (String yLabel : this.yDataSets.keySet()){ + double[] yDataValues = this.yDataSets.get(yLabel); + XYSeries series = new XYSeries(yLabel, true, false); + for (int i = 0; i < this.xData.two.length; i++){ // our methods have guaranteed the sizes match! + series.add(this.xData.two[i], yDataValues[i]); + } + dataset2D.addSeries(series); + } + return dataset2D; + } + private static PageFormat generateAlternatePageFormat(){ + java.awt.print.PageFormat pageFormat = java.awt.print.PrinterJob.getPrinterJob().defaultPage(); + Paper alternatePaper = new Paper(); // We want to try and increase the margins + double altOriginX = alternatePaper.getImageableX() / 2, altOriginY = alternatePaper.getImageableY() / 2; + double altWidth = alternatePaper.getWidth() - 2 * altOriginX, altHeight = alternatePaper.getHeight() - 2 * altOriginY; + alternatePaper.setImageableArea(altOriginX, altOriginY, altWidth, altHeight); + pageFormat.setPaper(alternatePaper); + pageFormat.setOrientation(PageFormat.LANDSCAPE); + return pageFormat; } } From 05d0086813c95678d579047ddca269a206d19853 Mon Sep 17 00:00:00 2001 From: Logan Drescher Date: Thu, 16 Jan 2025 12:06:44 -0500 Subject: [PATCH 04/17] Expanding testing --- .../ChartCouldNotBeProducedException.java | 19 ++++++++ .../vcell/plotting/TestJFreeChartLibrary.java | 26 ++++------ .../vcell/plotting/TestResults2DLinePlot.java | 48 +++++++++++++++++++ 3 files changed, 76 insertions(+), 17 deletions(-) create mode 100644 vcell-core/src/main/java/org/vcell/plotting/ChartCouldNotBeProducedException.java create mode 100644 vcell-core/src/test/java/org/vcell/plotting/TestResults2DLinePlot.java diff --git a/vcell-core/src/main/java/org/vcell/plotting/ChartCouldNotBeProducedException.java b/vcell-core/src/main/java/org/vcell/plotting/ChartCouldNotBeProducedException.java new file mode 100644 index 0000000000..f09d11c0e5 --- /dev/null +++ b/vcell-core/src/main/java/org/vcell/plotting/ChartCouldNotBeProducedException.java @@ -0,0 +1,19 @@ +package org.vcell.plotting; + +public class ChartCouldNotBeProducedException extends RuntimeException { + public ChartCouldNotBeProducedException(){ + super(); + } + + public ChartCouldNotBeProducedException(String message){ + super(message); + } + + public ChartCouldNotBeProducedException(Throwable cause){ + super(cause); + } + + public ChartCouldNotBeProducedException(String message, Throwable cause){ + super(message, cause); + } +} diff --git a/vcell-core/src/test/java/org/vcell/plotting/TestJFreeChartLibrary.java b/vcell-core/src/test/java/org/vcell/plotting/TestJFreeChartLibrary.java index 5cd41910b0..4e53aae73e 100644 --- a/vcell-core/src/test/java/org/vcell/plotting/TestJFreeChartLibrary.java +++ b/vcell-core/src/test/java/org/vcell/plotting/TestJFreeChartLibrary.java @@ -1,24 +1,18 @@ package org.vcell.plotting; import cbit.vcell.publish.PDFWriter; + import com.lowagie.text.DocumentException; + import org.jfree.chart.*; import org.jfree.chart.labels.StandardXYItemLabelGenerator; -import org.jfree.chart.labels.XYItemLabelGenerator; import org.jfree.chart.plot.XYPlot; -import org.jfree.chart.renderer.category.LineAndShapeRenderer; -import org.jfree.data.*; import org.jfree.data.xy.*; -import org.jfree.data.category.CategoryDataset; -import org.jfree.data.general.DefaultKeyedValues2DDataset; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; -import org.vcell.sbml.BMDB_SBML_Files; + import org.vcell.util.Pair; -import scala.collection.immutable.Page; import java.awt.*; import java.awt.image.BufferedImage; @@ -26,7 +20,6 @@ import java.awt.print.Paper; import java.io.*; import java.nio.charset.StandardCharsets; -import java.text.DecimalFormat; import java.util.ArrayList; import java.util.List; @@ -70,21 +63,20 @@ public void testDirectChartCreationFromCSV() throws IOException, DocumentExcepti } private void generateChart(XYDataset dataset2D, PageFormat pageFormat, String xAxisLabel) throws IOException, DocumentException { - int AXIS_LABEL_FONT_SIZE = 15; String yAxisLabel = ""; JFreeChart chart = ChartFactory.createXYLineChart("Test Sim Results", xAxisLabel, yAxisLabel, dataset2D); // Tweak Chart so it looks better chart.setBorderVisible(true); + chart.getPlot().setBackgroundPaint(Color.white); XYPlot chartPlot = chart.getXYPlot(); - chartPlot.getDomainAxis().setLabelFont(new Font(xAxisLabel, Font.PLAIN, AXIS_LABEL_FONT_SIZE)); - chartPlot.getRangeAxis().setLabelFont(new Font(yAxisLabel, Font.PLAIN, AXIS_LABEL_FONT_SIZE)); - if (dataset2D.getItemCount(0) <= 10) { // if it's too crowded, having data point labels is bad + chartPlot.getDomainAxis().setLabelFont(new Font(xAxisLabel, Font.PLAIN, Results2DLinePlot.AXIS_LABEL_FONT_SIZE)); + chartPlot.getRangeAxis().setLabelFont(new Font(yAxisLabel, Font.PLAIN, Results2DLinePlot.AXIS_LABEL_FONT_SIZE)); + if (dataset2D.getItemCount(0) <= Results2DLinePlot.MAX_SERIES_DATA_POINT_LABELS) { // if it's too crowded, having data point labels is bad for (int i = 0; i < dataset2D.getSeriesCount(); i++){ //DecimalFormat decimalformat1 = new DecimalFormat("##"); - chartPlot.getRenderer().setSeriesItemLabelGenerator(0, - new StandardXYItemLabelGenerator("({1}, {2})")); - chartPlot.getRenderer().setSeriesItemLabelFont(i, new Font(null, Font.PLAIN, 8)); + chartPlot.getRenderer().setSeriesItemLabelGenerator(i, new StandardXYItemLabelGenerator("({1}, {2})")); + chartPlot.getRenderer().setSeriesItemLabelFont(i, new Font(null, Font.PLAIN, Results2DLinePlot.SERIES_DATA_POINT_LABEL_FONT_SIZE)); chartPlot.getRenderer().setSeriesItemLabelsVisible(i, true); } } diff --git a/vcell-core/src/test/java/org/vcell/plotting/TestResults2DLinePlot.java b/vcell-core/src/test/java/org/vcell/plotting/TestResults2DLinePlot.java new file mode 100644 index 0000000000..4f8c9b367a --- /dev/null +++ b/vcell-core/src/test/java/org/vcell/plotting/TestResults2DLinePlot.java @@ -0,0 +1,48 @@ +package org.vcell.plotting; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Assertions; + +import java.util.Arrays; + +@Tag("Fast") +public class TestResults2DLinePlot { + + @Test + public void testConstructors(){ + double[] xValuesPrim = {0.0, 2.0, 4.0}; + Double[] xValuesWrap = {0.0, 2.0, 4.0}; + Results2DLinePlot[] testInstances = { + new Results2DLinePlot(), + new Results2DLinePlot("Title"), + new Results2DLinePlot("Label", xValuesWrap), + new Results2DLinePlot("Label", xValuesPrim), + new Results2DLinePlot("Title", "Label", xValuesWrap), + new Results2DLinePlot("Title", "Label", xValuesPrim), + }; + + for (Results2DLinePlot instance : testInstances){ + Assertions.assertTrue("".equals(instance.getTitle()) || "Title".equals(instance.getTitle())); + Assertions.assertTrue("".equals(instance.getXLabel()) || "Label".equals(instance.getXLabel())); + Assertions.assertTrue(instance.getXDataValues().length == 0 || instance.getXDataValues().length == 3); + if (instance.getXDataValues().length == 3){ + Assertions.assertArrayEquals(xValuesPrim, instance.getXDataValues()); + } + } + } + + @Test + public void testSettingAndGetting(){ + double[] xValuesPrim = {0.0, 2.0, 4.0}; + Double[] xValuesWrap = {0.0, 2.0, 4.0}; + Results2DLinePlot[] testInstances = { + new Results2DLinePlot(), + new Results2DLinePlot("Title"), + new Results2DLinePlot("Label", xValuesWrap), + new Results2DLinePlot("Label", xValuesPrim), + new Results2DLinePlot("Title", "Label", xValuesWrap), + new Results2DLinePlot("Title", "Label", xValuesPrim), + }; + } +} From 2ba740dd856e155b9c5811a5ca1d776cb6de1fb3 Mon Sep 17 00:00:00 2001 From: Logan Drescher Date: Fri, 17 Jan 2025 07:46:59 -0500 Subject: [PATCH 05/17] Added comparison chart test (part I) --- .../vcell/plotting/TestResults2DLinePlot.java | 82 +++++++++++++++++- .../org/vcell/plotting/Parabolic.png | Bin 0 -> 60271 bytes 2 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 vcell-core/src/test/resources/org/vcell/plotting/Parabolic.png diff --git a/vcell-core/src/test/java/org/vcell/plotting/TestResults2DLinePlot.java b/vcell-core/src/test/java/org/vcell/plotting/TestResults2DLinePlot.java index 4f8c9b367a..12403829de 100644 --- a/vcell-core/src/test/java/org/vcell/plotting/TestResults2DLinePlot.java +++ b/vcell-core/src/test/java/org/vcell/plotting/TestResults2DLinePlot.java @@ -1,13 +1,47 @@ package org.vcell.plotting; +import org.jfree.chart.ChartFactory; +import org.jfree.chart.JFreeChart; +import org.jfree.data.xy.XYDataItem; +import org.jfree.data.xy.XYDataset; +import org.jfree.data.xy.XYSeries; +import org.jfree.data.xy.XYSeriesCollection; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Assertions; +import org.vcell.util.Pair; +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; @Tag("Fast") public class TestResults2DLinePlot { + private static List paraData = List.of( + new XYDataItem(0.0, 0.0), + new XYDataItem(0.5, 0.25), + new XYDataItem(1.0, 1.0), + new XYDataItem(1.5, 2.25), + new XYDataItem(2.0, 4.0), + new XYDataItem(2.5, 6.25), + new XYDataItem(3.0, 9.0), + new XYDataItem(3.5, 12.25), + new XYDataItem(4.0, 16.0), + new XYDataItem(4.5, 20.25), + new XYDataItem(5.0, 25.0), + new XYDataItem(5.5, 30.25) + ); + + private static Pair parabolicData = new Pair<>( + Stream.of(0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5).mapToDouble(Double::valueOf).toArray(), + Stream.of(0.0, 0.25, 1.0, 2.25, 4.0, 6.25, 9.0, 12.25, 16.0, 20.25, 25.0, 30.25).mapToDouble(Double::valueOf).toArray() + ); @Test public void testConstructors(){ @@ -25,7 +59,7 @@ public void testConstructors(){ for (Results2DLinePlot instance : testInstances){ Assertions.assertTrue("".equals(instance.getTitle()) || "Title".equals(instance.getTitle())); Assertions.assertTrue("".equals(instance.getXLabel()) || "Label".equals(instance.getXLabel())); - Assertions.assertTrue(instance.getXDataValues().length == 0 || instance.getXDataValues().length == 3); + Assertions.assertTrue(instance.getXDataValues().length == 0 || instance.getXDataValues().length == xValuesPrim.length); if (instance.getXDataValues().length == 3){ Assertions.assertArrayEquals(xValuesPrim, instance.getXDataValues()); } @@ -45,4 +79,50 @@ public void testSettingAndGetting(){ new Results2DLinePlot("Title", "Label", xValuesPrim), }; } + + @Test + public void pngRoundTripTest() throws IOException { + File dupe = File.createTempFile("VCellPNG::", ".png"); + XYSeries series = new XYSeries("key"); + for (int i = 0; i < TestResults2DLinePlot.parabolicData.one.length; i++){ + series.add(TestResults2DLinePlot.parabolicData.one[i], TestResults2DLinePlot.parabolicData.two[i]); + } + XYSeriesCollection dataset = new XYSeriesCollection(); + dataset.addSeries(series); + JFreeChart chart = ChartFactory.createXYLineChart("Test", "X-Axis","Y-Axis", dataset); + BufferedImage originalImage = chart.createBufferedImage(1000,1000); + ImageIO.write(originalImage, "png", dupe); + BufferedImage roundTrippedImage = ImageIO.read(dupe); + Assertions.assertEquals(originalImage.getWidth(), roundTrippedImage.getWidth()); + Assertions.assertEquals(originalImage.getHeight(), roundTrippedImage.getHeight()); + for (int wPix = 0; wPix < originalImage.getWidth(); wPix++){ + for (int hPix = 0; hPix < originalImage.getHeight(); hPix++){ + Assertions.assertEquals(originalImage.getRGB(wPix, hPix), roundTrippedImage.getRGB(wPix, hPix)); + } + } + } + + @Test + public void pngLibraryLevelTest() throws IOException { + String STANDARD_IMAGE_LOCAL_PATH = "Parabolic.png"; + InputStream standardImageStream = TestResults2DLinePlot.class.getResourceAsStream(STANDARD_IMAGE_LOCAL_PATH); + if (standardImageStream == null) + throw new FileNotFoundException(String.format("can not find `%s`; maybe it moved?", STANDARD_IMAGE_LOCAL_PATH)); + BufferedImage standardImage = ImageIO.read(standardImageStream); + XYSeries series = new XYSeries("key"); + for (int i = 0; i < TestResults2DLinePlot.parabolicData.one.length; i++){ + series.add(TestResults2DLinePlot.parabolicData.one[i], TestResults2DLinePlot.parabolicData.two[i]); + } + XYSeriesCollection dataset = new XYSeriesCollection(); + dataset.addSeries(series); + JFreeChart chart = ChartFactory.createXYLineChart("Test", "X-Axis","Y-Axis", dataset); + BufferedImage currentImage = chart.createBufferedImage(1000,1000); + Assertions.assertEquals(currentImage.getWidth(), standardImage.getWidth()); + Assertions.assertEquals(currentImage.getHeight(), standardImage.getHeight()); + for (int wPix = 0; wPix < currentImage.getWidth(); wPix++){ + for (int hPix = 0; hPix < currentImage.getHeight(); hPix++){ + Assertions.assertEquals(currentImage.getRGB(wPix, hPix), standardImage.getRGB(wPix, hPix)); + } + } + } } diff --git a/vcell-core/src/test/resources/org/vcell/plotting/Parabolic.png b/vcell-core/src/test/resources/org/vcell/plotting/Parabolic.png new file mode 100644 index 0000000000000000000000000000000000000000..9b7294544490c0b2974b3c16f9b131130e0fa599 GIT binary patch literal 60271 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 ZJk7V!ykfI$JNodp&AWe(-E`!_{{dIRQI7xs literal 0 HcmV?d00001 From bc846c5b2eb18a23ccc5648dcb129214261a7483 Mon Sep 17 00:00:00 2001 From: Logan Drescher Date: Fri, 17 Jan 2025 11:56:54 -0500 Subject: [PATCH 06/17] Brought CSV Transpose to Java --- .../java/org/vcell/cli/run/ExecuteImpl.java | 3 +- .../main/java/org/vcell/cli/run/RunUtils.java | 99 ++++++++++--------- .../java/org/vcell/cli/run/RunUtilsTest.java | 34 +++++++ 3 files changed, 88 insertions(+), 48 deletions(-) create mode 100644 vcell-cli/src/test/java/org/vcell/cli/run/RunUtilsTest.java diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/ExecuteImpl.java b/vcell-cli/src/main/java/org/vcell/cli/run/ExecuteImpl.java index ce4df84908..1d34b0ee6b 100644 --- a/vcell-cli/src/main/java/org/vcell/cli/run/ExecuteImpl.java +++ b/vcell-cli/src/main/java/org/vcell/cli/run/ExecuteImpl.java @@ -205,8 +205,7 @@ public static void singleExecVcml(File vcmlFile, File outputDir, CLIRecordable c for (String simName : resultsHash.keySet()) { String CSVFilePath = Paths.get(outDirForCurrentVcml.toString(), simName + ".csv").toString(); - RunUtils.createCSVFromODEResultSet(resultsHash.get(simName), new File(CSVFilePath)); - PythonCalls.transposeVcmlCsv(CSVFilePath); + RunUtils.createCSVFromODEResultSet(resultsHash.get(simName), new File(CSVFilePath), true); } } catch (IOException e) { Tracer.failure(e, "IOException while processing VCML " + vcmlFile.getName()); diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/RunUtils.java b/vcell-cli/src/main/java/org/vcell/cli/run/RunUtils.java index 1e3d73000b..6efa37c67e 100644 --- a/vcell-cli/src/main/java/org/vcell/cli/run/RunUtils.java +++ b/vcell-cli/src/main/java/org/vcell/cli/run/RunUtils.java @@ -23,7 +23,6 @@ import org.jlibsedml.*; import org.jlibsedml.execution.IXPathToVariableIDResolver; import org.jlibsedml.modelsupport.SBMLSupport; -import org.vcell.cli.CLIUtils; import org.vcell.sbml.vcell.SBMLNonspatialSimResults; import org.vcell.util.DataAccessException; import org.vcell.util.GenericExtensionFilter; @@ -511,75 +510,83 @@ public static boolean removeAndMakeDirs(File f) { return true; } - public static void createCSVFromODEResultSet(ODESolverResultSet resultSet, File f) throws ExpressionException { + public static void createCSVFromODEResultSet(ODESolverResultSet resultSet, File f, boolean shouldTranspose) throws ExpressionException { ColumnDescription[] descriptions = resultSet.getColumnDescriptions(); - StringBuilder sb = new StringBuilder(); - - - int numberOfColumns = descriptions.length; - int numberOfRows = resultSet.getRowCount(); + Map> resultsMapping = new LinkedHashMap<>(); - double[][] dataPoints = new double[numberOfColumns][]; - // Write headers - for (ColumnDescription description : descriptions) { - sb.append(description.getDisplayName()); - sb.append(","); + for (int i = 0; i < descriptions.length; i++){ + resultsMapping.put(descriptions[i].getDisplayName(), Arrays.stream(resultSet.extractColumn(i)).boxed().toList()); } - sb.deleteCharAt(sb.lastIndexOf(",")); - sb.append("\n"); + try (PrintWriter out = new PrintWriter(f)) { + out.print(RunUtils.formatCSVContents(resultsMapping, !shouldTranspose)); + out.flush(); + } catch (FileNotFoundException e) { + logger.error("Unable to find path, failed with err: " + e.getMessage(), e); + } + } - // Write rows - for (int i = 0; i < numberOfColumns; i++) { - dataPoints[i] = resultSet.extractColumn(i); + public static String formatCSVContents(Map> csvContents, boolean organizeDataVertically){ + if (!(csvContents instanceof LinkedHashMap>)) + logger.warn("Warning; using a non-linked hash map will result in random ordering of lines!"); + List> csvLines = new ArrayList<>(); + int numTopics = csvContents.size(); + int maxNumValuesPerTopic = 0; + for (List values : csvContents.values()) + if (values.size() > maxNumValuesPerTopic) + maxNumValuesPerTopic = values.size(); + + // Initialize with empties + for (int i = 0; i < (organizeDataVertically ? maxNumValuesPerTopic + 1 : numTopics); i++){ + csvLines.add(new ArrayList<>()); } - for (int rowNum = 0; rowNum < numberOfRows; rowNum++) { - for (int colNum = 0; colNum < numberOfColumns; colNum++) { - sb.append(dataPoints[colNum][rowNum]); - sb.append(","); + // Fill out lines + List topics = new ArrayList<>(csvContents.keySet()); + if (organizeDataVertically){ + for (String topic : topics) csvLines.get(0).add(topic); + } else { + for (int topicNum = 0; topicNum < topics.size(); topicNum++){ + csvLines.get(topicNum).add(topics.get(topicNum)); } + } + for (int topicNum = 0; topicNum < topics.size(); topicNum++){ + String topic = topics.get(topicNum); + List data = csvContents.get(topic); - sb.deleteCharAt(sb.lastIndexOf(",")); - sb.append("\n"); + for (int i = 0; i < maxNumValuesPerTopic; i++){ + String value = i < data.size() ? data.get(i).toString() : ""; + csvLines.get(organizeDataVertically ? i + 1 : topicNum).add(value); + } } - PrintWriter out = null; - try { - out = new PrintWriter(f); - out.print(sb.toString()); - out.flush(); - } catch (FileNotFoundException e) { - logger.error("Unable to find path, failed with err: " + e.getMessage(), e); - } finally { - if (out != null) out.close(); + // Build CSV + StringBuilder sb = new StringBuilder(); + for (List row : csvLines){ + for (String value : row){ + sb.append(value).append(","); + } + sb.deleteCharAt(sb.lastIndexOf(",")).append("\n"); } + return sb.deleteCharAt(sb.lastIndexOf("\n")).toString(); } + @SuppressWarnings({"ConstantConditions", "ResultOfMethodCallIgnored"}) public static void removeIntermediarySimFiles(File path) { + if (!path.isDirectory()) throw new IllegalArgumentException("Provided path does not lead to a directory!"); File[] files = path.listFiles(); for (File f : files) { - if (f.getName().endsWith(".csv")) { - // Do nothing - continue; - } else { - f.delete(); - } + if (f.getName().endsWith(".csv")) continue; + f.delete(); } } + @SuppressWarnings("ConstantConditions") public static boolean containsExtension(String folder, String ext) { GenericExtensionFilter filter = new GenericExtensionFilter(ext); File dir = new File(folder); - if (dir.isDirectory() == false) { - return false; - } - String[] list = dir.list(filter); - if (list.length > 0) { - return true; - } - return false; + return dir.isDirectory() && dir.list(filter).length > 0; } private static List getListOfVariableNames(DataIdentifier... dataIDArr){ diff --git a/vcell-cli/src/test/java/org/vcell/cli/run/RunUtilsTest.java b/vcell-cli/src/test/java/org/vcell/cli/run/RunUtilsTest.java new file mode 100644 index 0000000000..e001efe76e --- /dev/null +++ b/vcell-cli/src/test/java/org/vcell/cli/run/RunUtilsTest.java @@ -0,0 +1,34 @@ +package org.vcell.cli.run; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@Tag("Fast") +public class RunUtilsTest { + private final String VERTICAL_CSV = """ + Street-based,Sea-based,Snow-based + Skateboard,Wake Board,Snowboard + Rollerblades,Water Skis,Alpine / Cross-country Skis + Bike,Water Bike, + Motorcycle,Boatercycle (Jet-Ski),Snow Bike / Snowmobile"""; + private final String HORIZONTAL_CSV = """ + Street-based,Skateboard,Rollerblades,Bike,Motorcycle + Sea-based,Wake Board,Water Skis,Water Bike,Boatercycle (Jet-Ski) + Snow-based,Snowboard,Alpine / Cross-country Skis,,Snow Bike / Snowmobile"""; + + @Test + public void testCSVFormatting(){ + Map> csvContents = new LinkedHashMap<>(); + csvContents.put("Street-based", List.of("Skateboard", "Rollerblades", "Bike", "Motorcycle")); + csvContents.put("Sea-based", List.of("Wake Board", "Water Skis", "Water Bike", "Boatercycle (Jet-Ski)")); + csvContents.put("Snow-based", List.of("Snowboard", "Alpine / Cross-country Skis", "", "Snow Bike / Snowmobile")); + Assertions.assertEquals(VERTICAL_CSV, RunUtils.formatCSVContents(csvContents, true)); + Assertions.assertEquals(HORIZONTAL_CSV, RunUtils.formatCSVContents(csvContents, false)); + } +} From d1de15ae53ccf3e8e60a356877a946ebaefc253a Mon Sep 17 00:00:00 2001 From: Logan Drescher Date: Fri, 17 Jan 2025 11:57:24 -0500 Subject: [PATCH 07/17] Removed Python from CLI --- .../java/org/vcell/cli/CLIPythonManager.java | 27 +++++++++ .../biosimulation/BiosimulationsCommand.java | 13 +---- .../org/vcell/cli/run/ExecuteCommand.java | 4 +- .../java/org/vcell/cli/run/ExecuteImpl.java | 3 +- .../org/vcell/cli/run/ExecuteOmexCommand.java | 4 +- .../java/org/vcell/cli/run/ExecutionJob.java | 18 +++--- .../java/org/vcell/cli/run/PythonCalls.java | 55 ------------------- .../main/java/org/vcell/cli/run/SedmlJob.java | 4 +- .../vcell/cli/run/BSTSBasedOmexExecTest.java | 2 - .../org/vcell/cli/run/ExecuteImplTest.java | 2 - .../org/vcell/cli/run/QuantOmexExecTest.java | 2 - .../org/vcell/cli/run/SpatialExecTest.java | 3 - 12 files changed, 44 insertions(+), 93 deletions(-) delete mode 100644 vcell-cli/src/main/java/org/vcell/cli/run/PythonCalls.java diff --git a/vcell-cli/src/main/java/org/vcell/cli/CLIPythonManager.java b/vcell-cli/src/main/java/org/vcell/cli/CLIPythonManager.java index a363ecc063..6c0d9f32f7 100644 --- a/vcell-cli/src/main/java/org/vcell/cli/CLIPythonManager.java +++ b/vcell-cli/src/main/java/org/vcell/cli/CLIPythonManager.java @@ -46,7 +46,10 @@ public class CLIPythonManager { /** * Retrieve the Python Manager, or create and return if it doesn't exist. * @return the manager + * @deprecated CLIPythonManager is no longer used in CLI, and has no current use; should this be used again + * you need to check whether there are still bugs with Arm Macs, and whether single-instance python shells are not a viable alternative */ + @Deprecated public static CLIPythonManager getInstance(){ lg.trace("Getting Python instance"); if (instance == null){ @@ -62,7 +65,9 @@ public static CLIPythonManager getInstance(){ * @param arguments the arguments to provide to the function call, each as their own string, in the correct order * @return the return response from Python * @throws PythonStreamException if there is any exception encountered in this exchange. + * @deprecated See getInstance() */ + @Deprecated public String callPython(String functionName, String... arguments) throws PythonStreamException { String command = this.formatPythonFunctionCall(functionName, arguments); try { @@ -82,7 +87,9 @@ public String callPython(String functionName, String... arguments) throws Python * @param cliCommand the command to run * @throws InterruptedException if the python process was interrupted * @throws IOException if there was a system IO failure + * @deprecated See getInstance() */ + @Deprecated private static String callNonSharedPython(String cliCommand) throws InterruptedException, IOException, PythonStreamException { Path cliWorkingDir = Paths.get(PropertyLoader.getRequiredProperty(PropertyLoader.cliWorkingDir)); @@ -96,7 +103,9 @@ private static String callNonSharedPython(String cliCommand) * Shuts down the python session and cleans up. * * @throws IOException if there is a system IO issue. + * @deprecated See getInstance() */ + @Deprecated public void closePythonProcess() throws IOException { // Exit the living Python Process lg.debug("Closing Python Instance"); @@ -122,7 +131,9 @@ public void closePythonProcess() throws IOException { * as means of installation verification. * * @throws IOException if there is a problem with System I/O + * @deprecated this entire system is unstable on ARM Macs, and still a bit slow. */ + @Deprecated public void instantiatePythonProcess() throws IOException, PythonStreamException { if (this.pythonProcess != null) return; // prevent override lg.info("Initializing Python..."); @@ -338,10 +349,26 @@ private static String runAndPrintProcessStreams(ProcessBuilder pb, String outStr return os; } + /** + * Checks whether python returned successfully or not + * @param returnedString + * @throws PythonStreamException + * @deprecated See getInstance() + */ + @Deprecated public void parsePythonReturn(String returnedString) throws PythonStreamException { this.parsePythonReturn(returnedString, null, null); } + /** + * Checks whether python returned successfully or not + * @param returnedString + * @param outString + * @param errString + * @throws PythonStreamException + * @deprecated See getInstance() + */ + @Deprecated public void parsePythonReturn(String returnedString, String outString, String errString) throws PythonStreamException { boolean DEBUG_NORMAL_OUTPUT = lg.isTraceEnabled(); // Consider getting rid of this, currently redundant String ERROR_PHRASE1 = "Traceback", ERROR_PHRASE2 = "File \"\""; diff --git a/vcell-cli/src/main/java/org/vcell/cli/biosimulation/BiosimulationsCommand.java b/vcell-cli/src/main/java/org/vcell/cli/biosimulation/BiosimulationsCommand.java index 97933e8441..14a1dfad10 100644 --- a/vcell-cli/src/main/java/org/vcell/cli/biosimulation/BiosimulationsCommand.java +++ b/vcell-cli/src/main/java/org/vcell/cli/biosimulation/BiosimulationsCommand.java @@ -42,10 +42,10 @@ public class BiosimulationsCommand implements Callable { private boolean help; public Integer call() { - CLIRecorder cliRecorder = null; + CLIRecorder cliRecorder; int returnCode; - if ((returnCode = this.noFurtherActionNeeded(bQuiet, bDebug, bVersion)) != -1) + if ((returnCode = BiosimulationsCommand.noFurtherActionNeeded(bQuiet, bDebug, bVersion)) != -1) return returnCode; try { @@ -94,9 +94,7 @@ public Integer call() { logger.info("Beginning execution"); File tmpDir = Files.createTempDirectory("VCell_CLI_" + Long.toHexString(new Date().getTime())).toFile(); try { - CLIPythonManager.getInstance().instantiatePythonProcess(); ExecuteImpl.singleMode(ARCHIVE, tmpDir, cliRecorder, true); - CLIPythonManager.getInstance().closePythonProcess(); // Give the process time to finish if (!Tracer.hasErrors()) return 0; if (!bQuiet) { logger.error("Errors occurred during execution"); @@ -104,11 +102,6 @@ public Integer call() { } return 1; } finally { - try { - CLIPythonManager.getInstance().closePythonProcess(); // WARNING: Python will need reinstantiation after this is called - } catch (Exception e) { - logger.error(e.getMessage(), e); - } logger.debug("Finished all execution."); FileUtils.copyDirectoryContents(tmpDir, OUT_DIR, true, null); } @@ -121,7 +114,7 @@ public Integer call() { } } - private int noFurtherActionNeeded(boolean bQuiet, boolean bDebug, boolean bVersion){ + private static int noFurtherActionNeeded(boolean bQuiet, boolean bDebug, boolean bVersion){ logger.debug("Validating CLI arguments"); if (bVersion) { String version = PropertyLoader.getRequiredProperty(PropertyLoader.vcellSoftwareVersion); diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/ExecuteCommand.java b/vcell-cli/src/main/java/org/vcell/cli/run/ExecuteCommand.java index 1026ac4cdb..4dedf942a2 100644 --- a/vcell-cli/src/main/java/org/vcell/cli/run/ExecuteCommand.java +++ b/vcell-cli/src/main/java/org/vcell/cli/run/ExecuteCommand.java @@ -110,8 +110,6 @@ public Integer call() { System.err.println("cannot specify both debug and quiet, try --help for usage"); return 1; } - - CLIPythonManager.getInstance().instantiatePythonProcess(); Executable.setGlobalTimeoutMS(EXECUTABLE_MAX_WALLCLOCK_MILLIS); @@ -132,7 +130,7 @@ public Integer call() { bEncapsulateOutput, bSmallMeshOverride); } } - CLIPythonManager.getInstance().closePythonProcess(); + // WARNING: Python needs re-instantiation once the above line is called! FileUtils.copyDirectoryContents(tmpDir, outputFilePath, true, null); return 0; diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/ExecuteImpl.java b/vcell-cli/src/main/java/org/vcell/cli/run/ExecuteImpl.java index 1d34b0ee6b..e69d1242a8 100644 --- a/vcell-cli/src/main/java/org/vcell/cli/run/ExecuteImpl.java +++ b/vcell-cli/src/main/java/org/vcell/cli/run/ExecuteImpl.java @@ -108,7 +108,8 @@ public static void batchMode(File dirOfArchivesToProcess, File outputDir, CLIRec private static void runSingleExecOmex(File inputFile, File outputDir, CLIRecordable cliLogger, boolean bKeepTempFiles, boolean bExactMatchOnly, boolean bSmallMeshOverride) - throws IOException, ExecutionException, PythonStreamException, InterruptedException, BiosimulationsHdfWriterException { + throws IOException, ExecutionException, PythonStreamException, BiosimulationsHdfWriterException { + String bioModelBaseName = inputFile.getName().substring(0, inputFile.getName().indexOf(".")); // ".omex"?? Files.createDirectories(Paths.get(outputDir.getAbsolutePath() + File.separator + bioModelBaseName)); // make output subdir final boolean bEncapsulateOutput = true; diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/ExecuteOmexCommand.java b/vcell-cli/src/main/java/org/vcell/cli/run/ExecuteOmexCommand.java index 3b9a17716c..ba28a756ab 100644 --- a/vcell-cli/src/main/java/org/vcell/cli/run/ExecuteOmexCommand.java +++ b/vcell-cli/src/main/java/org/vcell/cli/run/ExecuteOmexCommand.java @@ -93,8 +93,6 @@ public Integer call() { config.updateLoggers(); - CLIPythonManager.getInstance().instantiatePythonProcess(); - Executable.setGlobalTimeoutMS(EXECUTABLE_MAX_WALLCLOCK_MILLIS); logger.info("Beginning execution"); File tmpDir = Files.createTempDirectory("VCell_CLI_" + Long.toHexString(new Date().getTime())).toFile(); @@ -102,7 +100,7 @@ public Integer call() { Tracer.clearTraceEvents(); ExecuteImpl.singleMode(inputFilePath, tmpDir, cliTracer, bKeepTempFiles, bExactMatchOnly, bEncapsulateOutput, bSmallMeshOverride); - CLIPythonManager.getInstance().closePythonProcess(); + // WARNING: Python needs re-instantiation once the above line is called! FileUtils.copyDirectoryContents(tmpDir, outputFilePath, true, null); final OmexExecSummary omexExecSummary; diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/ExecutionJob.java b/vcell-cli/src/main/java/org/vcell/cli/run/ExecutionJob.java index 459b9d7114..6619dc3f9e 100644 --- a/vcell-cli/src/main/java/org/vcell/cli/run/ExecutionJob.java +++ b/vcell-cli/src/main/java/org/vcell/cli/run/ExecutionJob.java @@ -26,8 +26,7 @@ public class ExecutionJob { private long startTime_ms, endTime_ms; private boolean bExactMatchOnly, bSmallMeshOverride, bKeepTempFiles; - private StringBuilder logOmexMessage; - private String inputFilePath; + private final StringBuilder logOmexMessage; private String bioModelBaseName; private String outputDir; private boolean anySedmlDocumentHasSucceeded = false; // set to true if at least one sedml document run is successful @@ -56,8 +55,7 @@ public ExecutionJob(File inputFile, File rootOutputDir, CLIRecordable cliRecorde this(); this.inputFile = inputFile; this.cliRecorder = cliRecorder; - - this.inputFilePath = inputFile.getAbsolutePath(); + this.bioModelBaseName = FileUtils.getBaseName(inputFile.getName()); // input file without the path String outputBaseDir = rootOutputDir.getAbsolutePath(); this.outputDir = bEncapsulateOutput ? Paths.get(outputBaseDir, bioModelBaseName).toString() : outputBaseDir; @@ -67,7 +65,7 @@ public ExecutionJob(File inputFile, File rootOutputDir, CLIRecordable cliRecorde } private ExecutionJob(){ - this.logOmexMessage = new StringBuilder(""); + this.logOmexMessage = new StringBuilder(); } /** @@ -77,29 +75,29 @@ private ExecutionJob(){ * @throws PythonStreamException if calls to the python-shell instance are not working correctly * @throws IOException if there are system I/O issues. */ - public void preprocessArchive() throws PythonStreamException, IOException { + public void preprocessArchive() throws IOException { // Start the clock this.startTime_ms = System.currentTimeMillis(); // Beginning of Execution logger.info("Executing OMEX archive `{}`", this.inputFile.getName()); - logger.info("Archive location: {}", this.inputFilePath); + logger.info("Archive location: {}", this.inputFile.getAbsolutePath()); RunUtils.drawBreakLine("-", 100); // Unpack the Omex Archive try { // It's unlikely, but if we get errors here they're fatal. this.sedmlPath2d3d = Paths.get(this.outputDir, "temp"); - this.omexHandler = new OmexHandler(this.inputFilePath, this.outputDir); + this.omexHandler = new OmexHandler(this.inputFile.getAbsolutePath(), this.outputDir); this.omexHandler.extractOmex(); this.sedmlLocations = this.omexHandler.getSedmlLocationsAbsolute(); } catch (IOException e){ - String error = e.getMessage() + ", error for OmexHandler with " + this.inputFilePath; + String error = e.getMessage() + ", error for OmexHandler with " + this.inputFile.getAbsolutePath(); this.cliRecorder.writeErrorList(e, this.bioModelBaseName); this.cliRecorder.writeDetailedResultList(this.bioModelBaseName + ", " + "IO error with OmexHandler"); logger.error(error); throw new RuntimeException(error, e); } catch (Exception e) { - String error = e.getMessage() + ", error for archive " + this.inputFilePath; + String error = e.getMessage() + ", error for archive " + this.inputFile.getAbsolutePath(); logger.error(error); if (this.omexHandler != null) this.omexHandler.deleteExtractedOmex(); this.cliRecorder.writeErrorList(e, this.bioModelBaseName); diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/PythonCalls.java b/vcell-cli/src/main/java/org/vcell/cli/run/PythonCalls.java deleted file mode 100644 index 5dfc0afec0..0000000000 --- a/vcell-cli/src/main/java/org/vcell/cli/run/PythonCalls.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.vcell.cli.run; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.vcell.cli.CLIPythonManager; -import org.vcell.cli.PythonStreamException; - -import java.io.IOException; - - -public class PythonCalls { - - private final static Logger logger = LogManager.getLogger(PythonCalls.class); - - public static void genSedmlForSed2DAnd3D(String omexFilePath, String outputDir) throws PythonStreamException { - logger.trace("Dialing Python function genSedml2d3d"); - CLIPythonManager cliPythonManager = CLIPythonManager.getInstance(); - String results = cliPythonManager.callPython("genSedml2d3d", omexFilePath, outputDir); - cliPythonManager.parsePythonReturn(results, "", "Failed generating SED-ML for plot2d and 3D "); - } - - public static void genPlots(String sedmlPath, String resultOutDir) throws PythonStreamException { - logger.trace("Dialing Python function genPlotPdfs"); - CLIPythonManager cliPythonManager = CLIPythonManager.getInstance(); - String results = cliPythonManager.callPython("genPlotPdfs", sedmlPath, resultOutDir); - cliPythonManager.parsePythonReturn(results); - } - - public static void transposeVcmlCsv(String csvFilePath) throws PythonStreamException { - logger.trace("Dialing Python function transposeVcmlCsv"); - CLIPythonManager cliPythonManager = CLIPythonManager.getInstance(); - String results = cliPythonManager.callPython("transposeVcmlCsv", csvFilePath); - cliPythonManager.parsePythonReturn(results); - } - - // Due to what appears to be a leaky python function call, this method will continue using execShellCommand until the underlying python is fixed - public static void genPlotsPseudoSedml(String sedmlPath, String resultOutDir) throws PythonStreamException, InterruptedException, IOException { - logger.trace("Dialing Python function genPlotsPseudoSedml"); - //CLIPythonManager.callNonSharedPython("genPlotsPseudoSedml", sedmlPath, resultOutDir); - CLIPythonManager cliPythonManager = CLIPythonManager.getInstance(); - String results = cliPythonManager.callPython("genPlotsPseudoSedml", sedmlPath, resultOutDir); - cliPythonManager.parsePythonReturn(results); - } - - private static String stripIllegalChars(String s){ - StringBuilder fStr = new StringBuilder(); - for (char c : s.toCharArray()){ - char cAppend = ((int)c) < 16 ? ' ' : c; - if (cAppend == '"') - cAppend = '\''; - fStr.append(cAppend); - } - return fStr.toString(); - } -} diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/SedmlJob.java b/vcell-cli/src/main/java/org/vcell/cli/run/SedmlJob.java index e76c935a69..e54bd7cf91 100644 --- a/vcell-cli/src/main/java/org/vcell/cli/run/SedmlJob.java +++ b/vcell-cli/src/main/java/org/vcell/cli/run/SedmlJob.java @@ -172,7 +172,7 @@ public boolean preProcessDoc() throws PythonStreamException, InterruptedExceptio Path path = Paths.get(this.plotFile.getAbsolutePath()); if (!Files.exists(path)){ // SED-ML file generated by python VCell_cli_util - PythonCalls.genSedmlForSed2DAnd3D(this.MASTER_OMEX_ARCHIVE.getAbsolutePath(), this.RESULTS_DIRECTORY_PATH); + //PythonCalls.genSedmlForSed2DAnd3D(this.MASTER_OMEX_ARCHIVE.getAbsolutePath(), this.RESULTS_DIRECTORY_PATH); } if (!Files.exists(path)) { String message = "Failed to create plot file " + this.plotFile.getAbsolutePath(); @@ -379,7 +379,7 @@ private void generateCSV(SolverHandler solverHandler) throws DataAccessException private void generatePlots() throws PythonStreamException, InterruptedException, IOException { logger.info("Generating Plots... "); - PythonCalls.genPlotsPseudoSedml(this.SEDML_LOCATION, this.OUTPUT_DIRECTORY_FOR_CURRENT_SEDML.toString()); // generate the plots + //PythonCalls.genPlotsPseudoSedml(this.SEDML_LOCATION, this.OUTPUT_DIRECTORY_FOR_CURRENT_SEDML.toString()); // generate the plots // We assume if no exception is returned that the plots pass for (Output output : this.sedml.getOutputs()){ if (!(output instanceof Plot2D plot)) continue; diff --git a/vcell-cli/src/test/java/org/vcell/cli/run/BSTSBasedOmexExecTest.java b/vcell-cli/src/test/java/org/vcell/cli/run/BSTSBasedOmexExecTest.java index 69cb4f6212..57aa1f8347 100644 --- a/vcell-cli/src/test/java/org/vcell/cli/run/BSTSBasedOmexExecTest.java +++ b/vcell-cli/src/test/java/org/vcell/cli/run/BSTSBasedOmexExecTest.java @@ -43,13 +43,11 @@ public static void setup() throws PythonStreamException, IOException { PropertyLoader.setProperty(PropertyLoader.cliWorkingDir, new File("../vcell-cli-utils").getAbsolutePath()); VCMongoMessage.enabled = false; - CLIPythonManager.getInstance().instantiatePythonProcess(); omexTestCases = OmexTestingDatabase.loadOmexTestCases(); } @AfterAll public static void teardown() throws Exception { - CLIPythonManager.getInstance().closePythonProcess(); VCellUtilityHub.shutdown(); } diff --git a/vcell-cli/src/test/java/org/vcell/cli/run/ExecuteImplTest.java b/vcell-cli/src/test/java/org/vcell/cli/run/ExecuteImplTest.java index c7bb4b4a65..70f0d8b194 100644 --- a/vcell-cli/src/test/java/org/vcell/cli/run/ExecuteImplTest.java +++ b/vcell-cli/src/test/java/org/vcell/cli/run/ExecuteImplTest.java @@ -26,7 +26,6 @@ public void test_singleExecOmex() throws Exception { PropertyLoader.setProperty(PropertyLoader.cliWorkingDir, new File("../vcell-cli-utils").getAbsolutePath()); VCMongoMessage.enabled = false; try { - CLIPythonManager.getInstance().instantiatePythonProcess(); InputStream omexInputStream = ExecuteImplTest.class.getResourceAsStream("/BioModel1.omex"); File tempOutputDir = Files.createTempDirectory("ExecuteImplTest_temp").toFile(); @@ -46,7 +45,6 @@ public void test_singleExecOmex() throws Exception { tempOmexFile.delete(); } finally { - CLIPythonManager.getInstance().closePythonProcess(); } } } diff --git a/vcell-cli/src/test/java/org/vcell/cli/run/QuantOmexExecTest.java b/vcell-cli/src/test/java/org/vcell/cli/run/QuantOmexExecTest.java index 470e921dad..1e7fcf5431 100644 --- a/vcell-cli/src/test/java/org/vcell/cli/run/QuantOmexExecTest.java +++ b/vcell-cli/src/test/java/org/vcell/cli/run/QuantOmexExecTest.java @@ -40,12 +40,10 @@ public static void setup() throws PythonStreamException, IOException { PropertyLoader.setProperty(PropertyLoader.cliWorkingDir, new File("../vcell-cli-utils").getAbsolutePath()); VCMongoMessage.enabled = false; - CLIPythonManager.getInstance().instantiatePythonProcess(); } @AfterAll public static void teardown() throws Exception { - CLIPythonManager.getInstance().closePythonProcess(); VCellUtilityHub.shutdown(); } diff --git a/vcell-cli/src/test/java/org/vcell/cli/run/SpatialExecTest.java b/vcell-cli/src/test/java/org/vcell/cli/run/SpatialExecTest.java index 303e7c166b..137fdad7f5 100644 --- a/vcell-cli/src/test/java/org/vcell/cli/run/SpatialExecTest.java +++ b/vcell-cli/src/test/java/org/vcell/cli/run/SpatialExecTest.java @@ -49,13 +49,10 @@ public static void setup() throws PythonStreamException, IOException { config.updateLoggers(); config.getConfiguration().getLoggerConfig(LogManager.getLogger("io.jhdf").getName()).setLevel(Level.WARN); config.updateLoggers(); - - CLIPythonManager.getInstance().instantiatePythonProcess(); } @AfterAll public static void teardown() throws Exception { - CLIPythonManager.getInstance().closePythonProcess(); VCellUtilityHub.shutdown(); } From 15a991e1f6ade0fc359356a3c0c01152b64bfba0 Mon Sep 17 00:00:00 2001 From: Logan Drescher Date: Tue, 21 Jan 2025 12:04:10 -0500 Subject: [PATCH 08/17] Post-python removal patching, bugfixing, and log cleaning --- .../exceptions/PreProcessingException.java | 20 ++ .../java/org/vcell/cli/run/ExecuteImpl.java | 14 +- .../java/org/vcell/cli/run/ExecutionJob.java | 74 +++-- .../main/java/org/vcell/cli/run/RunUtils.java | 20 +- .../main/java/org/vcell/cli/run/SedmlJob.java | 260 +++++++----------- .../org/vcell/cli/run/SedmlStatistics.java | 8 +- .../java/org/vcell/cli/run/SolverHandler.java | 48 ++-- .../run/hdf5/BiosimulationsHdf5Writer.java | 2 +- .../run/hdf5/NonspatialResultsConverter.java | 3 +- .../surface/GeometrySurfaceUtils.java | 17 +- .../vcell/mapping/DiffEquMathMapping.java | 2 +- .../vcell/mapping/LangevinMathMapping.java | 2 +- .../vcell/mapping/ParticleMathMapping.java | 2 +- .../cbit/vcell/solver/SolverUtilities.java | 4 +- .../main/java/cbit/vcell/xml/XmlHelper.java | 2 +- .../main/java/cbit/vcell/xml/XmlReader.java | 2 +- .../main/java/org/jlibsedml/SEDMLReader.java | 2 +- .../validation/SchemaValidatorImpl.java | 2 +- .../validation/SchematronValidator.java | 2 +- .../java/org/vcell/sedml/SEDMLImporter.java | 63 +++-- .../org/vcell/sedml/log/BiosimulationLog.java | 2 +- 21 files changed, 253 insertions(+), 298 deletions(-) create mode 100644 vcell-cli/src/main/java/org/vcell/cli/exceptions/PreProcessingException.java diff --git a/vcell-cli/src/main/java/org/vcell/cli/exceptions/PreProcessingException.java b/vcell-cli/src/main/java/org/vcell/cli/exceptions/PreProcessingException.java new file mode 100644 index 0000000000..41b3a8b904 --- /dev/null +++ b/vcell-cli/src/main/java/org/vcell/cli/exceptions/PreProcessingException.java @@ -0,0 +1,20 @@ +package org.vcell.cli.exceptions; + +public class PreProcessingException extends RuntimeException { + public PreProcessingException(String message) { + super(message); + } + + public PreProcessingException(Throwable cause) { + super(cause); + } + + public PreProcessingException(String message, Throwable cause){ + super(message, cause); + } + + @Override + public int hashCode() { + return super.hashCode(); + } +} diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/ExecuteImpl.java b/vcell-cli/src/main/java/org/vcell/cli/run/ExecuteImpl.java index e69d1242a8..f5757064a8 100644 --- a/vcell-cli/src/main/java/org/vcell/cli/run/ExecuteImpl.java +++ b/vcell-cli/src/main/java/org/vcell/cli/run/ExecuteImpl.java @@ -55,9 +55,9 @@ public static void batchMode(File dirOfArchivesToProcess, File outputDir, CLIRec String targetOutputDir = Paths.get(outputBaseDir, bioModelBaseName).toString(); Span span = null; - try { + // Initialization call generates Status YAML + try (BiosimulationLog bioSimLog = BiosimulationLog.initialize(inputFile.getAbsolutePath(), targetOutputDir)) { span = Tracer.startSpan(Span.ContextType.OMEX_EXECUTE, inputFileName, Map.of("filename", inputFileName)); - BiosimulationLog.initialize(inputFile.getAbsolutePath(), targetOutputDir); // generate Status YAML System.out.println("\n\n"); logger.info("Processing " + inputFileName + "(" + inputFile + ")"); @@ -79,7 +79,6 @@ public static void batchMode(File dirOfArchivesToProcess, File outputDir, CLIRec if (span != null) { span.close(); } - BiosimulationLog.instance().close(); } } if (failedFiles.isEmpty()){ @@ -134,19 +133,16 @@ public static void singleMode(File inputFile, File rootOutputDir, CLIRecordable String targetOutputDir = bEncapsulateOutput ? Paths.get(outputBaseDir, bioModelBaseName).toString() : outputBaseDir; File adjustedOutputDir = new File(targetOutputDir); - logger.info("Preparing output directory..."); + if (logger.isDebugEnabled()) logger.info("Preparing output directory..."); // we don't want to accidentally delete the input... // if the output directory is a subset of the input file's housing directory, we shouldn't delete!! if (!inputFile.getParentFile().getCanonicalPath().contains(adjustedOutputDir.getCanonicalPath())) RunUtils.removeAndMakeDirs(adjustedOutputDir); - try { - BiosimulationLog.initialize(inputFile.getAbsolutePath(), targetOutputDir); // generate Status YAML - + // Initialization line generates Status YAML + try (BiosimulationLog bioSimLog = BiosimulationLog.initialize(inputFile.getAbsolutePath(), targetOutputDir)) { ExecuteImpl.singleExecOmex(inputFile, rootOutputDir, cliLogger, bKeepTempFiles, bExactMatchOnly, bEncapsulateOutput, bSmallMeshOverride, bBioSimMode); - } finally { - BiosimulationLog.instance().close(); } } diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/ExecutionJob.java b/vcell-cli/src/main/java/org/vcell/cli/run/ExecutionJob.java index 6619dc3f9e..218b2dfd4d 100644 --- a/vcell-cli/src/main/java/org/vcell/cli/run/ExecutionJob.java +++ b/vcell-cli/src/main/java/org/vcell/cli/run/ExecutionJob.java @@ -1,10 +1,12 @@ package org.vcell.cli.run; +import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.vcell.cli.CLIRecordable; import org.vcell.cli.PythonStreamException; import org.vcell.cli.exceptions.ExecutionException; +import org.vcell.cli.exceptions.PreProcessingException; import org.vcell.cli.run.hdf5.BiosimulationsHdf5Writer; import org.vcell.cli.run.hdf5.BiosimulationsHdfWriterException; import org.vcell.cli.run.hdf5.HDF5ExecutionResults; @@ -71,8 +73,7 @@ private ExecutionJob(){ /** * Run the neexed steps to prepare an archive for execution. * Follow-up call: `executeArchive()` - * - * @throws PythonStreamException if calls to the python-shell instance are not working correctly + * * @throws IOException if there are system I/O issues. */ public void preprocessArchive() throws IOException { @@ -81,7 +82,7 @@ public void preprocessArchive() throws IOException { // Beginning of Execution logger.info("Executing OMEX archive `{}`", this.inputFile.getName()); - logger.info("Archive location: {}", this.inputFile.getAbsolutePath()); + if (logger.isDebugEnabled()) logger.info("Archive location: {}", this.inputFile.getAbsolutePath()); RunUtils.drawBreakLine("-", 100); // Unpack the Omex Archive @@ -112,50 +113,43 @@ public void preprocessArchive() throws IOException { /** * Run solvers on all the models in the archive. - * + *
* Called after: `preprocessArchive()` * Called before: `postProcessArchive()` - * - * @throws InterruptedException if there is an issue with accessing data - * @throws PythonStreamException if calls to the python-shell instance are not working correctly - * @throws IOException if there are system I/O issues - * @throws ExecutionException if an execution specfic error occurs + * + * @throws ExecutionException if an execution specific error occurs */ - public void executeArchive(boolean isBioSimSedml) throws BiosimulationsHdfWriterException, PythonStreamException, ExecutionException { + public void executeArchive(boolean isBioSimSedml) throws BiosimulationsHdfWriterException, ExecutionException { + HDF5ExecutionResults cumulativeHdf5Results = new HDF5ExecutionResults(isBioSimSedml); + try { - HDF5ExecutionResults masterHdf5File = new HDF5ExecutionResults(isBioSimSedml); - this.queueAllSedml(); - - for (String sedmlLocation : this.sedmlLocations){ - SedmlJob job = new SedmlJob(sedmlLocation, this.omexHandler, this.inputFile, - this.outputDir, this.sedmlPath2d3d.toString(), this.cliRecorder, - this.bKeepTempFiles, this.bExactMatchOnly, this.bSmallMeshOverride, this.logOmexMessage); - if (!job.preProcessDoc()){ - SedmlStatistics stats = job.getDocStatistics(); // Must process document first - logger.error("Statistics of failed SedML:\n" + stats.toString()); + for (String sedmlLocation : this.sedmlLocations) { + try { + this.executeSedmlDocument(sedmlLocation, cumulativeHdf5Results); + } catch (PreProcessingException e) { this.anySedmlDocumentHasFailed = true; } - SedmlStatistics stats = job.getDocStatistics(); - boolean hasSucceeded = job.simulateSedml(masterHdf5File); - this.anySedmlDocumentHasSucceeded |= hasSucceeded; - this.anySedmlDocumentHasFailed &= hasSucceeded; - if (hasSucceeded){ - String formattedStats = stats.toFormattedString(); - logger.info("Processing of SedML succeeded.\n" + formattedStats); - } - else logger.error("Processing of SedML has failed.\n" + stats.toString()); } - BiosimulationsHdf5Writer.writeHdf5(masterHdf5File, new File(this.outputDir)); - - } catch(PythonStreamException e){ - logger.error("Python-processing encountered fatal error. Execution is unable to properly continue.", e); - throw e; - } catch(InterruptedException | IOException e){ + BiosimulationsHdf5Writer.writeHdf5(cumulativeHdf5Results, new File(this.outputDir)); + + } catch (IOException e){ logger.error("System IO encountered a fatal error"); throw new ExecutionException(e); } } + private void executeSedmlDocument(String sedmlLocation, HDF5ExecutionResults cumulativeHdf5Results) throws IOException, PreProcessingException { + BiosimulationLog.instance().updateSedmlDocStatusYml(sedmlLocation, BiosimulationLog.Status.QUEUED); + SedmlJob job = new SedmlJob(sedmlLocation, this.omexHandler, this.inputFile, + this.outputDir, this.sedmlPath2d3d.toString(), this.cliRecorder, + this.bKeepTempFiles, this.bExactMatchOnly, this.bSmallMeshOverride, this.logOmexMessage); + SedmlStatistics stats = job.preProcessDoc(); + boolean hasSucceeded = job.simulateSedml(cumulativeHdf5Results); + this.anySedmlDocumentHasSucceeded |= hasSucceeded; + this.anySedmlDocumentHasFailed &= hasSucceeded; + logger.log(hasSucceeded ? Level.INFO : Level.ERROR, "Processing of SedML ({}) {}", stats.getSedmlName(), hasSucceeded ? "succeeded." : "failed!"); + } + /** * Clean up and analyze the results of the archive's execution * @@ -171,7 +165,7 @@ public void postProcessessArchive() throws IOException { this.endTime_ms = System.currentTimeMillis(); long elapsedTime_ms = this.endTime_ms - this.startTime_ms; double duration_s = elapsedTime_ms / 1000.0; - logger.info("Omex " + inputFile.getName() + " processing completed (" + duration_s + "s)"); + logger.info("Omex `" + inputFile.getName() + "` processing completed (" + duration_s + "s)"); // // failure if at least one of the documents in the omex archive fails // @@ -192,13 +186,7 @@ public void postProcessessArchive() throws IOException { } BiosimulationLog.instance().setOutputMessage("null", "null", "omex", logOmexMessage.toString()); - logger.debug("Finished Execution of Archive: " + bioModelBaseName); - } - - private void queueAllSedml() throws PythonStreamException, InterruptedException, IOException { - for (String sedmlLocation: sedmlLocations){ - BiosimulationLog.instance().updateSedmlDocStatusYml(sedmlLocation, BiosimulationLog.Status.QUEUED); - } + if (logger.isDebugEnabled()) logger.info("Finished Execution of Archive: {}", this.bioModelBaseName); } } diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/RunUtils.java b/vcell-cli/src/main/java/org/vcell/cli/run/RunUtils.java index 6efa37c67e..1fa6e16ed2 100644 --- a/vcell-cli/src/main/java/org/vcell/cli/run/RunUtils.java +++ b/vcell-cli/src/main/java/org/vcell/cli/run/RunUtils.java @@ -192,13 +192,13 @@ public static HashMap generateReportsAsCSV(SedML sedml, SolverHand for (Output sedmlOutput : allSedmlOutputs) { // We only want Reports if (!(sedmlOutput instanceof Report sedmlReport)) { - logger.info("Ignoring unsupported output `" + sedmlOutput.getId() + "` while CSV generation."); + if (logger.isDebugEnabled()) logger.info("Ignoring unsupported output `" + sedmlOutput.getId() + "` while CSV generation."); continue; } StringBuilder sb = new StringBuilder(); - logger.info("Generating report `" + sedmlReport.getId() +"`."); + if (logger.isDebugEnabled()) logger.info("Generating report `" + sedmlReport.getId() +"`."); /* * we go through each entry (dataset) in the list of datasets * for each dataset, we use the data reference to obtain the data generator @@ -371,16 +371,16 @@ public static HashMap generateReportsAsCSV(SedML sedml, SolverHand } // end of dataset - if (sb.length() > 0) { + if (!sb.isEmpty()) { File f = new File(outDirForCurrentSedml, sedmlReport.getId() + ".csv"); PrintWriter out = new PrintWriter(f); - out.print(sb.toString()); + out.print(sb); out.flush(); out.close(); - logger.info("created csv file for report " + sedmlReport.getId() + ": " + f.getAbsolutePath()); + if (logger.isDebugEnabled()) logger.info("created csv file for report " + sedmlReport.getId() + ": " + f.getAbsolutePath()); reportsHash.put(sedmlReport.getId(), f); } else { - logger.info("no csv file for report " + sedmlReport.getId()); + if (logger.isDebugEnabled()) logger.info("no csv file for report " + sedmlReport.getId()); } } catch (Exception e) { throw new RuntimeException("CSV generation failed: " + e.getMessage(), e); @@ -442,15 +442,13 @@ public static void zipResFiles(File dirPath) throws IOException { srcFiles = listFilesForFolder(dirPath, ext); if (srcFiles.isEmpty()) { - logger.warn("No " + ext.toUpperCase() + " files found, skipping archiving `" + extensionListMap.get(ext) + "` files"); + if (logger.isDebugEnabled()) logger.warn("No {} files found, skipping archiving `{}` files", ext.toUpperCase(), extensionListMap.get(ext)); } else { fileOutputStream = new FileOutputStream(Paths.get(dirPath.toString(), extensionListMap.get(ext)).toFile()); zipOutputStream = new ZipOutputStream(fileOutputStream); - if (!srcFiles.isEmpty()) logger.info("Archiving resultant " + ext.toUpperCase() + " files to `" + extensionListMap.get(ext) + "`."); + if (!srcFiles.isEmpty() && logger.isDebugEnabled()) logger.info("Archiving resultant {} files to `{}`.", ext.toUpperCase(), extensionListMap.get(ext)); for (File srcFile : srcFiles) { - fileInputstream = new FileInputStream(srcFile); - // get relative path relativePath = dirPath.toURI().relativize(srcFile.toURI()).toString(); zipEntry = new ZipEntry(relativePath); @@ -472,7 +470,7 @@ public static void zipResFiles(File dirPath) throws IOException { public static String getTempDir() throws IOException { String tempPath = String.valueOf(java.nio.file.Files.createTempDirectory( RunUtils.VCELL_TEMP_DIR_PREFIX + UUID.randomUUID()).toAbsolutePath()); - logger.info("TempPath Created: " + tempPath); + logger.trace("TempPath Created: " + tempPath); return tempPath; } diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/SedmlJob.java b/vcell-cli/src/main/java/org/vcell/cli/run/SedmlJob.java index e54bd7cf91..127fc9822b 100644 --- a/vcell-cli/src/main/java/org/vcell/cli/run/SedmlJob.java +++ b/vcell-cli/src/main/java/org/vcell/cli/run/SedmlJob.java @@ -8,8 +8,8 @@ import org.apache.logging.log4j.Logger; import org.jlibsedml.*; import org.vcell.cli.CLIRecordable; -import org.vcell.cli.PythonStreamException; import org.vcell.cli.exceptions.ExecutionException; +import org.vcell.cli.exceptions.PreProcessingException; import org.vcell.cli.run.hdf5.HDF5ExecutionResults; import org.vcell.cli.run.hdf5.Hdf5DataContainer; import org.vcell.cli.run.hdf5.Hdf5DataExtractor; @@ -22,7 +22,6 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; @@ -36,15 +35,15 @@ public class SedmlJob { private final boolean SHOULD_KEEP_TEMP_FILES, ACCEPT_EXACT_MATCH_ONLY, SHOULD_OVERRIDE_FOR_SMALL_MESH; - private final String SEDML_LOCATION, BIOMODEL_BASE_NAME, RESULTS_DIRECTORY_PATH; + private final String SEDML_NAME, SEDML_LOCATION, BIOMODEL_BASE_NAME, RESULTS_DIRECTORY_PATH; + private final String[] SEDML_NAME_SPLIT; private final StringBuilder LOG_OMEX_MESSAGE; - private final SedmlStatistics DOC_STATISTICS; + private final SedmlStatistics DOC_STATISTICS; // We keep this in object memory for debugging private final File MASTER_OMEX_ARCHIVE, PLOTS_DIRECTORY, OUTPUT_DIRECTORY_FOR_CURRENT_SEDML; private final CLIRecordable CLI_RECORDER; private boolean somethingFailed, hasScans, hasOverrides; - private String logDocumentMessage, logDocumentError, sedmlName; + private String logDocumentMessage, logDocumentError; private SedML sedml; - private File plotFile; private final static Logger logger = LogManager.getLogger(SedmlJob.class); @@ -67,10 +66,15 @@ public SedmlJob(String sedmlLocation, OmexHandler omexHandler, File masterOmexAr String resultsDirPath, String sedmlPath2d3dString, CLIRecordable cliRecorder, boolean bKeepTempFiles, boolean bExactMatchOnly, boolean bSmallMeshOverride, StringBuilder logOmexMessage){ + final String SAFE_WINDOWS_FILE_SEPARATOR = "\\\\"; + final String SAFE_UNIX_FILE_SEPARATOR = "/"; this.MASTER_OMEX_ARCHIVE = masterOmexArchive; this.SEDML_LOCATION = sedmlLocation; this.OUTPUT_DIRECTORY_FOR_CURRENT_SEDML = new File(omexHandler.getOutputPathFromSedml(sedmlLocation)); - this.DOC_STATISTICS = new SedmlStatistics(); + this.SEDML_NAME_SPLIT = this.SEDML_LOCATION.split(OperatingSystemInfo.getInstance().isWindows() ? + SAFE_WINDOWS_FILE_SEPARATOR : SAFE_UNIX_FILE_SEPARATOR, -2); + this.SEDML_NAME = this.SEDML_NAME_SPLIT[this.SEDML_NAME_SPLIT.length - 1]; + this.DOC_STATISTICS = new SedmlStatistics(this.SEDML_NAME); this.BIOMODEL_BASE_NAME = FileUtils.getBaseName(masterOmexArchive.getName()); this.RESULTS_DIRECTORY_PATH = resultsDirPath; this.LOG_OMEX_MESSAGE = logOmexMessage; @@ -81,143 +85,92 @@ public SedmlJob(String sedmlLocation, OmexHandler omexHandler, File masterOmexAr this.SHOULD_OVERRIDE_FOR_SMALL_MESH = bSmallMeshOverride; this.somethingFailed = false; - this.plotFile = null; this.logDocumentMessage = "Initializing SED-ML document... "; this.logDocumentError = ""; - } - /** - * Returns an object with variables containing useful information about the sedml document - * @return the statistics of the document - */ - public SedmlStatistics getDocStatistics(){ - return this.DOC_STATISTICS; - } /** * Prepare the SedML model for execution * Called before: `simulateSedml()` - * - * @throws InterruptedException if there is an issue with accessing data - * @throws PythonStreamException if calls to the python-shell instance are not working correctly + * * @throws IOException if there are system I/O issues */ - public boolean preProcessDoc() throws PythonStreamException, InterruptedException, IOException { - final String SAFE_WINDOWS_FILE_SEPARATOR = "\\\\"; - final String SAFE_UNIX_FILE_SEPARATOR = "/"; - logger.info("Initializing SED-ML document..."); + public SedmlStatistics preProcessDoc() throws IOException, PreProcessingException { BiosimulationLog biosimLog = BiosimulationLog.instance(); - Span span = null; - try { - span = Tracer.startSpan(Span.ContextType.PROCESSING_SEDML, "preProcessDoc", null); - SedML sedmlFromOmex, sedmlFromPython; - String[] sedmlNameSplit; - - RunUtils.removeAndMakeDirs(this.OUTPUT_DIRECTORY_FOR_CURRENT_SEDML); - - sedmlNameSplit = this.SEDML_LOCATION.split(OperatingSystemInfo.getInstance().isWindows() ? - SAFE_WINDOWS_FILE_SEPARATOR : SAFE_UNIX_FILE_SEPARATOR, -2); - sedmlFromOmex = SedmlJob.getSedMLFile(sedmlNameSplit, this.MASTER_OMEX_ARCHIVE); - this.sedmlName = sedmlNameSplit[sedmlNameSplit.length - 1]; - this.LOG_OMEX_MESSAGE.append("Processing ").append(this.sedmlName).append(". "); - logger.info("Processing SED-ML: " + this.sedmlName); - biosimLog.updateSedmlDocStatusYml(this.SEDML_LOCATION, BiosimulationLog.Status.RUNNING); - - this.DOC_STATISTICS.setNumModels(sedmlFromOmex.getModels().size()); - for(Model m : sedmlFromOmex.getModels()) { - List changes = m.getListOfChanges(); // change attribute caused by a math override - if(!changes.isEmpty()) { //m.getListOfChanges will never return null(?) - this.hasOverrides = true; - } - } - - for(AbstractTask at : sedmlFromOmex.getTasks()) { - if(at instanceof RepeatedTask) { - RepeatedTask rt = (RepeatedTask)at; - List changes = rt.getChanges(); - if(changes != null && !changes.isEmpty()) { - this.hasScans = true; - } - } - } - this.DOC_STATISTICS.setNumTasks(sedmlFromOmex.getTasks().size()); - List outputs = sedmlFromOmex.getOutputs(); - this.DOC_STATISTICS.setNumOutputs(outputs.size()); - for (Output output : outputs) { - if (output instanceof Report) this.DOC_STATISTICS.setReportsCount(this.DOC_STATISTICS.getReportsCount() + 1); - if (output instanceof Plot2D) this.DOC_STATISTICS.setPlots2DCount(this.DOC_STATISTICS.getPlots2DCount() + 1); - if (output instanceof Plot3D) this.DOC_STATISTICS.setPlots3DCount(this.DOC_STATISTICS.getPlots3DCount() + 1); - } - this.DOC_STATISTICS.setNumSimulations(sedmlFromOmex.getSimulations().size()); - String summarySedmlContentString = "Found one SED-ML document with " - + this.DOC_STATISTICS.getNumModels() + " model(s), " - + this.DOC_STATISTICS.getNumSimulations() + " simulation(s), " - + this.DOC_STATISTICS.getNumTasks() + " task(s), " - + this.DOC_STATISTICS.getReportsCount() + " report(s), " - + this.DOC_STATISTICS.getPlots2DCount() + " plot2D(s), and " - + this.DOC_STATISTICS.getPlots3DCount() + " plot3D(s)\n"; - logger.info(summarySedmlContentString); - - this.logDocumentMessage += "done. "; - String str = "Successful translation of SED-ML file"; - this.logDocumentMessage += str + ". "; - logger.info(str + " : " + this.sedmlName); - RunUtils.drawBreakLine("-", 100); - - // For appending data for SED Plot2D and Plot3D to HDF5 files following a temp convention - logger.info("Creating pseudo SED-ML for HDF5 conversion..."); - this.plotFile = new File(this.PLOTS_DIRECTORY, "simulation_" + this.sedmlName); - Path path = Paths.get(this.plotFile.getAbsolutePath()); - if (!Files.exists(path)){ - // SED-ML file generated by python VCell_cli_util - //PythonCalls.genSedmlForSed2DAnd3D(this.MASTER_OMEX_ARCHIVE.getAbsolutePath(), this.RESULTS_DIRECTORY_PATH); - } - if (!Files.exists(path)) { - String message = "Failed to create plot file " + this.plotFile.getAbsolutePath(); - this.CLI_RECORDER.writeDetailedResultList(this.BIOMODEL_BASE_NAME + "," + this.sedmlName + "," + message); - RuntimeException exception = new RuntimeException(message); - Tracer.failure(exception, this.BIOMODEL_BASE_NAME + "," + this.sedmlName + "," + message); - throw exception; - } - - // Converting pseudo SED-ML to biomodel - logger.info("Creating Biomodel from pseudo SED-ML"); - sedmlFromPython = Libsedml.readDocument(this.plotFile).getSedMLModel(); + Span span = Tracer.startSpan(Span.ContextType.PROCESSING_SEDML, "preProcessDoc", null); + RunUtils.removeAndMakeDirs(this.OUTPUT_DIRECTORY_FOR_CURRENT_SEDML); - /* If SED-ML has only plots as an output, we will use SED-ML that got generated from vcell_cli_util python code - * As of now, we are going to create a resultant dataSet for Plot output, using their respective data generators */ + this.LOG_OMEX_MESSAGE.append("Processing ").append(this.SEDML_NAME).append(". "); - // We need the name and path of the sedml file, which sedmlFromPseudo doesn't have! - - this.sedml = SedmlJob.repairSedML(sedmlFromPython, sedmlNameSplit); - + // Load SedML + logger.info("Initializing and Pre-Processing SedML document: {}", this.SEDML_NAME); + biosimLog.updateSedmlDocStatusYml(this.SEDML_LOCATION, BiosimulationLog.Status.RUNNING); + try { + this.sedml = SedmlJob.getSedMLFile(this.SEDML_NAME_SPLIT, this.MASTER_OMEX_ARCHIVE); } catch (Exception e) { - String prefix = "SED-ML processing for " + this.SEDML_LOCATION + " failed with error: "; - this.logDocumentError = prefix + e.getMessage(); + String prefix = "SedML pre-processing for " + this.SEDML_LOCATION + " failed"; + this.logDocumentError = prefix + ": " + e.getMessage(); Tracer.failure(e, prefix); this.reportProblem(e); - this.somethingFailed = somethingDidFail(); + this.somethingFailed = SedmlJob.somethingDidFail(); biosimLog.updateSedmlDocStatusYml(this.SEDML_LOCATION, BiosimulationLog.Status.FAILED); - return false; - } finally { - if (span != null) { - span.close(); - } + span.close(); + throw new PreProcessingException(prefix, e); } - return true; + + // If we got here, we have a successful load!! + this.logDocumentMessage += "done. "; + String resultString = String.format("Successfully loaded and translated SED-ML file: %s.\n", this.SEDML_NAME); + this.logDocumentMessage += resultString; + + // Generate Doc Statistics + this.DOC_STATISTICS.setNumModels(this.sedml.getModels().size()); + this.DOC_STATISTICS.setNumSimulations(this.sedml.getSimulations().size()); + this.DOC_STATISTICS.setNumTasks(this.sedml.getTasks().size()); + this.DOC_STATISTICS.setNumOutputs(this.sedml.getOutputs().size()); + + for (Output output : this.sedml.getOutputs()) { + if (output instanceof Report) this.DOC_STATISTICS.setReportsCount(this.DOC_STATISTICS.getReportsCount() + 1); + if (output instanceof Plot2D) this.DOC_STATISTICS.setPlots2DCount(this.DOC_STATISTICS.getPlots2DCount() + 1); + if (output instanceof Plot3D) this.DOC_STATISTICS.setPlots3DCount(this.DOC_STATISTICS.getPlots3DCount() + 1); + } + + String summarySedmlContentString = "Found:\n" + + "\t" + this.DOC_STATISTICS.getNumModels() + " model(s)\n" + + "\t" + this.DOC_STATISTICS.getNumSimulations() + " simulation(s)\n" + + "\t" + this.DOC_STATISTICS.getNumTasks() + " task(s)\n" + + "\t" + this.DOC_STATISTICS.getReportsCount() + " report(s)\n" + + "\t" + this.DOC_STATISTICS.getPlots2DCount() + " plot2D(s)\n" + + "\t" + this.DOC_STATISTICS.getPlots3DCount() + " plot3D(s)\n"; + logger.info("{}{}", resultString, summarySedmlContentString); + + // Check for overrides + for(Model m : this.sedml.getModels()) { + if (m.getListOfChanges().isEmpty()) continue; + this.hasOverrides = true; + break; + } + + // Check for parameter scans + for(AbstractTask at : this.sedml.getTasks()) { + if (!(at instanceof RepeatedTask rt)) continue; + List changes = rt.getChanges(); + if(changes == null || changes.isEmpty()) continue; + this.hasScans = true; + } + span.close(); + return this.DOC_STATISTICS; } /** * Prepare the SedML model for execution * Called after: `preProcessDoc()` - * - * @throws InterruptedException if there is an issue with accessing data - * @throws PythonStreamException if calls to the python-shell instance are not working correctly + * * @throws IOException if there are system I/O issues */ - public boolean simulateSedml(HDF5ExecutionResults masterHdf5File) throws InterruptedException, PythonStreamException, IOException { + public boolean simulateSedml(HDF5ExecutionResults masterHdf5File) throws IOException { /* temp code to test plot name correctness String idNamePlotsMap = utils.generateIdNamePlotsMap(sedml, outDirForCurrentSedml); utils.execPlotOutputSedDoc(inputFile, idNamePlotsMap, this.resultsDirPath); @@ -260,6 +213,7 @@ private void runSimulations(SolverHandler solverHandler, ExternalDocInfo externa String str = "Building solvers and starting simulation of all tasks... "; logger.info(str); this.logDocumentMessage += str; + RunUtils.drawBreakLine("-", 100); solverHandler.simulateAllTasks(externalDocInfo, this.sedml, this.CLI_RECORDER, this.OUTPUT_DIRECTORY_FOR_CURRENT_SEDML, this.RESULTS_DIRECTORY_PATH, this.SEDML_LOCATION, this.SHOULD_KEEP_TEMP_FILES, @@ -285,7 +239,7 @@ private void runSimulations(SolverHandler solverHandler, ExternalDocInfo externa this.recordRunDetails(solverHandler); } - private void processOutputs(SolverHandler solverHandler, HDF5ExecutionResults masterHdf5File) throws InterruptedException, ExecutionException, PythonStreamException { + private void processOutputs(SolverHandler solverHandler, HDF5ExecutionResults masterHdf5File) throws ExecutionException { // WARNING!!! Current logic dictates that if any task fails we fail the sedml document // change implemented on Nov 11, 2021 // Previous logic was that if at least one task produces some results we declare the sedml document status as successful @@ -294,8 +248,8 @@ private void processOutputs(SolverHandler solverHandler, HDF5ExecutionResults ma if (solverHandler.nonSpatialResults.containsValue(null) || solverHandler.spatialResults.containsValue(null)) { // some tasks failed, but not all this.somethingFailed = somethingDidFail(); this.logDocumentMessage += "Failed to execute one or more tasks. "; - Tracer.failure(new Exception("Failed to execute one or more tasks in " + this.sedmlName), "Failed to execute one or more tasks in " + this.sedmlName); - logger.info("Failed to execute one or more tasks in " + this.sedmlName); + Tracer.failure(new Exception("Failed to execute one or more tasks in " + this.SEDML_NAME), "Failed to execute one or more tasks in " + this.SEDML_NAME); + logger.info("Failed to execute one or more tasks in " + this.SEDML_NAME); } this.logDocumentMessage += "Generating outputs... "; @@ -322,7 +276,7 @@ private void processOutputs(SolverHandler solverHandler, HDF5ExecutionResults ma } } - private boolean evaluateResults() throws PythonStreamException, InterruptedException, IOException { + private boolean evaluateResults() throws IOException { if (this.somethingFailed) { // something went wrong but no exception was fired Exception e = new RuntimeException("Failure executing the sed document. "); this.logDocumentError += e.getMessage(); @@ -340,15 +294,15 @@ private boolean evaluateResults() throws PythonStreamException, InterruptedExcep //Files.copy(new File(outDirForCurrentSedml,"reports.h5").toPath(),Paths.get(this.resultsDirPath,"reports.h5")); // archiving result files - logger.info("Archiving result files"); + if (logger.isDebugEnabled()) logger.info("Archiving result files"); RunUtils.zipResFiles(new File(this.RESULTS_DIRECTORY_PATH)); org.apache.commons.io.FileUtils.deleteDirectory(this.PLOTS_DIRECTORY); // removing sedml dir which stages results. // Declare success! BiosimulationLog biosimLog = BiosimulationLog.instance(); - biosimLog.setOutputMessage(this.SEDML_LOCATION, this.sedmlName, "sedml", this.logDocumentMessage); + biosimLog.setOutputMessage(this.SEDML_LOCATION, this.SEDML_NAME, "sedml", this.logDocumentMessage); biosimLog.updateSedmlDocStatusYml(this.SEDML_LOCATION, BiosimulationLog.Status.SUCCEEDED); - logger.info("SED-ML : " + this.sedmlName + " successfully completed"); + logger.info("SED-ML : " + this.SEDML_NAME + " successfully completed"); return true; } @@ -377,7 +331,7 @@ private void generateCSV(SolverHandler solverHandler) throws DataAccessException } } - private void generatePlots() throws PythonStreamException, InterruptedException, IOException { + private void generatePlots() throws IOException { logger.info("Generating Plots... "); //PythonCalls.genPlotsPseudoSedml(this.SEDML_LOCATION, this.OUTPUT_DIRECTORY_FOR_CURRENT_SEDML.toString()); // generate the plots // We assume if no exception is returned that the plots pass @@ -388,8 +342,8 @@ private void generatePlots() throws PythonStreamException, InterruptedException, } private void generateHDF5(SolverHandler solverHandler, HDF5ExecutionResults masterHdf5File) { - this.logDocumentMessage += "Generating HDF5 file... "; - logger.info("Generating HDF5 file... "); + this.logDocumentMessage += "Indexing HDF5 data... "; + logger.info("Indexing HDF5 data... "); Hdf5DataExtractor hdf5Extractor = new Hdf5DataExtractor(this.sedml, solverHandler.taskToTempSimulationMap); @@ -407,27 +361,19 @@ private void generateHDF5(SolverHandler solverHandler, HDF5ExecutionResults mast } // This method is a bit weird; it uses a temp file as a reference to compare against while getting the file straight from the archive. - private static SedML getSedMLFile(String[] tokenizedPath, File inputFile) throws XMLException, IOException { - SedML file = null; - Path convertedPath = SedmlJob.getRelativePath(tokenizedPath); - if (convertedPath == null){ - RuntimeException exception = new RuntimeException("Was not able to get relative path to " + inputFile.getName()); - logger.error(exception); - throw exception; - } + private static SedML getSedMLFile(String[] tokenizedPathToSedml, File inputFile) throws XMLException, IOException { + Path convertedPath = SedmlJob.getRelativePath(tokenizedPathToSedml); + if (convertedPath == null) throw new RuntimeException("Was not able to get relative path to " + inputFile.getName()); String identifyingPath = FilenameUtils.separatorsToUnix(convertedPath.toString()); - FileInputStream omexStream = new FileInputStream(inputFile); - ArchiveComponents omexComponents = Libsedml.readSEDMLArchive(omexStream); - List sedmlDocuments = omexComponents.getSedmlDocuments(); - for (SEDMLDocument doc : sedmlDocuments){ - SedML potentiallyCorrectFile = doc.getSedMLModel(); - if (identifyingPath.equals(potentiallyCorrectFile.getPathForURI() + potentiallyCorrectFile.getFileName())){ - file = potentiallyCorrectFile; - break; + try (FileInputStream omexStream = new FileInputStream(inputFile)) { + for (SEDMLDocument doc : Libsedml.readSEDMLArchive(omexStream).getSedmlDocuments()){ + SedML potentiallyCorrectFile = doc.getSedMLModel(); + String potentiallyCorrectPath = potentiallyCorrectFile.getPathForURI() + potentiallyCorrectFile.getFileName(); + if (!identifyingPath.equals(potentiallyCorrectPath)) continue; + return potentiallyCorrectFile; } } - omexStream.close(); - return file; + throw new PreProcessingException("Unable to find desired SedML within path"); } private static Path getRelativePath(String[] tokenizedPath){ @@ -446,28 +392,12 @@ private static boolean somethingDidFail(){ return true; } - /** - * In its current state, the sed-ml generated with python is missing two important fields; - * this function fixes that. - */ - private static SedML repairSedML(SedML brokenSedML, String[] tokenizedPath){ - Path relativePath = getRelativePath(tokenizedPath); - if (relativePath == null) return null; - String name = relativePath.getFileName().toString(); - brokenSedML.setFileName(name); - // Take the relative path, remove the file name, and... - String source = relativePath.toString().substring(0, relativePath.toString().length() - name.length()); - // Convert to unix file separators (java URI does not do windows style) - brokenSedML.setPathForURI(FilenameUtils.separatorsToUnix(source)); - return brokenSedML; // now fixed! - } - - private void reportProblem(Exception e) throws PythonStreamException, InterruptedException, IOException{ + private void reportProblem(Exception e) throws IOException{ logger.error(e.getMessage(), e); String type = e.getClass().getSimpleName(); BiosimulationLog biosimLog = BiosimulationLog.instance(); - biosimLog.setOutputMessage(this.SEDML_LOCATION, this.sedmlName, "sedml", this.logDocumentMessage); - biosimLog.setExceptionMessage(this.SEDML_LOCATION, this.sedmlName, "sedml", type, this.logDocumentError); + biosimLog.setOutputMessage(this.SEDML_LOCATION, this.SEDML_NAME, "sedml", this.logDocumentMessage); + biosimLog.setExceptionMessage(this.SEDML_LOCATION, this.SEDML_NAME, "sedml", type, this.logDocumentError); this.CLI_RECORDER.writeDetailedErrorList(e, this.BIOMODEL_BASE_NAME + ", doc: " + type + ": " + this.logDocumentError); biosimLog.updateSedmlDocStatusYml(this.SEDML_LOCATION, BiosimulationLog.Status.FAILED); } diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/SedmlStatistics.java b/vcell-cli/src/main/java/org/vcell/cli/run/SedmlStatistics.java index cac38ab4ef..ef9d93e91f 100644 --- a/vcell-cli/src/main/java/org/vcell/cli/run/SedmlStatistics.java +++ b/vcell-cli/src/main/java/org/vcell/cli/run/SedmlStatistics.java @@ -14,11 +14,13 @@ * - hasScans :: if the sedml has parameter scans */ public class SedmlStatistics { + private String sedmlName; private Integer nModels, nSimulations, nTasks, nOutputs, nReportsCount, nPlots2DCount, nPlots3DCount; public boolean hasOverrides, hasScans; - public SedmlStatistics(){ + public SedmlStatistics(String sedmlName){ // -1 indicates the value has not been initialized. + this.sedmlName = sedmlName; this.nModels = null; this.nSimulations = null; this.nTasks = null; @@ -30,6 +32,10 @@ public SedmlStatistics(){ this.hasScans = false; } + public String getSedmlName(){ + return this.sedmlName; + } + public int getNumModels(){ return this.nModels == null ? 0 : this.nModels; } diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/SolverHandler.java b/vcell-cli/src/main/java/org/vcell/cli/run/SolverHandler.java index ba9fb83167..5cf4a02e89 100644 --- a/vcell-cli/src/main/java/org/vcell/cli/run/SolverHandler.java +++ b/vcell-cli/src/main/java/org/vcell/cli/run/SolverHandler.java @@ -279,10 +279,10 @@ public void initialize(List bioModelList, SedML sedml) throws Expressi taskToChangeTargetMap.put(rt, targetIdSet); } } - System.out.println("taskToSimulationMap: " + this.taskToTempSimulationMap.size()); - System.out.println("taskToListOfSubTasksMap: " + taskToListOfSubTasksMap.size()); - System.out.println("taskToVariableMap: " + taskToVariableMap.size()); - System.out.println("topTaskToBaseTask: " + topTaskToBaseTask.size()); + if (logger.isDebugEnabled()){ + logger.info("Initialization Statistics:\n\t> taskToSimulationMap: {}\n\t> taskToListOfSubTasksMap: {}\n\t> taskToVariableMap: {}\n\t> topTaskToBaseTask: {}\n", + this.taskToTempSimulationMap.size(), this.taskToListOfSubTasksMap.size(), this.taskToVariableMap.size(), this.topTaskToBaseTask.size()); + } } private static class TempSimulationJob extends SimulationJob { @@ -326,7 +326,7 @@ public void simulateAllTasks(ExternalDocInfo externalDocInfo, SedML sedml, CLIRe String docName = null; List tempSims = null; //String outDirRoot = outputDirForSedml.toString().substring(0, outputDirForSedml.toString().lastIndexOf(System.getProperty("file.separator"))); - this.sedmlImporter = new SEDMLImporter(sedmlImportLogger, externalDocInfo, sedml, exactMatchOnly); + this.sedmlImporter = new SEDMLImporter(sedmlImportLogger, externalDocInfo.getFile(), sedml, exactMatchOnly); try { bioModelList = this.sedmlImporter.getBioModels(); } catch (Exception e) { @@ -431,22 +431,24 @@ public void simulateAllTasks(ExternalDocInfo externalDocInfo, SedML sedml, CLIRe } if (solver instanceof AbstractCompiledSolver) { ((AbstractCompiledSolver) solver).runSolver(); - logger.info("Solver: " + solver); - logger.info("Status: " + solver.getSolverStatus()); - if (solver instanceof ODESolver) { - odeSolverResultSet = ((ODESolver) solver).getODESolverResultSet(); - } else if (solver instanceof GibsonSolver) { - odeSolverResultSet = ((GibsonSolver) solver).getStochSolverResultSet(); - } else if (solver instanceof HybridSolver) { - odeSolverResultSet = ((HybridSolver) solver).getHybridSolverResultSet(); + if (logger.isDebugEnabled()){ + logger.info("Solver: " + solver); + logger.info("Status: " + solver.getSolverStatus()); + } + if (solver instanceof ODESolver odeSolver) { + odeSolverResultSet = odeSolver.getODESolverResultSet(); + } else if (solver instanceof GibsonSolver gibsonSolver) { + odeSolverResultSet = gibsonSolver.getStochSolverResultSet(); + } else if (solver instanceof HybridSolver hybridSolver) { + odeSolverResultSet = hybridSolver.getHybridSolverResultSet(); } else { - String str = "Solver results are not compatible with CSV format. "; + String str = "Solver results will not be compatible with CSV format. "; logger.warn(str); // keepTempFiles = true; // temp fix for Jasraj // throw new RuntimeException(str); } - } else if (solver instanceof AbstractJavaSolver) { - ((AbstractJavaSolver) solver).runSolver(); + } else if (solver instanceof AbstractJavaSolver abstractJavaSolver) { + abstractJavaSolver.runSolver(); odeSolverResultSet = ((ODESolver) solver).getODESolverResultSet(); // must interpolate data for uniform time course which is not supported natively by the Java solvers org.jlibsedml.Simulation sedmlSim = sedml.getSimulation(task.getSimulationReference()); @@ -478,8 +480,6 @@ public void simulateAllTasks(ExternalDocInfo externalDocInfo, SedML sedml, CLIRe if (solver.getSolverStatus().getStatus() == SolverStatus.SOLVER_FINISHED) { logTaskMessage += "done. "; - logger.info("Succesful execution: Model '" + docName + "' Task '" + sim.getDescription() + "'."); - long endTimeTask_ms = System.currentTimeMillis(); long elapsedTime_ms = endTimeTask_ms - startTimeTask_ms; int duration_ms = (int) elapsedTime_ms; @@ -489,14 +489,13 @@ public void simulateAllTasks(ExternalDocInfo externalDocInfo, SedML sedml, CLIRe simDuration_ms += duration_ms; simDurationMap_ms.put(originalSim, simDuration_ms); - String msg = "Running simulation " + simTask.getSimulation().getName() + ", " + elapsedTime_ms + " ms"; - logger.info(msg); + logger.info("Successful execution ({}s): Model '{}' Task '{}' ({}).", + ((double)elapsedTime_ms)/1000, docName, sim.getDescription(), simTask.getSimulation().getName()); countSuccessfulSimulationRuns++; // we only count the number of simulations (tasks) that succeeded if (simStatusMap.get(originalSim) != BiosimulationLog.Status.ABORTED && simStatusMap.get(originalSim) != BiosimulationLog.Status.FAILED) { simStatusMap.put(originalSim, BiosimulationLog.Status.SUCCEEDED); } BiosimulationLog.instance().setOutputMessage(sedmlLocation, sim.getImportedTaskID(), "task", logTaskMessage); - RunUtils.drawBreakLine("-", 100); } else { String error = solver.getSolverStatus().getSimulationMessage().getDisplayMessage(); solverStatus = solver.getSolverStatus().getStatus(); @@ -554,14 +553,15 @@ public void simulateAllTasks(ExternalDocInfo externalDocInfo, SedML sedml, CLIRe } else { cliLogger.writeDetailedErrorList(e,bioModelBaseName + ", solver: " + sdl + ": " + type + ": " + logTaskError); } - RunUtils.drawBreakLine("-", 100); } finally { if (sim_span != null) { sim_span.close(); } } + if (sd.isSpatial()) { + logger.info("Processing spatial results of execution..."); File hdf5Results = new File(outDir + System.getProperty("file.separator") + task.getId() + "_job_" + tempSimulationJob.getJobIndex() + "_results.h5"); try { RunUtils.exportPDE2HDF5(tempSimulationJob, outputDirForSedml, hdf5Results); @@ -573,6 +573,7 @@ public void simulateAllTasks(ExternalDocInfo externalDocInfo, SedML sedml, CLIRe spatialResults.put(new TaskJob(task.getId(), tempSimulationJob.getJobIndex()), null); } } else { + logger.info("Processing non-spatial results of execution..."); MathSymbolMapping mathMapping = (MathSymbolMapping) simTask.getSimulation().getMathDescription().getSourceSymbolMapping(); SBMLSymbolMapping sbmlMapping = this.sedmlImporter.getSBMLSymbolMapping(bioModel); @@ -581,9 +582,10 @@ public void simulateAllTasks(ExternalDocInfo externalDocInfo, SedML sedml, CLIRe this.nonSpatialResults.put(taskJob, nonspatialSimResults); } - if (keepTempFiles == false) { + if (!keepTempFiles) { RunUtils.removeIntermediarySimFiles(outputDirForSedml); } + RunUtils.drawBreakLine("-", 100); simulationJobCount++; } for (Map.Entry entry : simStatusMap.entrySet()) { diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/hdf5/BiosimulationsHdf5Writer.java b/vcell-cli/src/main/java/org/vcell/cli/run/hdf5/BiosimulationsHdf5Writer.java index 9bf5c0135f..5febd21a5e 100644 --- a/vcell-cli/src/main/java/org/vcell/cli/run/hdf5/BiosimulationsHdf5Writer.java +++ b/vcell-cli/src/main/java/org/vcell/cli/run/hdf5/BiosimulationsHdf5Writer.java @@ -36,7 +36,7 @@ public static void writeHdf5(HDF5ExecutionResults hdf5ExecutionResults, File out boolean didFail = false; // Create and open the Hdf5 file - logger.info("Creating hdf5 file `reports.h5` in {}", outDirForCurrentSedml.getAbsolutePath()); + logger.info("Creating HDF5 file `reports.h5` in {}", outDirForCurrentSedml.getAbsolutePath()); File tempFile = new File(outDirForCurrentSedml, "reports.h5"); try { try (WritableHdfFile hdf5File = HdfFile.write(tempFile.toPath())){ diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/hdf5/NonspatialResultsConverter.java b/vcell-cli/src/main/java/org/vcell/cli/run/hdf5/NonspatialResultsConverter.java index a9a0854b18..b335c82bfa 100644 --- a/vcell-cli/src/main/java/org/vcell/cli/run/hdf5/NonspatialResultsConverter.java +++ b/vcell-cli/src/main/java/org/vcell/cli/run/hdf5/NonspatialResultsConverter.java @@ -201,7 +201,8 @@ private static List getReports(List outputs){ if (out instanceof Report){ reports.add((Report)out); } else { - logger.info("Ignoring unsupported output `" + out.getId() + "` while CSV generation."); + if (logger.isDebugEnabled()) + logger.info("Ignoring unsupported output `{}` while CSV generation.", out.getId()); } } return reports; diff --git a/vcell-core/src/main/java/cbit/vcell/geometry/surface/GeometrySurfaceUtils.java b/vcell-core/src/main/java/cbit/vcell/geometry/surface/GeometrySurfaceUtils.java index c2df955759..34b4e7aa4c 100644 --- a/vcell-core/src/main/java/cbit/vcell/geometry/surface/GeometrySurfaceUtils.java +++ b/vcell-core/src/main/java/cbit/vcell/geometry/surface/GeometrySurfaceUtils.java @@ -85,7 +85,7 @@ public static GeometricRegion[] getUpdatedGeometricRegions(GeometrySurfaceDescri cbit.vcell.geometry.RegionImage.RegionInfo regionInfos[] = regionImage.getRegionInfos(); for(int i = 0; i < regionInfos.length; i++){ cbit.vcell.geometry.RegionImage.RegionInfo regionInfo = regionInfos[i]; - lg.info(regionInfo); + if (lg.isDebugEnabled()) lg.info(regionInfo); cbit.vcell.geometry.SubVolume subVolume = geometrySpec.getSubVolume(regionInfo.getPixelValue()); String name = subVolume.getName() + regionInfo.getRegionIndex(); int numPixels = regionInfo.getNumPixels(); @@ -214,18 +214,16 @@ public static GeometricRegion[] getUpdatedGeometricRegions(GeometrySurfaceDescri } size -= sizeOfPixel * 0.125 * numOctantsToRemove; - if(lg.isInfoEnabled()){ - lg.info("size=" + size); - } + if(lg.isDebugEnabled()) lg.info("size={}", size); + break; } } VolumeGeometricRegion volumeRegion = new VolumeGeometricRegion(name, size, volumeUnit, subVolume, regionInfo.getRegionIndex()); regionList.add(volumeRegion); - if(lg.isInfoEnabled()){ - lg.info("added volumeRegion(" + volumeRegion.getName() + ")"); - } + if(lg.isDebugEnabled()) lg.info("added volumeRegion({})", volumeRegion.getName()); + } // // parse surfaceCollection into ResolvedMembraneLocations @@ -268,9 +266,8 @@ public static GeometricRegion[] getUpdatedGeometricRegions(GeometrySurfaceDescri } surfaceRegion.addAdjacentGeometricRegion(interiorVolumeRegion); interiorVolumeRegion.addAdjacentGeometricRegion(surfaceRegion); - if(lg.isInfoEnabled()){ - lg.info("added surfaceRegion(" + surfaceRegion.getName() + ")"); - } + if(lg.isDebugEnabled()) lg.info("added surfaceRegion({})", surfaceRegion.getName()); + } return regionList.toArray(GeometricRegion[]::new); diff --git a/vcell-core/src/main/java/cbit/vcell/mapping/DiffEquMathMapping.java b/vcell-core/src/main/java/cbit/vcell/mapping/DiffEquMathMapping.java index 6bda321e71..aa934c2a97 100644 --- a/vcell-core/src/main/java/cbit/vcell/mapping/DiffEquMathMapping.java +++ b/vcell-core/src/main/java/cbit/vcell/mapping/DiffEquMathMapping.java @@ -1454,7 +1454,7 @@ private void refreshMathDescription() throws MappingException, MatrixException, for(int i = 0; i < mappedSMs.length; i++){ if(mappedSMs[i] instanceof FeatureMapping){ if(mappedFM != null){ - lg.info("WARNING:::: MathMapping.refreshMathDescription() ... assigning boundary condition types not unique"); + if (lg.isDebugEnabled()) lg.warn("WARNING:::: MathMapping.refreshMathDescription() ... assigning boundary condition types not unique"); } mappedFM = (FeatureMapping) mappedSMs[i]; } diff --git a/vcell-core/src/main/java/cbit/vcell/mapping/LangevinMathMapping.java b/vcell-core/src/main/java/cbit/vcell/mapping/LangevinMathMapping.java index 55b000aa46..262aa576f7 100644 --- a/vcell-core/src/main/java/cbit/vcell/mapping/LangevinMathMapping.java +++ b/vcell-core/src/main/java/cbit/vcell/mapping/LangevinMathMapping.java @@ -415,7 +415,7 @@ protected void refreshMathDescription() throws MappingException, MatrixException for (int i = 0; i < mappedSMs.length; i++) { if (mappedSMs[i] instanceof FeatureMapping){ if (mappedFM!=null){ - lg.warn("WARNING:::: MathMapping.refreshMathDescription() ... assigning boundary condition types not unique"); + if (lg.isDebugEnabled()) lg.warn("WARNING:::: MathMapping.refreshMathDescription() ... assigning boundary condition types not unique"); } mappedFM = (FeatureMapping)mappedSMs[i]; } diff --git a/vcell-core/src/main/java/cbit/vcell/mapping/ParticleMathMapping.java b/vcell-core/src/main/java/cbit/vcell/mapping/ParticleMathMapping.java index 537216c22b..96fc17ef6f 100644 --- a/vcell-core/src/main/java/cbit/vcell/mapping/ParticleMathMapping.java +++ b/vcell-core/src/main/java/cbit/vcell/mapping/ParticleMathMapping.java @@ -487,7 +487,7 @@ private void refreshMathDescription() throws MappingException, MatrixException, for (int i = 0; i < mappedSMs.length; i++) { if (mappedSMs[i] instanceof FeatureMapping){ if (mappedFM!=null){ - lg.warn("WARNING:::: MathMapping.refreshMathDescription() ... assigning boundary condition types not unique"); + if (lg.isDebugEnabled()) lg.warn("WARNING:::: MathMapping.refreshMathDescription() ... assigning boundary condition types not unique"); } mappedFM = (FeatureMapping)mappedSMs[i]; } diff --git a/vcell-core/src/main/java/cbit/vcell/solver/SolverUtilities.java b/vcell-core/src/main/java/cbit/vcell/solver/SolverUtilities.java index 047f049b67..b42b6faf70 100644 --- a/vcell-core/src/main/java/cbit/vcell/solver/SolverUtilities.java +++ b/vcell-core/src/main/java/cbit/vcell/solver/SolverUtilities.java @@ -162,10 +162,10 @@ public static SolverDescription matchSolverWithKisaoId(String originalId, boolea private static List matchByKisaoId(KisaoTerm candidate) { List solverDescriptions = new ArrayList<>(); for (SolverDescription sd : SolverDescription.values()) { - if(sd.getKisao().contains(":") || sd.getKisao().contains("_")) { + if (sd.getKisao().contains(":") || sd.getKisao().contains("_")) { logger.trace(sd.getKisao()); } else { - logger.warn(sd.getKisao() + " - bad format, skipping"); + if (logger.isDebugEnabled()) logger.warn("`{}` is bad KiSAO formating, skipping", sd.getKisao()); continue; } String s1 = candidate.getId(); diff --git a/vcell-core/src/main/java/cbit/vcell/xml/XmlHelper.java b/vcell-core/src/main/java/cbit/vcell/xml/XmlHelper.java index 5f9ff632f8..4af94c50ac 100644 --- a/vcell-core/src/main/java/cbit/vcell/xml/XmlHelper.java +++ b/vcell-core/src/main/java/cbit/vcell/xml/XmlHelper.java @@ -501,7 +501,7 @@ public static List readOmex(File omexFile, VCLogger vcLogger) throws E public static List importSEDML(VCLogger transLogger, ExternalDocInfo externalDocInfo, SedML sedml, boolean exactMatchOnly) throws Exception { - SEDMLImporter sedmlImporter = new SEDMLImporter(transLogger, externalDocInfo, + SEDMLImporter sedmlImporter = new SEDMLImporter(transLogger, externalDocInfo.getFile(), sedml, exactMatchOnly); return sedmlImporter.getBioModels(); } diff --git a/vcell-core/src/main/java/cbit/vcell/xml/XmlReader.java b/vcell-core/src/main/java/cbit/vcell/xml/XmlReader.java index fdc9d4067a..b9eeda2ce8 100644 --- a/vcell-core/src/main/java/cbit/vcell/xml/XmlReader.java +++ b/vcell-core/src/main/java/cbit/vcell/xml/XmlReader.java @@ -4562,7 +4562,7 @@ public Model getModel(Element param) throws XmlParseException{ if(element != null){ getRbmModelContainer(element, newmodel); } else { - lg.info("RbmModelContainer is missing."); + if (lg.isDebugEnabled()) lg.info("RbmModelContainer is missing."); } //Add SpeciesContexts diff --git a/vcell-core/src/main/java/org/jlibsedml/SEDMLReader.java b/vcell-core/src/main/java/org/jlibsedml/SEDMLReader.java index 8b3973e1fd..a9d4cbd9b8 100644 --- a/vcell-core/src/main/java/org/jlibsedml/SEDMLReader.java +++ b/vcell-core/src/main/java/org/jlibsedml/SEDMLReader.java @@ -246,7 +246,7 @@ Algorithm getAlgorithm(Element algorithmElement) { if (eChild.getName().equals(SEDMLTags.ALGORITHM_PARAMETER_LIST)) { addAlgorithmParameters(alg, eChild); } else { - log.warn("Unexpected " + eChild); + if (log.isDebugEnabled()) log.warn("Unexpected " + eChild); } } return alg; diff --git a/vcell-core/src/main/java/org/jlibsedml/validation/SchemaValidatorImpl.java b/vcell-core/src/main/java/org/jlibsedml/validation/SchemaValidatorImpl.java index 2b10a4827b..43fc326c44 100644 --- a/vcell-core/src/main/java/org/jlibsedml/validation/SchemaValidatorImpl.java +++ b/vcell-core/src/main/java/org/jlibsedml/validation/SchemaValidatorImpl.java @@ -90,7 +90,7 @@ private String getSchema(String xmlAsString) throws JDOMException, return SEDML_L1_V2_SCHEMA; } else { // probably level 3, but trying anyway to interpret with level 2 - System.out.println("SED-ML version level not supported, import may fail"); + if (log.isDebugEnabled()) log.warn("SED-ML version level not supported, import may fail"); return SEDML_L1_V3_SCHEMA; // throw new IllegalArgumentException( // "Invalid level/version combingation - must be 1-1 or 1-2 but was " diff --git a/vcell-core/src/main/java/org/jlibsedml/validation/SchematronValidator.java b/vcell-core/src/main/java/org/jlibsedml/validation/SchematronValidator.java index fd72478d4a..fe9cc0d43f 100644 --- a/vcell-core/src/main/java/org/jlibsedml/validation/SchematronValidator.java +++ b/vcell-core/src/main/java/org/jlibsedml/validation/SchematronValidator.java @@ -116,7 +116,7 @@ private String getSchematronXSL() { } else if (sedml.isL1V2()) { return "validatorl1v2.xsl"; } else { - System.out.println("Unsupported version, import may fail"); + if (lg.isDebugEnabled()) lg.warn("Unsupported version, import may fail"); return "validatorl1v2.xsl"; // throw new UnsupportedOperationException(MessageFormat.format( // "Invalid level and version - {0}-{1}", sedml.getLevel(), diff --git a/vcell-core/src/main/java/org/vcell/sedml/SEDMLImporter.java b/vcell-core/src/main/java/org/vcell/sedml/SEDMLImporter.java index 2a27d5a876..bd2fa95e93 100644 --- a/vcell-core/src/main/java/org/vcell/sedml/SEDMLImporter.java +++ b/vcell-core/src/main/java/org/vcell/sedml/SEDMLImporter.java @@ -63,8 +63,7 @@ */ public class SEDMLImporter { private final static Logger logger = LogManager.getLogger(SEDMLImporter.class); - private final SedML sedml; - private final ExternalDocInfo externalDocInfo; + private SedML sedml; private final boolean exactMatchOnly; private final VCLogger transLogger; @@ -76,31 +75,47 @@ public class SEDMLImporter { private final HashMap importMap = new HashMap<>(); /** - * Prepares a sedml for import as biomodels + * Builds the importer for future initialization * * @param transLogger the VC logger to use - * @param externalDocInfo contextual information necessary for import - * @param sedml the sedml to import + * @param exactMatchOnly do not substitute for "compatible" kisao solvers, use the exact solver only. + */ + public SEDMLImporter(VCLogger transLogger, boolean exactMatchOnly) { + this.transLogger = transLogger; + this.sedml = null; + this.exactMatchOnly = exactMatchOnly; + } + + /** + * Prepares a sedml for import as biomodels + * + * @param transLogger the VC logger to use * @param exactMatchOnly do not substitute for "compatible" kisao solvers, use the exact solver only. * @throws FileNotFoundException if the sedml archive can not be found * @throws XMLException if the sedml has invalid xml. */ - public SEDMLImporter(VCLogger transLogger, ExternalDocInfo externalDocInfo, SedML sedml, boolean exactMatchOnly) + public SEDMLImporter(VCLogger transLogger, File fileWithSedmlToProcess, SedML sedml, boolean exactMatchOnly) throws XMLException, IOException { - this.transLogger = transLogger; - this.externalDocInfo = externalDocInfo; - this.sedml = sedml; - this.exactMatchOnly = exactMatchOnly; - - this.initialize(); + this(transLogger, exactMatchOnly); + this.initialize(fileWithSedmlToProcess, sedml); } - - private void initialize() throws XMLException, IOException { - // extract bioModel name from sedx (or sedml) file - this.bioModelBaseName = FileUtils.getBaseName(this.externalDocInfo.getFile().getAbsolutePath()); - if(this.externalDocInfo.getFile().getPath().toLowerCase().endsWith("sedx") - || this.externalDocInfo.getFile().getPath().toLowerCase().endsWith("omex")) { - this.ac = Libsedml.readSEDMLArchive(Files.newInputStream(this.externalDocInfo.getFile().toPath())); + + /** + * Initialize the importer to process a specific set of SedML within a document or archive. + * @param fileWithSedmlToProcess the file containing SedML + * @param sedml the SedML to be processed (since the file may have more than 1 sedml) + * @throws IOException if the sedml archive can not be found, or the IO stream reading it failed + * @throws XMLException if the sedml has invalid xml. + */ + public void initialize(File fileWithSedmlToProcess, SedML sedml) throws XMLException, IOException { + // extract bioModel name from sedml (or sedml) file + if (fileWithSedmlToProcess == null) throw new IllegalArgumentException("Source file of SedML can not be null!"); + if (sedml == null) throw new IllegalArgumentException("Provided SedML can not be null!"); + this.sedml = sedml; + this.bioModelBaseName = FileUtils.getBaseName(fileWithSedmlToProcess.getAbsolutePath()); + if(fileWithSedmlToProcess.getPath().toLowerCase().endsWith("sedx") + || fileWithSedmlToProcess.getPath().toLowerCase().endsWith("omex")) { + this.ac = Libsedml.readSEDMLArchive(Files.newInputStream(fileWithSedmlToProcess.toPath())); } this.resolver = new ModelResolver(this.sedml); if(this.ac != null) { @@ -109,7 +124,7 @@ private void initialize() throws XMLException, IOException { this.resolver.add(amr); } else { this.resolver.add(new FileModelResolver()); // assumes absolute paths - String sedmlRelativePrefix = this.externalDocInfo.getFile().getParent() + File.separator; + String sedmlRelativePrefix = fileWithSedmlToProcess.getParent() + File.separator; this.resolver.add(new RelativeFileModelResolver(sedmlRelativePrefix)); // in case model URIs are relative paths } this.sbmlSupport = new SBMLSupport(); @@ -180,7 +195,7 @@ public List getBioModels() { // try to find a match in the ontology tree SolverDescription solverDescription = SolverUtilities.matchSolverWithKisaoId(kisaoID, this.exactMatchOnly); if (solverDescription != null) { - logger.info("Task (id='{}') is compatible, solver match found in ontology: '{}' matched to {}", selectedTask.getId(), kisaoID, solverDescription); + if (logger.isDebugEnabled()) logger.info("Task (id='{}') is compatible, solver match found in ontology: '{}' matched to {}", selectedTask.getId(), kisaoID, solverDescription); } else { // give it a try anyway with our deterministic default solver solverDescription = SolverDescription.CombinedSundials; @@ -426,10 +441,12 @@ private List mergeBioModels(List bioModels) { BioModel bm0 = bioModels.get(0); for (int i = 1; i < bioModels.size(); i++) { - System.out.println("----comparing model from----"+bioModels.get(i)+" with model from "+bm0); + if (logger.isDebugEnabled()) + logger.info("--------------------\ncomparing model from `{}`\n with model from `{}`\n--------------------", + bioModels.get(i), bm0); RelationVisitor rvNotStrict = new ModelRelationVisitor(false); boolean equivalent = bioModels.get(i).getModel().relate(bm0.getModel(),rvNotStrict); - System.out.println(equivalent); + if (logger.isDebugEnabled()) logger.info("Equivalent => {}", equivalent); if (!equivalent) return bioModels; } // all have matchable model, try to merge by pooling SimContexts diff --git a/vcell-core/src/main/java/org/vcell/sedml/log/BiosimulationLog.java b/vcell-core/src/main/java/org/vcell/sedml/log/BiosimulationLog.java index 87366a8506..7020ae1253 100644 --- a/vcell-core/src/main/java/org/vcell/sedml/log/BiosimulationLog.java +++ b/vcell-core/src/main/java/org/vcell/sedml/log/BiosimulationLog.java @@ -22,7 +22,7 @@ import java.util.ArrayList; import java.util.List; -public class BiosimulationLog { +public class BiosimulationLog implements AutoCloseable { public static class LogValidationException extends RuntimeException { public LogValidationException(String message) { From 508d2c4f6bdaff1fec31e5ed66cfdd56b0ff4306 Mon Sep 17 00:00:00 2001 From: Logan Drescher Date: Thu, 23 Jan 2025 12:58:14 -0500 Subject: [PATCH 09/17] Moved dependency from core to CLI --- vcell-cli/pom.xml | 5 + .../ChartCouldNotBeProducedException.java | 2 +- .../cli/run/plotting/Results2DLinePlot.java | 325 ++++++++++++++++++ .../cli/run}/plotting/ResultsLinePlot.java | 2 +- .../cli/run/plotting/SingleAxisSeries.java | 68 ++++ .../run}/plotting/TestJFreeChartLibrary.java | 2 +- .../run/plotting/TestResults2DLinePlot.java | 244 +++++++++++++ .../org/vcell/cli/run}/plotting/Parabolic.png | Bin .../run}/plotting/plot2d_SimpleSimulation.csv | 0 vcell-core/pom.xml | 5 - .../org/vcell/plotting/Results2DLinePlot.java | 203 ----------- .../vcell/plotting/TestResults2DLinePlot.java | 128 ------- 12 files changed, 645 insertions(+), 339 deletions(-) rename {vcell-core/src/main/java/org/vcell => vcell-cli/src/main/java/org/vcell/cli/run}/plotting/ChartCouldNotBeProducedException.java (92%) create mode 100644 vcell-cli/src/main/java/org/vcell/cli/run/plotting/Results2DLinePlot.java rename {vcell-core/src/main/java/org/vcell => vcell-cli/src/main/java/org/vcell/cli/run}/plotting/ResultsLinePlot.java (95%) create mode 100644 vcell-cli/src/main/java/org/vcell/cli/run/plotting/SingleAxisSeries.java rename {vcell-core/src/test/java/org/vcell => vcell-cli/src/test/java/org/vcell/cli/run}/plotting/TestJFreeChartLibrary.java (99%) create mode 100644 vcell-cli/src/test/java/org/vcell/cli/run/plotting/TestResults2DLinePlot.java rename {vcell-core/src/test/resources/org/vcell => vcell-cli/src/test/resources/org/vcell/cli/run}/plotting/Parabolic.png (100%) rename {vcell-core/src/test/resources/org/vcell => vcell-cli/src/test/resources/org/vcell/cli/run}/plotting/plot2d_SimpleSimulation.csv (100%) delete mode 100644 vcell-core/src/main/java/org/vcell/plotting/Results2DLinePlot.java delete mode 100644 vcell-core/src/test/java/org/vcell/plotting/TestResults2DLinePlot.java diff --git a/vcell-cli/pom.xml b/vcell-cli/pom.xml index 1d2605e44b..40e292a049 100644 --- a/vcell-cli/pom.xml +++ b/vcell-cli/pom.xml @@ -98,6 +98,11 @@ picocli ${picocli.version}
+ + org.jfree + jfreechart + 1.5.5 + org.junit.jupiter junit-jupiter diff --git a/vcell-core/src/main/java/org/vcell/plotting/ChartCouldNotBeProducedException.java b/vcell-cli/src/main/java/org/vcell/cli/run/plotting/ChartCouldNotBeProducedException.java similarity index 92% rename from vcell-core/src/main/java/org/vcell/plotting/ChartCouldNotBeProducedException.java rename to vcell-cli/src/main/java/org/vcell/cli/run/plotting/ChartCouldNotBeProducedException.java index f09d11c0e5..64ecddf8dc 100644 --- a/vcell-core/src/main/java/org/vcell/plotting/ChartCouldNotBeProducedException.java +++ b/vcell-cli/src/main/java/org/vcell/cli/run/plotting/ChartCouldNotBeProducedException.java @@ -1,4 +1,4 @@ -package org.vcell.plotting; +package org.vcell.cli.run.plotting; public class ChartCouldNotBeProducedException extends RuntimeException { public ChartCouldNotBeProducedException(){ diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/plotting/Results2DLinePlot.java b/vcell-cli/src/main/java/org/vcell/cli/run/plotting/Results2DLinePlot.java new file mode 100644 index 0000000000..04d413a8ac --- /dev/null +++ b/vcell-cli/src/main/java/org/vcell/cli/run/plotting/Results2DLinePlot.java @@ -0,0 +1,325 @@ +package org.vcell.cli.run.plotting; + +import cbit.vcell.publish.PDFWriter; + +import com.lowagie.text.DocumentException; + +import org.jfree.chart.ChartFactory; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.labels.StandardXYItemLabelGenerator; +import org.jfree.chart.plot.XYPlot; +import org.jfree.data.xy.XYDataset; +import org.jfree.data.xy.XYSeries; +import org.jfree.data.xy.XYSeriesCollection; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.print.PageFormat; +import java.awt.print.Paper; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.*; +import java.util.List; + +/** + * Stores all relevant info to create a 2D plot, and lazily builds a PDF on request + */ +public class Results2DLinePlot implements ResultsLinePlot { + + public static final int AXIS_LABEL_FONT_SIZE = 15; + public static final int MAX_SERIES_DATA_POINT_LABELS = 10; + public static final int SERIES_DATA_POINT_LABEL_FONT_SIZE = 8; + + private static final Logger lg = LogManager.getLogger(Results2DLinePlot.class); + + private String plotTitle, xAxisTitle; + private final Map> dataSetMappings; + private final Set xLabels, yLabels; + private int largestSeriesSize; + + public Results2DLinePlot(){ + this("", ""); + } + + public Results2DLinePlot(String plotTitle, String xAxisTitle){ + this.plotTitle = plotTitle; + this.xAxisTitle = xAxisTitle; + this.dataSetMappings = new LinkedHashMap<>(); + this.xLabels = new HashSet<>(); + this.yLabels = new HashSet<>(); + this.largestSeriesSize = 0; + } + + @Override + public void setTitle(String newTitle) { + this.plotTitle = newTitle; + } + + @Override + public String getTitle() { + return this.plotTitle; + } + + public void setXAxisTitle(String newTitle) { + this.xAxisTitle = newTitle; + } + + public String getXAxisTitle() { + return this.xAxisTitle; + } + + public int getLargestSeriesSize(){ + return this.largestSeriesSize; + } + + /** + * Adds an XY pairing to this plot; will not duplicate pairings. + * @param xData the x-axis of data + * @param yData the y-axis of data + * @throws IllegalArgumentException if the length of data on each axis doesn't match + */ + public void addXYData(SingleAxisSeries xData, SingleAxisSeries yData){ + if (xData == null) throw new IllegalArgumentException("Parameter `xData` can not be null!"); + if (yData == null) throw new IllegalArgumentException("Parameter `yData` can not be null!"); + if (xData.data().size() != yData.data().size()) throw new IllegalArgumentException("Data lengths do not match!"); + if (this.xLabels.contains(xData.label()) && !this.dataSetMappings.containsKey(xData)) + throw new IllegalArgumentException("plot already has data for x-axis with the label`" + xData.label() + "` (but it has different values) "); + if (this.yLabels.contains(yData.label())) throw new IllegalArgumentException("plot already has data for y-axis `" + yData.label() + "`"); + if (!this.dataSetMappings.containsKey(xData)) this.dataSetMappings.put(xData, new LinkedHashSet<>()); + this.dataSetMappings.get(xData).add(yData); + this.xLabels.add(xData.label()); + this.yLabels.add(yData.label()); + if (this.largestSeriesSize < xData.data().size()) this.largestSeriesSize = xData.data().size(); + } + + /** + * Replaces the x-axis data associated with a certain label, with a newly provided set. + * @param axisLabel the label of the data you want to change + * @param axisData the data to replace with + * @throws IllegalArgumentException if changing the length of the data would create an illegal pairing. + */ + public void changeXAxisData(String axisLabel, double[] axisData){ + if (axisData == null) throw new IllegalArgumentException("Parameter `axisData` may not be null!"); + this.changeXAxisData(axisLabel, Arrays.stream(axisData).boxed().toList()); + } + + /** + * Replaces the x-axis data associated with a certain label, with a newly provided set. + * @param axisLabel the label of the data you want to change + * @param axisData the data to replace with + * @throws IllegalArgumentException if changing the length of the data would create an illegal pairing. + */ + public void changeXAxisData(String axisLabel, List axisData){ + this.changeXAxisData(new SingleAxisSeries(axisLabel, axisData)); + } + + /** + * Replaces the x-axis data associated with a certain label, with a newly provided set. + * @param axis the axis to replace with; matches by label. + * @throws IllegalArgumentException if changing the length of the data would create an illegal pairing. + */ + public void changeXAxisData(SingleAxisSeries axis){ + SingleAxisSeries axisToChange; + if (null == (axisToChange = this.getXAxisSeries(axis.label()))) throw new IllegalArgumentException("Axis with label `" + axis.label() + "` does not exist!"); + if (axis.data().size() != axisToChange.data().size()) throw new IllegalArgumentException("Data lengths do not match!"); + Set yAxes = this.dataSetMappings.remove(axisToChange); + this.dataSetMappings.put(axis, yAxes); + } + + /** + * Removes an XY paring to this plot, if it exists + *
+ * WARNING: Do not provide this method with freshly constructed series; `SingleAxisSeries` hashes by object_id for efficiency. + *
+ * See {@link SingleAxisSeries} for further details + * @return true if the mapping formerly existed, else false + */ + public boolean removeXYData(SingleAxisSeries xSeries, SingleAxisSeries ySeries){ + if (!this.dataSetMappings.containsKey(xSeries) || !this.dataSetMappings.get(xSeries).contains(ySeries)) return false; + this.dataSetMappings.get(xSeries).remove(ySeries); + if (this.dataSetMappings.get(xSeries).isEmpty()) this.dataSetMappings.remove(xSeries); + this.xLabels.remove(xSeries.label()); + this.xLabels.remove(ySeries.label()); + this.largestSeriesSize = this.findLargestSeriesSize(); + return true; + } + + /** + * Removes an XY paring to this plot, if it exists + *
+ * WARNING: this method must use iteration, and is the medium speed of it's counterparts. + * @param xSeries the x-series to find and remove + * @param yLabel the label of the y-series to find and remove + * @return true if the mapping formerly existed, else false + */ + public boolean removeXYData(SingleAxisSeries xSeries, String yLabel){ + if (yLabel == null) throw new IllegalArgumentException("Parameter `yLabel` can not be null!"); + if (!this.dataSetMappings.containsKey(xSeries)) return false; + for (SingleAxisSeries ySeries : this.dataSetMappings.get(xSeries)){ + if (!yLabel.equals(ySeries.label())) continue; + return this.removeXYData(xSeries, ySeries); + } + return false; + } + + /** + * Removes an XY paring to this plot, if it exists + *
+ * WARNING: this method can not utilize hashing, and is the slowest of its counterparts! + * @param xLabel the label of the x-series to find and remove + * @param yLabel the label of the y-series to find and remove + * @return true if the mapping formerly existed, else false + */ + public boolean removeXYData(String xLabel, String yLabel){ + SingleAxisSeries xSeries; + if (null == (xSeries = this.getXAxisSeries(xLabel))) return false; + return this.removeXYData(xSeries, yLabel); + } + + /** + * Gets the x-axis series with the provided label. + * @param label the x-axis series to find + * @return null if no axis series could be found, else the desired series + */ + public SingleAxisSeries getXAxisSeries(String label){ + for (SingleAxisSeries xSeries : this.dataSetMappings.keySet()){ + if (!xSeries.label().equals(label)) continue; + return xSeries; + } + return null; + } + + /** + * Gets the y-axis series with the provided label. + * @param label the y-axis series to find + * @return null if no axis series could be found, else the desired series + */ + public SingleAxisSeries getYAxisSeries(String label){ + for (SingleAxisSeries xSeries : this.dataSetMappings.keySet()){ + for (SingleAxisSeries ySeries : this.dataSetMappings.get(xSeries)){ + if (!ySeries.label().equals(label)) continue; + return ySeries; + } + } + return null; + } + + public int getNumXSeries(){ + return this.dataSetMappings.keySet().size(); + } + + public int getNumYSeries(){ + int sum = 0; + for (SingleAxisSeries xAxis : this.dataSetMappings.keySet()){ + sum += this.getNumYSeries(xAxis); + } + return sum; + } + + public int getNumYSeries(SingleAxisSeries xData){ + if (!this.dataSetMappings.containsKey(xData)) return 0; + return this.dataSetMappings.get(xData).size(); + } + + public void generatePng(String desiredFileName, File desiredParentDirectory) throws ChartCouldNotBeProducedException { + JFreeChart chart = this.createChart(); + PageFormat pageFormat = Results2DLinePlot.generateAlternatePageFormat(); + + File testfile = new File(desiredParentDirectory, desiredFileName); + try { + if (testfile.exists() && !testfile.isFile()) throw new IllegalArgumentException("desired PDF already exists, and is not a regular file"); + if (!testfile.exists() && !testfile.createNewFile()) throw new IllegalArgumentException("Unable to create desired PDF; creation itself failed."); + BufferedImage bfi = chart.createBufferedImage((int) pageFormat.getImageableWidth(), (int) pageFormat.getImageableHeight()); + ImageIO.write(bfi, "png", testfile); + } catch (IOException e){ + lg.error("Error while preparing PNG; see exception below"); + throw new ChartCouldNotBeProducedException(e); + } + } + + + @Override + public void generatePdf(String desiredFileName, File desiredParentDirectory) throws ChartCouldNotBeProducedException { + JFreeChart chart = this.createChart(); + + // Prepare for export + PDFWriter pdfWriter = new PDFWriter(); + PageFormat pageFormat = Results2DLinePlot.generateAlternatePageFormat(); + + File testfile = new File(desiredParentDirectory, desiredFileName); + try { + if (testfile.exists() && !testfile.isFile()) throw new IllegalArgumentException("desired PDF already exists, and is not a regular file"); + if (!testfile.exists() && !testfile.createNewFile()) throw new IllegalArgumentException("Unable to create desired PDF; creation itself failed."); + try (FileOutputStream fos = new FileOutputStream(testfile)) { + BufferedImage bfi = chart.createBufferedImage((int) pageFormat.getImageableWidth(), (int) pageFormat.getImageableHeight()); + pdfWriter.writePlotImageDocument("Test Document", fos, pageFormat, bfi); + } catch (DocumentException e) { + lg.error("Error while building PDF; see exception below"); + throw new ChartCouldNotBeProducedException(e); + } + } catch (IOException e){ + lg.error("Error while preparing PDF; see exception below"); + throw new ChartCouldNotBeProducedException(e); + } + } + + private XYDataset generateChartLibraryDataset(){ + XYSeriesCollection dataset2D = new XYSeriesCollection(); + for (SingleAxisSeries xAxis : this.dataSetMappings.keySet()){ + for (SingleAxisSeries yAxis : this.dataSetMappings.get(xAxis)){ + XYSeries xySeries = new XYSeries(yAxis.label(), true, false); + for (int i = 0; i < yAxis.data().size(); i++){ + xySeries.add(xAxis.data().get(i), yAxis.data().get(i)); + } + dataset2D.addSeries(xySeries); + } + } + return dataset2D; + } + + private JFreeChart createChart(){ + String yAxisLabel = ""; + XYDataset dataset2D = this.generateChartLibraryDataset(); + JFreeChart chart = ChartFactory.createXYLineChart(this.plotTitle, this.xAxisTitle, yAxisLabel, dataset2D); + + // Tweak Chart so it looks better + chart.setBorderVisible(true); + chart.getPlot().setBackgroundPaint(Color.white); + XYPlot chartPlot = chart.getXYPlot(); + chartPlot.getDomainAxis().setLabelFont(new Font(this.xAxisTitle, Font.PLAIN, Results2DLinePlot.AXIS_LABEL_FONT_SIZE)); + chartPlot.getRangeAxis().setLabelFont(new Font(yAxisLabel, Font.PLAIN, Results2DLinePlot.AXIS_LABEL_FONT_SIZE)); + if (this.largestSeriesSize <= Results2DLinePlot.MAX_SERIES_DATA_POINT_LABELS) { // if it's too crowded, having data point labels is bad + for (int i = 0; i < dataset2D.getSeriesCount(); i++) { + //DecimalFormat decimalformat1 = new DecimalFormat("##"); // Should we ever need it, this object is used in formating the labels. + chartPlot.getRenderer().setSeriesItemLabelGenerator(i, new StandardXYItemLabelGenerator("({1}, {2})")); + chartPlot.getRenderer().setSeriesItemLabelFont(i, new Font(null, Font.PLAIN, Results2DLinePlot.SERIES_DATA_POINT_LABEL_FONT_SIZE)); + chartPlot.getRenderer().setSeriesItemLabelsVisible(i, true); + } + } + return chart; + } + + private int findLargestSeriesSize(){ + int newMax = 0; + for (SingleAxisSeries xAxis : this.dataSetMappings.keySet()){ + if (newMax < xAxis.data().size()) newMax = xAxis.data().size(); + } + return newMax; + } + + private static PageFormat generateAlternatePageFormat(){ + java.awt.print.PageFormat pageFormat = java.awt.print.PrinterJob.getPrinterJob().defaultPage(); + Paper alternatePaper = new Paper(); // We want to try and increase the margins + double altOriginX = alternatePaper.getImageableX() / 2, altOriginY = alternatePaper.getImageableY() / 2; + double altWidth = alternatePaper.getWidth() - 2 * altOriginX, altHeight = alternatePaper.getHeight() - 2 * altOriginY; + alternatePaper.setImageableArea(altOriginX, altOriginY, altWidth, altHeight); + pageFormat.setPaper(alternatePaper); + pageFormat.setOrientation(PageFormat.LANDSCAPE); + return pageFormat; + } +} diff --git a/vcell-core/src/main/java/org/vcell/plotting/ResultsLinePlot.java b/vcell-cli/src/main/java/org/vcell/cli/run/plotting/ResultsLinePlot.java similarity index 95% rename from vcell-core/src/main/java/org/vcell/plotting/ResultsLinePlot.java rename to vcell-cli/src/main/java/org/vcell/cli/run/plotting/ResultsLinePlot.java index 371167d26b..ccd562d38d 100644 --- a/vcell-core/src/main/java/org/vcell/plotting/ResultsLinePlot.java +++ b/vcell-cli/src/main/java/org/vcell/cli/run/plotting/ResultsLinePlot.java @@ -1,4 +1,4 @@ -package org.vcell.plotting; +package org.vcell.cli.run.plotting; import java.io.File; diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/plotting/SingleAxisSeries.java b/vcell-cli/src/main/java/org/vcell/cli/run/plotting/SingleAxisSeries.java new file mode 100644 index 0000000000..392ba0643a --- /dev/null +++ b/vcell-cli/src/main/java/org/vcell/cli/run/plotting/SingleAxisSeries.java @@ -0,0 +1,68 @@ +package org.vcell.cli.run.plotting; + +import java.util.List; + +/** + * "Record" class for storing label and data combos + *
+ * NOTE: This class can not realistically use `double[]`, because there is no direct immutable array type in java + * (except direct initializer; not useful at runtime). So we instead use UnmodifiableCollections. + * But that means extra-legwork making this happen. + */ +public class SingleAxisSeries { + final String immutableLabel; + final List immutableData; + final String STRING_REPRESENTATION; + + /** + * Record class for storing label and data combos + *
+ * NOTE: This class can not realistically use `double[]`, because there is no direct immutable array type in java + * (except direct initializer; not useful at runtime). So we instead use UnmodifiableCollections. + * But that means extra-legwork making this happen. + * @param label the label to assign to the data in a plot + * @param data the ***IMMUTABLE COLLECTION*** of data to be represented. + */ + public SingleAxisSeries(String label, List data) { + if (label == null) throw new IllegalArgumentException("Argument for `label` can not be null!"); + if (data == null) throw new IllegalArgumentException("Argument for `data` can not be null!"); + // We need to verify the data provided was provided as immutable! + try { + data.add(0.0); + throw new IllegalArgumentException("Provided data list can *not* be mutable. Use `Collections.unmodifiableList(List.copyOf())` on the data!"); + } catch (UnsupportedOperationException ignored) { + // Perfect! this is what we want! + } + this.immutableLabel = label; // Strings are one of the few objects classified as pass by value + this.immutableData = data; + StringBuilder stringRep = new StringBuilder(); + stringRep.append(label).append("(length:").append(data.size()).append(")::["); + for (Double d : data) stringRep.append(d).append(","); + this.STRING_REPRESENTATION = stringRep.deleteCharAt(stringRep.length() - 1).append(']').toString(); + } + + public String label(){ + return this.immutableLabel; + } + + public List data(){ + return this.immutableData; + } + + @Override + public String toString(){ + return this.STRING_REPRESENTATION; + } + + @Override + public int hashCode(){ + // By adding the length of the data in the StringRepresentation, no "label" could emulate the data, and thus this is safe. + return this.STRING_REPRESENTATION.hashCode(); + } + + @Override + public boolean equals(Object other){ + if (!(other instanceof SingleAxisSeries sas)) return false; + return this.STRING_REPRESENTATION.equals(sas.STRING_REPRESENTATION); + } +} diff --git a/vcell-core/src/test/java/org/vcell/plotting/TestJFreeChartLibrary.java b/vcell-cli/src/test/java/org/vcell/cli/run/plotting/TestJFreeChartLibrary.java similarity index 99% rename from vcell-core/src/test/java/org/vcell/plotting/TestJFreeChartLibrary.java rename to vcell-cli/src/test/java/org/vcell/cli/run/plotting/TestJFreeChartLibrary.java index 4e53aae73e..1e2a94211e 100644 --- a/vcell-core/src/test/java/org/vcell/plotting/TestJFreeChartLibrary.java +++ b/vcell-cli/src/test/java/org/vcell/cli/run/plotting/TestJFreeChartLibrary.java @@ -1,4 +1,4 @@ -package org.vcell.plotting; +package org.vcell.cli.run.plotting; import cbit.vcell.publish.PDFWriter; diff --git a/vcell-cli/src/test/java/org/vcell/cli/run/plotting/TestResults2DLinePlot.java b/vcell-cli/src/test/java/org/vcell/cli/run/plotting/TestResults2DLinePlot.java new file mode 100644 index 0000000000..60a51c997a --- /dev/null +++ b/vcell-cli/src/test/java/org/vcell/cli/run/plotting/TestResults2DLinePlot.java @@ -0,0 +1,244 @@ +package org.vcell.cli.run.plotting; + +import cbit.vcell.mongodb.VCMongoMessage; +import cbit.vcell.resource.PropertyLoader; +import org.jfree.chart.ChartFactory; +import org.jfree.chart.JFreeChart; +import org.jfree.data.xy.XYDataItem; +import org.jfree.data.xy.XYSeries; +import org.jfree.data.xy.XYSeriesCollection; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Assertions; +import org.vcell.cli.biosimulation.BiosimulationsCommand; +import org.vcell.cli.run.ExecuteImpl; +import org.vcell.util.Pair; +import org.vcell.util.VCellUtilityHub; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +@Tag("Fast") +public class TestResults2DLinePlot { + private static List paraData = List.of( + new XYDataItem(0.0, 0.0), + new XYDataItem(0.5, 0.25), + new XYDataItem(1.0, 1.0), + new XYDataItem(1.5, 2.25), + new XYDataItem(2.0, 4.0), + new XYDataItem(2.5, 6.25), + new XYDataItem(3.0, 9.0), + new XYDataItem(3.5, 12.25), + new XYDataItem(4.0, 16.0), + new XYDataItem(4.5, 20.25), + new XYDataItem(5.0, 25.0), + new XYDataItem(5.5, 30.25) + ); + + private static Pair parabolicData = new Pair<>( + Stream.of(0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5).mapToDouble(Double::valueOf).toArray(), + Stream.of(0.0, 0.25, 1.0, 2.25, 4.0, 6.25, 9.0, 12.25, 16.0, 20.25, 25.0, 30.25).mapToDouble(Double::valueOf).toArray() + ); + + private static Pair, List> parabolicListData = new Pair<>( + List.of(0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0), + List.of(0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0) + ); + + private static Pair, List> linearListData = new Pair<>( + List.of(0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0), + List.of(0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0) + ); + + @Test + public void testConstructors(){ + Results2DLinePlot[] testInstances = { + new Results2DLinePlot(), + new Results2DLinePlot("Title", "Label") + }; + + for (Results2DLinePlot instance : testInstances){ + Assertions.assertTrue("".equals(instance.getTitle()) || "Title".equals(instance.getTitle())); + Assertions.assertTrue("".equals(instance.getXAxisTitle()) || "Label".equals(instance.getXAxisTitle())); + } + } + + @Test + public void testSettingAndGetting(){ + Results2DLinePlot[] testInstances = { + new Results2DLinePlot(), + new Results2DLinePlot("Title", "Label") + }; + for (Results2DLinePlot plot : testInstances){ + String alternateTitle = "AltTitle", alternateAxisTitle = "AltAxisTitle"; + plot.setTitle(alternateTitle); + Assertions.assertEquals(alternateTitle, plot.getTitle()); + plot.setXAxisTitle(alternateAxisTitle); + Assertions.assertEquals(alternateAxisTitle, plot.getXAxisTitle()); + + Assertions.assertThrows(IllegalArgumentException.class, () -> plot.addXYData(null, new SingleAxisSeries("Linear", linearListData.two))); + Assertions.assertThrows(IllegalArgumentException.class, () -> plot.addXYData(new SingleAxisSeries("Numbers", linearListData.one), null)); + Assertions.assertThrows(IllegalArgumentException.class, + () -> plot.addXYData(new SingleAxisSeries(null, linearListData.one), new SingleAxisSeries("Linear", linearListData.two))); + Assertions.assertThrows(IllegalArgumentException.class, + () -> plot.addXYData(new SingleAxisSeries("Numbers", null), new SingleAxisSeries("Linear", linearListData.two))); + Assertions.assertThrows(IllegalArgumentException.class, + () -> plot.addXYData(new SingleAxisSeries("Numbers", linearListData.one), new SingleAxisSeries(null, linearListData.two))); + Assertions.assertThrows(IllegalArgumentException.class, + () -> plot.addXYData(new SingleAxisSeries("Numbers", linearListData.one), new SingleAxisSeries("Linear", null))); + Assertions.assertThrows(IllegalArgumentException.class, + () -> plot.addXYData(new SingleAxisSeries("Numbers", linearListData.one), new SingleAxisSeries("Linear", new ArrayList<>(List.of(0.2, 1.2))))); + + + SingleAxisSeries xNumbers = new SingleAxisSeries("Numbers", linearListData.one); + plot.addXYData(xNumbers, new SingleAxisSeries("Linear", linearListData.two)); + plot.addXYData(xNumbers, new SingleAxisSeries("Quadratic", parabolicListData.two)); + plot.addXYData(new SingleAxisSeries("ToRemoveX", List.of(0.0, 1.0)), new SingleAxisSeries("ToRemoveY1", List.of(0.1, 1.1))); + plot.addXYData(new SingleAxisSeries("ToRemoveX", List.of(0.0, 1.0)), new SingleAxisSeries("ToRemoveY2", List.of(0.2, 1.2))); + plot.addXYData(new SingleAxisSeries("ToRemoveX", List.of(0.0, 1.0)), new SingleAxisSeries("ToRemoveY3", List.of(0.3, 1.3))); + Assertions.assertEquals(plot.getXAxisSeries("ToRemoveX"), new SingleAxisSeries("ToRemoveX", List.of(0.0, 1.0))); + Assertions.assertEquals(plot.getYAxisSeries("ToRemoveY2"), new SingleAxisSeries("ToRemoveY2", List.of(0.2, 1.2))); + + Assertions.assertEquals(3, plot.getNumYSeries(new SingleAxisSeries("ToRemoveX", List.of(0.0, 1.0)))); + Assertions.assertTrue(plot.removeXYData("ToRemoveX", "ToRemoveY3")); + Assertions.assertFalse(plot.removeXYData("ToRemoveX", "ToRemoveY3")); + Assertions.assertEquals(2, plot.getNumYSeries(new SingleAxisSeries("ToRemoveX", List.of(0.0, 1.0)))); + Assertions.assertTrue(plot.removeXYData("ToRemoveX", "ToRemoveY2")); + Assertions.assertEquals(2, plot.getNumXSeries()); + Assertions.assertTrue(plot.removeXYData("ToRemoveX", "ToRemoveY1")); + Assertions.assertEquals(1, plot.getNumXSeries()); + Assertions.assertEquals(0, plot.getNumYSeries(new SingleAxisSeries("ToRemoveX", List.of(0.0, 1.0)))); + Assertions.assertNull(plot.getXAxisSeries("ToRemoveX")); + Assertions.assertEquals(2, plot.getNumYSeries(xNumbers)); + Assertions.assertEquals(2, plot.getNumYSeries()); + + + Assertions.assertEquals(parabolicListData.one.size(), plot.getLargestSeriesSize()); + Assertions.assertThrows(IllegalArgumentException.class, ()->plot.changeXAxisData(null, linearListData.one)); + Assertions.assertThrows(IllegalArgumentException.class, ()->plot.changeXAxisData("Numbers", (double[]) null)); + Assertions.assertThrows(IllegalArgumentException.class, ()->plot.changeXAxisData("Nummmmbers", linearListData.one)); + Assertions.assertThrows(IllegalArgumentException.class, ()->plot.changeXAxisData("Numbers", new double[0])); + Assertions.assertThrows(IllegalArgumentException.class, ()->plot.changeXAxisData(new SingleAxisSeries("Nummmmbers", linearListData.one))); + Assertions.assertThrows(IllegalArgumentException.class, ()->plot.changeXAxisData(new SingleAxisSeries("Numbers", List.of()))); + plot.changeXAxisData(new SingleAxisSeries("Numbers", parabolicListData.one)); + Assertions.assertThrows(IllegalArgumentException.class, ()-> plot.changeXAxisData("Numbers", parabolicData.one)); + plot.changeXAxisData("Numbers", parabolicListData.one.stream().mapToDouble(Double::doubleValue).toArray()); + plot.changeXAxisData("Numbers", parabolicListData.one); + } + + } + + @Test + public void pngRoundTripTest() throws IOException { + File dupe = File.createTempFile("VCellPNG::", ".png"); + XYSeries series = new XYSeries("key"); + for (int i = 0; i < TestResults2DLinePlot.parabolicData.one.length; i++){ + series.add(TestResults2DLinePlot.parabolicData.one[i], TestResults2DLinePlot.parabolicData.two[i]); + } + XYSeriesCollection dataset = new XYSeriesCollection(); + dataset.addSeries(series); + JFreeChart chart = ChartFactory.createXYLineChart("Test", "X-Axis","Y-Axis", dataset); + BufferedImage originalImage = chart.createBufferedImage(1000,1000); + ImageIO.write(originalImage, "png", dupe); + BufferedImage roundTrippedImage = ImageIO.read(dupe); + Assertions.assertEquals(originalImage.getWidth(), roundTrippedImage.getWidth()); + Assertions.assertEquals(originalImage.getHeight(), roundTrippedImage.getHeight()); + for (int wPix = 0; wPix < originalImage.getWidth(); wPix++){ + for (int hPix = 0; hPix < originalImage.getHeight(); hPix++){ + Assertions.assertEquals(originalImage.getRGB(wPix, hPix), roundTrippedImage.getRGB(wPix, hPix)); + } + } + } + + @Test + public void pngLibraryLevelTest() throws IOException { + String STANDARD_IMAGE_LOCAL_PATH = "Parabolic.png"; + InputStream standardImageStream = TestResults2DLinePlot.class.getResourceAsStream(STANDARD_IMAGE_LOCAL_PATH); + if (standardImageStream == null) + throw new FileNotFoundException(String.format("can not find `%s`; maybe it moved?", STANDARD_IMAGE_LOCAL_PATH)); + BufferedImage standardImage = ImageIO.read(standardImageStream); + XYSeries series = new XYSeries("key"); + for (int i = 0; i < TestResults2DLinePlot.parabolicData.one.length; i++){ + series.add(TestResults2DLinePlot.parabolicData.one[i], TestResults2DLinePlot.parabolicData.two[i]); + } + XYSeriesCollection dataset = new XYSeriesCollection(); + dataset.addSeries(series); + JFreeChart chart = ChartFactory.createXYLineChart("Test", "X-Axis","Y-Axis", dataset); + BufferedImage currentImage = chart.createBufferedImage(1000,1000); + Assertions.assertEquals(currentImage.getWidth(), standardImage.getWidth()); + Assertions.assertEquals(currentImage.getHeight(), standardImage.getHeight()); + for (int wPix = 0; wPix < currentImage.getWidth(); wPix++){ + for (int hPix = 0; hPix < currentImage.getHeight(); hPix++){ + Assertions.assertEquals(currentImage.getRGB(wPix, hPix), standardImage.getRGB(wPix, hPix)); + } + } + } + + @Test + public void pngExecutionLevelTest() throws IOException { + String INPUT_FILE_LOCAL_PATH = "MultiplePlotsTest.omex"; + Path inputFilePath = Files.createTempFile("VCellInputOmex", ".omex"); + Path outputFile = Files.createTempDirectory("VCellCliOut"); + try (InputStream inputFileStream = TestResults2DLinePlot.class.getResourceAsStream(INPUT_FILE_LOCAL_PATH)){ + if (inputFileStream == null) + throw new FileNotFoundException(String.format("can not find `%s`; maybe it moved?", INPUT_FILE_LOCAL_PATH)); + Files.copy(inputFileStream, inputFilePath, StandardCopyOption.REPLACE_EXISTING); + } + + ///////////////////////////////////////// + File installRoot = new File(".."); + PropertyLoader.setProperty(PropertyLoader.installationRoot, installRoot.getAbsolutePath()); + VCMongoMessage.enabled = false; + VCellUtilityHub.startup(VCellUtilityHub.MODE.CLI); + int result = BiosimulationsCommand.executeVCellBiosimulationsMode(inputFilePath.toFile(), outputFile.toFile()); + if (result != 0) throw new RuntimeException("VCell Execution failed!"); + ///////////////////////////////////////// + + File generatedPlot0 = new File(outputFile.toFile(), "BIOMD0000000912_sim.sedml/plot_0.png"); + File generatedPlot1 = new File(outputFile.toFile(), "BIOMD0000000912_sim.sedml/plot_1.png"); + BufferedImage generatedImage0 = ImageIO.read(generatedPlot0); + if (generatedImage0 == null) throw new RuntimeException("Plot_0 PNG was not found; check paths?"); + BufferedImage generatedImage1 = ImageIO.read(generatedPlot1); + if (generatedImage1 == null) throw new RuntimeException("Plot_1 PNG was not found; check paths?"); + + + String PLOT_0_PATH = "plot_0.png"; + String PLOT_1_PATH = "plot_1.png"; + InputStream standardImageStream0 = TestResults2DLinePlot.class.getResourceAsStream(PLOT_0_PATH); + if (standardImageStream0 == null) + throw new FileNotFoundException(String.format("can not find `%s`; maybe it moved?", PLOT_0_PATH)); + BufferedImage standardImage0 = ImageIO.read(standardImageStream0); + InputStream standardImageStream1 = TestResults2DLinePlot.class.getResourceAsStream(PLOT_1_PATH); + if (standardImageStream1 == null) + throw new FileNotFoundException(String.format("can not find `%s`; maybe it moved?", PLOT_1_PATH)); + BufferedImage standardImage1 = ImageIO.read(standardImageStream1); + + Assertions.assertEquals(standardImage0.getWidth(), generatedImage0.getWidth()); + Assertions.assertEquals(standardImage0.getHeight(), generatedImage0.getHeight()); + Assertions.assertEquals(standardImage1.getWidth(), generatedImage1.getWidth()); + Assertions.assertEquals(standardImage1.getHeight(), generatedImage1.getHeight()); + + + for (int wPix = 0; wPix < generatedImage0.getWidth(); wPix++){ + for (int hPix = 0; hPix < generatedImage0.getHeight(); hPix++){ + Assertions.assertEquals(generatedImage0.getRGB(wPix, hPix), standardImage0.getRGB(wPix, hPix)); + } + } + + for (int wPix = 0; wPix < generatedImage1.getWidth(); wPix++){ + for (int hPix = 0; hPix < generatedImage1.getHeight(); hPix++){ + Assertions.assertEquals(generatedImage1.getRGB(wPix, hPix), standardImage1.getRGB(wPix, hPix)); + } + } + } +} diff --git a/vcell-core/src/test/resources/org/vcell/plotting/Parabolic.png b/vcell-cli/src/test/resources/org/vcell/cli/run/plotting/Parabolic.png similarity index 100% rename from vcell-core/src/test/resources/org/vcell/plotting/Parabolic.png rename to vcell-cli/src/test/resources/org/vcell/cli/run/plotting/Parabolic.png diff --git a/vcell-core/src/test/resources/org/vcell/plotting/plot2d_SimpleSimulation.csv b/vcell-cli/src/test/resources/org/vcell/cli/run/plotting/plot2d_SimpleSimulation.csv similarity index 100% rename from vcell-core/src/test/resources/org/vcell/plotting/plot2d_SimpleSimulation.csv rename to vcell-cli/src/test/resources/org/vcell/cli/run/plotting/plot2d_SimpleSimulation.csv diff --git a/vcell-core/pom.xml b/vcell-core/pom.xml index b00999072a..c543edcf0b 100644 --- a/vcell-core/pom.xml +++ b/vcell-core/pom.xml @@ -171,11 +171,6 @@
- - org.jfree - jfreechart - 1.5.5 - org.jgrapht jgrapht-core diff --git a/vcell-core/src/main/java/org/vcell/plotting/Results2DLinePlot.java b/vcell-core/src/main/java/org/vcell/plotting/Results2DLinePlot.java deleted file mode 100644 index 9db00a3cad..0000000000 --- a/vcell-core/src/main/java/org/vcell/plotting/Results2DLinePlot.java +++ /dev/null @@ -1,203 +0,0 @@ -package org.vcell.plotting; - -import cbit.vcell.publish.ITextWriter; -import cbit.vcell.publish.PDFWriter; - -import com.lowagie.text.DocumentException; - -import org.jfree.chart.ChartFactory; -import org.jfree.chart.JFreeChart; -import org.jfree.chart.labels.StandardXYItemLabelGenerator; -import org.jfree.chart.plot.XYPlot; -import org.jfree.data.xy.XYDataset; -import org.jfree.data.xy.XYSeries; -import org.jfree.data.xy.XYSeriesCollection; - -import org.vcell.util.Pair; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.awt.*; -import java.awt.image.BufferedImage; -import java.awt.print.PageFormat; -import java.awt.print.Paper; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.*; -import java.util.stream.Stream; - -/** - * Stores all relevant info to create a 2D plot, and lazily builds a PDF on request - */ -public class Results2DLinePlot implements ResultsLinePlot { - - public static final int AXIS_LABEL_FONT_SIZE = 15; - public static final int MAX_SERIES_DATA_POINT_LABELS = 10; - public static final int SERIES_DATA_POINT_LABEL_FONT_SIZE = 8; - - private static final Logger lg = LogManager.getLogger(Results2DLinePlot.class); - - private String plotTitle; - private Pair xData; - private final Map yDataSets; - - public Results2DLinePlot(){ - this(""); - } - - public Results2DLinePlot(String plotTitle){ - this(plotTitle, "", new double[0]); - } - - public Results2DLinePlot(String xLabel, Double[] xDataValues){ - this("", xLabel, xDataValues); - } - - public Results2DLinePlot(String xLabel, double[] xDataValues){ - this("", xLabel, xDataValues); - } - - public Results2DLinePlot(String plotTitle, String xLabel, Double[] xDataValues){ - this(plotTitle, xLabel, Stream.of(xDataValues).mapToDouble(Double::doubleValue).toArray()); - } - - public Results2DLinePlot(String plotTitle, String xLabel, double[] xDataValues){ - this.plotTitle = plotTitle; - this.xData = new Pair<>(xLabel, xDataValues); - this.yDataSets = new HashMap<>(); - } - - @Override - public void setTitle(String newTitle) { - this.plotTitle = newTitle; - } - - @Override - public String getTitle() { - return this.plotTitle; - } - - public void setXLabel(String newXLabel){ - this.xData = new Pair<>(newXLabel, this.xData.two); - } - - public void setXData(Double[] newXData){ - this.setXData(Stream.of(newXData).mapToDouble(Double::doubleValue).toArray()); - } - - public void setXData(double[] newXData){ - this.setXData(this.xData.one, newXData); - } - - public void setXData(String newXLabel, double[] newXData){ - if (this.xData.two.length != newXData.length){ - lg.warn("Changing xData to different length! YData will be purged!"); - this.yDataSets.clear(); - } - this.xData = new Pair<>(newXLabel, newXData); - } - - public String getXLabel(){ - return this.xData.one; - } - - public double[] getXDataValues(){ - return this.xData.two; - } - - public Pair getXData(){ - return this.xData; - } - - public void setYData(String yLabel, double[] newYData){ - if (this.xData.two.length != newYData.length){ - lg.error("Error adding dataset `{}`; see exception below", yLabel); - String exceptionMessage = String.format("Can not accept yDataSet: size (%d) does not map with domain size (%d)", newYData.length, this.xData.two.length); - throw new IllegalArgumentException(exceptionMessage); - } - this.yDataSets.put(yLabel, newYData); - } - - public int getNumYDataSets(){ - return this.yDataSets.size(); - } - - public double[] getYData(String yLabel){ - if (this.yDataSets.containsKey(yLabel)) return this.yDataSets.get(yLabel); - throw new IllegalArgumentException(String.format("`%s` is not a known dataset in this object", yLabel)); - } - - public Set getYDataSetLabels(){ - return this.yDataSets.keySet(); - } - - - @Override - public void generatePdf(String desiredFileName, File desiredParentDirectory) throws ChartCouldNotBeProducedException { - String yAxisLabel = ""; - XYDataset dataset2D = this.generateChartLibraryDataset(); - JFreeChart chart = ChartFactory.createXYLineChart("Test Sim Results", this.xData.one, yAxisLabel, dataset2D); - - // Tweak Chart so it looks better - chart.setBorderVisible(true); - chart.getPlot().setBackgroundPaint(Color.white); - XYPlot chartPlot = chart.getXYPlot(); - chartPlot.getDomainAxis().setLabelFont(new Font(this.xData.one, Font.PLAIN, Results2DLinePlot.AXIS_LABEL_FONT_SIZE)); - chartPlot.getRangeAxis().setLabelFont(new Font(yAxisLabel, Font.PLAIN, Results2DLinePlot.AXIS_LABEL_FONT_SIZE)); - if (this.xData.two.length <= Results2DLinePlot.MAX_SERIES_DATA_POINT_LABELS) { // if it's too crowded, having data point labels is bad - for (int i = 0; i < dataset2D.getSeriesCount(); i++) { - //DecimalFormat decimalformat1 = new DecimalFormat("##"); // Should we ever need it, this object is used in formating the labels. - chartPlot.getRenderer().setSeriesItemLabelGenerator(i, new StandardXYItemLabelGenerator("({1}, {2})")); - chartPlot.getRenderer().setSeriesItemLabelFont(i, new Font(null, Font.PLAIN, Results2DLinePlot.SERIES_DATA_POINT_LABEL_FONT_SIZE)); - chartPlot.getRenderer().setSeriesItemLabelsVisible(i, true); - } - } - - // Prepare for export - PDFWriter pdfWriter = new PDFWriter(); - PageFormat pageFormat = Results2DLinePlot.generateAlternatePageFormat(); - - File testfile = new File(desiredParentDirectory, desiredFileName); - try { - if (testfile.exists() && !testfile.isFile()) throw new IllegalArgumentException("desired PDF already exists, and is not a regular file"); - if (!testfile.exists() && !testfile.createNewFile()) throw new IllegalArgumentException("Unable to create desired PDF; creation itself failed."); - try (FileOutputStream fos = new FileOutputStream(testfile)) { - BufferedImage bfi = chart.createBufferedImage((int) pageFormat.getImageableWidth(), (int) pageFormat.getImageableHeight()); - pdfWriter.writePlotImageDocument("Test Document", fos, pageFormat, bfi); - } catch (DocumentException e) { - lg.error("Error while building PDF; see exception below"); - throw new ChartCouldNotBeProducedException(e); - } - } catch (IOException e){ - lg.error("Error while preparing PDF; see exception below"); - throw new ChartCouldNotBeProducedException(e); - } - } - - private XYDataset generateChartLibraryDataset(){ - XYSeriesCollection dataset2D = new XYSeriesCollection(); - for (String yLabel : this.yDataSets.keySet()){ - double[] yDataValues = this.yDataSets.get(yLabel); - XYSeries series = new XYSeries(yLabel, true, false); - for (int i = 0; i < this.xData.two.length; i++){ // our methods have guaranteed the sizes match! - series.add(this.xData.two[i], yDataValues[i]); - } - dataset2D.addSeries(series); - } - return dataset2D; - } - - private static PageFormat generateAlternatePageFormat(){ - java.awt.print.PageFormat pageFormat = java.awt.print.PrinterJob.getPrinterJob().defaultPage(); - Paper alternatePaper = new Paper(); // We want to try and increase the margins - double altOriginX = alternatePaper.getImageableX() / 2, altOriginY = alternatePaper.getImageableY() / 2; - double altWidth = alternatePaper.getWidth() - 2 * altOriginX, altHeight = alternatePaper.getHeight() - 2 * altOriginY; - alternatePaper.setImageableArea(altOriginX, altOriginY, altWidth, altHeight); - pageFormat.setPaper(alternatePaper); - pageFormat.setOrientation(PageFormat.LANDSCAPE); - return pageFormat; - } -} diff --git a/vcell-core/src/test/java/org/vcell/plotting/TestResults2DLinePlot.java b/vcell-core/src/test/java/org/vcell/plotting/TestResults2DLinePlot.java deleted file mode 100644 index 12403829de..0000000000 --- a/vcell-core/src/test/java/org/vcell/plotting/TestResults2DLinePlot.java +++ /dev/null @@ -1,128 +0,0 @@ -package org.vcell.plotting; - -import org.jfree.chart.ChartFactory; -import org.jfree.chart.JFreeChart; -import org.jfree.data.xy.XYDataItem; -import org.jfree.data.xy.XYDataset; -import org.jfree.data.xy.XYSeries; -import org.jfree.data.xy.XYSeriesCollection; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Assertions; -import org.vcell.util.Pair; - -import javax.imageio.ImageIO; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Stream; - -@Tag("Fast") -public class TestResults2DLinePlot { - private static List paraData = List.of( - new XYDataItem(0.0, 0.0), - new XYDataItem(0.5, 0.25), - new XYDataItem(1.0, 1.0), - new XYDataItem(1.5, 2.25), - new XYDataItem(2.0, 4.0), - new XYDataItem(2.5, 6.25), - new XYDataItem(3.0, 9.0), - new XYDataItem(3.5, 12.25), - new XYDataItem(4.0, 16.0), - new XYDataItem(4.5, 20.25), - new XYDataItem(5.0, 25.0), - new XYDataItem(5.5, 30.25) - ); - - private static Pair parabolicData = new Pair<>( - Stream.of(0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5).mapToDouble(Double::valueOf).toArray(), - Stream.of(0.0, 0.25, 1.0, 2.25, 4.0, 6.25, 9.0, 12.25, 16.0, 20.25, 25.0, 30.25).mapToDouble(Double::valueOf).toArray() - ); - - @Test - public void testConstructors(){ - double[] xValuesPrim = {0.0, 2.0, 4.0}; - Double[] xValuesWrap = {0.0, 2.0, 4.0}; - Results2DLinePlot[] testInstances = { - new Results2DLinePlot(), - new Results2DLinePlot("Title"), - new Results2DLinePlot("Label", xValuesWrap), - new Results2DLinePlot("Label", xValuesPrim), - new Results2DLinePlot("Title", "Label", xValuesWrap), - new Results2DLinePlot("Title", "Label", xValuesPrim), - }; - - for (Results2DLinePlot instance : testInstances){ - Assertions.assertTrue("".equals(instance.getTitle()) || "Title".equals(instance.getTitle())); - Assertions.assertTrue("".equals(instance.getXLabel()) || "Label".equals(instance.getXLabel())); - Assertions.assertTrue(instance.getXDataValues().length == 0 || instance.getXDataValues().length == xValuesPrim.length); - if (instance.getXDataValues().length == 3){ - Assertions.assertArrayEquals(xValuesPrim, instance.getXDataValues()); - } - } - } - - @Test - public void testSettingAndGetting(){ - double[] xValuesPrim = {0.0, 2.0, 4.0}; - Double[] xValuesWrap = {0.0, 2.0, 4.0}; - Results2DLinePlot[] testInstances = { - new Results2DLinePlot(), - new Results2DLinePlot("Title"), - new Results2DLinePlot("Label", xValuesWrap), - new Results2DLinePlot("Label", xValuesPrim), - new Results2DLinePlot("Title", "Label", xValuesWrap), - new Results2DLinePlot("Title", "Label", xValuesPrim), - }; - } - - @Test - public void pngRoundTripTest() throws IOException { - File dupe = File.createTempFile("VCellPNG::", ".png"); - XYSeries series = new XYSeries("key"); - for (int i = 0; i < TestResults2DLinePlot.parabolicData.one.length; i++){ - series.add(TestResults2DLinePlot.parabolicData.one[i], TestResults2DLinePlot.parabolicData.two[i]); - } - XYSeriesCollection dataset = new XYSeriesCollection(); - dataset.addSeries(series); - JFreeChart chart = ChartFactory.createXYLineChart("Test", "X-Axis","Y-Axis", dataset); - BufferedImage originalImage = chart.createBufferedImage(1000,1000); - ImageIO.write(originalImage, "png", dupe); - BufferedImage roundTrippedImage = ImageIO.read(dupe); - Assertions.assertEquals(originalImage.getWidth(), roundTrippedImage.getWidth()); - Assertions.assertEquals(originalImage.getHeight(), roundTrippedImage.getHeight()); - for (int wPix = 0; wPix < originalImage.getWidth(); wPix++){ - for (int hPix = 0; hPix < originalImage.getHeight(); hPix++){ - Assertions.assertEquals(originalImage.getRGB(wPix, hPix), roundTrippedImage.getRGB(wPix, hPix)); - } - } - } - - @Test - public void pngLibraryLevelTest() throws IOException { - String STANDARD_IMAGE_LOCAL_PATH = "Parabolic.png"; - InputStream standardImageStream = TestResults2DLinePlot.class.getResourceAsStream(STANDARD_IMAGE_LOCAL_PATH); - if (standardImageStream == null) - throw new FileNotFoundException(String.format("can not find `%s`; maybe it moved?", STANDARD_IMAGE_LOCAL_PATH)); - BufferedImage standardImage = ImageIO.read(standardImageStream); - XYSeries series = new XYSeries("key"); - for (int i = 0; i < TestResults2DLinePlot.parabolicData.one.length; i++){ - series.add(TestResults2DLinePlot.parabolicData.one[i], TestResults2DLinePlot.parabolicData.two[i]); - } - XYSeriesCollection dataset = new XYSeriesCollection(); - dataset.addSeries(series); - JFreeChart chart = ChartFactory.createXYLineChart("Test", "X-Axis","Y-Axis", dataset); - BufferedImage currentImage = chart.createBufferedImage(1000,1000); - Assertions.assertEquals(currentImage.getWidth(), standardImage.getWidth()); - Assertions.assertEquals(currentImage.getHeight(), standardImage.getHeight()); - for (int wPix = 0; wPix < currentImage.getWidth(); wPix++){ - for (int hPix = 0; hPix < currentImage.getHeight(); hPix++){ - Assertions.assertEquals(currentImage.getRGB(wPix, hPix), standardImage.getRGB(wPix, hPix)); - } - } - } -} From d9ff823c940cee72af482dc3ef51e8f3448fa42f Mon Sep 17 00:00:00 2001 From: Logan Drescher Date: Thu, 23 Jan 2025 12:59:07 -0500 Subject: [PATCH 10/17] Moving Files / Classes around --- .../vcell/cli/run/hdf5/Hdf5DataExtractor.java | 40 ++- .../run/hdf5/Hdf5SedmlResultsNonspatial.java | 7 +- .../cli/run/hdf5/Hdf5SedmlResultsSpatial.java | 2 +- .../run/hdf5/NonspatialResultsConverter.java | 312 ----------------- .../NonSpatialDataMapping.java} | 6 +- .../results/NonSpatialResultsConverter.java | 314 ++++++++++++++++++ .../run/results/NonSpatialValueHolder.java | 30 ++ .../ReorganizedSpatialResults.java | 5 +- .../SimpleDataGenCalculator.java | 2 +- .../{hdf5 => results}/SpatialDataMapping.java | 4 +- .../SpatialResultsConverter.java | 4 +- 11 files changed, 381 insertions(+), 345 deletions(-) delete mode 100644 vcell-cli/src/main/java/org/vcell/cli/run/hdf5/NonspatialResultsConverter.java rename vcell-cli/src/main/java/org/vcell/cli/run/{hdf5/NonspatialDataMapping.java => results/NonSpatialDataMapping.java} (94%) create mode 100644 vcell-cli/src/main/java/org/vcell/cli/run/results/NonSpatialResultsConverter.java create mode 100644 vcell-cli/src/main/java/org/vcell/cli/run/results/NonSpatialValueHolder.java rename vcell-cli/src/main/java/org/vcell/cli/run/{hdf5 => results}/ReorganizedSpatialResults.java (97%) rename vcell-cli/src/main/java/org/vcell/cli/run/{hdf5 => results}/SimpleDataGenCalculator.java (98%) rename vcell-cli/src/main/java/org/vcell/cli/run/{hdf5 => results}/SpatialDataMapping.java (95%) rename vcell-cli/src/main/java/org/vcell/cli/run/{hdf5 => results}/SpatialResultsConverter.java (99%) diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/hdf5/Hdf5DataExtractor.java b/vcell-cli/src/main/java/org/vcell/cli/run/hdf5/Hdf5DataExtractor.java index 1e4e3cebf2..5f1267472f 100644 --- a/vcell-cli/src/main/java/org/vcell/cli/run/hdf5/Hdf5DataExtractor.java +++ b/vcell-cli/src/main/java/org/vcell/cli/run/hdf5/Hdf5DataExtractor.java @@ -2,13 +2,16 @@ import cbit.vcell.solver.TempSimulation; +import org.jlibsedml.DataGenerator; import org.jlibsedml.Report; import org.jlibsedml.SedML; import org.jlibsedml.AbstractTask; -import org.vcell.sbml.vcell.SBMLNonspatialSimResults; +import org.vcell.cli.run.results.NonSpatialValueHolder; +import org.vcell.cli.run.results.NonSpatialResultsConverter; import org.vcell.cli.run.TaskJob; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.vcell.cli.run.results.SpatialResultsConverter; import java.io.File; import java.nio.file.Paths; @@ -21,9 +24,9 @@ * Factory class to create Hdf5DataWrappers from a sedml object and simulation data. */ public class Hdf5DataExtractor { - private SedML sedml; - private Map taskToSimulationMap; - private String sedmlLocation, sedmlRoot; + private final SedML sedml; + private final Map taskToSimulationMap; + private final String sedmlLocation; private final static Logger logger = LogManager.getLogger(Hdf5DataExtractor.class); @@ -36,30 +39,31 @@ public class Hdf5DataExtractor { public Hdf5DataExtractor(SedML sedml, Map taskToSimulationMap){ this.sedml = sedml; this.taskToSimulationMap = taskToSimulationMap; - this.sedmlRoot = Paths.get(sedml.getPathForURI()).toString(); - this.sedmlLocation = Paths.get(this.sedmlRoot, this.sedml.getFileName()).toString(); + this.sedmlLocation = Paths.get(sedml.getPathForURI(), sedml.getFileName()).toString(); } /** * - * @param nonSpatialResults the nonspatial results set of a sedml execution + * @param organizedNonSpatialResults the nonspatial results set of a sedml execution * @param spatialResults the spatial results set of a sedml execution * @return a wrapper for hdf5 relevant data - * @see NonspatialResultsConverter::convertNonspatialResultsToSedmlFormat - * @see SpatialResultsConverter::collectSpatialDatasets + * @see NonSpatialResultsConverter ::convertNonspatialResultsToSedmlFormat + * @see SpatialResultsConverter ::collectSpatialDatasets */ - public Hdf5DataContainer extractHdf5RelevantData(Map nonSpatialResults, Map spatialResults, boolean isBioSimMode) { + public Hdf5DataContainer extractHdf5RelevantData(Map organizedNonSpatialResults, Map spatialResults, boolean isBioSimMode) { Map> wrappers = new LinkedHashMap<>(); Hdf5DataContainer hdf5FileWrapper = new Hdf5DataContainer(isBioSimMode); Exception nonSpatialException = null, spatialException = null; - try { - Map> nonspatialWrappers = NonspatialResultsConverter.convertNonspatialResultsToSedmlFormat( - this.sedml, nonSpatialResults, this.taskToSimulationMap, this.sedmlLocation); - Hdf5DataExtractor.addWrappers(wrappers, nonspatialWrappers); - } catch (Exception e){ - logger.warn("Collection of nonspatial datasets failed for " + this.sedml.getFileName(), e); - nonSpatialException = e; + if (!organizedNonSpatialResults.isEmpty()){ + try { + Map> nonSpatialWrappers = NonSpatialResultsConverter.prepareNonSpatialDataForHdf5( + this.sedml, organizedNonSpatialResults, this.sedmlLocation); + Hdf5DataExtractor.addWrappers(wrappers, nonSpatialWrappers); + } catch (Exception e){ + logger.warn("Collection of non-spatial datasets failed for " + this.sedml.getFileName(), e); + nonSpatialException = e; + } } try { @@ -77,7 +81,7 @@ public Hdf5DataContainer extractHdf5RelevantData(Map dataMapping = new HashMap<>(); + public Map dataMapping = new HashMap<>(); } diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/hdf5/NonspatialResultsConverter.java b/vcell-cli/src/main/java/org/vcell/cli/run/hdf5/NonspatialResultsConverter.java deleted file mode 100644 index b335c82bfa..0000000000 --- a/vcell-cli/src/main/java/org/vcell/cli/run/hdf5/NonspatialResultsConverter.java +++ /dev/null @@ -1,312 +0,0 @@ -package org.vcell.cli.run.hdf5; - -import cbit.vcell.parser.Expression; -import cbit.vcell.parser.ExpressionException; -import cbit.vcell.solver.MathOverrides; -import cbit.vcell.solver.Simulation; -import cbit.vcell.solver.TempSimulation; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.jlibsedml.*; -import org.jlibsedml.execution.IXPathToVariableIDResolver; -import org.jlibsedml.modelsupport.SBMLSupport; -import org.vcell.cli.run.TaskJob; -import org.vcell.sbml.vcell.SBMLNonspatialSimResults; -import org.vcell.sedml.log.BiosimulationLog; - -import java.io.IOException; -import java.nio.file.Paths; -import java.util.*; -public class NonspatialResultsConverter { - private final static Logger logger = LogManager.getLogger(NonspatialResultsConverter.class); - - - public static Map> convertNonspatialResultsToSedmlFormat(SedML sedml, Map nonspatialResultsHash, Map taskToSimulationMap, String sedmlLocation) throws ExpressionException { - Map> results = new LinkedHashMap<>(); - - for (Report report : NonspatialResultsConverter.getReports(sedml.getOutputs())){ - Map dataSetValues = new LinkedHashMap<>(); - - // go through each entry (dataset) - for (DataSet dataset : report.getListOfDataSets()) { - List varIDs = new ArrayList<>(); - Map resultsByVariable = new HashMap<>(); - int maxLengthOfAllData = 0; // We have to pad up to this value - - // use the data reference to obtain the data generator - DataGenerator datagen = sedml.getDataGeneratorWithId(dataset.getDataReference()); assert datagen != null; - - // get the list of variables associated with the data reference - for (Variable var : datagen.getListOfVariables()) { - // for each variable we recover the task - AbstractTask topLevelTask = sedml.getTaskWithId(var.getReference()); - AbstractTask baseTask = NonspatialResultsConverter.getBaseTask(topLevelTask, sedml); // if !RepeatedTask, baseTask == topLevelTask - - // from the task we get the sbml model - org.jlibsedml.Simulation sedmlSim = sedml.getSimulation(baseTask.getSimulationReference()); - - if (!(sedmlSim instanceof UniformTimeCourse)){ - logger.error("only uniform time course simulations are supported"); - continue; - } - - // must get variable ID from SBML model - String vcellVarId = convertToVCellSymbol(var); - - // If the task isn't in our results hash, it's unwanted and skippable. - boolean bFoundTaskInNonspatial = nonspatialResultsHash.keySet().stream().anyMatch(taskJob -> taskJob.getTaskId().equals(topLevelTask.getId())); - if (!bFoundTaskInNonspatial){ - logger.warn("Was not able to find simulation data for task with ID: " + topLevelTask.getId()); - break; - } - - // ================================================================================== - - ArrayList taskJobs = new ArrayList<>(); - - for (Map.Entry entry : nonspatialResultsHash.entrySet()) { - TaskJob taskJob = entry.getKey(); - if (entry.getValue() != null && taskJob.getTaskId().equals(topLevelTask.getId())) { - taskJobs.add(taskJob); - if (!(topLevelTask instanceof RepeatedTask)) - break; // No need to keep looking if it's not a repeated task - } - } - - if (taskJobs.isEmpty()) continue; - - varIDs.add(var.getId()); - - // we want to keep the last outputNumberOfPoints only - int outputNumberOfPoints = ((UniformTimeCourse) sedmlSim).getNumberOfPoints(); - double outputStartTime = ((UniformTimeCourse) sedmlSim).getOutputStartTime(); - NonspatialValueHolder resultsHolder; - - for (TaskJob taskJob : taskJobs) { - SBMLNonspatialSimResults nonspatialResults = nonspatialResultsHash.get(taskJob); - double[] data = nonspatialResults.getDataForSBMLVar(vcellVarId, outputStartTime, outputNumberOfPoints); - - maxLengthOfAllData = Integer.max(maxLengthOfAllData, data.length); - if (topLevelTask instanceof RepeatedTask && resultsByVariable.containsKey(var)) { // double[] exists - resultsHolder = resultsByVariable.get(var); - } else { // this is the first double[] - resultsHolder = new NonspatialValueHolder(taskToSimulationMap.get(topLevelTask)); - } - resultsHolder.listOfResultSets.add(data); - resultsByVariable.put(var, resultsHolder); - } - } - - - if (resultsByVariable.isEmpty()) continue; - int numJobs, maxLengthOfData = 0; - String exampleReference = resultsByVariable.keySet().iterator().next().getReference(); - NonspatialValueHolder synthesizedResults = new NonspatialValueHolder(taskToSimulationMap.get(sedml.getTaskWithId(exampleReference))); - SimpleDataGenCalculator calc = new SimpleDataGenCalculator(datagen); - - // Get padding value - for (NonspatialValueHolder nvh : resultsByVariable.values()){ - for (double[] dataSet : nvh.listOfResultSets){ - if (dataSet.length > maxLengthOfData){ - maxLengthOfData = dataSet.length; - } - } - } - - // Determine the num of jobs - numJobs = resultsByVariable.values().iterator().next().getNumSets(); - - // Perform the math! - for (int jobNum = 0; jobNum < numJobs; jobNum++){ - double[] synthesizedDataset = new double[maxLengthOfData]; - for (int datumIndex = 0; datumIndex < synthesizedDataset.length; datumIndex++){ - - for (Variable var : resultsByVariable.keySet()){ - //if (processedDataSet == null) processedDataSet = new NonspatialValueHolder(sedml.getTaskWithId(var.getReference())); - if (jobNum >= resultsByVariable.get(var).getNumSets()) continue; - NonspatialValueHolder nonspatialValue = resultsByVariable.get(var); - double[] specficJobDataSet = nonspatialValue.listOfResultSets.get(jobNum); - double datum = datumIndex >= specficJobDataSet.length ? Double.NaN : specficJobDataSet[datumIndex]; - calc.setArgument(var.getId(), datum); - if (!synthesizedResults.vcSimulation.equals(nonspatialValue.vcSimulation)){ - logger.warn("Simulations differ across variables; need to fix data structures to accomodate?"); - } - } - synthesizedDataset[datumIndex] = calc.evaluateWithCurrentArguments(true); - } - synthesizedResults.listOfResultSets.add(synthesizedDataset); - //synthesizedResults.vcSimulation; - } - - dataSetValues.put(dataset, synthesizedResults); - BiosimulationLog.instance().updateDatasetStatusYml(sedmlLocation, report.getId(), dataset.getId(), BiosimulationLog.Status.SUCCEEDED); - } // end of current dataset processing - - if (dataSetValues.isEmpty()) { - logger.warn("We did not get any entries in the final data set. " + - "This may mean a problem has been encountered."); - continue; - } - List shapes = new LinkedList<>(); - Hdf5SedmlResultsNonspatial dataSourceNonspatial = new Hdf5SedmlResultsNonspatial(); - Hdf5SedmlResults hdf5DatasetWrapper = new Hdf5SedmlResults(); - - if (dataSetValues.entrySet().iterator().next().getValue().isEmpty()) continue; // Check if we have data to work with. - - hdf5DatasetWrapper.datasetMetadata._type = NonspatialResultsConverter.getKind(report.getId()); - hdf5DatasetWrapper.datasetMetadata.sedmlId = NonspatialResultsConverter.removeVCellPrefixes(report.getId(), report.getId()); - hdf5DatasetWrapper.datasetMetadata.sedmlName = report.getName(); - hdf5DatasetWrapper.datasetMetadata.uri = Paths.get(sedmlLocation, report.getId()).toString(); - - for (DataSet dataSet : dataSetValues.keySet()){ - NonspatialValueHolder dataSetValuesSource = dataSetValues.get(dataSet); - - dataSourceNonspatial.dataItems.put(report, dataSet, new LinkedList<>()); - dataSourceNonspatial.scanBounds = dataSetValuesSource.vcSimulation.getMathOverrides().getScanBounds(); - dataSourceNonspatial.scanParameterNames = dataSetValuesSource.vcSimulation.getMathOverrides().getScannedConstantNames(); - double[][] scanValues = new double[dataSourceNonspatial.scanBounds.length][]; - for (int nameIndex = 0; nameIndex < dataSourceNonspatial.scanBounds.length; nameIndex++){ - String nameKey = dataSourceNonspatial.scanParameterNames[nameIndex]; - scanValues[nameIndex] = new double[dataSourceNonspatial.scanBounds[nameIndex] + 1]; - for (int scanIndex = 0; scanIndex < dataSourceNonspatial.scanBounds[nameIndex] + 1; scanIndex++){ - Expression overrideExp = dataSetValuesSource.vcSimulation.getMathOverrides().getActualExpression(nameKey, new MathOverrides.ScanIndex(scanIndex)); - try { scanValues[nameIndex][scanIndex] = overrideExp.evaluateConstant(); } - catch (ExpressionException e){ throw new RuntimeException(e); } - } - } - dataSourceNonspatial.scanParameterValues = scanValues; - - for (double[] data : dataSetValuesSource.listOfResultSets) { - dataSourceNonspatial.dataItems.get(report, dataSet).add(data); - shapes.add(Integer.toString(data.length)); - } - - hdf5DatasetWrapper.dataSource = dataSourceNonspatial; // Using upcasting - hdf5DatasetWrapper.datasetMetadata.sedmlDataSetDataTypes.add("float64"); - hdf5DatasetWrapper.datasetMetadata.sedmlDataSetIds.add( - NonspatialResultsConverter.removeVCellPrefixes(dataSet.getId(), hdf5DatasetWrapper.datasetMetadata.sedmlId)); - hdf5DatasetWrapper.datasetMetadata.sedmlDataSetLabels.add(dataSet.getLabel()); - hdf5DatasetWrapper.datasetMetadata.sedmlDataSetNames.add(dataSet.getName()); - hdf5DatasetWrapper.datasetMetadata.sedmlDataSetShapes = shapes; - } - if (!results.containsKey(report)) results.put(report, new LinkedList<>()); - results.get(report).add(hdf5DatasetWrapper); - } // outputs/reports - return results; - } - - private static List getReports(List outputs){ - List reports = new LinkedList<>(); - for (Output out : outputs) { - if (out instanceof Report){ - reports.add((Report)out); - } else { - if (logger.isDebugEnabled()) - logger.info("Ignoring unsupported output `{}` while CSV generation.", out.getId()); - } - } - return reports; - } - - private static AbstractTask getBaseTask(AbstractTask task, SedML sedml){ - while (task instanceof RepeatedTask) { // We need to find the original task burried beneath. - // We assume that we can never have a sequential repeated task at this point, we check for that in SEDMLImporter - SubTask st = ((RepeatedTask)task).getSubTasks().entrySet().iterator().next().getValue(); // single subtask - task = sedml.getTaskWithId(st.getTaskId()); - } - return task; - } - - private static String convertToVCellSymbol(Variable var){ - // must get variable ID from SBML model - String sbmlVarId = ""; - if (var.getSymbol() != null) { // it is a predefined symbol - // search the sbml model to find the vcell variable name associated with the run - switch(var.getSymbol().name()){ - case "TIME": { // TIME is t, etc - sbmlVarId = "t"; // this is VCell reserved symbold for time - break; - } - default:{ - sbmlVarId = var.getSymbol().name(); - } - // etc, TODO: check spec for other symbols (CSymbols?) - // Delay? Avogadro? rateOf? - } - } else { // it is an XPATH target in model - String target = var.getTarget(); - IXPathToVariableIDResolver resolver = new SBMLSupport(); - sbmlVarId = resolver.getIdFromXPathIdentifer(target); - } - return sbmlVarId; - } - - private static class NonspatialValueHolder { - List listOfResultSets = new ArrayList<>(); - final Simulation vcSimulation; - - public NonspatialValueHolder(Simulation simulation) { - this.vcSimulation = simulation; - } - - public int getNumSets() { - return listOfResultSets.size(); - } - - public boolean isEmpty(){ - return listOfResultSets.size() == 0 ? true : false; - } - - /*public int[] getJobCoordinate(int index){ - String[] names = vcSimulation.getMathOverrides().getScannedConstantNames(); - java.util.Arrays.sort(names); // must do things in a consistent way - int[] bounds = new int[names.length]; // bounds of scanning matrix - for (int i = 0; i < names.length; i++){ - bounds[i] = vcSimulation.getMathOverrides().getConstantArraySpec(names[i]).getNumValues() - 1; - } - int[] coordinates = BeanUtils.indexToCoordinate(index, bounds); - return coordinates; - }*/ - } - - - private static String getKind(String prefixedSedmlId){ - String plotPrefix = "__plot__"; - if (prefixedSedmlId.startsWith(plotPrefix)) - return "SedPlot2D"; - return "SedReport"; - } - - /** - * We need the sedmlId to help remove prefixes, but the sedmlId itself may need to be fixed. - * - * If a sedmlId is being checked, just provide itself twice - * - * The reason for this, is having an overload with just "(String s)" as a requirment is misleading. - */ - private static String removeVCellPrefixes(String s, String sedmlId){ - String plotPrefix = "__plot__"; - String reservedPrefix = "__vcell_reserved_data_set_prefix__"; - - String checkedId = sedmlId.startsWith(plotPrefix) ? sedmlId.replace(plotPrefix, "") : sedmlId; - if (sedmlId.equals(s)) return checkedId; - - if (s.startsWith(plotPrefix)){ - s = s.replace(plotPrefix, ""); - } - - if (s.startsWith(reservedPrefix)){ - s = s.replace(reservedPrefix, ""); - } - - if (s.startsWith(checkedId + "_")){ - s = s.replace(checkedId + "_", ""); - } - - if (s.startsWith(checkedId)){ - s = s.replace(checkedId, ""); - } - - return s; - } -} diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/hdf5/NonspatialDataMapping.java b/vcell-cli/src/main/java/org/vcell/cli/run/results/NonSpatialDataMapping.java similarity index 94% rename from vcell-cli/src/main/java/org/vcell/cli/run/hdf5/NonspatialDataMapping.java rename to vcell-cli/src/main/java/org/vcell/cli/run/results/NonSpatialDataMapping.java index 4c774ae643..4ef34b01f9 100644 --- a/vcell-cli/src/main/java/org/vcell/cli/run/hdf5/NonspatialDataMapping.java +++ b/vcell-cli/src/main/java/org/vcell/cli/run/results/NonSpatialDataMapping.java @@ -1,4 +1,4 @@ -package org.vcell.cli.run.hdf5; +package org.vcell.cli.run.results; import org.jlibsedml.DataSet; import org.jlibsedml.Report; @@ -7,10 +7,10 @@ import java.util.List; import java.util.Map; -public class NonspatialDataMapping { +public class NonSpatialDataMapping { private final Map>> dataMapping; - public NonspatialDataMapping(){ + public NonSpatialDataMapping(){ this.dataMapping = new LinkedHashMap<>(); } diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/results/NonSpatialResultsConverter.java b/vcell-cli/src/main/java/org/vcell/cli/run/results/NonSpatialResultsConverter.java new file mode 100644 index 0000000000..79e8356a59 --- /dev/null +++ b/vcell-cli/src/main/java/org/vcell/cli/run/results/NonSpatialResultsConverter.java @@ -0,0 +1,314 @@ +package org.vcell.cli.run.results; + +import cbit.vcell.parser.Expression; +import cbit.vcell.parser.ExpressionException; +import cbit.vcell.solver.MathOverrides; +import cbit.vcell.solver.TempSimulation; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jlibsedml.*; +import org.jlibsedml.execution.IXPathToVariableIDResolver; +import org.jlibsedml.modelsupport.SBMLSupport; +import org.vcell.cli.run.TaskJob; +import org.vcell.cli.run.hdf5.Hdf5SedmlResults; +import org.vcell.cli.run.hdf5.Hdf5SedmlResultsNonspatial; +import org.vcell.sbml.vcell.SBMLNonspatialSimResults; +import org.vcell.sedml.log.BiosimulationLog; + +import java.nio.file.Paths; +import java.util.*; +public class NonSpatialResultsConverter { + private final static Logger logger = LogManager.getLogger(NonSpatialResultsConverter.class); + + public static Map organizeNonSpatialResultsBySedmlDataGenerator(SedML sedml, Map nonSpatialResultsHash, Map taskToSimulationMap) throws ExpressionException { + Map nonSpatialOrganizedResultsMap = new HashMap<>(); + if (nonSpatialResultsHash.isEmpty()) return nonSpatialOrganizedResultsMap; + + for (Output output : NonSpatialResultsConverter.getValidOutputs(sedml)){ + List dataGeneratorsToProcess; + if (output instanceof Report report){ + dataGeneratorsToProcess = new ArrayList<>(); + for (DataSet dataSet : report.getListOfDataSets()){ + // use the data reference to obtain the data generator + dataGeneratorsToProcess.add(sedml.getDataGeneratorWithId(dataSet.getDataReference())); + BiosimulationLog.instance().updateDatasetStatusYml(Paths.get(sedml.getPathForURI(), sedml.getFileName()).toString(), output.getId(), dataSet.getId(), BiosimulationLog.Status.SUCCEEDED); + } + } + else if (output instanceof Plot2D plot2D){ + Set uniqueDataGens = new LinkedHashSet<>(); + for (Curve curve : plot2D.getListOfCurves()){ + uniqueDataGens.add(sedml.getDataGeneratorWithId(curve.getXDataReference())); + uniqueDataGens.add(sedml.getDataGeneratorWithId(curve.getYDataReference())); + } + dataGeneratorsToProcess = uniqueDataGens.stream().toList(); + } else { + if (logger.isDebugEnabled()) logger.warn("Unrecognized output type: `{}` (id={})", output.getClass().getName(), output.getId()); + continue; + } + + for (DataGenerator dataGen : dataGeneratorsToProcess) { + NonSpatialValueHolder valueHolder = NonSpatialResultsConverter.getNonSpatialValueHolderForDataGenerator(sedml, dataGen, nonSpatialResultsHash, taskToSimulationMap); + if (valueHolder == null) continue; + nonSpatialOrganizedResultsMap.put(dataGen, valueHolder); + } + } + + return nonSpatialOrganizedResultsMap; + } + + + public static Map> prepareNonSpatialDataForHdf5(SedML sedml, Map organizedNonSpatialResults, String sedmlLocation) { + Map> results = new LinkedHashMap<>(); + + // Just get the Reports from the outputs, and nothing else + for (Report report : sedml.getOutputs().stream().filter(Report.class::isInstance).map(Report.class::cast).toList()){ + Map dataSetValues = new LinkedHashMap<>(); + + for (DataSet dataset : report.getListOfDataSets()) { + int maxLengthOfAllData = 0; // We have to pad up to this value + + // use the data reference to obtain the data generator + DataGenerator dataGen = sedml.getDataGeneratorWithId(dataset.getDataReference()); + if (dataGen == null || !organizedNonSpatialResults.containsKey(dataGen)) + throw new RuntimeException("No data for Data Generator `" + dataset.getDataReference() + "` can be found!"); + NonSpatialValueHolder value = organizedNonSpatialResults.get(dataGen); + dataSetValues.put(dataset, value); + } // end of current dataset processing + + if (dataSetValues.isEmpty()) { + logger.warn("We did not get any entries in the final data set. This may mean a problem has been encountered."); + continue; + } + + List shapes = new LinkedList<>(); + Hdf5SedmlResultsNonspatial dataSourceNonSpatial = new Hdf5SedmlResultsNonspatial(); + Hdf5SedmlResults hdf5DatasetWrapper = new Hdf5SedmlResults(); + + if (dataSetValues.entrySet().iterator().next().getValue().isEmpty()) continue; // Check if we have data to work with. + + hdf5DatasetWrapper.datasetMetadata._type = NonSpatialResultsConverter.getKind(report.getId()); + hdf5DatasetWrapper.datasetMetadata.sedmlId = NonSpatialResultsConverter.removeVCellPrefixes(report.getId(), report.getId()); + hdf5DatasetWrapper.datasetMetadata.sedmlName = report.getName(); + hdf5DatasetWrapper.datasetMetadata.uri = Paths.get(sedmlLocation, report.getId()).toString(); + + for (DataSet dataSet : dataSetValues.keySet()){ + NonSpatialValueHolder dataSetValuesSource = dataSetValues.get(dataSet); + + dataSourceNonSpatial.dataItems.put(report, dataSet, new LinkedList<>()); + dataSourceNonSpatial.scanBounds = dataSetValuesSource.vcSimulation.getMathOverrides().getScanBounds(); + dataSourceNonSpatial.scanParameterNames = dataSetValuesSource.vcSimulation.getMathOverrides().getScannedConstantNames(); + double[][] scanValues = new double[dataSourceNonSpatial.scanBounds.length][]; + for (int nameIndex = 0; nameIndex < dataSourceNonSpatial.scanBounds.length; nameIndex++){ + String nameKey = dataSourceNonSpatial.scanParameterNames[nameIndex]; + scanValues[nameIndex] = new double[dataSourceNonSpatial.scanBounds[nameIndex] + 1]; + for (int scanIndex = 0; scanIndex < dataSourceNonSpatial.scanBounds[nameIndex] + 1; scanIndex++){ + Expression overrideExp = dataSetValuesSource.vcSimulation.getMathOverrides().getActualExpression(nameKey, new MathOverrides.ScanIndex(scanIndex)); + try { scanValues[nameIndex][scanIndex] = overrideExp.evaluateConstant(); } + catch (ExpressionException e){ throw new RuntimeException(e); } + } + } + dataSourceNonSpatial.scanParameterValues = scanValues; + + for (double[] data : dataSetValuesSource.listOfResultSets) { + dataSourceNonSpatial.dataItems.get(report, dataSet).add(data); + shapes.add(Integer.toString(data.length)); + } + + hdf5DatasetWrapper.dataSource = dataSourceNonSpatial; // Using upcasting + hdf5DatasetWrapper.datasetMetadata.sedmlDataSetDataTypes.add("float64"); + hdf5DatasetWrapper.datasetMetadata.sedmlDataSetIds.add( + NonSpatialResultsConverter.removeVCellPrefixes(dataSet.getId(), hdf5DatasetWrapper.datasetMetadata.sedmlId)); + hdf5DatasetWrapper.datasetMetadata.sedmlDataSetLabels.add(dataSet.getLabel()); + hdf5DatasetWrapper.datasetMetadata.sedmlDataSetNames.add(dataSet.getName()); + hdf5DatasetWrapper.datasetMetadata.sedmlDataSetShapes = shapes; + } + if (!results.containsKey(report)) results.put(report, new LinkedList<>()); + results.get(report).add(hdf5DatasetWrapper); + } // outputs/reports + return results; + } + + private static NonSpatialValueHolder getNonSpatialValueHolderForDataGenerator(SedML sedml, DataGenerator dataGen, Map nonSpatialResultsHash, Map taskToSimulationMap) throws ExpressionException { + if (dataGen == null) throw new IllegalArgumentException("Provided Data Generator can not be null!"); + Map resultsByVariable = new HashMap<>(); + + // get the list of variables associated with the data reference + for (Variable var : dataGen.getListOfVariables()) { + // for each variable we recover the task + AbstractTask topLevelTask = sedml.getTaskWithId(var.getReference()); + AbstractTask baseTask = NonSpatialResultsConverter.getBaseTask(topLevelTask, sedml); // if !RepeatedTask, baseTask == topLevelTask + + // from the task we get the sbml model + org.jlibsedml.Simulation sedmlSim = sedml.getSimulation(baseTask.getSimulationReference()); + + if (!(sedmlSim instanceof UniformTimeCourse utcSim)){ + logger.error("only uniform time course simulations are supported"); + continue; + } + + // must get variable ID from SBML model + String vcellVarId = convertToVCellSymbol(var); + + // If the task isn't in our results hash, it's unwanted and skippable. + boolean bFoundTaskInNonspatial = nonSpatialResultsHash.keySet().stream().anyMatch(taskJob -> taskJob.getTaskId().equals(topLevelTask.getId())); + if (!bFoundTaskInNonspatial){ + logger.warn("Was not able to find simulation data for task with ID: {}", topLevelTask.getId()); + break; + } + + // ================================================================================== + + ArrayList taskJobs = new ArrayList<>(); + + // We can have multiple TaskJobs sharing IDs (in the case of repeated tasks), so we need to get them all + for (TaskJob taskJob : nonSpatialResultsHash.keySet()) { + SBMLNonspatialSimResults simResults = nonSpatialResultsHash.get(taskJob); + if (simResults == null || !taskJob.getTaskId().equals(topLevelTask.getId())) continue; + taskJobs.add(taskJob); + if (!(topLevelTask instanceof RepeatedTask)) break; // No need to keep looking if it's not a repeated task, one "loop" is good + } + + if (taskJobs.isEmpty()) continue; + + boolean resultsAlreadyExist = topLevelTask instanceof RepeatedTask && resultsByVariable.containsKey(var); + NonSpatialValueHolder individualVarResultsHolder = resultsAlreadyExist ? resultsByVariable.get(var) : + new NonSpatialValueHolder(taskToSimulationMap.get(topLevelTask)); + for (TaskJob taskJob : taskJobs) { + // Leaving intermediate variables for debugging access + SBMLNonspatialSimResults nonSpatialResults = nonSpatialResultsHash.get(taskJob); + double[] data = nonSpatialResults.getDataForSBMLVar(vcellVarId, utcSim.getOutputStartTime(), utcSim.getNumberOfPoints()); + individualVarResultsHolder.listOfResultSets.add(data); + } + resultsByVariable.put(var, individualVarResultsHolder); + } + if (resultsByVariable.isEmpty()) return null; + + // We now need to condense the multiple variables into a single resolved value + + String exampleReference = resultsByVariable.keySet().iterator().next().getReference(); + int numJobs = resultsByVariable.values().iterator().next().listOfResultSets.size(); + NonSpatialValueHolder synthesizedResults = new NonSpatialValueHolder(taskToSimulationMap.get(sedml.getTaskWithId(exampleReference))); + SimpleDataGenCalculator calc = new SimpleDataGenCalculator(dataGen); + + // Get padding value + int maxLengthOfData = 0; + for (NonSpatialValueHolder nvh : resultsByVariable.values()){ + for (double[] dataSet : nvh.listOfResultSets){ + if (dataSet.length <= maxLengthOfData) continue; + maxLengthOfData = dataSet.length; + } + } + + // Perform the math! + for (int jobNum = 0; jobNum < numJobs; jobNum++){ + double[] synthesizedDataset = new double[maxLengthOfData]; + for (int datumIndex = 0; datumIndex < synthesizedDataset.length; datumIndex++){ + + for (Variable var : resultsByVariable.keySet()){ + //if (processedDataSet == null) processedDataSet = new NonspatialValueHolder(sedml.getTaskWithId(var.getReference())); + if (jobNum >= resultsByVariable.get(var).listOfResultSets.size()) continue; + NonSpatialValueHolder nonspatialValue = resultsByVariable.get(var); + double[] specficJobDataSet = nonspatialValue.listOfResultSets.get(jobNum); + double datum = datumIndex >= specficJobDataSet.length ? Double.NaN : specficJobDataSet[datumIndex]; + calc.setArgument(var.getId(), datum); + if (!synthesizedResults.vcSimulation.equals(nonspatialValue.vcSimulation)){ + logger.warn("Simulations differ across variables; need to fix data structures to accomodate?"); + } + } + synthesizedDataset[datumIndex] = calc.evaluateWithCurrentArguments(true); + } + synthesizedResults.listOfResultSets.add(synthesizedDataset); + } + return synthesizedResults; + } + + private static List getValidOutputs(SedML sedml){ + List nonPlot3DOutputs = new ArrayList<>(); + List plot3DOutputs = new ArrayList<>(); + for (Output output : sedml.getOutputs()){ + if (output instanceof Plot3D plot3D) plot3DOutputs.add(plot3D); + else nonPlot3DOutputs.add(output); + } + + if (!plot3DOutputs.isEmpty()) logger.warn("VCell currently does not support creation of 3D plots, {} plot{} will be skipped.", + plot3DOutputs.size(), plot3DOutputs.size() == 1 ? "" : "s"); + return nonPlot3DOutputs; + + } + + private static AbstractTask getBaseTask(AbstractTask task, SedML sedml){ + if (task == null) throw new IllegalArgumentException("task arguement is `null`!"); + while (task instanceof RepeatedTask repeatedTask) { // We need to find the original task burried beneath. + // We assume that we can never have a sequential repeated task at this point, we check for that in SEDMLImporter + SubTask st = repeatedTask.getSubTasks().entrySet().iterator().next().getValue(); // single subtask + task = sedml.getTaskWithId(st.getTaskId()); + if (task == null) throw new IllegalArgumentException("Bad SedML formatting; task with id " + st.getTaskId() +" not found"); + } + return task; + } + + private static String convertToVCellSymbol(Variable var){ + // must get variable ID from SBML model + String sbmlVarId = ""; + if (var.getSymbol() != null) { // it is a predefined symbol + // search the sbml model to find the vcell variable name associated with the run + switch(var.getSymbol().name()){ + case "TIME": { // TIME is t, etc + sbmlVarId = "t"; // this is VCell reserved symbold for time + break; + } + default:{ + sbmlVarId = var.getSymbol().name(); + } + // etc, TODO: check spec for other symbols (CSymbols?) + // Delay? Avogadro? rateOf? + } + } else { // it is an XPATH target in model + String target = var.getTarget(); + IXPathToVariableIDResolver resolver = new SBMLSupport(); + sbmlVarId = resolver.getIdFromXPathIdentifer(target); + } + return sbmlVarId; + } + + + private static String getKind(String prefixedSedmlId){ + String plotPrefix = "__plot__"; + if (prefixedSedmlId.startsWith(plotPrefix)) + return "SedPlot2D"; + return "SedReport"; + } + + /** + * We need the sedmlId to help remove prefixes, but the sedmlId itself may need to be fixed. + * + * If a sedmlId is being checked, just provide itself twice + * + * The reason for this, is having an overload with just "(String s)" as a requirment is misleading. + */ + private static String removeVCellPrefixes(String s, String sedmlId){ + String plotPrefix = "__plot__"; + String reservedPrefix = "__vcell_reserved_data_set_prefix__"; + + String checkedId = sedmlId.startsWith(plotPrefix) ? sedmlId.replace(plotPrefix, "") : sedmlId; + if (sedmlId.equals(s)) return checkedId; + + if (s.startsWith(plotPrefix)){ + s = s.replace(plotPrefix, ""); + } + + if (s.startsWith(reservedPrefix)){ + s = s.replace(reservedPrefix, ""); + } + + if (s.startsWith(checkedId + "_")){ + s = s.replace(checkedId + "_", ""); + } + + if (s.startsWith(checkedId)){ + s = s.replace(checkedId, ""); + } + + return s; + } +} diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/results/NonSpatialValueHolder.java b/vcell-cli/src/main/java/org/vcell/cli/run/results/NonSpatialValueHolder.java new file mode 100644 index 0000000000..acf869c639 --- /dev/null +++ b/vcell-cli/src/main/java/org/vcell/cli/run/results/NonSpatialValueHolder.java @@ -0,0 +1,30 @@ +package org.vcell.cli.run.results; + +import cbit.vcell.solver.Simulation; + +import java.util.ArrayList; +import java.util.List; + +public class NonSpatialValueHolder { + public final List listOfResultSets = new ArrayList<>(); + final Simulation vcSimulation; + + public NonSpatialValueHolder(Simulation simulation) { + this.vcSimulation = simulation; + } + + public boolean isEmpty() { + return listOfResultSets.isEmpty(); + } + + /*public int[] getJobCoordinate(int index){ + String[] names = vcSimulation.getMathOverrides().getScannedConstantNames(); + java.util.Arrays.sort(names); // must do things in a consistent way + int[] bounds = new int[names.length]; // bounds of scanning matrix + for (int i = 0; i < names.length; i++){ + bounds[i] = vcSimulation.getMathOverrides().getConstantArraySpec(names[i]).getNumValues() - 1; + } + int[] coordinates = BeanUtils.indexToCoordinate(index, bounds); + return coordinates; + }*/ +} diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/hdf5/ReorganizedSpatialResults.java b/vcell-cli/src/main/java/org/vcell/cli/run/results/ReorganizedSpatialResults.java similarity index 97% rename from vcell-cli/src/main/java/org/vcell/cli/run/hdf5/ReorganizedSpatialResults.java rename to vcell-cli/src/main/java/org/vcell/cli/run/results/ReorganizedSpatialResults.java index 715d30c2d1..a5a76412a0 100644 --- a/vcell-cli/src/main/java/org/vcell/cli/run/hdf5/ReorganizedSpatialResults.java +++ b/vcell-cli/src/main/java/org/vcell/cli/run/results/ReorganizedSpatialResults.java @@ -1,4 +1,4 @@ -package org.vcell.cli.run.hdf5; +package org.vcell.cli.run.results; import cbit.vcell.solver.TempSimulation; import io.jhdf.api.Dataset; @@ -8,6 +8,9 @@ import org.apache.logging.log4j.Logger; import org.jlibsedml.AbstractTask; import org.vcell.cli.run.TaskJob; +import org.vcell.cli.run.hdf5.Hdf5DataSourceSpatialSimMetadata; +import org.vcell.cli.run.hdf5.Hdf5DataSourceSpatialSimVars; +import org.vcell.cli.run.hdf5.Hdf5DataSourceSpatialVarDataLocation; import java.io.File; import java.nio.file.Paths; diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/hdf5/SimpleDataGenCalculator.java b/vcell-cli/src/main/java/org/vcell/cli/run/results/SimpleDataGenCalculator.java similarity index 98% rename from vcell-cli/src/main/java/org/vcell/cli/run/hdf5/SimpleDataGenCalculator.java rename to vcell-cli/src/main/java/org/vcell/cli/run/results/SimpleDataGenCalculator.java index 22b18c7c84..1b1379a8b4 100644 --- a/vcell-cli/src/main/java/org/vcell/cli/run/hdf5/SimpleDataGenCalculator.java +++ b/vcell-cli/src/main/java/org/vcell/cli/run/results/SimpleDataGenCalculator.java @@ -1,4 +1,4 @@ -package org.vcell.cli.run.hdf5; +package org.vcell.cli.run.results; import cbit.vcell.parser.DivideByZeroException; import cbit.vcell.parser.Expression; diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/hdf5/SpatialDataMapping.java b/vcell-cli/src/main/java/org/vcell/cli/run/results/SpatialDataMapping.java similarity index 95% rename from vcell-cli/src/main/java/org/vcell/cli/run/hdf5/SpatialDataMapping.java rename to vcell-cli/src/main/java/org/vcell/cli/run/results/SpatialDataMapping.java index 535b40316f..172fd61ee4 100644 --- a/vcell-cli/src/main/java/org/vcell/cli/run/hdf5/SpatialDataMapping.java +++ b/vcell-cli/src/main/java/org/vcell/cli/run/results/SpatialDataMapping.java @@ -1,8 +1,8 @@ -package org.vcell.cli.run.hdf5; +package org.vcell.cli.run.results; import org.jlibsedml.DataSet; import org.jlibsedml.Report; -import org.jlibsedml.Variable; +import org.vcell.cli.run.hdf5.Hdf5DataSourceSpatialVarDataItem; import java.util.LinkedHashMap; import java.util.Map; diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/hdf5/SpatialResultsConverter.java b/vcell-cli/src/main/java/org/vcell/cli/run/results/SpatialResultsConverter.java similarity index 99% rename from vcell-cli/src/main/java/org/vcell/cli/run/hdf5/SpatialResultsConverter.java rename to vcell-cli/src/main/java/org/vcell/cli/run/results/SpatialResultsConverter.java index aab2a102e8..9dcd3f71e6 100644 --- a/vcell-cli/src/main/java/org/vcell/cli/run/hdf5/SpatialResultsConverter.java +++ b/vcell-cli/src/main/java/org/vcell/cli/run/results/SpatialResultsConverter.java @@ -1,4 +1,4 @@ -package org.vcell.cli.run.hdf5; +package org.vcell.cli.run.results; import cbit.vcell.parser.Expression; @@ -11,10 +11,10 @@ import org.jlibsedml.execution.IXPathToVariableIDResolver; import org.jlibsedml.modelsupport.SBMLSupport; import org.vcell.cli.run.TaskJob; +import org.vcell.cli.run.hdf5.*; import org.vcell.sedml.log.BiosimulationLog; import java.io.File; -import java.io.IOException; import java.nio.file.Paths; import java.util.*; import java.util.stream.Collectors; From c69ec60e90c784bf71bbee205962757d87fae31f Mon Sep 17 00:00:00 2001 From: Logan Drescher Date: Thu, 23 Jan 2025 12:59:22 -0500 Subject: [PATCH 11/17] Integrated Plots into CLI --- .../main/java/org/vcell/cli/run/SedmlJob.java | 35 ++++++-- .../run/plotting/PlottingDataExtractor.java | 90 +++++++++++++++++++ 2 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 vcell-cli/src/main/java/org/vcell/cli/run/plotting/PlottingDataExtractor.java diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/SedmlJob.java b/vcell-cli/src/main/java/org/vcell/cli/run/SedmlJob.java index 127fc9822b..bf1833ce74 100644 --- a/vcell-cli/src/main/java/org/vcell/cli/run/SedmlJob.java +++ b/vcell-cli/src/main/java/org/vcell/cli/run/SedmlJob.java @@ -13,6 +13,11 @@ import org.vcell.cli.run.hdf5.HDF5ExecutionResults; import org.vcell.cli.run.hdf5.Hdf5DataContainer; import org.vcell.cli.run.hdf5.Hdf5DataExtractor; +import org.vcell.cli.run.results.NonSpatialValueHolder; +import org.vcell.cli.run.results.NonSpatialResultsConverter; +import org.vcell.cli.run.plotting.PlottingDataExtractor; +import org.vcell.cli.run.plotting.ChartCouldNotBeProducedException; +import org.vcell.cli.run.plotting.Results2DLinePlot; import org.vcell.sedml.log.BiosimulationLog; import org.vcell.trace.Span; import org.vcell.trace.Tracer; @@ -27,6 +32,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; +import java.util.Map; /** * Class that deals with the processing quest of a sedml file. @@ -252,15 +258,20 @@ private void processOutputs(SolverHandler solverHandler, HDF5ExecutionResults ma logger.info("Failed to execute one or more tasks in " + this.SEDML_NAME); } + this.logDocumentMessage += "Generating outputs... "; logger.info("Generating outputs... "); + ///////////////////////////////////////////////////// + Map organizedNonSpatialResults = + NonSpatialResultsConverter.organizeNonSpatialResultsBySedmlDataGenerator( + this.sedml, solverHandler.nonSpatialResults, solverHandler.taskToTempSimulationMap); if (!solverHandler.nonSpatialResults.isEmpty()) { this.generateCSV(solverHandler); - this.generatePlots(); + this.generatePlots(organizedNonSpatialResults); } - this.generateHDF5(solverHandler, masterHdf5File); + this.indexHDF5Data(organizedNonSpatialResults, solverHandler, masterHdf5File); } catch (Exception e) { this.somethingFailed = somethingDidFail(); @@ -331,23 +342,29 @@ private void generateCSV(SolverHandler solverHandler) throws DataAccessException } } - private void generatePlots() throws IOException { + private void generatePlots(Map organizedNonSpatialResults) throws ExecutionException { logger.info("Generating Plots... "); - //PythonCalls.genPlotsPseudoSedml(this.SEDML_LOCATION, this.OUTPUT_DIRECTORY_FOR_CURRENT_SEDML.toString()); // generate the plots // We assume if no exception is returned that the plots pass - for (Output output : this.sedml.getOutputs()){ - if (!(output instanceof Plot2D plot)) continue; - BiosimulationLog.instance().updatePlotStatusYml(this.SEDML_LOCATION, plot.getId(), BiosimulationLog.Status.SUCCEEDED); + PlottingDataExtractor plotExtractor = new PlottingDataExtractor(this.sedml); + Map plot2Ds = plotExtractor.extractPlotRelevantData(organizedNonSpatialResults); + for (Results2DLinePlot plotToExport : plot2Ds.keySet()){ + try { + plotToExport.generatePng(plot2Ds.get(plotToExport) + ".png", this.OUTPUT_DIRECTORY_FOR_CURRENT_SEDML); + plotToExport.generatePdf(plot2Ds.get(plotToExport) + ".pdf", this.OUTPUT_DIRECTORY_FOR_CURRENT_SEDML); + } catch (ChartCouldNotBeProducedException e){ + logger.error("Failed creating plot:", e); + throw new ExecutionException("Failed to create plot: " + plotToExport.getTitle(), e); + } } } - private void generateHDF5(SolverHandler solverHandler, HDF5ExecutionResults masterHdf5File) { + private void indexHDF5Data(Map organizedNonSpatialResults, SolverHandler solverHandler, HDF5ExecutionResults masterHdf5File) { this.logDocumentMessage += "Indexing HDF5 data... "; logger.info("Indexing HDF5 data... "); Hdf5DataExtractor hdf5Extractor = new Hdf5DataExtractor(this.sedml, solverHandler.taskToTempSimulationMap); - Hdf5DataContainer partialHdf5File = hdf5Extractor.extractHdf5RelevantData(solverHandler.nonSpatialResults, solverHandler.spatialResults, masterHdf5File.isBioSimHdf5); + Hdf5DataContainer partialHdf5File = hdf5Extractor.extractHdf5RelevantData(organizedNonSpatialResults, solverHandler.spatialResults, masterHdf5File.isBioSimHdf5); masterHdf5File.addResults(this.sedml, partialHdf5File); for (File tempH5File : solverHandler.spatialResults.values()) { diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/plotting/PlottingDataExtractor.java b/vcell-cli/src/main/java/org/vcell/cli/run/plotting/PlottingDataExtractor.java new file mode 100644 index 0000000000..b33981629c --- /dev/null +++ b/vcell-cli/src/main/java/org/vcell/cli/run/plotting/PlottingDataExtractor.java @@ -0,0 +1,90 @@ +package org.vcell.cli.run.plotting; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jlibsedml.*; +import org.vcell.cli.run.hdf5.*; +import org.vcell.cli.run.results.NonSpatialValueHolder; +import org.vcell.cli.run.results.NonSpatialResultsConverter; +import org.vcell.cli.run.results.SpatialResultsConverter; + +import java.nio.file.Paths; +import java.util.*; + +public class PlottingDataExtractor { + private final SedML sedml; + private final String sedmlLocation; + + private final static Logger logger = LogManager.getLogger(Hdf5DataExtractor.class); + + /** + * Constructor to initialize the factory for a given set of simulation and model data. + * + * @param sedml the sedml object to get outputs, datasets, and data generators from. + */ + public PlottingDataExtractor(SedML sedml){ + this.sedml = sedml; + this.sedmlLocation = Paths.get(sedml.getPathForURI(), sedml.getFileName()).toString(); + } + + /** + * + * @param organizedNonSpatialResults the non-spatial results set of a sedml execution + * @return a wrapper for hdf5 relevant data + * @see NonSpatialResultsConverter ::convertNonspatialResultsToSedmlFormat + * @see SpatialResultsConverter ::collectSpatialDatasets + */ + public Map extractPlotRelevantData(Map organizedNonSpatialResults) { + Map plots = new LinkedHashMap<>(); + Set xAxisNames = new LinkedHashSet<>(); + if (organizedNonSpatialResults.isEmpty()) return plots; + + for (Plot2D requestedPlot : this.sedml.getOutputs().stream().filter(Plot2D.class::isInstance).map(Plot2D.class::cast).toList()){ + Results2DLinePlot plot = new Results2DLinePlot(); + plot.setTitle(requestedPlot.getName()); + + for (Curve curve : requestedPlot.getListOfCurves()){ + NonSpatialValueHolder xResults, yResults; + DataGenerator requestedXGenerator = this.sedml.getDataGeneratorWithId(curve.getXDataReference()); + DataGenerator requestedYGenerator = this.sedml.getDataGeneratorWithId(curve.getYDataReference()); + if (requestedXGenerator == null || requestedYGenerator == null) throw new RuntimeException("Unexpected null returns"); + if (null == (xResults = organizedNonSpatialResults.get(requestedXGenerator))) throw new RuntimeException("Unexpected lack of x-axis results!"); + if (null == (yResults = organizedNonSpatialResults.get(requestedYGenerator))) throw new RuntimeException("Unexpected lack of y-axis results!"); + + // There's two cases: 1 x-axis, n y-axes; or n x-axes, n y-axes. + final boolean hasSingleXSeries = xResults.listOfResultSets.size() == 1; + final boolean hasSingleYSeries = yResults.listOfResultSets.size() == 1; + boolean hasPairsOfSeries = xResults.listOfResultSets.size() != yResults.listOfResultSets.size(); + if (!hasSingleXSeries && !hasPairsOfSeries){ + throw new RuntimeException("Unexpected mismatch between number of x data sets, and y data sets!"); + } + + boolean hasBadXName = requestedXGenerator.getName() == null || "".equals(requestedXGenerator.getName()); + boolean hasBadYName = requestedYGenerator.getName() == null || "".equals(requestedYGenerator.getName()); + String xLabel = hasBadXName ? requestedXGenerator.getId() : requestedXGenerator.getName(); + String yLabel = hasBadYName ? requestedYGenerator.getId() : requestedYGenerator.getName(); + xAxisNames.add(xLabel); + + for (int i = 0; i < yResults.listOfResultSets.size(); i++){ + double[] xDataArray = xResults.listOfResultSets.get(hasSingleXSeries ? 0 : i); + double[] yDataArray = yResults.listOfResultSets.get(i); + List xData = Arrays.stream(xDataArray).boxed().toList(); + List yData = Arrays.stream(yDataArray).boxed().toList(); + + String xSeriesLabel = xLabel + (hasSingleXSeries ? "" : " (" + i + ")"); + String ySeriesLabel = yLabel + (hasSingleYSeries ? "" : " (" + i + ")"); + SingleAxisSeries xSeries = new SingleAxisSeries(xSeriesLabel, xData); + SingleAxisSeries ySeries = new SingleAxisSeries(ySeriesLabel, yData); + plot.addXYData(xSeries, ySeries); + } + } + + plot.setXAxisTitle(String.join("/", xAxisNames)); + boolean hasBadPlotName = requestedPlot.getName() == null || "".equals(requestedPlot.getName()); + String plotFileName = hasBadPlotName ? requestedPlot.getId() : requestedPlot.getName(); + plots.put(plot, plotFileName); + } + + return plots; + } +} From a7ce9223070d76228afa1654f0206894dd99f8c7 Mon Sep 17 00:00:00 2001 From: Logan Drescher Date: Thu, 23 Jan 2025 12:59:29 -0500 Subject: [PATCH 12/17] Added execution test --- .../biosimulation/BiosimulationsCommand.java | 60 ++++++++++-------- .../cli/run/plotting/MultiplePlotsTest.omex | Bin 0 -> 194347 bytes .../org/vcell/cli/run/plotting/plot_0.png | Bin 0 -> 30036 bytes .../org/vcell/cli/run/plotting/plot_1.png | Bin 0 -> 25203 bytes 4 files changed, 34 insertions(+), 26 deletions(-) create mode 100644 vcell-cli/src/test/resources/org/vcell/cli/run/plotting/MultiplePlotsTest.omex create mode 100644 vcell-cli/src/test/resources/org/vcell/cli/run/plotting/plot_0.png create mode 100644 vcell-cli/src/test/resources/org/vcell/cli/run/plotting/plot_1.png diff --git a/vcell-cli/src/main/java/org/vcell/cli/biosimulation/BiosimulationsCommand.java b/vcell-cli/src/main/java/org/vcell/cli/biosimulation/BiosimulationsCommand.java index 14a1dfad10..70d41883b2 100644 --- a/vcell-cli/src/main/java/org/vcell/cli/biosimulation/BiosimulationsCommand.java +++ b/vcell-cli/src/main/java/org/vcell/cli/biosimulation/BiosimulationsCommand.java @@ -42,14 +42,38 @@ public class BiosimulationsCommand implements Callable { private boolean help; public Integer call() { - CLIRecorder cliRecorder; int returnCode; - + if ((returnCode = BiosimulationsCommand.noFurtherActionNeeded(bQuiet, bDebug, bVersion)) != -1) return returnCode; - + + if (this.ARCHIVE == null) { + logger.error("ARCHIVE file not specified, try --help for usage"); + return 1; + } + if (this.OUT_DIR == null) { + logger.error("OUT_DIR not specified, try --help for usage"); + return 1; + } + String trace_args = String.format( + "Arguments:\nArchive\t: \"%s\"\nOut Dir\t: \"%s\"\nDebug\t: %b\n" + + "Quiet\t: %b\nVersion\t: %b\nHelp\t: %b", + ARCHIVE.getAbsolutePath(), OUT_DIR.getAbsolutePath(), bDebug, bQuiet, bVersion, help + ); + + logger.trace(trace_args); + return BiosimulationsCommand.executeVCellBiosimulationsMode(ARCHIVE, OUT_DIR, bQuiet, bDebug); + } + + public static int executeVCellBiosimulationsMode(File inFile, File outDir){ + return BiosimulationsCommand.executeVCellBiosimulationsMode(inFile, outDir, false, false); + } + + public static int executeVCellBiosimulationsMode(File inFile, File outDir, boolean bQuiet, boolean bDebug){ + CLIRecorder cliRecorder; + try { - cliRecorder = new CLIRecorder(OUT_DIR); // CLILogger will throw an execption if our output dir isn't valid. + cliRecorder = new CLIRecorder(outDir); // CLILogger will throw an execption if our output dir isn't valid. Level logLevel = logger.getLevel(); if (!bQuiet && bDebug) { logLevel = Level.DEBUG; @@ -63,38 +87,22 @@ public Integer call() { logger.debug("Biosimulations mode requested"); - String trace_args = String.format( - "Arguments:\nArchive\t: \"%s\"\nOut Dir\t: \"%s\"\nDebug\t: %b\n" + - "Quiet\t: %b\nVersion\t: %b\nHelp\t: %b", - ARCHIVE.getAbsolutePath(), OUT_DIR.getAbsolutePath(), bDebug, bQuiet, bVersion, help - ); - - logger.trace(trace_args); - logger.trace("Validating input"); - if (ARCHIVE == null) { - logger.error("ARCHIVE file not specified, try --help for usage"); - return 1; - } - if (!ARCHIVE.isFile()) { - logger.error("ARCHIVE file " + ARCHIVE.getAbsolutePath() + " not found, try --help for usage"); + if (!inFile.isFile()) { + logger.error("ARCHIVE file " + inFile.getAbsolutePath() + " not found, try --help for usage"); return 1; } logger.trace("Validating output"); - if (OUT_DIR == null) { - logger.error("OUT_DIR not specified, try --help for usage"); - return 1; - } - if (!OUT_DIR.isDirectory()) { - logger.error("OUT_DIR " + OUT_DIR.getAbsolutePath() + " not found or is not a directory, try --help for usage"); + if (!outDir.isDirectory()) { + logger.error("OUT_DIR " + outDir.getAbsolutePath() + " not found or is not a directory, try --help for usage"); return 1; } logger.info("Beginning execution"); File tmpDir = Files.createTempDirectory("VCell_CLI_" + Long.toHexString(new Date().getTime())).toFile(); try { - ExecuteImpl.singleMode(ARCHIVE, tmpDir, cliRecorder, true); + ExecuteImpl.singleMode(inFile, tmpDir, cliRecorder, true); if (!Tracer.hasErrors()) return 0; if (!bQuiet) { logger.error("Errors occurred during execution"); @@ -103,7 +111,7 @@ public Integer call() { return 1; } finally { logger.debug("Finished all execution."); - FileUtils.copyDirectoryContents(tmpDir, OUT_DIR, true, null); + FileUtils.copyDirectoryContents(tmpDir, outDir, true, null); } } catch (Exception e) { if (!bQuiet) { diff --git a/vcell-cli/src/test/resources/org/vcell/cli/run/plotting/MultiplePlotsTest.omex b/vcell-cli/src/test/resources/org/vcell/cli/run/plotting/MultiplePlotsTest.omex new file mode 100644 index 0000000000000000000000000000000000000000..2262596e0b8c7070a17dd381520fd0f4bd6d40c9 GIT binary patch literal 194347 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

h<`0-WytP~q=AVKK`U|n9E<*=v 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 OKyQxA$ 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 Date: Thu, 23 Jan 2025 13:34:24 -0500 Subject: [PATCH 13/17] Fix syntax bug --- .../java/org/vcell/cli/run/plotting/PlottingDataExtractor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/plotting/PlottingDataExtractor.java b/vcell-cli/src/main/java/org/vcell/cli/run/plotting/PlottingDataExtractor.java index b33981629c..85a8b08fff 100644 --- a/vcell-cli/src/main/java/org/vcell/cli/run/plotting/PlottingDataExtractor.java +++ b/vcell-cli/src/main/java/org/vcell/cli/run/plotting/PlottingDataExtractor.java @@ -54,7 +54,7 @@ public Map extractPlotRelevantData(Map Date: Thu, 23 Jan 2025 13:34:35 -0500 Subject: [PATCH 14/17] Cleanup of syntax --- .../src/main/java/org/vcell/cli/run/ExecuteImpl.java | 5 ++--- .../src/main/java/org/vcell/cli/run/ExecutionJob.java | 7 ++----- .../java/org/vcell/cli/run/hdf5/Hdf5DataExtractor.java | 4 ++-- .../org/vcell/cli/run/plotting/PlottingDataExtractor.java | 6 +----- .../vcell/cli/run/results/NonSpatialResultsConverter.java | 8 +++----- .../vcell/cli/run/results/SpatialResultsConverter.java | 2 +- .../java/org/vcell/cli/run/BSTSBasedOmexExecTest.java | 4 +--- .../test/java/org/vcell/cli/run/QuantOmexExecTest.java | 5 +---- .../src/test/java/org/vcell/cli/run/SpatialExecTest.java | 7 +------ 9 files changed, 14 insertions(+), 34 deletions(-) diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/ExecuteImpl.java b/vcell-cli/src/main/java/org/vcell/cli/run/ExecuteImpl.java index f5757064a8..d6bb8ff4e0 100644 --- a/vcell-cli/src/main/java/org/vcell/cli/run/ExecuteImpl.java +++ b/vcell-cli/src/main/java/org/vcell/cli/run/ExecuteImpl.java @@ -5,7 +5,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.vcell.cli.CLIRecordable; -import org.vcell.cli.PythonStreamException; import org.vcell.cli.exceptions.ExecutionException; import org.vcell.cli.run.hdf5.BiosimulationsHdfWriterException; import org.vcell.sedml.log.BiosimulationLog; @@ -107,7 +106,7 @@ public static void batchMode(File dirOfArchivesToProcess, File outputDir, CLIRec private static void runSingleExecOmex(File inputFile, File outputDir, CLIRecordable cliLogger, boolean bKeepTempFiles, boolean bExactMatchOnly, boolean bSmallMeshOverride) - throws IOException, ExecutionException, PythonStreamException, BiosimulationsHdfWriterException { + throws IOException, ExecutionException, BiosimulationsHdfWriterException { String bioModelBaseName = inputFile.getName().substring(0, inputFile.getName().indexOf(".")); // ".omex"?? Files.createDirectories(Paths.get(outputDir.getAbsolutePath() + File.separator + bioModelBaseName)); // make output subdir @@ -233,7 +232,7 @@ public static void singleExecVcml(File vcmlFile, File outputDir, CLIRecordable c private static void singleExecOmex(File inputFile, File rootOutputDir, CLIRecordable cliRecorder, boolean bKeepTempFiles, boolean bExactMatchOnly, boolean bEncapsulateOutput, boolean bSmallMeshOverride, boolean bBioSimMode) - throws ExecutionException, PythonStreamException, IOException, BiosimulationsHdfWriterException { + throws ExecutionException, IOException, BiosimulationsHdfWriterException { ExecutionJob requestedExecution = new ExecutionJob(inputFile, rootOutputDir, cliRecorder, bKeepTempFiles, bExactMatchOnly, bEncapsulateOutput, bSmallMeshOverride); diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/ExecutionJob.java b/vcell-cli/src/main/java/org/vcell/cli/run/ExecutionJob.java index 218b2dfd4d..d62f3d1765 100644 --- a/vcell-cli/src/main/java/org/vcell/cli/run/ExecutionJob.java +++ b/vcell-cli/src/main/java/org/vcell/cli/run/ExecutionJob.java @@ -4,7 +4,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.vcell.cli.CLIRecordable; -import org.vcell.cli.PythonStreamException; import org.vcell.cli.exceptions.ExecutionException; import org.vcell.cli.exceptions.PreProcessingException; import org.vcell.cli.run.hdf5.BiosimulationsHdf5Writer; @@ -152,11 +151,9 @@ private void executeSedmlDocument(String sedmlLocation, HDF5ExecutionResults cum /** * Clean up and analyze the results of the archive's execution - * + *
* Called after: `executeArchive()` - * - * @throws InterruptedException if there is an issue with accessing data - * @throws PythonStreamException if calls to the python-shell instance are not working correctly + * * @throws IOException if there are system I/O issues */ public void postProcessessArchive() throws IOException { diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/hdf5/Hdf5DataExtractor.java b/vcell-cli/src/main/java/org/vcell/cli/run/hdf5/Hdf5DataExtractor.java index 5f1267472f..4bfcf7d1e2 100644 --- a/vcell-cli/src/main/java/org/vcell/cli/run/hdf5/Hdf5DataExtractor.java +++ b/vcell-cli/src/main/java/org/vcell/cli/run/hdf5/Hdf5DataExtractor.java @@ -81,8 +81,8 @@ public Hdf5DataContainer extractHdf5RelevantData(Map organizeNonSpatialResult if (nonSpatialResultsHash.isEmpty()) return nonSpatialOrganizedResultsMap; for (Output output : NonSpatialResultsConverter.getValidOutputs(sedml)){ - List dataGeneratorsToProcess; + Set dataGeneratorsToProcess; if (output instanceof Report report){ - dataGeneratorsToProcess = new ArrayList<>(); + dataGeneratorsToProcess = new LinkedHashSet<>(); for (DataSet dataSet : report.getListOfDataSets()){ // use the data reference to obtain the data generator dataGeneratorsToProcess.add(sedml.getDataGeneratorWithId(dataSet.getDataReference())); @@ -40,7 +40,7 @@ else if (output instanceof Plot2D plot2D){ uniqueDataGens.add(sedml.getDataGeneratorWithId(curve.getXDataReference())); uniqueDataGens.add(sedml.getDataGeneratorWithId(curve.getYDataReference())); } - dataGeneratorsToProcess = uniqueDataGens.stream().toList(); + dataGeneratorsToProcess = uniqueDataGens; } else { if (logger.isDebugEnabled()) logger.warn("Unrecognized output type: `{}` (id={})", output.getClass().getName(), output.getId()); continue; @@ -65,8 +65,6 @@ public static Map> prepareNonSpatialDataForHdf5(S Map dataSetValues = new LinkedHashMap<>(); for (DataSet dataset : report.getListOfDataSets()) { - int maxLengthOfAllData = 0; // We have to pad up to this value - // use the data reference to obtain the data generator DataGenerator dataGen = sedml.getDataGeneratorWithId(dataset.getDataReference()); if (dataGen == null || !organizedNonSpatialResults.containsKey(dataGen)) diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/results/SpatialResultsConverter.java b/vcell-cli/src/main/java/org/vcell/cli/run/results/SpatialResultsConverter.java index 9dcd3f71e6..bb826a3e79 100644 --- a/vcell-cli/src/main/java/org/vcell/cli/run/results/SpatialResultsConverter.java +++ b/vcell-cli/src/main/java/org/vcell/cli/run/results/SpatialResultsConverter.java @@ -258,7 +258,7 @@ private static String removeVCellPrefixes(String s, String sedmlId){ return s; } -// public static Map> convertSpatialResultsToSedmlFormat(SedML sedml, Map spatialResultsHash, Map taskToSimulationMap, String sedmlLocation, String outDir) throws PythonStreamException { +// public static Map> convertSpatialResultsToSedmlFormat(SedML sedml, Map spatialResultsHash, Map taskToSimulationMap, String sedmlLocation, String outDir) { // Map> results = new LinkedHashMap<>(); // List allReports = SpatialResultsConverter.getReports(sedml.getOutputs()); // diff --git a/vcell-cli/src/test/java/org/vcell/cli/run/BSTSBasedOmexExecTest.java b/vcell-cli/src/test/java/org/vcell/cli/run/BSTSBasedOmexExecTest.java index 57aa1f8347..8fea1f979f 100644 --- a/vcell-cli/src/test/java/org/vcell/cli/run/BSTSBasedOmexExecTest.java +++ b/vcell-cli/src/test/java/org/vcell/cli/run/BSTSBasedOmexExecTest.java @@ -8,9 +8,7 @@ import org.junit.jupiter.api.Tag; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -import org.vcell.cli.CLIPythonManager; import org.vcell.cli.CLIRecordable; -import org.vcell.cli.PythonStreamException; import org.vcell.sedml.testsupport.FailureType; import org.vcell.sedml.testsupport.OmexTestCase; import org.vcell.sedml.testsupport.OmexTestingDatabase; @@ -36,7 +34,7 @@ public class BSTSBasedOmexExecTest { static List omexTestCases; @BeforeAll - public static void setup() throws PythonStreamException, IOException { + public static void setup() throws IOException { PropertyLoader.setProperty(PropertyLoader.installationRoot, new File("..").getAbsolutePath()); VCellUtilityHub.startup(VCellUtilityHub.MODE.CLI); diff --git a/vcell-cli/src/test/java/org/vcell/cli/run/QuantOmexExecTest.java b/vcell-cli/src/test/java/org/vcell/cli/run/QuantOmexExecTest.java index 1e7fcf5431..5b9846dab3 100644 --- a/vcell-cli/src/test/java/org/vcell/cli/run/QuantOmexExecTest.java +++ b/vcell-cli/src/test/java/org/vcell/cli/run/QuantOmexExecTest.java @@ -8,9 +8,7 @@ import org.junit.jupiter.api.Tag; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -import org.vcell.cli.CLIPythonManager; import org.vcell.cli.CLIRecordable; -import org.vcell.cli.PythonStreamException; import org.vcell.sedml.testsupport.FailureType; import org.vcell.sedml.testsupport.OmexTestCase; import org.vcell.sedml.testsupport.OmexTestingDatabase; @@ -33,13 +31,12 @@ @Tag("BSTS_IT") public class QuantOmexExecTest { @BeforeAll - public static void setup() throws PythonStreamException, IOException { + public static void setup() { PropertyLoader.setProperty(PropertyLoader.installationRoot, new File("..").getAbsolutePath()); VCellUtilityHub.startup(VCellUtilityHub.MODE.CLI); PropertyLoader.setProperty(PropertyLoader.cliWorkingDir, new File("../vcell-cli-utils").getAbsolutePath()); VCMongoMessage.enabled = false; - } @AfterAll diff --git a/vcell-cli/src/test/java/org/vcell/cli/run/SpatialExecTest.java b/vcell-cli/src/test/java/org/vcell/cli/run/SpatialExecTest.java index 137fdad7f5..0477032744 100644 --- a/vcell-cli/src/test/java/org/vcell/cli/run/SpatialExecTest.java +++ b/vcell-cli/src/test/java/org/vcell/cli/run/SpatialExecTest.java @@ -11,9 +11,7 @@ import org.junit.jupiter.api.Tag; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -import org.vcell.cli.CLIPythonManager; import org.vcell.cli.CLIRecordable; -import org.vcell.cli.PythonStreamException; import org.vcell.sedml.testsupport.FailureType; import org.vcell.sedml.testsupport.OmexTestCase; import org.vcell.sedml.testsupport.OmexTestingDatabase; @@ -24,19 +22,16 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; import java.util.function.Predicate; import java.util.stream.Collectors; -import static org.junit.jupiter.api.Assertions.assertFalse; - @Tag("Spatial_IT") public class SpatialExecTest { @BeforeAll - public static void setup() throws PythonStreamException, IOException { + public static void setup() { PropertyLoader.setProperty(PropertyLoader.installationRoot, new File("..").getAbsolutePath()); VCellUtilityHub.startup(VCellUtilityHub.MODE.CLI); From 11845a6e79a999461d3831370c2dda313932fb4c Mon Sep 17 00:00:00 2001 From: Logan Drescher Date: Thu, 23 Jan 2025 14:31:16 -0500 Subject: [PATCH 15/17] Added proper output checks --- vcell-cli/src/main/java/org/vcell/cli/run/SedmlJob.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/SedmlJob.java b/vcell-cli/src/main/java/org/vcell/cli/run/SedmlJob.java index bf1833ce74..9e5032b121 100644 --- a/vcell-cli/src/main/java/org/vcell/cli/run/SedmlJob.java +++ b/vcell-cli/src/main/java/org/vcell/cli/run/SedmlJob.java @@ -266,9 +266,11 @@ private void processOutputs(SolverHandler solverHandler, HDF5ExecutionResults ma NonSpatialResultsConverter.organizeNonSpatialResultsBySedmlDataGenerator( this.sedml, solverHandler.nonSpatialResults, solverHandler.taskToTempSimulationMap); + boolean hasReports = !this.sedml.getOutputs().stream().filter(Report.class::isInstance).map(Report.class::cast).toList().isEmpty(); + boolean has2DPlots = !this.sedml.getOutputs().stream().filter(Plot2D.class::isInstance).map(Plot2D.class::cast).toList().isEmpty(); if (!solverHandler.nonSpatialResults.isEmpty()) { - this.generateCSV(solverHandler); - this.generatePlots(organizedNonSpatialResults); + if (hasReports) this.generateCSV(solverHandler); + if (has2DPlots) this.generatePlots(organizedNonSpatialResults); } this.indexHDF5Data(organizedNonSpatialResults, solverHandler, masterHdf5File); From 8e5fdc53c66e61a9f678a201e6ea5c41e14fc4e1 Mon Sep 17 00:00:00 2001 From: Logan Drescher Date: Thu, 23 Jan 2025 14:31:34 -0500 Subject: [PATCH 16/17] Corrected expected output and added failure type --- vcell-cli/src/main/resources/test_cases.ndjson | 4 ++-- .../main/java/org/vcell/sedml/testsupport/FailureType.java | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/vcell-cli/src/main/resources/test_cases.ndjson b/vcell-cli/src/main/resources/test_cases.ndjson index 6cebbbdbc4..bb1a93b056 100644 --- a/vcell-cli/src/main/resources/test_cases.ndjson +++ b/vcell-cli/src/main/resources/test_cases.ndjson @@ -99,8 +99,8 @@ {"test_collection":"VCELL_BSTS_SYNTHS","file_path":"sedml/SimulatorProducesLinear2DPlots/1.execution-should-succeed.omex","should_fail":false,"known_status":null,"known_failure_type":null,"known_failure_desc":null} {"test_collection":"VCELL_BSTS_SYNTHS","file_path":"sedml/SimulatorProducesLinear2DPlots/2.execution-should-succeed.omex","should_fail":false,"known_status":null,"known_failure_type":null,"known_failure_desc":null} {"test_collection":"VCELL_BSTS_SYNTHS","file_path":"sedml/SimulatorSupportsRepeatedTasksWithNestedFunctionalRanges/1.execution-should-succeed.omex","should_fail":false,"known_status":null,"known_failure_type":null,"known_failure_desc":null} -{"test_collection":"VCELL_BSTS_SYNTHS","file_path":"sedml/SimulatorSupportsRepeatedTasksWithSubTasksOfMixedTypes/1.execution-should-succeed.omex","should_fail":false,"known_status":null,"known_failure_type":null,"known_failure_desc":null} -{"test_collection":"VCELL_BSTS_SYNTHS","file_path":"sedml/SimulatorSupportsRepeatedTasksWithSubTasksOfMixedTypes/2.execution-should-succeed.omex","should_fail":false,"known_status":null,"known_failure_type":null,"known_failure_desc":null} +{"test_collection":"VCELL_BSTS_SYNTHS","file_path":"sedml/SimulatorSupportsRepeatedTasksWithSubTasksOfMixedTypes/1.execution-should-succeed.omex","should_fail":false,"known_status":"SKIP","known_failure_type":"NESTED_SEDML_REPEATED_TASK","known_failure_desc":null} +{"test_collection":"VCELL_BSTS_SYNTHS","file_path":"sedml/SimulatorSupportsRepeatedTasksWithSubTasksOfMixedTypes/2.execution-should-succeed.omex","should_fail":false,"known_status":"SKIP","known_failure_type":"NESTED_SEDML_REPEATED_TASK","known_failure_desc":null} {"test_collection":"VCELL_BSTS_SYNTHS","file_path":"sedml/SimulatorCanResolveModelSourcesDefinedByUriFragments/1.execution-should-succeed.omex","should_fail":false,"known_status":null,"known_failure_type":null,"known_failure_desc":null} {"test_collection":"VCELL_BSTS_SYNTHS","file_path":"sedml/SimulatorSupportsSubstitutingAlgorithms/3.execute-should-fail.omex","should_fail":true,"known_status":null,"known_failure_type":null,"known_failure_desc":null} {"test_collection":"VCELL_BSTS_SYNTHS","file_path":"sedml/SimulatorSupportsSubstitutingAlgorithms/4.execution-should-succeed.omex","should_fail":false,"known_status":null,"known_failure_type":null,"known_failure_desc":null} diff --git a/vcell-core/src/main/java/org/vcell/sedml/testsupport/FailureType.java b/vcell-core/src/main/java/org/vcell/sedml/testsupport/FailureType.java index c5e9c9ce59..42c5118aa6 100644 --- a/vcell-core/src/main/java/org/vcell/sedml/testsupport/FailureType.java +++ b/vcell-core/src/main/java/org/vcell/sedml/testsupport/FailureType.java @@ -13,6 +13,7 @@ public enum FailureType { MATH_GENERATION_FAILURE, MATH_OVERRIDES_A_FUNCTION, MATH_OVERRIDES_INVALID, + NESTED_SEDML_REPEATED_TASK, // We can do a repeated task of a normal task, but not another repeated task. NULL_POINTER_EXCEPTION, OPERATION_NOT_SUPPORTED, // VCell simply doesn't have the necessary features to run this archive. SBML_IMPORT_FAILURE, From 7c99bd71914af6e6c2ab25c49447ecbace2a377b Mon Sep 17 00:00:00 2001 From: Logan Drescher Date: Thu, 23 Jan 2025 16:13:13 -0500 Subject: [PATCH 17/17] Re: Jim and Logan Logging level fixes --- .../vcell/cli/biosimulation/BiosimulationsCommand.java | 4 +++- .../vcell/geometry/surface/GeometrySurfaceUtils.java | 8 ++++---- .../java/cbit/vcell/mapping/DiffEquMathMapping.java | 2 +- .../java/cbit/vcell/mapping/LangevinMathMapping.java | 2 +- .../java/cbit/vcell/mapping/ParticleMathMapping.java | 2 +- .../main/java/cbit/vcell/solver/SolverUtilities.java | 4 +++- vcell-core/src/main/java/cbit/vcell/xml/XmlReader.java | 2 +- .../src/main/java/org/jlibsedml/SEDMLReader.java | 2 +- .../org/jlibsedml/validation/SchemaValidatorImpl.java | 5 ++++- .../org/jlibsedml/validation/SchematronValidator.java | 2 +- .../src/main/java/org/vcell/sedml/SEDMLImporter.java | 10 ++++------ 11 files changed, 24 insertions(+), 19 deletions(-) diff --git a/vcell-cli/src/main/java/org/vcell/cli/biosimulation/BiosimulationsCommand.java b/vcell-cli/src/main/java/org/vcell/cli/biosimulation/BiosimulationsCommand.java index 70d41883b2..0af384a69b 100644 --- a/vcell-cli/src/main/java/org/vcell/cli/biosimulation/BiosimulationsCommand.java +++ b/vcell-cli/src/main/java/org/vcell/cli/biosimulation/BiosimulationsCommand.java @@ -82,7 +82,9 @@ public static int executeVCellBiosimulationsMode(File inFile, File outDir, boole } LoggerContext config = (LoggerContext)(LogManager.getContext(false)); config.getConfiguration().getLoggerConfig(LogManager.getLogger("org.vcell").getName()).setLevel(logLevel); - config.getConfiguration().getLoggerConfig(LogManager.getLogger("cbit").getName()).setLevel(logLevel); + + config.getConfiguration().getLoggerConfig(LogManager.getLogger("cbit").getName()).setLevel(bDebug ? logLevel : Level.WARN ); + config.getConfiguration().getLoggerConfig(LogManager.getLogger("org.jlibsedml").getName()).setLevel(bDebug ? logLevel : Level.WARN); config.updateLoggers(); logger.debug("Biosimulations mode requested"); diff --git a/vcell-core/src/main/java/cbit/vcell/geometry/surface/GeometrySurfaceUtils.java b/vcell-core/src/main/java/cbit/vcell/geometry/surface/GeometrySurfaceUtils.java index 34b4e7aa4c..3f67ac33b3 100644 --- a/vcell-core/src/main/java/cbit/vcell/geometry/surface/GeometrySurfaceUtils.java +++ b/vcell-core/src/main/java/cbit/vcell/geometry/surface/GeometrySurfaceUtils.java @@ -85,7 +85,7 @@ public static GeometricRegion[] getUpdatedGeometricRegions(GeometrySurfaceDescri cbit.vcell.geometry.RegionImage.RegionInfo regionInfos[] = regionImage.getRegionInfos(); for(int i = 0; i < regionInfos.length; i++){ cbit.vcell.geometry.RegionImage.RegionInfo regionInfo = regionInfos[i]; - if (lg.isDebugEnabled()) lg.info(regionInfo); + lg.debug(regionInfo); cbit.vcell.geometry.SubVolume subVolume = geometrySpec.getSubVolume(regionInfo.getPixelValue()); String name = subVolume.getName() + regionInfo.getRegionIndex(); int numPixels = regionInfo.getNumPixels(); @@ -214,7 +214,7 @@ public static GeometricRegion[] getUpdatedGeometricRegions(GeometrySurfaceDescri } size -= sizeOfPixel * 0.125 * numOctantsToRemove; - if(lg.isDebugEnabled()) lg.info("size={}", size); + lg.debug("size={}", size); break; } @@ -222,7 +222,7 @@ public static GeometricRegion[] getUpdatedGeometricRegions(GeometrySurfaceDescri VolumeGeometricRegion volumeRegion = new VolumeGeometricRegion(name, size, volumeUnit, subVolume, regionInfo.getRegionIndex()); regionList.add(volumeRegion); - if(lg.isDebugEnabled()) lg.info("added volumeRegion({})", volumeRegion.getName()); + lg.debug("added volumeRegion({})", volumeRegion.getName()); } // @@ -266,7 +266,7 @@ public static GeometricRegion[] getUpdatedGeometricRegions(GeometrySurfaceDescri } surfaceRegion.addAdjacentGeometricRegion(interiorVolumeRegion); interiorVolumeRegion.addAdjacentGeometricRegion(surfaceRegion); - if(lg.isDebugEnabled()) lg.info("added surfaceRegion({})", surfaceRegion.getName()); + lg.debug("added surfaceRegion({})", surfaceRegion.getName()); } diff --git a/vcell-core/src/main/java/cbit/vcell/mapping/DiffEquMathMapping.java b/vcell-core/src/main/java/cbit/vcell/mapping/DiffEquMathMapping.java index aa934c2a97..6bda321e71 100644 --- a/vcell-core/src/main/java/cbit/vcell/mapping/DiffEquMathMapping.java +++ b/vcell-core/src/main/java/cbit/vcell/mapping/DiffEquMathMapping.java @@ -1454,7 +1454,7 @@ private void refreshMathDescription() throws MappingException, MatrixException, for(int i = 0; i < mappedSMs.length; i++){ if(mappedSMs[i] instanceof FeatureMapping){ if(mappedFM != null){ - if (lg.isDebugEnabled()) lg.warn("WARNING:::: MathMapping.refreshMathDescription() ... assigning boundary condition types not unique"); + lg.info("WARNING:::: MathMapping.refreshMathDescription() ... assigning boundary condition types not unique"); } mappedFM = (FeatureMapping) mappedSMs[i]; } diff --git a/vcell-core/src/main/java/cbit/vcell/mapping/LangevinMathMapping.java b/vcell-core/src/main/java/cbit/vcell/mapping/LangevinMathMapping.java index 262aa576f7..5b51b62947 100644 --- a/vcell-core/src/main/java/cbit/vcell/mapping/LangevinMathMapping.java +++ b/vcell-core/src/main/java/cbit/vcell/mapping/LangevinMathMapping.java @@ -415,7 +415,7 @@ protected void refreshMathDescription() throws MappingException, MatrixException for (int i = 0; i < mappedSMs.length; i++) { if (mappedSMs[i] instanceof FeatureMapping){ if (mappedFM!=null){ - if (lg.isDebugEnabled()) lg.warn("WARNING:::: MathMapping.refreshMathDescription() ... assigning boundary condition types not unique"); + lg.info("WARNING:::: MathMapping.refreshMathDescription() ... assigning boundary condition types not unique"); } mappedFM = (FeatureMapping)mappedSMs[i]; } diff --git a/vcell-core/src/main/java/cbit/vcell/mapping/ParticleMathMapping.java b/vcell-core/src/main/java/cbit/vcell/mapping/ParticleMathMapping.java index 96fc17ef6f..024d4ee7d9 100644 --- a/vcell-core/src/main/java/cbit/vcell/mapping/ParticleMathMapping.java +++ b/vcell-core/src/main/java/cbit/vcell/mapping/ParticleMathMapping.java @@ -487,7 +487,7 @@ private void refreshMathDescription() throws MappingException, MatrixException, for (int i = 0; i < mappedSMs.length; i++) { if (mappedSMs[i] instanceof FeatureMapping){ if (mappedFM!=null){ - if (lg.isDebugEnabled()) lg.warn("WARNING:::: MathMapping.refreshMathDescription() ... assigning boundary condition types not unique"); + lg.info("WARNING:::: MathMapping.refreshMathDescription() ... assigning boundary condition types not unique"); } mappedFM = (FeatureMapping)mappedSMs[i]; } diff --git a/vcell-core/src/main/java/cbit/vcell/solver/SolverUtilities.java b/vcell-core/src/main/java/cbit/vcell/solver/SolverUtilities.java index b42b6faf70..bf8a8b831c 100644 --- a/vcell-core/src/main/java/cbit/vcell/solver/SolverUtilities.java +++ b/vcell-core/src/main/java/cbit/vcell/solver/SolverUtilities.java @@ -164,8 +164,10 @@ private static List matchByKisaoId(KisaoTerm candidate) { for (SolverDescription sd : SolverDescription.values()) { if (sd.getKisao().contains(":") || sd.getKisao().contains("_")) { logger.trace(sd.getKisao()); + } else if ("KISAO".equals(sd.getKisao())){ + logger.info("Skipping not-yet-created KiSAO term"); } else { - if (logger.isDebugEnabled()) logger.warn("`{}` is bad KiSAO formating, skipping", sd.getKisao()); + logger.warn("`{}` is bad KiSAO formating, skipping", sd.getKisao()); continue; } String s1 = candidate.getId(); diff --git a/vcell-core/src/main/java/cbit/vcell/xml/XmlReader.java b/vcell-core/src/main/java/cbit/vcell/xml/XmlReader.java index b9eeda2ce8..43e664866f 100644 --- a/vcell-core/src/main/java/cbit/vcell/xml/XmlReader.java +++ b/vcell-core/src/main/java/cbit/vcell/xml/XmlReader.java @@ -4562,7 +4562,7 @@ public Model getModel(Element param) throws XmlParseException{ if(element != null){ getRbmModelContainer(element, newmodel); } else { - if (lg.isDebugEnabled()) lg.info("RbmModelContainer is missing."); + lg.debug("RbmModelContainer is missing."); } //Add SpeciesContexts diff --git a/vcell-core/src/main/java/org/jlibsedml/SEDMLReader.java b/vcell-core/src/main/java/org/jlibsedml/SEDMLReader.java index a9d4cbd9b8..8b3973e1fd 100644 --- a/vcell-core/src/main/java/org/jlibsedml/SEDMLReader.java +++ b/vcell-core/src/main/java/org/jlibsedml/SEDMLReader.java @@ -246,7 +246,7 @@ Algorithm getAlgorithm(Element algorithmElement) { if (eChild.getName().equals(SEDMLTags.ALGORITHM_PARAMETER_LIST)) { addAlgorithmParameters(alg, eChild); } else { - if (log.isDebugEnabled()) log.warn("Unexpected " + eChild); + log.warn("Unexpected " + eChild); } } return alg; diff --git a/vcell-core/src/main/java/org/jlibsedml/validation/SchemaValidatorImpl.java b/vcell-core/src/main/java/org/jlibsedml/validation/SchemaValidatorImpl.java index 43fc326c44..7ec5444523 100644 --- a/vcell-core/src/main/java/org/jlibsedml/validation/SchemaValidatorImpl.java +++ b/vcell-core/src/main/java/org/jlibsedml/validation/SchemaValidatorImpl.java @@ -88,9 +88,12 @@ private String getSchema(String xmlAsString) throws JDOMException, return SEDML_L1_V1_SCHEMA; } else if (level.equals("1") && version.equals("2")) { return SEDML_L1_V2_SCHEMA; + } else if (level.equals("1") && version.equals("3")) { + log.info("Version 3 support still in development."); + return SEDML_L1_V3_SCHEMA; } else { // probably level 3, but trying anyway to interpret with level 2 - if (log.isDebugEnabled()) log.warn("SED-ML version level not supported, import may fail"); + log.info("WARNING: SED-ML L{}V{} not supported, import may fail", level, version); return SEDML_L1_V3_SCHEMA; // throw new IllegalArgumentException( // "Invalid level/version combingation - must be 1-1 or 1-2 but was " diff --git a/vcell-core/src/main/java/org/jlibsedml/validation/SchematronValidator.java b/vcell-core/src/main/java/org/jlibsedml/validation/SchematronValidator.java index fe9cc0d43f..92450d345b 100644 --- a/vcell-core/src/main/java/org/jlibsedml/validation/SchematronValidator.java +++ b/vcell-core/src/main/java/org/jlibsedml/validation/SchematronValidator.java @@ -116,7 +116,7 @@ private String getSchematronXSL() { } else if (sedml.isL1V2()) { return "validatorl1v2.xsl"; } else { - if (lg.isDebugEnabled()) lg.warn("Unsupported version, import may fail"); + lg.warn("Unsupported sedml version `L{}V{}` detected, validating as L1V2", sedml.getLevel(), sedml.getVersion()); return "validatorl1v2.xsl"; // throw new UnsupportedOperationException(MessageFormat.format( // "Invalid level and version - {0}-{1}", sedml.getLevel(), diff --git a/vcell-core/src/main/java/org/vcell/sedml/SEDMLImporter.java b/vcell-core/src/main/java/org/vcell/sedml/SEDMLImporter.java index bd2fa95e93..a58ee8ddbe 100644 --- a/vcell-core/src/main/java/org/vcell/sedml/SEDMLImporter.java +++ b/vcell-core/src/main/java/org/vcell/sedml/SEDMLImporter.java @@ -195,7 +195,7 @@ public List getBioModels() { // try to find a match in the ontology tree SolverDescription solverDescription = SolverUtilities.matchSolverWithKisaoId(kisaoID, this.exactMatchOnly); if (solverDescription != null) { - if (logger.isDebugEnabled()) logger.info("Task (id='{}') is compatible, solver match found in ontology: '{}' matched to {}", selectedTask.getId(), kisaoID, solverDescription); + logger.info("Task (id='{}') is compatible, solver match found in ontology: '{}' matched to {}", selectedTask.getId(), kisaoID, solverDescription); } else { // give it a try anyway with our deterministic default solver solverDescription = SolverDescription.CombinedSundials; @@ -441,12 +441,10 @@ private List mergeBioModels(List bioModels) { BioModel bm0 = bioModels.get(0); for (int i = 1; i < bioModels.size(); i++) { - if (logger.isDebugEnabled()) - logger.info("--------------------\ncomparing model from `{}`\n with model from `{}`\n--------------------", - bioModels.get(i), bm0); + logger.debug("--------------------\ncomparing model from `{}`\n with model from `{}`\n--------------------", bioModels.get(i), bm0); RelationVisitor rvNotStrict = new ModelRelationVisitor(false); boolean equivalent = bioModels.get(i).getModel().relate(bm0.getModel(),rvNotStrict); - if (logger.isDebugEnabled()) logger.info("Equivalent => {}", equivalent); + logger.debug("Equivalent => {}", equivalent); if (!equivalent) return bioModels; } // all have matchable model, try to merge by pooling SimContexts @@ -1087,7 +1085,7 @@ private void translateAlgorithmParams(SolverTaskDescription simTaskDesc, org.jli NonspatialStochHybridOptions nonspatialSHO = simTaskDesc.getStochHybridOpt(); nonspatialSHO.setSDETolerance(Double.parseDouble(apValue)); } else { - logger.error("Algorithm parameter with kisao id '" + apKisaoID + "' not supported at this time, skipping."); + logger.warn("Algorithm parameter with kisao id '" + apKisaoID + "' not supported at this time, skipping."); } } simTaskDesc.setErrorTolerance(errorTolerance);