From 7b9081f4a35f5faeb62d13968536bc18dcbce787 Mon Sep 17 00:00:00 2001 From: kotik-coder Date: Wed, 15 Dec 2021 21:04:27 +0100 Subject: [PATCH 1/3] v1.93F Fixes Abstract data Made the fields protected for easier access by subclasses ExportManager Introduced a current working directory field, so that the file chooser keeps track of the browsing history. Exporter Fixed directory not updating after selection in the file chooser NetzschCSVReader / PulseCSVReader Added support to different locales (delimiter chars and decimal separator). ReaderManager Importing all files in a directory (e.g. with Linseis format) now invokes an Execution Service to take advantage of the concurrency. Problem It is now possible to select sample thickness as an optimisation variable. NumericPropertyFormatter Added missing condition to skip scientific formatting if html had been disabled. Normality tests Minor fixes to logic Status Prevent status from updating to QUEUED when task is in progress or has failed Launcher Removed pop-up exception windows completely, as this caused uncontrollable breeding of pop-up windows Chart Fixed value markers not taking into account the numerical conversion factor (ms -> s and vice versa). MouseOnMarkerListener Fixed wrong concurrent updates of both value markers when switching statuses. MainGraphFrame Prevented tasks from plotting while being in progress. Avoids ConcurrentModificationException. ExportDialog Changed from Save dialog to Open dialog to avoid ambiguity Other minors changes --- pom.xml | 2 +- src/main/java/pulse/AbstractData.java | 4 +- .../java/pulse/input/ExperimentalData.java | 4 +- .../java/pulse/io/export/ExportManager.java | 5 +- src/main/java/pulse/io/export/Exporter.java | 53 ++++++------ .../java/pulse/io/readers/AbstractReader.java | 1 + .../pulse/io/readers/NetzschCSVReader.java | 86 +++++++++++++------ .../io/readers/NetzschPulseCSVReader.java | 27 +++--- .../java/pulse/io/readers/ReaderManager.java | 33 ++++++- .../pulse/problem/statements/Problem.java | 11 +++ .../properties/NumericPropertyFormatter.java | 4 +- .../properties/NumericPropertyKeyword.java | 11 +-- .../pulse/search/direction/ActiveFlags.java | 4 +- .../statistics/AndersonDarlingTest.java | 7 +- .../java/pulse/search/statistics/KSTest.java | 7 +- .../search/statistics/NormalityTest.java | 23 ++--- src/main/java/pulse/tasks/Calculation.java | 3 +- src/main/java/pulse/ui/Launcher.java | 12 +-- src/main/java/pulse/ui/components/Chart.java | 12 +-- .../listeners/MouseOnMarkerListener.java | 32 ++++--- .../java/pulse/ui/frames/MainGraphFrame.java | 4 +- .../pulse/ui/frames/SearchOptionsFrame.java | 2 +- .../pulse/ui/frames/dialogs/ExportDialog.java | 17 ++-- src/main/resources/NumericProperty.xml | 20 ++--- src/main/resources/Version.txt | 2 +- 25 files changed, 223 insertions(+), 163 deletions(-) diff --git a/pom.xml b/pom.xml index a71c065..88351e3 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ kotik-coder PULsE - 1.93 + 1.93F PULsE Processing Unit for Laser flash Experiments diff --git a/src/main/java/pulse/AbstractData.java b/src/main/java/pulse/AbstractData.java index f1f5ab8..2f58d47 100644 --- a/src/main/java/pulse/AbstractData.java +++ b/src/main/java/pulse/AbstractData.java @@ -32,8 +32,8 @@ public abstract class AbstractData extends PropertyHolder { private int count; - private List time; - private List signal; + protected List time; + protected List signal; private String name; diff --git a/src/main/java/pulse/input/ExperimentalData.java b/src/main/java/pulse/input/ExperimentalData.java index 3b2e5bd..bbbb2e6 100644 --- a/src/main/java/pulse/input/ExperimentalData.java +++ b/src/main/java/pulse/input/ExperimentalData.java @@ -419,7 +419,7 @@ private void doSetRange() { } /** - * Retrieves the + * Retrieves the time limit. * * @see pulse.problem.schemes.DifferenceScheme * @return a double, equal to the last element of the {@code time List}. @@ -427,6 +427,6 @@ private void doSetRange() { @Override public double timeLimit() { return timeAt(indexRange.getUpperBound()); - } + } } diff --git a/src/main/java/pulse/io/export/ExportManager.java b/src/main/java/pulse/io/export/ExportManager.java index ec8c81a..2064c50 100644 --- a/src/main/java/pulse/io/export/ExportManager.java +++ b/src/main/java/pulse/io/export/ExportManager.java @@ -21,6 +21,9 @@ * */ public class ExportManager { + + //current working dir + private static File cwd = null; private ExportManager() { // intentionally blank @@ -85,7 +88,7 @@ public static Exporter findExporter(Class target) public static void askToExport(T target, JFrame parentWindow, String fileTypeLabel) { var exporter = findExporter(target); if (exporter != null) { - exporter.askToExport(target, parentWindow, fileTypeLabel); + cwd = exporter.askToExport(target, parentWindow, fileTypeLabel, cwd); } else { throw new IllegalArgumentException("No exporter for " + target.getClass().getSimpleName()); } diff --git a/src/main/java/pulse/io/export/Exporter.java b/src/main/java/pulse/io/export/Exporter.java index 71cb1ba..d2d35c0 100644 --- a/src/main/java/pulse/io/export/Exporter.java +++ b/src/main/java/pulse/io/export/Exporter.java @@ -89,12 +89,11 @@ public default void export(T target, File directory, Extension extension) { * @param target the exported target * @param parentWindow the parent frame. * @param fileTypeLabel the label describing the specific type of files that - * will be saved. + * @param directory the default directory of the file will be saved. + * @return the directory where files were exported */ - public default void askToExport(T target, JFrame parentWindow, String fileTypeLabel) { - var fileChooser = new JFileChooser(); - var workingDirectory = new File(System.getProperty("user.home")); - fileChooser.setCurrentDirectory(workingDirectory); + public default File askToExport(T target, JFrame parentWindow, String fileTypeLabel, File directory) { + var fileChooser = new JFileChooser(directory); fileChooser.setMultiSelectionEnabled(true); FileNameExtensionFilter choosable = null; @@ -113,29 +112,35 @@ public default void askToExport(T target, JFrame parentWindow, String fileTypeLa var file = fileChooser.getSelectedFile(); var path = file.getPath(); - if (!(fileChooser.getFileFilter() instanceof FileNameExtensionFilter)) { - return; - } + directory = file.isDirectory() ? file : file.getParentFile(); - var currentFilter = (FileNameExtensionFilter) fileChooser.getFileFilter(); - var ext = currentFilter.getExtensions()[0]; + if ((fileChooser.getFileFilter() instanceof FileNameExtensionFilter)) { + + var currentFilter = (FileNameExtensionFilter) fileChooser.getFileFilter(); + var ext = currentFilter.getExtensions()[0]; + + if (!path.contains(".")) { + file = new File(path + "." + ext); + } else { + file = new File(path.substring(0, path.indexOf(".") + 1) + ext); + } + + try { + var fos = new FileOutputStream(file); + printToStream(target, fos, Extension.valueOf(ext.toUpperCase())); + fos.close(); + } catch (IOException e) { + System.err.println("An exception has been encountered while writing the contents of " + + target.getClass().getSimpleName() + " to " + file); + e.printStackTrace(); + } - if (!path.contains(".")) { - file = new File(path + "." + ext); - } - else - file = new File(path.substring(0, path.indexOf(".") + 1) + ext); - - try { - var fos = new FileOutputStream(file); - printToStream(target, fos, Extension.valueOf(ext.toUpperCase())); - fos.close(); - } catch (IOException e) { - System.err.println("An exception has been encountered while writing the contents of " - + target.getClass().getSimpleName() + " to " + file); - e.printStackTrace(); } + } + + return directory; + } /** diff --git a/src/main/java/pulse/io/readers/AbstractReader.java b/src/main/java/pulse/io/readers/AbstractReader.java index a557fcb..d98036e 100644 --- a/src/main/java/pulse/io/readers/AbstractReader.java +++ b/src/main/java/pulse/io/readers/AbstractReader.java @@ -13,6 +13,7 @@ * lists, arrays and containers may (and usually will) change as a result of * using the reader. *

+ * @param */ public interface AbstractReader extends AbstractHandler { diff --git a/src/main/java/pulse/io/readers/NetzschCSVReader.java b/src/main/java/pulse/io/readers/NetzschCSVReader.java index d3320e3..a50d4e7 100644 --- a/src/main/java/pulse/io/readers/NetzschCSVReader.java +++ b/src/main/java/pulse/io/readers/NetzschCSVReader.java @@ -7,10 +7,16 @@ import java.io.File; import java.io.FileReader; import java.io.IOException; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Locale; import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; import pulse.AbstractData; import pulse.input.ExperimentalData; @@ -44,10 +50,16 @@ public class NetzschCSVReader implements CurveReader { /** * Note comma is included as a delimiter character here. */ - public final static String delims = "[#();,/°Cx%^]+"; - + private final static String ENGLISH_DELIMS = "[#(),/°Cx%^]+"; + private final static String GERMAN_DELIMS = "[#();/°Cx%^]+"; + + private static String delims = ENGLISH_DELIMS; + + //default number format (British format) + private static Locale locale = Locale.ENGLISH; + private NetzschCSVReader() { - // intentionally blank + //intentionally blank } /** @@ -87,19 +99,25 @@ public List read(File file) throws IOException { Objects.requireNonNull(file, Messages.getString("DATReader.1")); ExperimentalData curve = new ExperimentalData(); + + //gets the number format for this locale try (BufferedReader reader = new BufferedReader(new FileReader(file))) { int shotId = determineShotID(reader, file); + + var format = DecimalFormat.getInstance(locale); + format.setGroupingUsed(false); var tempTokens = findLineByLabel(reader, THICKNESS, delims).split(delims); - final double thickness = Double.parseDouble(tempTokens[tempTokens.length - 1]) * TO_METRES; + + final double thickness = format.parse(tempTokens[tempTokens.length - 1]).doubleValue() * TO_METRES; tempTokens = findLineByLabel(reader, DIAMETER, delims).split(delims); - final double diameter = Double.parseDouble(tempTokens[tempTokens.length - 1]) * TO_METRES; + final double diameter = format.parse(tempTokens[tempTokens.length - 1]).doubleValue() * TO_METRES; tempTokens = findLineByLabel(reader, SAMPLE_TEMPERATURE, delims).split(delims); - final double sampleTemperature = Double.parseDouble(tempTokens[tempTokens.length - 1]) + TO_KELVIN; + final double sampleTemperature = format.parse(tempTokens[tempTokens.length - 1]).doubleValue() + TO_KELVIN; /* * Finds the detector keyword. @@ -122,32 +140,52 @@ public List read(File file) throws IOException { curve.setMetadata(met); curve.setRange(new Range(curve.getTimeSequence())); + return new ArrayList<>(Arrays.asList(curve)); + } catch (ParseException ex) { + Logger.getLogger(NetzschCSVReader.class.getName()).log(Level.SEVERE, null, ex); } - return new ArrayList<>(Arrays.asList(curve)); + return null; } - protected static void populate(AbstractData data, BufferedReader reader) throws IOException { + protected static void populate(AbstractData data, BufferedReader reader) throws IOException, ParseException { double time; double power; String[] tokens; + var format = DecimalFormat.getInstance(locale); + format.setGroupingUsed(false); for (String line = reader.readLine(); line != null && !line.trim().isEmpty(); line = reader.readLine()) { tokens = line.split(delims); - time = Double.parseDouble(tokens[0]) * NetzschCSVReader.TO_SECONDS; - power = Double.parseDouble(tokens[1]); + time = format.parse(tokens[0]).doubleValue() * NetzschCSVReader.TO_SECONDS; + power = format.parse(tokens[1]).doubleValue(); data.addPoint(time, power); } } protected static int determineShotID(BufferedReader reader, File file) throws IOException { - String[] shotID = reader.readLine().split(delims); + String shotIDLine = reader.readLine(); + String[] shotID = shotIDLine.split(delims); int shotId = -1; + + if(shotID.length < 3) { + + if(locale == Locale.ENGLISH) { + delims = GERMAN_DELIMS; + locale = Locale.GERMAN; + } + else { + delims = ENGLISH_DELIMS; + locale = Locale.ENGLISH; + } + + shotID = shotIDLine.split(delims); + } //check if first entry makes sense if (!shotID[shotID.length - 2].equalsIgnoreCase(SHOT_DATA)) { @@ -160,19 +198,7 @@ protected static int determineShotID(BufferedReader reader, File file) throws IO return shotId; } - - /* - private double parseDoubleWithComma(String s) { - var format = NumberFormat.getInstance(Locale.GERMANY); - try { - return format.parse(s).doubleValue(); - } catch (ParseException e) { - System.out.println("Couldn't parse double from: " + s); - e.printStackTrace(); - } - return Double.NaN; - } - */ + protected static String findLineByLabel(BufferedReader reader, String label, String delims) throws IOException { String line = ""; @@ -195,7 +221,6 @@ protected static String findLineByLabel(BufferedReader reader, String label, Str return line; } - /** * As this class uses the singleton pattern, only one instance is created * using an empty no-argument constructor. @@ -205,5 +230,14 @@ protected static String findLineByLabel(BufferedReader reader, String label, Str public static CurveReader getInstance() { return instance; } - + + /** + * Get the standard delimiter chars. + * @return delims + */ + + public static String getDelims() { + return delims; + } + } diff --git a/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java b/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java index 2b67fe4..4ba3303 100644 --- a/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java +++ b/src/main/java/pulse/io/readers/NetzschPulseCSVReader.java @@ -4,7 +4,10 @@ import java.io.File; import java.io.FileReader; import java.io.IOException; +import java.text.ParseException; import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; import pulse.problem.laser.NumericPulseData; import pulse.ui.Messages; @@ -37,10 +40,12 @@ public String getSupportedExtension() { /** * This performs a basic check, finding the shot ID, which is then passed to - * a new {@code NumericPulseData} object. The latter is populated using the - * time-power sequence stored in this file. If the {@value PULSE} keyword is + * a new {@code NumericPulseData} object.The latter is populated using the + * time-power sequence stored in this file.If the {@value PULSE} keyword is * not found, the method will display an error. * + * @param file + * @throws java.io.IOException * @see pulse.io.readers.NetzschCSVReader.read() * @return a new {@code NumericPulseData} object encapsulating the contents * of {@code file} @@ -49,14 +54,14 @@ public String getSupportedExtension() { public NumericPulseData read(File file) throws IOException { Objects.requireNonNull(file, Messages.getString("DATReader.1")); - NumericPulseData data; + NumericPulseData data = null; try (BufferedReader reader = new BufferedReader(new FileReader(file))) { int shotId = NetzschCSVReader.determineShotID(reader, file); data = new NumericPulseData(shotId); - var pulseLabel = NetzschCSVReader.findLineByLabel(reader, PULSE, NetzschCSVReader.delims); + var pulseLabel = NetzschCSVReader.findLineByLabel(reader, PULSE, NetzschCSVReader.getDelims()); if (pulseLabel == null) { System.err.println("Skipping " + file.getName()); @@ -66,24 +71,14 @@ public NumericPulseData read(File file) throws IOException { reader.readLine(); NetzschCSVReader.populate(data, reader); + } catch (ParseException ex) { + Logger.getLogger(NetzschPulseCSVReader.class.getName()).log(Level.SEVERE, null, ex); } return data; } - /* - private double parseDoubleWithComma(String s) { - var format = NumberFormat.getInstance(Locale.GERMANY); - try { - return format.parse(s).doubleValue(); - } catch (ParseException e) { - System.out.println("Couldn't parse double from: " + s); - e.printStackTrace(); - } - return Double.NaN; - } - */ /** * As this class uses the singleton pattern, only one instance is created * using an empty no-argument constructor. diff --git a/src/main/java/pulse/io/readers/ReaderManager.java b/src/main/java/pulse/io/readers/ReaderManager.java index 83c72ee..97bc4a2 100644 --- a/src/main/java/pulse/io/readers/ReaderManager.java +++ b/src/main/java/pulse/io/readers/ReaderManager.java @@ -8,6 +8,12 @@ import java.util.Objects; import java.util.Scanner; import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.stream.Collectors; import org.apache.commons.io.FileUtils; @@ -207,13 +213,32 @@ public static Set readDirectory(List> readers, File dir throw new IllegalArgumentException("Not a directory: " + directory); } - var list = new HashSet(); - + var es = Executors.newSingleThreadExecutor(); + + var callableList = new ArrayList>(); + for (File f : directory.listFiles()) { - list.add(read(readers, f)); + Callable callable = () -> read(readers, f); + callableList.add(callable); + } + + Set result = new HashSet<>(); + + try { + List> futures = es.invokeAll(callableList); + + for(Future f : futures) + result.add(f.get()); + + } catch (InterruptedException ex) { + Logger.getLogger(ReaderManager.class.getName()).log(Level.SEVERE, + "Reading interrupted when loading files from " + directory.toString(), ex); + } catch (ExecutionException ex) { + Logger.getLogger(ReaderManager.class.getName()).log(Level.SEVERE, + "Error executing read operation using concurrency", ex); } - return list; + return result; } /** diff --git a/src/main/java/pulse/problem/statements/Problem.java b/src/main/java/pulse/problem/statements/Problem.java index acde58f..4b47ebd 100644 --- a/src/main/java/pulse/problem/statements/Problem.java +++ b/src/main/java/pulse/problem/statements/Problem.java @@ -16,6 +16,7 @@ import pulse.math.Segment; import pulse.math.transforms.InvLenSqTransform; import pulse.math.transforms.StandardTransformations; +import pulse.math.transforms.StickTransform; import pulse.problem.laser.DiscretePulse; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.Grid; @@ -228,6 +229,13 @@ public void optimisationVector(ParameterVector output, List flags) { var key = output.getIndex(i); switch (key) { + case THICKNESS: + final double l = (double) properties.getSampleThickness().getValue(); + var bounds = Segment.boundsFrom(THICKNESS); + output.setParameterBounds(i, bounds); + output.setTransform(i, new StickTransform(bounds)); + output.set(i, l); + break; case DIFFUSIVITY: final double a = (double) properties.getDiffusivity().getValue(); output.setTransform(i, new InvLenSqTransform(properties)); @@ -284,6 +292,9 @@ public void assign(ParameterVector params) throws SolverException { var key = params.getIndex(i); switch (key) { + case THICKNESS: + properties.setSampleThickness(derive(THICKNESS, params.inverseTransform(i) )); + break; case DIFFUSIVITY: properties.setDiffusivity(derive(DIFFUSIVITY, params.inverseTransform(i))); break; diff --git a/src/main/java/pulse/properties/NumericPropertyFormatter.java b/src/main/java/pulse/properties/NumericPropertyFormatter.java index 1f98dc4..f3c83cf 100644 --- a/src/main/java/pulse/properties/NumericPropertyFormatter.java +++ b/src/main/java/pulse/properties/NumericPropertyFormatter.java @@ -74,7 +74,9 @@ public NumberFormat numberFormat(NumericProperty p) { : (double) value; double absAdjustedValue = Math.abs(adjustedValue); - if ((absAdjustedValue > UPPER_LIMIT) || (absAdjustedValue < LOWER_LIMIT && absAdjustedValue > ZERO)) { + if (addHtmlTags && + ( (absAdjustedValue > UPPER_LIMIT) + || (absAdjustedValue < LOWER_LIMIT && absAdjustedValue > ZERO)) ) { //format with scientific notations f = new ScientificFormat(p.getDimensionFactor(), p.getDimensionDelta()); } else { diff --git a/src/main/java/pulse/properties/NumericPropertyKeyword.java b/src/main/java/pulse/properties/NumericPropertyKeyword.java index 3a45116..65cdf46 100644 --- a/src/main/java/pulse/properties/NumericPropertyKeyword.java +++ b/src/main/java/pulse/properties/NumericPropertyKeyword.java @@ -210,23 +210,20 @@ public enum NumericPropertyKeyword { */ OPTICAL_THICKNESS, /** - * Time shift (pulse sync) + * Time shift (pulse sync). */ TIME_SHIFT, /** - * Statistical significance. + * Statistical significance for calculating the critical value. */ SIGNIFICANCE, - /** - * Statistical probability. - */ - PROBABILITY, + /** * Optimiser statistic (usually, RSS). */ OPTIMISER_STATISTIC, /** - * Model selection criterion (AIC, BIC, etc.) + * Model selection criterion (AIC, BIC, etc.). */ MODEL_CRITERION, /** diff --git a/src/main/java/pulse/search/direction/ActiveFlags.java b/src/main/java/pulse/search/direction/ActiveFlags.java index 5edc827..e13af6e 100644 --- a/src/main/java/pulse/search/direction/ActiveFlags.java +++ b/src/main/java/pulse/search/direction/ActiveFlags.java @@ -74,8 +74,8 @@ public static List activeParameters(SearchTask t) { //problem dependent var allActiveParams = selectActiveAndListed(flags, c.getProblem()); //problem independent (lower/upper bound) - var listed = selectActiveAndListed(flags, t.getExperimentalCurve() ); - allActiveParams.addAll( selectActiveAndListed(flags, t.getExperimentalCurve() ) ); + var listed = selectActiveAndListed(flags, t.getExperimentalCurve().getRange() ); + allActiveParams.addAll(listed); return allActiveParams; } diff --git a/src/main/java/pulse/search/statistics/AndersonDarlingTest.java b/src/main/java/pulse/search/statistics/AndersonDarlingTest.java index 60b970a..99ac004 100644 --- a/src/main/java/pulse/search/statistics/AndersonDarlingTest.java +++ b/src/main/java/pulse/search/statistics/AndersonDarlingTest.java @@ -1,7 +1,6 @@ package pulse.search.statistics; import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.PROBABILITY; import static pulse.properties.NumericPropertyKeyword.TEST_STATISTIC; import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation; @@ -32,9 +31,9 @@ public boolean test(SearchTask task) { var testResult = GofStat.andersonDarling(residuals, nd); this.setStatistic(derive(TEST_STATISTIC, testResult[0])); - setProbability(derive(PROBABILITY, testResult[1])); - - return significanceTest(); + + //compare the p-value and the significance + return testResult[1] > significance; } @Override diff --git a/src/main/java/pulse/search/statistics/KSTest.java b/src/main/java/pulse/search/statistics/KSTest.java index d350d7e..ceb4ceb 100644 --- a/src/main/java/pulse/search/statistics/KSTest.java +++ b/src/main/java/pulse/search/statistics/KSTest.java @@ -1,7 +1,6 @@ package pulse.search.statistics; import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.PROBABILITY; import static pulse.properties.NumericPropertyKeyword.TEST_STATISTIC; import org.apache.commons.math3.distribution.NormalDistribution; @@ -23,8 +22,10 @@ public class KSTest extends NormalityTest { @Override public boolean test(SearchTask task) { evaluate(task); - setProbability(derive(PROBABILITY, TestUtils.kolmogorovSmirnovTest(nd, residuals))); - return significanceTest(); + + this.setStatistic(derive(TEST_STATISTIC, + TestUtils.kolmogorovSmirnovStatistic(nd, residuals))); + return !TestUtils.kolmogorovSmirnovTest(nd, residuals, this.significance); } @Override diff --git a/src/main/java/pulse/search/statistics/NormalityTest.java b/src/main/java/pulse/search/statistics/NormalityTest.java index f7bb3ed..a8de54c 100644 --- a/src/main/java/pulse/search/statistics/NormalityTest.java +++ b/src/main/java/pulse/search/statistics/NormalityTest.java @@ -3,7 +3,6 @@ import static pulse.properties.NumericProperties.def; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericProperty.requireType; -import static pulse.properties.NumericPropertyKeyword.PROBABILITY; import static pulse.properties.NumericPropertyKeyword.SIGNIFICANCE; import static pulse.properties.NumericPropertyKeyword.TEST_STATISTIC; @@ -17,28 +16,25 @@ * * For the test to pass, the model residuals need be distributed according to a * (0, σ) normal distribution, where σ is the variance of the model - * residuals. As this is the pre-requisite for optimizers based on the ordinary + * residuals. As this is the pre-requisite for optimisers based on the ordinary * least-square statistic, the normality test can also be used to estimate if a * fit 'failed' or 'succeeded' in describing the data. + * + * The test consists in testing the relation statistic < critValue, + * where the critical value is determined based on a given level of significance. * */ public abstract class NormalityTest extends ResidualStatistic { private double statistic; - private double probability; - private static double significance = (double) def(SIGNIFICANCE).getValue(); + protected static double significance = (double) def(SIGNIFICANCE).getValue(); private static String selectedTestDescriptor; protected NormalityTest() { - probability = (double) def(PROBABILITY).getValue(); statistic = (double) def(TEST_STATISTIC).getValue(); } - public boolean significanceTest() { - return probability > significance; - } - public static NumericProperty getStatisticalSignifiance() { return derive(SIGNIFICANCE, significance); } @@ -48,10 +44,6 @@ public static void setStatisticalSignificance(NumericProperty alpha) { NormalityTest.significance = (double) alpha.getValue(); } - public NumericProperty getProbability() { - return derive(PROBABILITY, probability); - } - public abstract boolean test(SearchTask task); @Override @@ -65,11 +57,6 @@ public void setStatistic(NumericProperty statistic) { this.statistic = (double) statistic.getValue(); } - public void setProbability(NumericProperty probability) { - requireType(probability, PROBABILITY); - this.probability = (double) probability.getValue(); - } - @Override public void set(NumericPropertyKeyword type, NumericProperty property) { if (type == TEST_STATISTIC) { diff --git a/src/main/java/pulse/tasks/Calculation.java b/src/main/java/pulse/tasks/Calculation.java index 797b9f8..60cdd4c 100644 --- a/src/main/java/pulse/tasks/Calculation.java +++ b/src/main/java/pulse/tasks/Calculation.java @@ -186,9 +186,10 @@ public boolean setStatus(Status status) { switch(this.status) { case DONE: + case IN_PROGRESS: + case FAILED: case EXECUTION_ERROR: case INCOMPLETE: - case IN_PROGRESS: //if the TaskManager attempts to run this calculation if(status == Status.QUEUED) return false; diff --git a/src/main/java/pulse/ui/Launcher.java b/src/main/java/pulse/ui/Launcher.java index 65e31e4..1ee2635 100644 --- a/src/main/java/pulse/ui/Launcher.java +++ b/src/main/java/pulse/ui/Launcher.java @@ -95,17 +95,7 @@ private void arrangeErrorOutput() { try { var dir = new File(decodedPath).getParent(); errorLog = new File(dir + File.separator + "ErrorLog_" + now() + ".log"); - setErr(new PrintStream(errorLog) { - - @Override - public void println(String str) { - super.println(str); - JOptionPane.showMessageDialog(null, "An exception has occurred. " - + "Please check the stored log!", "Exception", JOptionPane.ERROR_MESSAGE); - } - - } - ); + setErr(new PrintStream(errorLog)); } catch (FileNotFoundException e) { System.err.println("Unable to set up error stream"); e.printStackTrace(); diff --git a/src/main/java/pulse/ui/components/Chart.java b/src/main/java/pulse/ui/components/Chart.java index a485806..2158758 100644 --- a/src/main/java/pulse/ui/components/Chart.java +++ b/src/main/java/pulse/ui/components/Chart.java @@ -94,7 +94,7 @@ public void mouseDragged(MouseEvent e) { //process dragged events Range range = instance.getSelectedTask() .getExperimentalCurve().getRange(); - double value = xCoord(e); + double value = xCoord(e) / factor; //convert to seconds back from ms -- if needed if (lowerMarker.getState() != MovableValueMarker.State.IDLE) { if (range.boundLimits(false).contains(value)) { @@ -124,8 +124,8 @@ public void mouseDragged(MouseEvent e) { if (instance.getSelectedTask() == eventTask) { //update marker values var segment = eventTask.getExperimentalCurve().getRange().getSegment(); - lowerMarker.setValue(segment.getMinimum()); - upperMarker.setValue(segment.getMaximum()); + lowerMarker.setValue(segment.getMinimum() * factor); //convert to ms -- if needed + upperMarker.setValue(segment.getMaximum() * factor); //convert to ms -- if needed } }); } //tasks that have been finihed @@ -241,11 +241,11 @@ public void plot(SearchTask task, boolean extendedCurve) { lowerMarker = new MovableValueMarker(segment.getMinimum() * factor); upperMarker = new MovableValueMarker(segment.getMaximum() * factor); - final double margin = segment.getMaximum() / 20.0; + final double margin = (lowerMarker.getValue() + upperMarker.getValue())/20.0; //add listener to handle range adjustment - var lowerMarkerListener = new MouseOnMarkerListener(this, lowerMarker, margin); - var upperMarkerListener = new MouseOnMarkerListener(this, upperMarker, margin); + var lowerMarkerListener = new MouseOnMarkerListener(this, lowerMarker, upperMarker, margin); + var upperMarkerListener = new MouseOnMarkerListener(this, upperMarker, upperMarker, margin); chartPanel.addChartMouseListener(lowerMarkerListener); chartPanel.addChartMouseListener(upperMarkerListener); diff --git a/src/main/java/pulse/ui/components/listeners/MouseOnMarkerListener.java b/src/main/java/pulse/ui/components/listeners/MouseOnMarkerListener.java index 4a4d1ef..834dc1c 100644 --- a/src/main/java/pulse/ui/components/listeners/MouseOnMarkerListener.java +++ b/src/main/java/pulse/ui/components/listeners/MouseOnMarkerListener.java @@ -27,16 +27,19 @@ */ public class MouseOnMarkerListener implements ChartMouseListener { - private final MovableValueMarker marker; + private final MovableValueMarker lower; + private final MovableValueMarker upper; + private final Chart chart; private final double margin; private final static Cursor CROSSHAIR = new Cursor(Cursor.CROSSHAIR_CURSOR); private final static Cursor RESIZE = new Cursor(Cursor.E_RESIZE_CURSOR); - public MouseOnMarkerListener(Chart chart, MovableValueMarker marker, double margin) { + public MouseOnMarkerListener(Chart chart, MovableValueMarker lower, MovableValueMarker upper, double margin) { this.chart = chart; - this.marker = marker; + this.lower = lower; + this.upper = upper; this.margin = margin; } @@ -48,20 +51,29 @@ public void chartMouseClicked(ChartMouseEvent arg0) { @Override public void chartMouseMoved(ChartMouseEvent arg0) { double xCoord = chart.xCoord(arg0.getTrigger()); - highlightMarker(xCoord, marker); + highlightMarker(xCoord); } - private void highlightMarker(double xCoord, MovableValueMarker marker) { + private void highlightMarker(double xCoord) { - if (xCoord > (marker.getValue() - margin) - & xCoord < (marker.getValue() + margin)) { + if (xCoord > (lower.getValue() - margin) + & xCoord < (lower.getValue() + margin)) { - marker.setState(MovableValueMarker.State.SELECTED); + lower.setState(MovableValueMarker.State.SELECTED); chart.getChartPanel().setCursor(RESIZE); - } else { + } + else if (xCoord > (upper.getValue() - margin) + & xCoord < (upper.getValue() + margin)) { + + upper.setState(MovableValueMarker.State.SELECTED); + chart.getChartPanel().setCursor(RESIZE); + + } + else { - marker.setState(MovableValueMarker.State.IDLE); + lower.setState(MovableValueMarker.State.IDLE); + upper.setState(MovableValueMarker.State.IDLE); chart.getChartPanel().setCursor(CROSSHAIR); } diff --git a/src/main/java/pulse/ui/frames/MainGraphFrame.java b/src/main/java/pulse/ui/frames/MainGraphFrame.java index 3d81254..fff33ec 100644 --- a/src/main/java/pulse/ui/frames/MainGraphFrame.java +++ b/src/main/java/pulse/ui/frames/MainGraphFrame.java @@ -8,6 +8,7 @@ import javax.swing.JInternalFrame; import pulse.tasks.TaskManager; +import pulse.tasks.logs.Status; import pulse.ui.components.Chart; import pulse.ui.components.panels.ChartToolbar; import pulse.ui.components.panels.OpacitySlider; @@ -44,7 +45,8 @@ private void initComponents() { public void plot() { var task = TaskManager.getManagerInstance().getSelectedTask(); - if (task != null) { + //do not plot tasks that are not finished + if (task != null && task.getCurrentCalculation().getStatus() != Status.IN_PROGRESS) { Executors.newSingleThreadExecutor().submit(() -> chart.plot(task, false)); } } diff --git a/src/main/java/pulse/ui/frames/SearchOptionsFrame.java b/src/main/java/pulse/ui/frames/SearchOptionsFrame.java index d8b0b6c..2d7edda 100644 --- a/src/main/java/pulse/ui/frames/SearchOptionsFrame.java +++ b/src/main/java/pulse/ui/frames/SearchOptionsFrame.java @@ -50,7 +50,7 @@ public class SearchOptionsFrame extends JInternalFrame { private final static Font FONT = new Font(getString("PropertyHolderTable.FontName"), ITALIC, 16); private final static List pathSolvers = instancesOf(PathOptimiser.class); - private final NumericPropertyKeyword[] mandatorySelection = new NumericPropertyKeyword[]{DIFFUSIVITY, MAXTEMP}; + private final NumericPropertyKeyword[] mandatorySelection = new NumericPropertyKeyword[]{MAXTEMP}; /** * Create the frame. diff --git a/src/main/java/pulse/ui/frames/dialogs/ExportDialog.java b/src/main/java/pulse/ui/frames/dialogs/ExportDialog.java index f8a9300..ed0f7ee 100644 --- a/src/main/java/pulse/ui/frames/dialogs/ExportDialog.java +++ b/src/main/java/pulse/ui/frames/dialogs/ExportDialog.java @@ -14,6 +14,7 @@ import java.awt.Dimension; import java.io.File; +import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; @@ -81,12 +82,12 @@ public ExportDialog() { } private File directoryQuery() { - var returnVal = fileChooser.showSaveDialog(this); + var returnVal = fileChooser.showOpenDialog(this); File f = null; if (returnVal == APPROVE_OPTION) { - f = fileChooser.getCurrentDirectory(); + dir = f = fileChooser.getSelectedFile(); } return f; @@ -171,8 +172,9 @@ private void initComponents() { fileChooser = new JFileChooser(); fileChooser.setMultiSelectionEnabled(false); fileChooser.setFileSelectionMode(DIRECTORIES_ONLY); - // Checkboxex - dir = fileChooser.getCurrentDirectory(); + + //get cwd + dir = new File("").getAbsoluteFile(); var directoryField = new JTextField(dir.getPath() + separator + projectName + separator); directoryField.setEditable(false); @@ -247,11 +249,8 @@ public void removeUpdate(DocumentEvent e) { var browseBtn = new JButton("Browse..."); - browseBtn.addActionListener(e -> { - if (directoryQuery() != null) { - directoryField.setText(dir.getPath() + separator + projectName + separator); - } - }); + browseBtn.addActionListener(e -> directoryField.setText(directoryQuery() + .getPath() + separator + projectName + separator) ); var exportBtn = new JButton("Export"); diff --git a/src/main/resources/NumericProperty.xml b/src/main/resources/NumericProperty.xml index 89dca9d..9c78b74 100644 --- a/src/main/resources/NumericProperty.xml +++ b/src/main/resources/NumericProperty.xml @@ -1,5 +1,10 @@ + + - - - + - - + minimum="1.0E-6" value="0.001" primitive-type="double" discreet="true" + default-search-variable="false"> Date: Thu, 17 Mar 2022 14:08:05 +0300 Subject: [PATCH 2/3] =?UTF-8?q?AbsorptionModel.java:=20Moved=20optimisatio?= =?UTF-8?q?nVector(=E2=80=A6)=20and=20assign(=E2=80=A6)=20from=20Problem?= =?UTF-8?q?=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADIScheme: Increased default time step to ensure adequate results Calculation.java: - Fixed problem with missing parent when creating a Calculation from a SearchTask. In the copy constructor, the parent is now removed, but the result is preserved. - Fixed incorrect status update in setStatus(…) method. Rules have been created to block status update in certain cases. - Introduced the isBetterThan(…) method, which performs a statistical check (e.g. BIC) plus an F-test, if the latter is possible, and uses this combination to assess which calculations gives a better result. CorrelationBuffer.java: truncate(…) excludes buffer elements which show very close parameter values and therefore negatively affect the correlation tests. CorrelationTest: Changed selector for correlation tests Details: Status may now indicate if the the new results are worse than the previous result. DiathermicMedium: Removed custom default number of data points DifferenceScheme: Added a check to see if the DiscretePulse had already been created. If so, the latter is only updated. This prevents adding superfluous listeners and creating objects DiscretePulse: Added Objects.requireNonNull when accessing the SearchTask ancestor of the Problem instance FTest.java: Added the Fischer test capability to compare two Calculation instances where one model is nested within the other. InstanceDescriptor: Instead of returning false in case if the descriptor is not recognised, the attemptUpdate(…) now throws an IllegalArgumentException. NetzschCSVReader: - Added detector spot size in the list of imported properties. -findLineByLabel now uses reader.mark(…) to keep track of previous valid position. If the search fails, the position of the reader will be reset to this mark. ParticipatingMedium & PenetrationProblem: Removed custom number of data points PenetrationProblem: Moved functionality specific to absoprtion model into the AbsorptionModel class PulseMainMenu: Changed initialisation of the correlation tests and communication of the selector with GUI. ResultTable & ResultTableModel: Added null checks for the result extracted from the Calculation instance ResultTableModel: Heavily modified the addRow(result) method where an entry will not be added to the results table if (a) a previous calculation had been previously completed and (b) the new result is rated worse than the previous result. SearchTask: Added storeCalculation(…) and findBestCalculation(…). TaskManager: Added call to storeCalculation(…) in the execute method, upon successful completion of the CompletableFuture. --- pom.xml | 2 +- src/main/java/pulse/input/Metadata.java | 2 + .../pulse/io/readers/NetzschCSVReader.java | 27 +++- .../pulse/problem/laser/DiscretePulse.java | 9 +- .../java/pulse/problem/schemes/ADIScheme.java | 2 +- .../problem/schemes/DifferenceScheme.java | 6 +- .../solvers/ImplicitTranslucentSolver.java | 11 +- .../problem/statements/DiathermicMedium.java | 2 - .../statements/ParticipatingMedium.java | 3 - .../statements/PenetrationProblem.java | 58 +------ .../statements/model/AbsorptionModel.java | 70 +++++++- .../model/BeerLambertAbsorption.java | 4 + .../problem/statements/model/Insulator.java | 2 - .../search/statistics/CorrelationTest.java | 26 +-- .../java/pulse/search/statistics/FTest.java | 149 ++++++++++++++++++ .../statistics/ModelSelectionCriterion.java | 11 +- .../pulse/search/statistics/Statistic.java | 5 - src/main/java/pulse/tasks/Calculation.java | 125 ++++++++++----- src/main/java/pulse/tasks/SearchTask.java | 37 +++-- src/main/java/pulse/tasks/TaskManager.java | 12 +- src/main/java/pulse/tasks/logs/Details.java | 17 +- .../tasks/processing/CorrelationBuffer.java | 25 +++ .../pulse/ui/components/PulseMainMenu.java | 21 ++- .../java/pulse/ui/components/ResultTable.java | 8 +- .../components/buttons/ExecutionButton.java | 4 +- .../components/models/ResultTableModel.java | 96 ++++++++--- .../models/StoredCalculationTableModel.java | 6 +- .../java/pulse/util/InstanceDescriptor.java | 7 +- .../java/pulse/util/UpwardsNavigable.java | 2 +- src/main/resources/NumericProperty.xml | 11 +- 30 files changed, 558 insertions(+), 202 deletions(-) create mode 100644 src/main/java/pulse/search/statistics/FTest.java diff --git a/pom.xml b/pom.xml index 88351e3..e27af65 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ kotik-coder PULsE - 1.93F + 1.94 PULsE Processing Unit for Laser flash Experiments diff --git a/src/main/java/pulse/input/Metadata.java b/src/main/java/pulse/input/Metadata.java index 89e8030..7b650e8 100644 --- a/src/main/java/pulse/input/Metadata.java +++ b/src/main/java/pulse/input/Metadata.java @@ -22,6 +22,7 @@ import pulse.problem.laser.RectangularPulse; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.FOV_OUTER; import pulse.properties.Property; import pulse.properties.SampleName; import pulse.tasks.Identifier; @@ -199,6 +200,7 @@ public Set listedKeywords() { set.add(LASER_ENERGY); set.add(DETECTOR_GAIN); set.add(DETECTOR_IRIS); + set.add(FOV_OUTER); return set; } diff --git a/src/main/java/pulse/io/readers/NetzschCSVReader.java b/src/main/java/pulse/io/readers/NetzschCSVReader.java index a50d4e7..c2e0b2a 100644 --- a/src/main/java/pulse/io/readers/NetzschCSVReader.java +++ b/src/main/java/pulse/io/readers/NetzschCSVReader.java @@ -45,6 +45,7 @@ public class NetzschCSVReader implements CurveReader { private final static String SHOT_DATA = "Shot_data"; private final static String DETECTOR = "DETECTOR"; private final static String THICKNESS = "Thickness_RT"; + private final static String DETECTOR_SPOT_SIZE = "Spotsize"; private final static String DIAMETER = "Diameter"; /** @@ -109,6 +110,13 @@ public List read(File file) throws IOException { var format = DecimalFormat.getInstance(locale); format.setGroupingUsed(false); + var spot = findLineByLabel(reader, DETECTOR_SPOT_SIZE, THICKNESS, delims); + double spotSize = 0; + if(spot != null) { + var spotTokens = spot.split(delims); + spotSize = format.parse(spotTokens[spotTokens.length - 1]).doubleValue() * TO_METRES; + } + var tempTokens = findLineByLabel(reader, THICKNESS, delims).split(delims); final double thickness = format.parse(tempTokens[tempTokens.length - 1]).doubleValue() * TO_METRES; @@ -135,7 +143,7 @@ public List read(File file) throws IOException { var met = new Metadata(derive(TEST_TEMPERATURE, sampleTemperature), shotId); met.set(NumericPropertyKeyword.THICKNESS, derive(NumericPropertyKeyword.THICKNESS, thickness)); met.set(NumericPropertyKeyword.DIAMETER, derive(NumericPropertyKeyword.DIAMETER, diameter)); - met.set(NumericPropertyKeyword.FOV_OUTER, derive(NumericPropertyKeyword.FOV_OUTER, 0.85 * diameter)); + met.set(NumericPropertyKeyword.FOV_OUTER, derive(NumericPropertyKeyword.FOV_OUTER, spotSize != 0 ? spotSize : 0.85 * diameter)); met.set(NumericPropertyKeyword.SPOT_DIAMETER, derive(NumericPropertyKeyword.SPOT_DIAMETER, 0.94 * diameter)); curve.setMetadata(met); @@ -198,12 +206,18 @@ protected static int determineShotID(BufferedReader reader, File file) throws IO return shotId; } - + protected static String findLineByLabel(BufferedReader reader, String label, String delims) throws IOException { + return findLineByLabel(reader, label, "!!!", delims); + } + + protected static String findLineByLabel(BufferedReader reader, String label, String stopLabel, String delims) throws IOException { String line = ""; String[] tokens; + reader.mark(1000); + //find keyword outer: for (line = reader.readLine(); line != null; line = reader.readLine()) { @@ -211,9 +225,17 @@ protected static String findLineByLabel(BufferedReader reader, String label, Str tokens = line.split(delims); for (String token : tokens) { + if (token.equalsIgnoreCase(label)) { break outer; } + + if(token.equalsIgnoreCase(stopLabel)) { + line = null; + reader.reset(); + break outer; + } + } } @@ -221,6 +243,7 @@ protected static String findLineByLabel(BufferedReader reader, String label, Str return line; } + /** * As this class uses the singleton pattern, only one instance is created * using an empty no-argument constructor. diff --git a/src/main/java/pulse/problem/laser/DiscretePulse.java b/src/main/java/pulse/problem/laser/DiscretePulse.java index dd397e2..d29c073 100644 --- a/src/main/java/pulse/problem/laser/DiscretePulse.java +++ b/src/main/java/pulse/problem/laser/DiscretePulse.java @@ -1,5 +1,7 @@ package pulse.problem.laser; +import java.util.Objects; +import pulse.input.ExperimentalData; import pulse.problem.schemes.Grid; import pulse.problem.statements.Problem; import pulse.problem.statements.Pulse; @@ -37,7 +39,11 @@ public DiscretePulse(Problem problem, Grid grid) { recalculate(); - var data = ((SearchTask) problem.specificAncestor(SearchTask.class)).getExperimentalCurve(); + Object ancestor = + Objects.requireNonNull( problem.specificAncestor(SearchTask.class), + "Problem has not been assigned to a SearchTask"); + + ExperimentalData data = ((SearchTask)ancestor).getExperimentalCurve(); pulse.getPulseShape().init(data, this); pulse.addListener(e -> { @@ -45,6 +51,7 @@ public DiscretePulse(Problem problem, Grid grid) { recalculate(); pulse.getPulseShape().init(data, this); }); + } /** diff --git a/src/main/java/pulse/problem/schemes/ADIScheme.java b/src/main/java/pulse/problem/schemes/ADIScheme.java index bad5501..2ff641f 100644 --- a/src/main/java/pulse/problem/schemes/ADIScheme.java +++ b/src/main/java/pulse/problem/schemes/ADIScheme.java @@ -19,7 +19,7 @@ public abstract class ADIScheme extends DifferenceScheme { * time factor. */ public ADIScheme() { - this(derive(GRID_DENSITY, 30), derive(TAU_FACTOR, 1.0)); + this(derive(GRID_DENSITY, 30), derive(TAU_FACTOR, 0.5)); } /** diff --git a/src/main/java/pulse/problem/schemes/DifferenceScheme.java b/src/main/java/pulse/problem/schemes/DifferenceScheme.java index 309e19b..9f54d46 100644 --- a/src/main/java/pulse/problem/schemes/DifferenceScheme.java +++ b/src/main/java/pulse/problem/schemes/DifferenceScheme.java @@ -108,7 +108,11 @@ public void copyFrom(DifferenceScheme df) { * @see pulse.problem.schemes.Grid.adjustTo() */ protected void prepare(Problem problem) { - discretePulse = problem.discretePulseOn(grid); + if(discretePulse == null) + discretePulse = problem.discretePulseOn(grid); + else + discretePulse.recalculate(); + grid.adjustTo(discretePulse); var hc = problem.getHeatingCurve(); diff --git a/src/main/java/pulse/problem/schemes/solvers/ImplicitTranslucentSolver.java b/src/main/java/pulse/problem/schemes/solvers/ImplicitTranslucentSolver.java index 2fcab7e..c28d401 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ImplicitTranslucentSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ImplicitTranslucentSolver.java @@ -11,6 +11,7 @@ import pulse.problem.statements.PenetrationProblem; import pulse.problem.statements.Problem; import pulse.problem.statements.model.AbsorptionModel; +import pulse.problem.statements.model.BeerLambertAbsorption; import pulse.properties.NumericProperty; public class ImplicitTranslucentSolver extends ImplicitScheme implements Solver { @@ -44,14 +45,16 @@ private void prepare(PenetrationProblem problem) { final double Bi1H = (double) problem.getProperties().getHeatLoss().getValue() * grid.getXStep(); final double hx = grid.getXStep(); + + absorption = problem.getAbsorptionModel(); + HH = hx * hx; _2Bi1HTAU = 2.0 * Bi1H * tau; b11 = 1.0 / (1.0 + 2.0 * tau / HH * (1 + Bi1H)); - absorption = problem.getAbsorptionModel(); final double EPS = 1E-7; - rearAbsorption = tau * absorption.absorption(LASER, (N - EPS) * hx); - frontAbsorption = tau * absorption.absorption(LASER, 0.0); + rearAbsorption = tau * absorption.absorption(LASER, (N - EPS) * hx); + frontAbsorption = tau * absorption.absorption(LASER, 0.0) + 2.0*tau/hx; var tridiagonal = new TridiagonalMatrixAlgorithm(grid) { @@ -61,7 +64,7 @@ public double phi(final int i) { } }; - + // coefficients for difference equation tridiagonal.setCoefA(1. / HH); tridiagonal.setCoefB(1. / tau + 2. / HH); diff --git a/src/main/java/pulse/problem/statements/DiathermicMedium.java b/src/main/java/pulse/problem/statements/DiathermicMedium.java index 2082d03..a34c744 100644 --- a/src/main/java/pulse/problem/statements/DiathermicMedium.java +++ b/src/main/java/pulse/problem/statements/DiathermicMedium.java @@ -34,11 +34,9 @@ */ public class DiathermicMedium extends ClassicalProblem { - private final static int DEFAULT_CURVE_POINTS = 300; public DiathermicMedium() { super(); - getHeatingCurve().setNumPoints(derive(NUMPOINTS, DEFAULT_CURVE_POINTS)); } public DiathermicMedium(Problem p) { diff --git a/src/main/java/pulse/problem/statements/ParticipatingMedium.java b/src/main/java/pulse/problem/statements/ParticipatingMedium.java index c61083f..2d60b1d 100644 --- a/src/main/java/pulse/problem/statements/ParticipatingMedium.java +++ b/src/main/java/pulse/problem/statements/ParticipatingMedium.java @@ -21,11 +21,8 @@ public class ParticipatingMedium extends NonlinearProblem { - private final static int DEFAULT_CURVE_POINTS = 300; - public ParticipatingMedium() { super(); - getHeatingCurve().setNumPoints(derive(NUMPOINTS, DEFAULT_CURVE_POINTS)); setComplexity(ProblemComplexity.HIGH); } diff --git a/src/main/java/pulse/problem/statements/PenetrationProblem.java b/src/main/java/pulse/problem/statements/PenetrationProblem.java index bca2c67..46a2b84 100644 --- a/src/main/java/pulse/problem/statements/PenetrationProblem.java +++ b/src/main/java/pulse/problem/statements/PenetrationProblem.java @@ -1,14 +1,8 @@ package pulse.problem.statements; -import static pulse.math.transforms.StandardTransformations.LOG; -import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.NUMPOINTS; - import java.util.List; import pulse.math.ParameterVector; -import pulse.math.Segment; -import static pulse.math.transforms.StandardTransformations.ABS; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.solvers.ImplicitTranslucentSolver; import pulse.problem.schemes.solvers.SolverException; @@ -21,8 +15,6 @@ public class PenetrationProblem extends ClassicalProblem { - private final static int DEFAULT_CURVE_POINTS = 300; - private InstanceDescriptor instanceDescriptor = new InstanceDescriptor( "Absorption Model Selector", AbsorptionModel.class); @@ -31,7 +23,6 @@ public class PenetrationProblem extends ClassicalProblem { public PenetrationProblem() { super(); - getHeatingCurve().setNumPoints(derive(NUMPOINTS, DEFAULT_CURVE_POINTS)); instanceDescriptor.setSelectedDescriptor(BeerLambertAbsorption.class.getSimpleName()); instanceDescriptor.addListener(() -> initAbsorption()); absorption.setParent(this); @@ -72,56 +63,13 @@ public InstanceDescriptor getAbsorptionSelector() { @Override public void optimisationVector(ParameterVector output, List flags) { super.optimisationVector(output, flags); - - for (int i = 0, size = output.dimension(); i < size; i++) { - var key = output.getIndex(i); - double value = 0; - - switch (key) { - case LASER_ABSORPTIVITY: - value = (double) (absorption.getLaserAbsorptivity()).getValue(); - break; - case THERMAL_ABSORPTIVITY: - value = (double) (absorption.getThermalAbsorptivity()).getValue(); - break; - case COMBINED_ABSORPTIVITY: - value = (double) (absorption.getCombinedAbsorptivity()).getValue(); - break; - default: - continue; - } - - //do this for the listed key values - output.setTransform(i, ABS); - output.set(i, value); - output.setParameterBounds(i, new Segment(1E-2, 1000.0)); - - } - + absorption.optimisationVector(output, flags); } @Override public void assign(ParameterVector params) throws SolverException { super.assign(params); - - double value; - - for (int i = 0, size = params.dimension(); i < size; i++) { - var key = params.getIndex(i); - - switch (key) { - case LASER_ABSORPTIVITY: - case THERMAL_ABSORPTIVITY: - case COMBINED_ABSORPTIVITY: - value = params.inverseTransform(i); - break; - default: - continue; - } - - absorption.set(key, derive(key, value)); - - } + absorption.assign(params); } @Override @@ -139,4 +87,4 @@ public Problem copy() { return new PenetrationProblem(this); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/statements/model/AbsorptionModel.java b/src/main/java/pulse/problem/statements/model/AbsorptionModel.java index 4a27a0f..434db3c 100644 --- a/src/main/java/pulse/problem/statements/model/AbsorptionModel.java +++ b/src/main/java/pulse/problem/statements/model/AbsorptionModel.java @@ -10,22 +10,25 @@ import java.util.List; import java.util.Map; import java.util.Set; +import pulse.math.ParameterVector; +import pulse.math.Segment; +import static pulse.math.transforms.StandardTransformations.ABS; +import pulse.math.transforms.Transformable; +import pulse.problem.schemes.solvers.SolverException; +import pulse.properties.Flag; +import static pulse.properties.NumericProperties.derive; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import static pulse.properties.NumericPropertyKeyword.COMBINED_ABSORPTIVITY; -import static pulse.properties.NumericPropertyKeyword.DIFFUSIVITY; -import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS; -import static pulse.properties.NumericPropertyKeyword.MAXTEMP; -import static pulse.properties.NumericPropertyKeyword.THICKNESS; -import pulse.properties.Property; import pulse.util.PropertyHolder; import pulse.util.Reflexive; +import pulse.search.Optimisable; -public abstract class AbsorptionModel extends PropertyHolder implements Reflexive { +public abstract class AbsorptionModel extends PropertyHolder implements Reflexive, Optimisable { private Map absorptionMap; - + protected AbsorptionModel() { setPrefix("Absorption model"); absorptionMap = new HashMap<>(); @@ -100,5 +103,58 @@ public Set listedKeywords() { set.add(COMBINED_ABSORPTIVITY); return set; } + + @Override + public void optimisationVector(ParameterVector output, List flags) { + for (int i = 0, size = output.dimension(); i < size; i++) { + var key = output.getIndex(i); + double value = 0; + + Transformable transform = ABS; + output.setParameterBounds(i, new Segment(1E-2, 1000.0)); + + switch (key) { + case LASER_ABSORPTIVITY: + value = (double) (getLaserAbsorptivity()).getValue(); + break; + case THERMAL_ABSORPTIVITY: + value = (double) (getThermalAbsorptivity()).getValue(); + break; + case COMBINED_ABSORPTIVITY: + value = (double) (getCombinedAbsorptivity()).getValue(); + break; + default: + continue; + } + + //do this for the listed key values + output.setTransform(i, transform); + output.set(i, value); + + } + + } + + @Override + public void assign(ParameterVector params) throws SolverException { + double value; + + for (int i = 0, size = params.dimension(); i < size; i++) { + var key = params.getIndex(i); + + switch (key) { + case LASER_ABSORPTIVITY: + case THERMAL_ABSORPTIVITY: + case COMBINED_ABSORPTIVITY: + value = params.inverseTransform(i); + break; + default: + continue; + } + + set(key, derive(key, value)); + + } + } } diff --git a/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java b/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java index c771987..3171335 100644 --- a/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java +++ b/src/main/java/pulse/problem/statements/model/BeerLambertAbsorption.java @@ -2,6 +2,10 @@ public class BeerLambertAbsorption extends AbsorptionModel { + public BeerLambertAbsorption() { + super(); + } + @Override public double absorption(SpectralRange range, double y) { double a = (double) (this.getAbsorptivity(range).getValue()); diff --git a/src/main/java/pulse/problem/statements/model/Insulator.java b/src/main/java/pulse/problem/statements/model/Insulator.java index b4c37b9..0cbba0c 100644 --- a/src/main/java/pulse/problem/statements/model/Insulator.java +++ b/src/main/java/pulse/problem/statements/model/Insulator.java @@ -4,12 +4,10 @@ import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.REFLECTANCE; -import java.util.List; import java.util.Set; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.properties.Property; public class Insulator extends AbsorptionModel { diff --git a/src/main/java/pulse/search/statistics/CorrelationTest.java b/src/main/java/pulse/search/statistics/CorrelationTest.java index f724a75..67191bc 100644 --- a/src/main/java/pulse/search/statistics/CorrelationTest.java +++ b/src/main/java/pulse/search/statistics/CorrelationTest.java @@ -7,18 +7,34 @@ import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import pulse.util.InstanceDescriptor; import pulse.util.PropertyHolder; import pulse.util.Reflexive; public abstract class CorrelationTest extends PropertyHolder implements Reflexive { private static double threshold = (double) def(CORRELATION_THRESHOLD).getValue(); - private static String selectedTestDescriptor; + private static InstanceDescriptor instanceDescriptor + = new InstanceDescriptor( + "Correlation Test Selector", CorrelationTest.class); + + static { + instanceDescriptor.setSelectedDescriptor(PearsonCorrelation.class.getSimpleName()); + } + public CorrelationTest() { //intentionally blank } + public static CorrelationTest init() { + return instanceDescriptor.newInstance(CorrelationTest.class); + } + + public final static InstanceDescriptor getTestDescriptor() { + return instanceDescriptor; + } + public abstract double evaluate(double[] x, double[] y); public boolean compareToThreshold(double value) { @@ -41,12 +57,4 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { } } - public static String getSelectedTestDescriptor() { - return selectedTestDescriptor; - } - - public static void setSelectedTestDescriptor(String selectedTestDescriptor) { - CorrelationTest.selectedTestDescriptor = selectedTestDescriptor; - } - } diff --git a/src/main/java/pulse/search/statistics/FTest.java b/src/main/java/pulse/search/statistics/FTest.java new file mode 100644 index 0000000..672bdc1 --- /dev/null +++ b/src/main/java/pulse/search/statistics/FTest.java @@ -0,0 +1,149 @@ +/* + * Copyright 2021 Artem Lunev . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package pulse.search.statistics; + +import org.apache.commons.math3.distribution.FDistribution; +import pulse.tasks.Calculation; + +/** + * A static class for testing two calculations based on the Fischer test (F-Test) + * implemented in Apache Commons Math. + * @author Artem Lunev + */ +public class FTest { + + /** + * False-rejection probability for the F-test, equal to {@value FALSE_REJECTION_PROBABILITY} + */ + + public final static double FALSE_REJECTION_PROBABILITY = 0.05; + + private FTest() { + //intentionall blank + } + + /** + * Tests two models to see which one is better according to the F-test + * @param a a calculation + * @param b another calculation + * @return {@code null} if the result is inconclusive, otherwise the + * best of two calculations. + * @see FTest.evaluate() + */ + + public static Calculation test(Calculation a, Calculation b) { + + double[] data = evaluate(a, b); + + Calculation best = null; + + if(data != null) { + + //Under the null hypothesis the general model does not provide + //a significantly better fit than the nested model + + Calculation nested = findNested(a, b); + + //if the F-statistic is greater than the F-critical, reject the null hypothesis. + + if(nested == a) + best = data[0] > data[1] ? b : a; + else + best = data[0] > data[1] ? a : b; + + } + + return best; + + } + + /** + * Evaluates the F-statistic for two calculations. + * @param a a calculation + * @param b another calculation + * @return {@code null} if the test is inconclusive, i.e., if models are not + * nested, or if the model selection criteria are based on a statistic different + * from least-squares, or if the calculations refer to different data ranges. + * Otherwise returns an double array, consisting of two elements {@code [fStatistic, fCritical] } + */ + + public static double[] evaluate(Calculation a, Calculation b) { + + Calculation nested = findNested(a, b); + + double[] result = null; + + //if one of the models is nested into the other + if(nested != null) { + Calculation general = nested == a ? b : a; + + ResidualStatistic nestedResiduals = nested.getModelSelectionCriterion().getOptimiserStatistic(); + ResidualStatistic generalResiduals = general.getModelSelectionCriterion().getOptimiserStatistic(); + + final int nNested = nestedResiduals.getResiduals().size(); //sample size + final int nGeneral = generalResiduals.getResiduals().size(); //sample size + + //if both models use a sum-of-square statistic for the model selection criteria + //and if both calculations refer to the same calculation range + if(nestedResiduals.getClass() == generalResiduals.getClass() + && nestedResiduals.getClass() == SumOfSquares.class + && nNested == nGeneral) { + + double rssNested = ( (Number) ((SumOfSquares)nestedResiduals).getStatistic().getValue() ).doubleValue(); + double rssGeneral = ( (Number) ((SumOfSquares)generalResiduals).getStatistic().getValue() ).doubleValue(); + + int kGeneral = general.getModelSelectionCriterion().getNumVariables(); + int kNested = nested.getModelSelectionCriterion().getNumVariables(); + + double fStatistic = (rssNested - rssGeneral) + /(kGeneral - kNested) + /(rssGeneral/(nGeneral - kGeneral)); + + var fDistribution = new FDistribution(kGeneral - kNested, nGeneral - kGeneral); + + double fCritical = fDistribution.inverseCumulativeProbability(1.0 - FALSE_REJECTION_PROBABILITY); + + result = new double[]{fStatistic, fCritical}; + + } + + } + + return result; + + } + + /** + * Tests two models to see which one is nested in the other. A model is + * considered nested if it refers to the same class of problems but has + * fewer parameters. + * @param a a calculation + * @param b another calculation + * @return {@code null} if the models refer to different problem classes. + * Otherwise returns the model that is nested in the second model. + */ + + public static Calculation findNested(Calculation a, Calculation b) { + if(a.getProblem().getClass() != b.getProblem().getClass()) + return null; + + int aParams = a.getModelSelectionCriterion().getNumVariables(); + int bParams = b.getModelSelectionCriterion().getNumVariables(); + + return aParams > bParams ? b : a; + } + +} \ No newline at end of file diff --git a/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java b/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java index 63e7bbb..324acc5 100644 --- a/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java +++ b/src/main/java/pulse/search/statistics/ModelSelectionCriterion.java @@ -28,7 +28,7 @@ public abstract class ModelSelectionCriterion extends Statistic { public ModelSelectionCriterion(OptimiserStatistic os) { super(); - setOptimiser(os); + setOptimiserStatistic(os); } public ModelSelectionCriterion(ModelSelectionCriterion another) { @@ -96,20 +96,15 @@ public double probability(List all) { return exp(-0.5 * di); } - @Override - public String getDescriptor() { - return "Akaike Information Criterion (AIC)"; - } - public int getNumVariables() { return kq; } - public OptimiserStatistic getOptimiser() { + public OptimiserStatistic getOptimiserStatistic() { return os; } - public void setOptimiser(OptimiserStatistic os) { + public void setOptimiserStatistic(OptimiserStatistic os) { this.os = os; } diff --git a/src/main/java/pulse/search/statistics/Statistic.java b/src/main/java/pulse/search/statistics/Statistic.java index 6ac94f1..7a3c5e1 100644 --- a/src/main/java/pulse/search/statistics/Statistic.java +++ b/src/main/java/pulse/search/statistics/Statistic.java @@ -1,6 +1,5 @@ package pulse.search.statistics; -import pulse.properties.NumericProperty; import pulse.tasks.SearchTask; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -14,8 +13,4 @@ public abstract class Statistic extends PropertyHolder implements Reflexive { public abstract void evaluate(SearchTask t); - public abstract NumericProperty getStatistic(); - - public abstract void setStatistic(NumericProperty statistic); - } diff --git a/src/main/java/pulse/tasks/Calculation.java b/src/main/java/pulse/tasks/Calculation.java index 60cdd4c..3863df1 100644 --- a/src/main/java/pulse/tasks/Calculation.java +++ b/src/main/java/pulse/tasks/Calculation.java @@ -19,7 +19,8 @@ import pulse.problem.statements.Problem; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import pulse.search.statistics.AICStatistic; +import pulse.search.statistics.BICStatistic; +import pulse.search.statistics.FTest; import pulse.search.statistics.ModelSelectionCriterion; import pulse.search.statistics.OptimiserStatistic; import pulse.tasks.logs.Status; @@ -43,38 +44,32 @@ public class Calculation extends PropertyHolder implements Comparable instanceDescriptor = new InstanceDescriptor<>( "Model Selection Criterion", ModelSelectionCriterion.class); + //BIC as default static { - instanceDescriptor.setSelectedDescriptor(AICStatistic.class.getSimpleName()); + instanceDescriptor.setSelectedDescriptor(BICStatistic.class.getSimpleName()); } - public Calculation() { + public Calculation(SearchTask t) { status = INCOMPLETE; this.initOptimiser(); + setParent(t); instanceDescriptor.addListener(() -> initModelCriterion()); } - public Calculation(Problem problem, DifferenceScheme scheme, ModelSelectionCriterion rs) { - this(); - this.problem = problem; - this.scheme = scheme; - this.os = rs.getOptimiser(); - this.rs = rs; - problem.setParent(this); - scheme.setParent(this); - os.setParent(this); - rs.setParent(this); - } - - public Calculation copy() { - var status = this.status; - var nCalc = new Calculation(problem.copy(), scheme.copy(), rs.copy()); - var p = nCalc.getProblem(); - p.getProperties().setMaximumTemperature(problem.getProperties().getMaximumTemperature()); - nCalc.status = status; - if (this.getResult() != null) { - nCalc.setResult(new Result(this.getResult())); + /** + * Creates an orphan Calculation, retaining some properties of the argument + * + * @param c another calculation to be archived. + */ + public Calculation(Calculation c) { + this.problem = c.problem.copy(); + this.scheme = c.scheme.copy(); + this.rs = c.rs.copy(); + this.os = c.os.copy(); + this.status = c.status; + if (c.getResult() != null) { + this.result = new Result(c.getResult()); } - return nCalc; } public void clear() { @@ -136,11 +131,10 @@ private void addProblemListeners(Problem problem, ExperimentalData curve) { /** * Adopts the {@code scheme} by this {@code SearchTask} and updates the time - * limit of { - * - * @scheme} to match {@code ExperimentalData}. + * limit of {@code scheme} to match {@code ExperimentalData}. * * @param scheme the {@code DiffenceScheme}. + * @param curve */ public void setScheme(DifferenceScheme scheme, ExperimentalData curve) { this.scheme = scheme; @@ -174,31 +168,44 @@ public Status getStatus() { /** * Attempts to set the status of this calculation to {@code status}. + * * @param status a status - * @return {@code true} if this attempt is successful, including the case + * @return {@code true} if this attempt is successful, including the case * when the status being set is equal to the current status. {@code false} - * if the current status is one of the following: {@code DONE}, {@code EXECUTION_ERROR}, - * {@code INCOMPLETE}, {@code IN_PROGRES}, AND the {@code status} being set - * is {@code QUEUED}. + * if the current status is one of the following: {@code DONE}, + * {@code EXECUTION_ERROR}, {@code INCOMPLETE}, {@code IN_PROGRES}, AND the + * {@code status} being set is {@code QUEUED}. */ - public boolean setStatus(Status status) { - switch(this.status) { - case DONE: + boolean changeStatus = true; + + switch (this.status) { + case QUEUED: case IN_PROGRESS: + switch (status) { + case QUEUED: + case READY: + case INCOMPLETE: + changeStatus = false; + break; + default: + } + break; case FAILED: case EXECUTION_ERROR: case INCOMPLETE: - //if the TaskManager attempts to run this calculation - if(status == Status.QUEUED) - return false; + //if the TaskManager attempts to run this calculation + changeStatus = status != Status.QUEUED; + break; default: } + + if(changeStatus) + this.status = status; - this.status = status; - return true; - + return changeStatus; + } public NumericProperty weight(List all) { @@ -259,10 +266,44 @@ public void set(NumericPropertyKeyword type, NumericProperty property) { // intentionally left blank } + /** + * Checks if this {@code Calculation} is better than {@code a}. + * + * @param a another completed calculation + * @return {@code true} if another calculation hasn't been completed or if + * this calculation's statistic is lower than statistic of {@code a}. + */ + public boolean isBetterThan(Calculation a) { + boolean result = true; + + if (a.getStatus() == Status.DONE) { + result = compareTo(a) < 0; //compare statistic + + //do F-test + Calculation fBest = FTest.test(this, a); + //if the models are nested and calculations can be compared + if (fBest != null) { + //use the F-test result instead + result = fBest == this; + } + + } + + return result; + } + + /** + * Compares two calculations based on their model selection criteria. + * + * @param arg0 another calculation + * @return the result of comparing the model selection statistics of + * {@code this} and {@code arg0}. + */ @Override public int compareTo(Calculation arg0) { - var s1 = arg0.getModelSelectionCriterion().getStatistic(); - return getModelSelectionCriterion().getStatistic().compareTo(s1); + var sAnother = arg0.getModelSelectionCriterion().getStatistic(); + var sThis = getModelSelectionCriterion().getStatistic();; + return sThis.compareTo(sAnother); } @Override diff --git a/src/main/java/pulse/tasks/SearchTask.java b/src/main/java/pulse/tasks/SearchTask.java index 33dbfb5..d657875 100644 --- a/src/main/java/pulse/tasks/SearchTask.java +++ b/src/main/java/pulse/tasks/SearchTask.java @@ -84,15 +84,14 @@ public class SearchTask extends Accessible implements Runnable { private CorrelationTest correlationTest; private NormalityTest normalityTest; - private Identifier identifier; - + private final Identifier identifier; /** * If {@code SearchTask} finishes, and its R2 value is * lower than this constant, the result will be considered * {@code AMBIGUOUS}. */ - private List listeners = new CopyOnWriteArrayList<>(); - private List statusChangeListeners = new CopyOnWriteArrayList<>(); + private List listeners; + private List statusChangeListeners; /** *

@@ -106,8 +105,9 @@ public class SearchTask extends Accessible implements Runnable { * @param curve the {@code ExperimentalData} */ public SearchTask(ExperimentalData curve) { - current = new Calculation(); - current.setParent(this); + this.statusChangeListeners = new CopyOnWriteArrayList<>(); + this.listeners = new CopyOnWriteArrayList<>(); + current = new Calculation(this); this.identifier = new Identifier(); this.curve = curve; curve.setParent(this); @@ -285,7 +285,7 @@ public void run() { /* search cycle */ - /* sets an independent thread for manipulating the buffer */ + /* sets an independent thread for manipulating the buffer */ List> bufferFutures = new ArrayList<>(bufferSize); var singleThreadExecutor = Executors.newSingleThreadExecutor(); @@ -598,7 +598,7 @@ public void initNormalityTest() { } public void initCorrelationTest() { - correlationTest = instantiate(CorrelationTest.class, CorrelationTest.getSelectedTestDescriptor()); + correlationTest = CorrelationTest.init(); correlationTest.setParent(this); } @@ -617,6 +617,11 @@ public Calculation getCurrentCalculation() { public List getStoredCalculations() { return this.stored; } + + public void storeCalculation() { + var copy = new Calculation(current); + stored.add(copy); + } public void switchTo(Calculation calc) { current.setParent(null); @@ -625,10 +630,20 @@ public void switchTo(Calculation calc) { var e = new TaskRepositoryEvent(TaskRepositoryEvent.State.TASK_MODEL_SWITCH, this.getIdentifier()); fireRepositoryEvent(e); } + + /** + * Finds the best calculation by comparing those already stored by their + * model selection statistics. + * @return the calculation showing the optimal value of the model selection statistic. + */ + + public Calculation findBestCalculation() { + var c = stored.stream().reduce((c1, c2) -> c1.compareTo(c2) > 0 ? c2 : c1); + return c.isPresent() ? c.get() : null; + } public void switchToBestModel() { - var best = stored.stream().reduce((c1, c2) -> c1.compareTo(c2) > 0 ? c2 : c1); - this.switchTo(best.get()); + this.switchTo(findBestCalculation()); var e = new TaskRepositoryEvent(TaskRepositoryEvent.State.BEST_MODEL_SELECTED, this.getIdentifier()); fireRepositoryEvent(e); } @@ -640,4 +655,4 @@ private void fireRepositoryEvent(TaskRepositoryEvent e) { } } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/tasks/TaskManager.java b/src/main/java/pulse/tasks/TaskManager.java index 44fa9c2..03aa334 100644 --- a/src/main/java/pulse/tasks/TaskManager.java +++ b/src/main/java/pulse/tasks/TaskManager.java @@ -38,6 +38,7 @@ import pulse.tasks.listeners.TaskRepositoryListener; import pulse.tasks.listeners.TaskSelectionEvent; import pulse.tasks.listeners.TaskSelectionListener; +import pulse.tasks.logs.Status; import pulse.tasks.processing.Result; import pulse.tasks.processing.ResultFormat; import pulse.util.Group; @@ -120,7 +121,7 @@ public static TaskManager getManagerInstance() { * @param t a {@code SearchTask} that will be executed */ public void execute(SearchTask t) { - t.checkProblems(true); + t.checkProblems(t.getCurrentCalculation().getStatus() != Status.DONE); //try to start cmputation // notify listeners computation is about to start @@ -139,12 +140,10 @@ public void execute(SearchTask t) { current.setResult(new Result(t, ResultFormat.getInstance())); //notify listeners before the task is re-assigned notifyListeners(e); - current.setParent(null); - t.getStoredCalculations().add(current.copy()); - current.setParent(t); - } else { - notifyListeners(e); + t.storeCalculation(); } + else + notifyListeners(e); }); } @@ -170,7 +169,6 @@ public void executeAll() { var queue = tasks.stream().filter(t -> { switch (t.getCurrentCalculation().getStatus()) { - case DONE: case IN_PROGRESS: case EXECUTION_ERROR: return false; diff --git a/src/main/java/pulse/tasks/logs/Details.java b/src/main/java/pulse/tasks/logs/Details.java index 1552909..9c8f46a 100644 --- a/src/main/java/pulse/tasks/logs/Details.java +++ b/src/main/java/pulse/tasks/logs/Details.java @@ -41,7 +41,22 @@ public enum Details { SIGNIFICANT_CORRELATION_BETWEEN_PARAMETERS, PARAMETER_VALUES_NOT_SENSIBLE, MAX_ITERATIONS_REACHED, - ABNORMAL_DISTRIBUTION_OF_RESIDUALS; + ABNORMAL_DISTRIBUTION_OF_RESIDUALS, + + /** + * Indicates that the result table had not been updated, as the selected + * model produced results worse than expected by the model selection criterion. + */ + + CALCULATION_RESULTS_WORSE_THAN_PREVIOUSLY_OBTAINED, + + + /** + * Indicates that the result table had been updated, as the current + * model selection criterion showed better result than already present. + */ + + BETTER_CALCULATION_RESULTS_THAN_PREVIOUSLY_OBTAINED; @Override public String toString() { diff --git a/src/main/java/pulse/tasks/processing/CorrelationBuffer.java b/src/main/java/pulse/tasks/processing/CorrelationBuffer.java index 5464396..31d3ef1 100644 --- a/src/main/java/pulse/tasks/processing/CorrelationBuffer.java +++ b/src/main/java/pulse/tasks/processing/CorrelationBuffer.java @@ -22,6 +22,8 @@ public class CorrelationBuffer { private static Set> excludePairList; private static Set excludeSingleList; + private final static double DEFAULT_THRESHOLD = 1E-3; + static { excludePairList = new HashSet<>(); excludeSingleList = new HashSet<>(); @@ -44,6 +46,27 @@ public void inflate(SearchTask t) { public void clear() { params.clear(); } + + /** + * Truncates the buffer by excluding nearly-converged results. + */ + + private void truncate(double threshold) { + int i = 0; + int size = params.size(); + final double thresholdSq = threshold*threshold; + + for(i = 0; i < size - 1; i = i + 2) { + + ParameterVector diff = new ParameterVector( params.get(i), params.get(i + 1).subtract(params.get(i) )); + if(diff.lengthSq()/params.get(i).lengthSq() < thresholdSq) + break; + } + + for(int j = size - 1; j > i; j--) + params.remove(j); + + } public Map, Double> evaluate(CorrelationTest t) { if (params.isEmpty()) { @@ -54,6 +77,8 @@ public Map, Double> evaluate(CorrelationTe return null; } + truncate(DEFAULT_THRESHOLD); + var indices = params.get(0).getIndices(); var map = indices.stream() .map(index -> new ImmutableDataEntry<>(index, params.stream().mapToDouble(v -> v.getParameterValue(index)).toArray())) diff --git a/src/main/java/pulse/ui/components/PulseMainMenu.java b/src/main/java/pulse/ui/components/PulseMainMenu.java index 992157d..74aeeab 100644 --- a/src/main/java/pulse/ui/components/PulseMainMenu.java +++ b/src/main/java/pulse/ui/components/PulseMainMenu.java @@ -51,6 +51,7 @@ import pulse.ui.frames.dialogs.ExportDialog; import pulse.ui.frames.dialogs.FormattedInputDialog; import pulse.ui.frames.dialogs.ResultChangeDialog; +import pulse.util.Reflexive; @SuppressWarnings("serial") public class PulseMainMenu extends JMenuBar { @@ -270,16 +271,30 @@ private JMenu initAnalysisSubmenu() { JRadioButtonMenuItem corrItem = null; + var ct = CorrelationTest.init(); + for (var corrName : allDescriptors(CorrelationTest.class)) { corrItem = new JRadioButtonMenuItem(corrName); corrItems.add(corrItem); correlationsSubMenu.add(corrItem); + + if(ct.getDescriptor().equalsIgnoreCase(corrName)) + corrItem.setSelected(true); + corrItem.addItemListener(e -> { if (((AbstractButton) e.getItem()).isSelected()) { var text = ((AbstractButton) e.getItem()).getText(); - CorrelationTest.setSelectedTestDescriptor(text); - getManagerInstance().getTaskList().stream().forEach(t -> t.initCorrelationTest()); + var allTests = Reflexive.instancesOf(CorrelationTest.class); + var optionalTest = allTests.stream().filter(test -> + test.getDescriptor().equalsIgnoreCase(corrName)).findAny(); + + if(optionalTest.isPresent()) { + CorrelationTest.getTestDescriptor() + .setSelectedDescriptor(optionalTest.get().getClass().getSimpleName()); + getManagerInstance().getTaskList().stream().forEach(t -> t.initCorrelationTest()); + } + } }); @@ -294,8 +309,6 @@ private JMenu initAnalysisSubmenu() { correlationsSubMenu.add(thrItem); thrItem.addActionListener(e -> thresholdDialog.setVisible(true)); - correlationsSubMenu.getItem(0).setSelected(true); - analysisSubMenu.add(correlationsSubMenu); return analysisSubMenu; } diff --git a/src/main/java/pulse/ui/components/ResultTable.java b/src/main/java/pulse/ui/components/ResultTable.java index b47a0ee..141d0f4 100644 --- a/src/main/java/pulse/ui/components/ResultTable.java +++ b/src/main/java/pulse/ui/components/ResultTable.java @@ -8,6 +8,7 @@ import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.Comparator; +import java.util.Objects; import javax.swing.JTable; import javax.swing.RowSorter; @@ -79,7 +80,9 @@ public ResultTable(ResultFormat fmt) { switch (e.getState()) { case TASK_FINISHED: var r = t.getCurrentCalculation().getResult(); - invokeLater(() -> ((ResultTableModel) getModel()).addRow(r)); + var resultTableModel = (ResultTableModel) getModel(); + Objects.requireNonNull(r, "Task finished with a null result!"); + invokeLater(() -> resultTableModel.addRow(r)); break; case TASK_REMOVED: case TASK_RESET: @@ -134,7 +137,7 @@ public double[][][] data() { for (var i = 0; i < data.length; i++) { for (var j = 0; j < data[0][0].length; j++) { - property = (NumericProperty) getValueAt(j, i) ; + property = (NumericProperty) getValueAt(j, i); data[i][0][j] = ((Number) property.getValue()).doubleValue() * property.getDimensionFactor().doubleValue() + property.getDimensionDelta().doubleValue(); data[i][1][j] = property.getError() == null ? 0 @@ -230,6 +233,7 @@ public void undo() { var instance = TaskManager.getManagerInstance(); instance.getTaskList().stream().map(t -> t.getStoredCalculations()).flatMap(list -> list.stream()) + .filter(Objects::nonNull) .forEach(c -> dtm.addRow(c.getResult())); } diff --git a/src/main/java/pulse/ui/components/buttons/ExecutionButton.java b/src/main/java/pulse/ui/components/buttons/ExecutionButton.java index 943ce3e..37b84d5 100644 --- a/src/main/java/pulse/ui/components/buttons/ExecutionButton.java +++ b/src/main/java/pulse/ui/components/buttons/ExecutionButton.java @@ -32,14 +32,14 @@ public ExecutionButton() { this.addActionListener((ActionEvent e) -> { /* - * STOP PRESSED? + * STOP PRESSED? */ if (state == STOP) { instance.cancelAllTasks(); return; } /* - * EXECUTE PRESSED? + * EXECUTE PRESSED? */ if (instance.getTaskList().isEmpty()) { showMessageDialog(getWindowAncestor((Component) e.getSource()), diff --git a/src/main/java/pulse/ui/components/models/ResultTableModel.java b/src/main/java/pulse/ui/components/models/ResultTableModel.java index 4bb5ef1..5910a00 100644 --- a/src/main/java/pulse/ui/components/models/ResultTableModel.java +++ b/src/main/java/pulse/ui/components/models/ResultTableModel.java @@ -1,23 +1,25 @@ package pulse.ui.components.models; import static java.lang.Math.abs; -import static java.util.stream.Collectors.toList; import static pulse.tasks.processing.AbstractResult.filterProperties; import java.util.ArrayList; -import java.util.Comparator; import java.util.List; +import java.util.Objects; import java.util.Optional; import static javax.swing.SwingUtilities.invokeLater; import javax.swing.table.DefaultTableModel; import pulse.properties.NumericProperties; -import pulse.properties.NumericProperty; import static pulse.properties.NumericPropertyKeyword.IDENTIFIER; import static pulse.properties.NumericPropertyKeyword.TEST_TEMPERATURE; +import pulse.tasks.Calculation; import pulse.tasks.Identifier; +import pulse.tasks.SearchTask; import pulse.tasks.listeners.ResultFormatEvent; +import pulse.tasks.logs.Details; +import pulse.tasks.logs.Status; import pulse.tasks.processing.AbstractResult; import pulse.tasks.processing.AverageResult; import pulse.tasks.processing.Result; @@ -78,9 +80,7 @@ public void changeFormat(ResultFormat fmt) { results.clear(); this.setColumnIdentifiers(fmt.abbreviations().toArray()); - for (var r : oldResults) { - addRow(r); - } + oldResults.stream().filter(Objects::nonNull).forEach(r -> addRow(r)); } else { this.setColumnIdentifiers(fmt.abbreviations().toArray()); @@ -93,21 +93,22 @@ public void changeFormat(ResultFormat fmt) { } /** - * Transforms the result model by merging individual results which: - * (a) correspond to test temperatures within a specified {@code temperatureDelta} - * (b) form a single sequence of measurements - * @param temperatureDelta the maximum difference between the test temperature of two results being merged + * Transforms the result model by merging individual results which: (a) + * correspond to test temperatures within a specified + * {@code temperatureDelta} (b) form a single sequence of measurements + * + * @param temperatureDelta the maximum difference between the test + * temperature of two results being merged */ - public void merge(double temperatureDelta) { List skipList = new ArrayList<>(); List avgResults = new ArrayList<>(); List sortedResults = new ArrayList<>(results); - + /*sort results in the order of their ids * This is essential for the algorithm below which assumes the results * are listed in the order of ascending ids. - */ + */ sortedResults.sort((AbstractResult arg0, AbstractResult arg1) -> { var id1 = arg0.getProperties().get(fmt.indexOf(IDENTIFIER)); var id2 = arg1.getProperties().get(fmt.indexOf(IDENTIFIER)); @@ -146,22 +147,23 @@ public void merge(double temperatureDelta) { invokeLater(() -> { setRowCount(0); results.clear(); - avgResults.stream().forEach(r -> addRow(r)); + avgResults.stream().filter(Objects::nonNull).forEach(r -> addRow(r)); }); } - + /** - * Takes a list of results, which should be mandatory sorted in the order of ascending id values, - * and searches for those results that can be merged with {@code r}, satisfying these criteria: - * (a) these results correspond to test temperatures within a specified {@code temperatureDelta} - * (b) they form a single sequence of measurements + * Takes a list of results, which should be mandatory sorted in the order of + * ascending id values, and searches for those results that can be merged + * with {@code r}, satisfying these criteria: (a) these results correspond + * to test temperatures within a specified {@code temperatureDelta} (b) they + * form a single sequence of measurements + * * @param listOfResults an orderer list of results, as explained above - * @param r the result of interest - * @param propertyInterval an interval for the temperature merging - * @return a group of results + * @param r the result of interest + * @param propertyInterval an interval for the temperature merging + * @return a group of results */ - public List group(List listOfResults, AbstractResult r, double propertyInterval) { List selection = new ArrayList<>(); @@ -214,8 +216,52 @@ private List tooltips() { } public void addRow(AbstractResult result) { - if (result == null) { - return; + Objects.requireNonNull(result, "Entry added to the results table must not be null"); + + //result must have a valid ancestor! + var ancestor = Objects.requireNonNull( + result.specificAncestor(SearchTask.class), + "Result " + result.toString() + " does not belong a SearchTask!"); + + //the ancestor then has the SearchTask type + SearchTask parentTask = (SearchTask) ancestor; + + //any old result asssociated withis this task + var oldResult = results.stream().filter(r + -> r.specificAncestor( + SearchTask.class) == parentTask).findAny(); + + //ignore average results + if (result instanceof Result && oldResult.isPresent()) { + AbstractResult oldResultExisting = oldResult.get(); + Optional oldCalculation = parentTask.getStoredCalculations().stream() + .filter(c -> c.getResult().equals(oldResultExisting)).findAny(); + + //old calculation found + if (oldCalculation.isPresent()) { + + //since the task has already been completed anyway + Status status = Status.DONE; + + //better result than already present -- update table + if (parentTask.getCurrentCalculation().isBetterThan(oldCalculation.get())) { + remove(oldResultExisting); + status.setDetails(Details.BETTER_CALCULATION_RESULTS_THAN_PREVIOUSLY_OBTAINED); + parentTask.setStatus(status); + } else { + //do not remove result and do not add new result + status.setDetails(Details.CALCULATION_RESULTS_WORSE_THAN_PREVIOUSLY_OBTAINED); + parentTask.setStatus(status); + return; + } + + } else { + //calculation has been purged -- delete previous result + + remove(oldResultExisting); + + } + } var propertyList = filterProperties(result, fmt); diff --git a/src/main/java/pulse/ui/components/models/StoredCalculationTableModel.java b/src/main/java/pulse/ui/components/models/StoredCalculationTableModel.java index e82d734..83255e3 100644 --- a/src/main/java/pulse/ui/components/models/StoredCalculationTableModel.java +++ b/src/main/java/pulse/ui/components/models/StoredCalculationTableModel.java @@ -32,8 +32,10 @@ public void update(SearchTask t) { var list = t.getStoredCalculations(); for (Calculation c : list) { - var problem = c.getProblem(); - var baseline = c.getProblem().getBaseline(); + //we assume all problem descriptions contain the word Problem after their titles + String problem = c.getProblem().toString().split("Problem")[0] + ""; + //likewise -- for baselines containing Baseline + String baseline = c.getProblem().getBaseline().getSimpleName().split("Baseline")[0]; var optimiser = c.getOptimiserStatistic(); var criterion = c.getModelSelectionCriterion(); var parameters = c.getModelSelectionCriterion().getNumVariables(); diff --git a/src/main/java/pulse/util/InstanceDescriptor.java b/src/main/java/pulse/util/InstanceDescriptor.java index 54ee4db..bead937 100644 --- a/src/main/java/pulse/util/InstanceDescriptor.java +++ b/src/main/java/pulse/util/InstanceDescriptor.java @@ -50,10 +50,13 @@ public Object getValue() { @Override public boolean attemptUpdate(Object object) { var string = object.toString(); - - if (selectedDescriptor.equals(string) || !allDescriptors.contains(string)) { + + if (selectedDescriptor.equals(string)) { return false; } + + if(!allDescriptors.contains(string)) + throw new IllegalArgumentException("Unknown descriptor: " + selectedDescriptor); this.selectedDescriptor = string; listeners.stream().forEach(l -> l.onDescriptorChanged()); diff --git a/src/main/java/pulse/util/UpwardsNavigable.java b/src/main/java/pulse/util/UpwardsNavigable.java index 7a3079c..70bc5e2 100644 --- a/src/main/java/pulse/util/UpwardsNavigable.java +++ b/src/main/java/pulse/util/UpwardsNavigable.java @@ -90,7 +90,7 @@ public UpwardsNavigable specificAncestor(Class aClas * @param parent the new parent that will adopt this * {@code UpwardsNavigable}. */ - public void setParent(UpwardsNavigable parent) { + public final void setParent(UpwardsNavigable parent) { this.parent = parent; } diff --git a/src/main/resources/NumericProperty.xml b/src/main/resources/NumericProperty.xml index 9c78b74..e5ca983 100644 --- a/src/main/resources/NumericProperty.xml +++ b/src/main/resources/NumericProperty.xml @@ -272,13 +272,20 @@ descriptor="Thermal diffusivity, <i>a</i> (mm<sup>2</sup>s<sup>-1</sup>) " dimensionfactor="1000000.0" keyword="DIFFUSIVITY" maximum="0.001" minimum="1.0E-10" value="1.0E-6" primitive-type="double" - discreet="false" default-search-variable="true" /> + discreet="false" default-search-variable="true"> + + THICKNESS + + + + DIFFUSIVITY + Date: Mon, 25 Apr 2022 00:09:26 +0300 Subject: [PATCH 3/3] PULsE v1.94 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #ParameterVector.java -Changed findMalformedElements to conform with the transforms defined by the class instance and simplified the method calls. #Solver implementations and subclasses of DifferenceScheme: -Enabled throwing SolverException in solve(), timeStep() and iteration() #ThermalProperties.java -Introduced findMalformedProperties() for automated property validation -Fixed an error with density and heat capacity values not being assigned to the corresponding variables in fill() - Fixed a caveat in the calculateEmissivity() method where, in some extreme cases, the emissivity values could be greater than unity or smaller than zero, which led to critical breakdown of radiative transfer calculations. - Created a toString() method with a formatted output of the thermal properties. #CoupledImplicitScheme.java -Added missing call to super method FixedPointIterations.super.finaliseIteration() -Added a throws SolverException line in setCalculationStatus(…) when the status is not NORMAL #Status.java -Added optional message to further customise the status updates. The message is accessible via getDetailedMessage() #ExplicitLinearisedSolver.java, ImplicitLinearisedSolver.java -Experimental feature: rear-surface heat source introduced through the ‘zeta’ factor. This slightly changes the right boundary expressions and the timeStep(…) method #RTECalculationStatus.java -Added a new status, ‘INVALID_FLUXES’, which indicates the RTE calculation resulted in invalid flux values. #StateEntry.java -In case of a Status having a non-empty detailed message, the message will be printed in the toString() method, which has been modified accordingly #ParticipatingMedium.java -Changed bounds for the parameters, including e.g. the Planck number and optical thickness -Replaced all transforms for all optical parameters to StickTransform. This has proven to be more fail-proof #ClassicalProblem.java -Introduced the bias property, which refers to the bias in the source power between the front and rear surfaces #Problem.java -Added ABS transformation for the MAXTEMP property in optimisationVector() -Added parameter bounds for the HEAT_LOSS property, linking them to the parameter bounds retrieved from the XML file -Removed unnecessary method calls and conditional statement in setHeatLossParameter() -Added check for malformed property values in assign(…). This is expected to be called by all subclasses of Problem #NonlinearProblem.java -Emissivity is now calculated following every call to assign(…), not just for selected properties #RadiativeTransferSolver.java -Added flux.init() call in init(…), when fluxes are not null #FixedPointIterations.java -Added check for malformed temperature value array. This checks for non-finite elements or sum of elements exceeding a maximum threshold and throws a SolverException #ExplicitCoupledSolver, ImplicitCoupledSolver, MixedCoupledSolver -Improved exception handling and monitoring of calculation health #IndexRange.java - Fixed an erratic statement in the closest(…) method where an exception would be thrown when sizeMinusOne were less than 1 #Fluxes.java - Added checkArrays() method, which checks whether the fluxes are finite - Introduced an overridable init() method #FluxesAndExplicitDerivatives.java - Instead of having an overriden setDensity() method, the latter is set final, with the init() method now overriding superclass method - Introduced #ThermoOpticalProperties - New overriden toString() method that outputs all thermo-optical properties #ExperimentalData - Changed MAX_REDUCTION_FACTOR to 256 - Changed calculateHalfTime(), where the running-average curve would be calculated iteratively changing the reduction factor until certain conditions are met #SearchTask.java - Added null check in addListeners(…) - run() in several places: replaced confusing System.err on SolverException by a detailed status update with the notifyFailedStatus(…) call #DifferenceScheme.java - Important change to runTimeSequence(…), where, after filling the solution curve array, the nominal number of points is replaced by the real number of points, should that number be smaller than the former. #Launcher.java - Added file lock to prevent launching multiple instances of PULsE simultaneously #NonlinearProblem.java - Added LASER_ENERGY as a possible search variable #LMOptimiser.java - Added check for malformed parameter vectors and gradient vectors, causing a SolverException to be thrown if found #Calculation.java - process() will first check for malformed properties before proceeding with the calculation #Details.java - added SOLVER_ERROR #NumericProperties.java - added checks for non-finite values in isValueSensible(...) #Other classes: Made some getter and setter methods final to improve encapsulation --- .../java/pulse/input/ExperimentalData.java | 30 +++-- src/main/java/pulse/input/IndexRange.java | 29 +++-- .../pulse/input/InterpolationDataset.java | 1 - .../pulse/io/readers/NetzschCSVReader.java | 1 - src/main/java/pulse/math/ParameterVector.java | 5 +- .../schemes/CoupledImplicitScheme.java | 21 ++-- .../problem/schemes/DifferenceScheme.java | 47 ++++---- .../problem/schemes/FixedPointIterations.java | 26 +++-- .../pulse/problem/schemes/ImplicitScheme.java | 12 +- .../problem/schemes/OneDimensionalScheme.java | 9 +- .../schemes/RadiativeTransferCoupling.java | 4 +- .../pulse/problem/schemes/rte/Fluxes.java | 22 +++- .../rte/FluxesAndExplicitDerivatives.java | 9 +- .../schemes/rte/RTECalculationStatus.java | 9 +- .../schemes/rte/RadiativeTransferSolver.java | 1 + .../rte/dom/DiscreteOrdinatesMethod.java | 6 +- .../schemes/rte/dom/DiscreteQuantities.java | 2 +- .../schemes/rte/dom/IterativeSolver.java | 4 - .../schemes/rte/dom/PhaseFunction.java | 2 +- .../schemes/solvers/ADILinearisedSolver.java | 2 +- .../solvers/ExplicitCoupledSolver.java | 30 ++--- .../solvers/ExplicitLinearisedSolver.java | 10 +- .../solvers/ExplicitNonlinearSolver.java | 4 +- .../solvers/ImplicitCoupledSolver.java | 4 +- .../solvers/ImplicitDiathermicSolver.java | 2 +- .../solvers/ImplicitLinearisedSolver.java | 11 +- .../solvers/ImplicitNonlinearSolver.java | 9 +- .../solvers/ImplicitTranslucentSolver.java | 4 +- .../schemes/solvers/MixedCoupledSolver.java | 31 ++--- .../solvers/MixedLinearisedSolver.java | 2 +- .../problem/statements/ClassicalProblem.java | 78 +++++++++++++ .../problem/statements/DiathermicMedium.java | 2 +- .../problem/statements/NonlinearProblem.java | 76 ++++++------- .../statements/ParticipatingMedium.java | 30 ++--- .../pulse/problem/statements/Problem.java | 36 +++--- .../statements/model/ThermalProperties.java | 37 ++++-- .../model/ThermoOpticalProperties.java | 20 ++-- .../pulse/properties/NumericProperties.java | 17 +-- .../properties/NumericPropertyKeyword.java | 9 +- .../direction/CompositePathOptimiser.java | 5 + .../pulse/search/direction/LMOptimiser.java | 17 ++- .../pulse/search/direction/PathOptimiser.java | 4 +- src/main/java/pulse/tasks/Calculation.java | 7 ++ src/main/java/pulse/tasks/SearchTask.java | 107 ++++++++++-------- src/main/java/pulse/tasks/logs/Details.java | 4 +- .../java/pulse/tasks/logs/StateEntry.java | 4 + src/main/java/pulse/tasks/logs/Status.java | 9 ++ src/main/java/pulse/ui/Launcher.java | 55 ++++++--- src/main/resources/NumericProperty.xml | 17 ++- src/main/resources/Version.txt | 2 +- src/main/resources/images/splash.png | Bin 68380 -> 67179 bytes 51 files changed, 568 insertions(+), 317 deletions(-) diff --git a/src/main/java/pulse/input/ExperimentalData.java b/src/main/java/pulse/input/ExperimentalData.java index bbbb2e6..bdbeabf 100644 --- a/src/main/java/pulse/input/ExperimentalData.java +++ b/src/main/java/pulse/input/ExperimentalData.java @@ -12,7 +12,6 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; -import java.util.Optional; import java.util.stream.Collectors; import pulse.AbstractData; @@ -53,11 +52,13 @@ public class ExperimentalData extends AbstractData { * Scientific Instruments, 91(6), 064902. */ public final static int REDUCTION_FACTOR = 32; + + public final static int MAX_REDUCTION_FACTOR = 256; /** * A fail-safe factor. */ - public final static double FAIL_SAFE_FACTOR = 3.0; + public final static double FAIL_SAFE_FACTOR = 10.0; private static Comparator pointComparator = (p1, p2) -> valueOf(p1.getY()).compareTo(valueOf(p2.getY())); @@ -217,19 +218,28 @@ public Point2D maxAdjustedSignal() { * @see getHalfTime() */ public void calculateHalfTime() { - var degraded = runningAverage(REDUCTION_FACTOR); - var max = (max(degraded, pointComparator)); var baseline = new FlatBaseline(); baseline.fitTo(this); - - double halfMax = (max.getY() + baseline.valueAt(0)) / 2.0; - - int cutoffIndex = degraded.indexOf(max); + + int curRedFactor = REDUCTION_FACTOR/2; // reduced twofold since first operation + // in the while loop will increase it likewise + int cutoffIndex = 0; + List degraded = null; //running average + Point2D max = null; + + do { + curRedFactor *= 2; + degraded = runningAverage(curRedFactor); + max = (max(degraded, pointComparator)); + cutoffIndex = degraded.indexOf(max); + } while(cutoffIndex < 1 && curRedFactor < MAX_REDUCTION_FACTOR); + + double halfMax = (max.getY() + baseline.valueAt(0)) / 2.0; degraded = degraded.subList(0, cutoffIndex); - + int index = IndexRange.closestLeft(halfMax, degraded.stream().map(point -> point.getY()).collect(Collectors.toList())); - + if (index < 1) { System.err.println(Messages.getString("ExperimentalData.HalfRiseError")); halfTime = max(getTimeSequence()) / FAIL_SAFE_FACTOR; diff --git a/src/main/java/pulse/input/IndexRange.java b/src/main/java/pulse/input/IndexRange.java index 742d77e..bdf4288 100644 --- a/src/main/java/pulse/input/IndexRange.java +++ b/src/main/java/pulse/input/IndexRange.java @@ -186,24 +186,31 @@ public static int closestRight(double of, List in) { } private static int closest(double of, List in, boolean reverseOrder) { - int sizeMinusOne = Math.max( in.size() - 1, 0); //has to be non-negative - - if (of > in.get(sizeMinusOne)) { - return sizeMinusOne; - } + int sizeMinusOne = in.size() - 1; //has to be non-negative + + int result = 0; + + if (sizeMinusOne < 1) { + result = 0; + } else if (of > in.get(sizeMinusOne)) { + result = sizeMinusOne; + } else { + + int start = reverseOrder ? sizeMinusOne - 1 : 0; + int increment = reverseOrder ? -1 : 1; - int start = reverseOrder ? sizeMinusOne - 1 : 0; - int increment = reverseOrder ? -1 : 1; + for (int i = start; reverseOrder ? (i > -1) : (i < sizeMinusOne); i += increment) { - for (int i = start; reverseOrder ? (i > -1) : (i < sizeMinusOne); i += increment) { + if (between(of, in.get(i), in.get(i + 1))) { + result = i; + break; + } - if (between(of, in.get(i), in.get(i + 1))) { - return i; } } - return 0; + return result; } diff --git a/src/main/java/pulse/input/InterpolationDataset.java b/src/main/java/pulse/input/InterpolationDataset.java index 63c9827..3db3182 100644 --- a/src/main/java/pulse/input/InterpolationDataset.java +++ b/src/main/java/pulse/input/InterpolationDataset.java @@ -52,7 +52,6 @@ public InterpolationDataset() { */ public double interpolateAt(double key) { return interpolation.value(key); - } /** diff --git a/src/main/java/pulse/io/readers/NetzschCSVReader.java b/src/main/java/pulse/io/readers/NetzschCSVReader.java index c2e0b2a..dae82be 100644 --- a/src/main/java/pulse/io/readers/NetzschCSVReader.java +++ b/src/main/java/pulse/io/readers/NetzschCSVReader.java @@ -8,7 +8,6 @@ import java.io.FileReader; import java.io.IOException; import java.text.DecimalFormat; -import java.text.NumberFormat; import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; diff --git a/src/main/java/pulse/math/ParameterVector.java b/src/main/java/pulse/math/ParameterVector.java index ca0cb49..295f5a3 100644 --- a/src/main/java/pulse/math/ParameterVector.java +++ b/src/main/java/pulse/math/ParameterVector.java @@ -236,9 +236,8 @@ public List findMalformedElements() { var list = new ArrayList(); for (int i = 0; i < dimension(); i++) { - var property = def(getIndex(i)); - boolean sensible = NumericProperties.isValueSensible(property, get(i)); - if (!sensible) { + var property = NumericProperties.derive(getIndex(i), inverseTransform(i)); + if (!property.validate()) { list.add(property); } } diff --git a/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java b/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java index 6981853..848482b 100644 --- a/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java +++ b/src/main/java/pulse/problem/schemes/CoupledImplicitScheme.java @@ -4,16 +4,14 @@ import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; -import java.util.List; import java.util.Set; import pulse.problem.schemes.rte.RTECalculationStatus; +import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.ParticipatingMedium; import pulse.problem.statements.Problem; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import static pulse.properties.NumericPropertyKeyword.BASELINE_INTERCEPT; -import pulse.properties.Property; public abstract class CoupledImplicitScheme extends ImplicitScheme implements FixedPointIterations { @@ -37,17 +35,20 @@ public CoupledImplicitScheme(NumericProperty N, NumericProperty timeFactor, Nume } @Override - public void timeStep(final int m) { + public void timeStep(final int m) throws SolverException { pls = pulse(m); doIterations(getCurrentSolution(), nonlinearPrecision, m); } @Override - public void iteration(final int m) { + public void iteration(final int m) throws SolverException { super.timeStep(m); } - public void finaliseIteration(double[] V) { + @Override + public void finaliseIteration(double[] V) throws SolverException { + FixedPointIterations.super.finaliseIteration(V); + var rte = coupling.getRadiativeTransferEquation(); setCalculationStatus(coupling.getRadiativeTransferEquation().compute(V)); } @@ -55,13 +56,13 @@ public RadiativeTransferCoupling getCoupling() { return coupling; } - public void setCoupling(RadiativeTransferCoupling coupling) { + public final void setCoupling(RadiativeTransferCoupling coupling) { this.coupling = coupling; this.coupling.setParent(this); } @Override - public void finaliseStep() { + public void finaliseStep() throws SolverException { super.finaliseStep(); coupling.getRadiativeTransferEquation().getFluxes().store(); } @@ -104,8 +105,10 @@ public RTECalculationStatus getCalculationStatus() { return calculationStatus; } - public void setCalculationStatus(RTECalculationStatus calculationStatus) { + public void setCalculationStatus(RTECalculationStatus calculationStatus) throws SolverException { this.calculationStatus = calculationStatus; + if(calculationStatus != RTECalculationStatus.NORMAL) + throw new SolverException(calculationStatus.toString()); } public double getCurrentPulseValue() { diff --git a/src/main/java/pulse/problem/schemes/DifferenceScheme.java b/src/main/java/pulse/problem/schemes/DifferenceScheme.java index 9f54d46..09ea1cf 100644 --- a/src/main/java/pulse/problem/schemes/DifferenceScheme.java +++ b/src/main/java/pulse/problem/schemes/DifferenceScheme.java @@ -5,16 +5,14 @@ import static pulse.properties.NumericProperty.requireType; import static pulse.properties.NumericPropertyKeyword.TIME_LIMIT; -import java.util.ArrayList; -import java.util.List; import java.util.Set; import pulse.problem.laser.DiscretePulse; +import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.Problem; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; -import pulse.properties.Property; +import static pulse.properties.NumericPropertyKeyword.NUMPOINTS; import pulse.util.PropertyHolder; import pulse.util.Reflexive; @@ -119,16 +117,14 @@ protected void prepare(Problem problem) { hc.clear(); } - public void runTimeSequence(Problem problem) { + public void runTimeSequence(Problem problem) throws SolverException { runTimeSequence(problem, 0, timeLimit); var curve = problem.getHeatingCurve(); final double maxTemp = (double) problem.getProperties().getMaximumTemperature().getValue(); curve.scale(maxTemp / curve.apparentMaximum()); } - public void runTimeSequence(Problem problem, final double offset, final double endTime) { - final var grid = getGrid(); - + public void runTimeSequence(Problem problem, final double offset, final double endTime) throws SolverException { var curve = problem.getHeatingCurve(); int adjustedNumPoints = (int) curve.getNumPoints().getValue(); @@ -137,7 +133,7 @@ public void runTimeSequence(Problem problem, final double offset, final double e final double timeSegment = (endTime - startTime - offset) / problem.getProperties().timeFactor(); final double tau = grid.getTimeStep(); - for (double dt = 0, factor = 1.0; dt < tau; adjustedNumPoints *= factor) { + for (double dt = 0, factor; dt < tau; adjustedNumPoints *= factor) { dt = timeSegment / (adjustedNumPoints - 1); factor = dt / tau; timeInterval = (int) factor; @@ -164,10 +160,19 @@ public void runTimeSequence(Problem problem, final double offset, final double e curve.addPoint(nextTime, signal()); } + + /** + * If the total number of points added by the procedure + * is actually less than the pre-set number of points -- change that number + */ + + if(curve.actualNumPoints() < (int)curve.getNumPoints().getValue()) { + curve.setNumPoints(derive(NUMPOINTS, curve.actualNumPoints())); + } } - private void timeSegment(final int m1, final int m2) { + private void timeSegment(final int m1, final int m2) throws SolverException { for (int m = m1; m < m2 && normalOperation(); m++) { timeStep(m); finaliseStep(); @@ -180,9 +185,9 @@ public double pulse(final int m) { public abstract double signal(); - public abstract void timeStep(final int m); + public abstract void timeStep(final int m) throws SolverException; - public abstract void finaliseStep(); + public abstract void finaliseStep() throws SolverException; public boolean normalOperation() { return true; @@ -209,7 +214,7 @@ public String toString() { * @return the discrete pulse * @see pulse.problem.statements.Pulse */ - public DiscretePulse getDiscretePulse() { + public final DiscretePulse getDiscretePulse() { return discretePulse; } @@ -219,7 +224,7 @@ public DiscretePulse getDiscretePulse() { * * @return the grid */ - public Grid getGrid() { + public final Grid getGrid() { return grid; } @@ -228,7 +233,7 @@ public Grid getGrid() { * * @param grid the grid */ - public void setGrid(Grid grid) { + public final void setGrid(Grid grid) { this.grid = grid; this.grid.setParent(this); } @@ -240,7 +245,7 @@ public void setGrid(Grid grid) { * * @return the time interval */ - public int getTimeInterval() { + public final int getTimeInterval() { return timeInterval; } @@ -249,7 +254,7 @@ public int getTimeInterval() { * * @param timeInterval a positive integer. */ - public void setTimeInterval(int timeInterval) { + public final void setTimeInterval(int timeInterval) { this.timeInterval = timeInterval; } @@ -259,7 +264,7 @@ public void setTimeInterval(int timeInterval) { * need to be displayed. */ @Override - public boolean areDetailsHidden() { + public final boolean areDetailsHidden() { return hideDetailedAdjustment; } @@ -269,7 +274,7 @@ public boolean areDetailsHidden() { * * @param b a boolean. */ - public static void setDetailsHidden(boolean b) { + public final static void setDetailsHidden(boolean b) { hideDetailedAdjustment = b; } @@ -281,7 +286,7 @@ public static void setDetailsHidden(boolean b) { * @return the {@code NumericProperty} with the type {@code TIME_LIMIT} * @see pulse.properties.NumericPropertyKeyword */ - public NumericProperty getTimeLimit() { + public final NumericProperty getTimeLimit() { return derive(TIME_LIMIT, timeLimit); } @@ -294,7 +299,7 @@ public NumericProperty getTimeLimit() { * {@code TIME_LIMIT} * @see pulse.properties.NumericPropertyKeyword */ - public void setTimeLimit(NumericProperty timeLimit) { + public final void setTimeLimit(NumericProperty timeLimit) { requireType(timeLimit, TIME_LIMIT); this.timeLimit = (double) timeLimit.getValue(); firePropertyChanged(this, timeLimit); diff --git a/src/main/java/pulse/problem/schemes/FixedPointIterations.java b/src/main/java/pulse/problem/schemes/FixedPointIterations.java index 221fe50..f2c3a1a 100644 --- a/src/main/java/pulse/problem/schemes/FixedPointIterations.java +++ b/src/main/java/pulse/problem/schemes/FixedPointIterations.java @@ -1,6 +1,8 @@ package pulse.problem.schemes; import static java.lang.Math.abs; +import java.util.Arrays; +import pulse.problem.schemes.solvers.SolverException; /** * @see Wiki @@ -10,18 +12,19 @@ public interface FixedPointIterations { /** - * Performs iterations until the convergence criterion is satisfied. The - * latter consists in having a difference two consequent iterations of V - * less than the specified error. At the end of each iteration, calls - * {@code finaliseIteration()}. + * Performs iterations until the convergence criterion is satisfied.The + latter consists in having a difference two consequent iterations of V + less than the specified error. At the end of each iteration, calls + {@code finaliseIteration()}. * * @param V the calculation array * @param error used in the convergence criterion * @param m time step + * @throws pulse.problem.schemes.solvers.SolverException if the calculation failed * @see finaliseIteration() * @see iteration() */ - public default void doIterations(double[] V, final double error, final int m) { + public default void doIterations(double[] V, final double error, final int m) throws SolverException { final int N = V.length - 1; @@ -39,16 +42,21 @@ public default void doIterations(double[] V, final double error, final int m) { * Performs an iteration at time {@code m} * * @param m time step + * @throws pulse.problem.schemes.solvers.SolverException if the calculation failed */ - public void iteration(final int m); + public void iteration(final int m) throws SolverException; /** - * Finalises the current iteration. By default, does nothing. + * Finalises the current iteration.By default, does nothing. * * @param V the current iteration + * @throws pulse.problem.schemes.solvers.SolverException if the calculation failed */ - public default void finaliseIteration(double[] V) { - // do nothing + public default void finaliseIteration(double[] V) throws SolverException { + final double threshold = 1E6; + double sum = Arrays.stream(V).sum(); + if( sum > threshold || !Double.isFinite(sum) ) + throw new SolverException("Invalid solution values in V array"); } } diff --git a/src/main/java/pulse/problem/schemes/ImplicitScheme.java b/src/main/java/pulse/problem/schemes/ImplicitScheme.java index f7b73bb..ae80435 100644 --- a/src/main/java/pulse/problem/schemes/ImplicitScheme.java +++ b/src/main/java/pulse/problem/schemes/ImplicitScheme.java @@ -1,5 +1,6 @@ package pulse.problem.schemes; +import pulse.problem.schemes.solvers.SolverException; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.GRID_DENSITY; import static pulse.properties.NumericPropertyKeyword.TAU_FACTOR; @@ -65,8 +66,17 @@ protected void prepare(Problem problem) { tridiagonal = new TridiagonalMatrixAlgorithm(getGrid()); } + /** + * Calculates the solution at the boundaries using the boundary conditions + * specific to the problem statement and runs the tridiagonal matrix algorithm + * to evaluate solution at the intermediate grid points. + * @param m the time step + * @throws SolverException if the calculation failed + * @see leftBoundary(), evalRightBoundary(), pulse.problem.schemes.TridiagonalMatrixAlgorithm.sweep() + */ + @Override - public void timeStep(final int m) { + public void timeStep(final int m) throws SolverException { leftBoundary(m); final var V = getCurrentSolution(); final int N = V.length - 1; diff --git a/src/main/java/pulse/problem/schemes/OneDimensionalScheme.java b/src/main/java/pulse/problem/schemes/OneDimensionalScheme.java index d899204..54b3395 100644 --- a/src/main/java/pulse/problem/schemes/OneDimensionalScheme.java +++ b/src/main/java/pulse/problem/schemes/OneDimensionalScheme.java @@ -1,5 +1,6 @@ package pulse.problem.schemes; +import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.Problem; import pulse.properties.NumericProperty; @@ -29,8 +30,14 @@ public double signal() { return V[V.length - 1]; } + /** + * Overwrites previously calculated temperature values with the calculations + * made at the current time step + * @throws SolverException if the calculation failed + */ + @Override - public void finaliseStep() { + public void finaliseStep() throws SolverException { System.arraycopy(V, 0, U, 0, V.length); } diff --git a/src/main/java/pulse/problem/schemes/RadiativeTransferCoupling.java b/src/main/java/pulse/problem/schemes/RadiativeTransferCoupling.java index e3e6a8a..73801b7 100644 --- a/src/main/java/pulse/problem/schemes/RadiativeTransferCoupling.java +++ b/src/main/java/pulse/problem/schemes/RadiativeTransferCoupling.java @@ -1,7 +1,5 @@ package pulse.problem.schemes; -import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import pulse.problem.schemes.rte.RadiativeTransferSolver; @@ -38,7 +36,7 @@ public void init(ParticipatingMedium problem, Grid grid) { newRTE(problem, grid); rte.init(problem, grid); }); - + } else { rte.init(problem, grid); } diff --git a/src/main/java/pulse/problem/schemes/rte/Fluxes.java b/src/main/java/pulse/problem/schemes/rte/Fluxes.java index 0dff546..7c3bc78 100644 --- a/src/main/java/pulse/problem/schemes/rte/Fluxes.java +++ b/src/main/java/pulse/problem/schemes/rte/Fluxes.java @@ -1,5 +1,8 @@ package pulse.problem.schemes.rte; +import java.util.Arrays; +import static pulse.problem.schemes.rte.RTECalculationStatus.INVALID_FLUXES; +import static pulse.problem.schemes.rte.RTECalculationStatus.NORMAL; import pulse.properties.NumericProperty; public abstract class Fluxes implements DerivativeCalculator { @@ -20,6 +23,17 @@ public Fluxes(NumericProperty gridDensity, NumericProperty opticalThickness) { public void store() { System.arraycopy(fluxes, 0, storedFluxes, 0, N + 1); // store previous results } + + /** + * Checks whether all stored values are finite. This is equivalent to summing + * all elements and checking whether the sum if finite. + * @return {@code true} if the elements are finite. + */ + + public RTECalculationStatus checkArrays() { + double sum = Arrays.stream(fluxes).sum() + Arrays.stream(storedFluxes).sum(); + return Double.isFinite(sum) ? NORMAL : INVALID_FLUXES; + } /** * Retrieves the currently calculated flux at the {@code i} grid point @@ -63,13 +77,17 @@ public double getOpticalThickness() { return opticalThickness; } - public void setDensity(NumericProperty gridDensity) { + public final void setDensity(NumericProperty gridDensity) { this.N = (int) gridDensity.getValue(); + init(); + } + + public void init() { fluxes = new double[N + 1]; storedFluxes = new double[N + 1]; } - public void setOpticalThickness(NumericProperty opticalThickness) { + public final void setOpticalThickness(NumericProperty opticalThickness) { this.opticalThickness = (double) opticalThickness.getValue(); } diff --git a/src/main/java/pulse/problem/schemes/rte/FluxesAndExplicitDerivatives.java b/src/main/java/pulse/problem/schemes/rte/FluxesAndExplicitDerivatives.java index 0d16403..2b4ac0f 100644 --- a/src/main/java/pulse/problem/schemes/rte/FluxesAndExplicitDerivatives.java +++ b/src/main/java/pulse/problem/schemes/rte/FluxesAndExplicitDerivatives.java @@ -1,5 +1,8 @@ package pulse.problem.schemes.rte; +import java.util.Arrays; +import static pulse.problem.schemes.rte.RTECalculationStatus.INVALID_FLUXES; +import static pulse.problem.schemes.rte.RTECalculationStatus.NORMAL; import pulse.properties.NumericProperty; public class FluxesAndExplicitDerivatives extends Fluxes { @@ -10,10 +13,10 @@ public class FluxesAndExplicitDerivatives extends Fluxes { public FluxesAndExplicitDerivatives(NumericProperty gridDensity, NumericProperty opticalThickness) { super(gridDensity, opticalThickness); } - + @Override - public void setDensity(NumericProperty gridDensity) { - super.setDensity(gridDensity); + public void init() { + super.init(); fd = new double[getDensity() + 1]; fdStored = new double[getDensity() + 1]; } diff --git a/src/main/java/pulse/problem/schemes/rte/RTECalculationStatus.java b/src/main/java/pulse/problem/schemes/rte/RTECalculationStatus.java index 06d7b40..f579004 100644 --- a/src/main/java/pulse/problem/schemes/rte/RTECalculationStatus.java +++ b/src/main/java/pulse/problem/schemes/rte/RTECalculationStatus.java @@ -21,5 +21,12 @@ public enum RTECalculationStatus { /** * The grid density required to reach the error threshold was too large. */ - GRID_TOO_LARGE; + GRID_TOO_LARGE, + + /** + * The radiative fluxes contain illegal values. + */ + + INVALID_FLUXES; + } diff --git a/src/main/java/pulse/problem/schemes/rte/RadiativeTransferSolver.java b/src/main/java/pulse/problem/schemes/rte/RadiativeTransferSolver.java index 98dd903..bb23d09 100644 --- a/src/main/java/pulse/problem/schemes/rte/RadiativeTransferSolver.java +++ b/src/main/java/pulse/problem/schemes/rte/RadiativeTransferSolver.java @@ -55,6 +55,7 @@ public RadiativeTransferSolver() { public void init(ParticipatingMedium p, Grid grid) { if (fluxes != null) { fluxes.setDensity(grid.getGridDensity()); + fluxes.init(); var properties = (ThermoOpticalProperties) p.getProperties(); fluxes.setOpticalThickness(properties.getOpticalThickness()); } diff --git a/src/main/java/pulse/problem/schemes/rte/dom/DiscreteOrdinatesMethod.java b/src/main/java/pulse/problem/schemes/rte/dom/DiscreteOrdinatesMethod.java index 92031b5..5ad9d0d 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/DiscreteOrdinatesMethod.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/DiscreteOrdinatesMethod.java @@ -77,7 +77,7 @@ public RTECalculationStatus compute(double[] tempArray) { if (status == RTECalculationStatus.NORMAL) { fluxesAndDerivatives(tempArray.length); } - + fireStatusUpdate(status); return status; } @@ -90,9 +90,11 @@ private void fluxesAndDerivatives(final int nExclusive) { var fluxes = (FluxesAndExplicitDerivatives) getFluxes(); for (int i = 0; i < nExclusive; i++) { - fluxes.setFlux(i, DOUBLE_PI * discrete.firstMoment(interpolation[0], i)); + double flux = DOUBLE_PI * discrete.firstMoment(interpolation[0], i); + fluxes.setFlux(i, flux); fluxes.setFluxDerivative(i, -DOUBLE_PI * discrete.firstMoment(interpolation[1], i)); } + } @Override diff --git a/src/main/java/pulse/problem/schemes/rte/dom/DiscreteQuantities.java b/src/main/java/pulse/problem/schemes/rte/dom/DiscreteQuantities.java index bdcf5d3..548e488 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/DiscreteQuantities.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/DiscreteQuantities.java @@ -5,7 +5,7 @@ * This includes the various intensity and flux arrays used internally by the * integrators. */ -class DiscreteQuantities { +public class DiscreteQuantities { private double[][] I; private double[][] Ik; diff --git a/src/main/java/pulse/problem/schemes/rte/dom/IterativeSolver.java b/src/main/java/pulse/problem/schemes/rte/dom/IterativeSolver.java index 66cf0f0..6bfb45c 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/IterativeSolver.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/IterativeSolver.java @@ -6,15 +6,11 @@ import static pulse.properties.NumericPropertyKeyword.DOM_ITERATION_ERROR; import static pulse.properties.NumericPropertyKeyword.RTE_MAX_ITERATIONS; -import java.util.ArrayList; -import java.util.List; import java.util.Set; import pulse.problem.schemes.rte.RTECalculationStatus; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import static pulse.properties.NumericPropertyKeyword.NONLINEAR_PRECISION; -import pulse.properties.Property; import pulse.util.PropertyHolder; import pulse.util.Reflexive; diff --git a/src/main/java/pulse/problem/schemes/rte/dom/PhaseFunction.java b/src/main/java/pulse/problem/schemes/rte/dom/PhaseFunction.java index 6ed02e4..0892aab 100644 --- a/src/main/java/pulse/problem/schemes/rte/dom/PhaseFunction.java +++ b/src/main/java/pulse/problem/schemes/rte/dom/PhaseFunction.java @@ -6,7 +6,7 @@ public abstract class PhaseFunction implements Reflexive { - private Discretisation intensities; + private final Discretisation intensities; private double anisotropy; private double halfAlbedo; diff --git a/src/main/java/pulse/problem/schemes/solvers/ADILinearisedSolver.java b/src/main/java/pulse/problem/schemes/solvers/ADILinearisedSolver.java index e24120f..7ce79f9 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ADILinearisedSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ADILinearisedSolver.java @@ -162,7 +162,7 @@ private void initConst() { } @Override - public void solve(ClassicalProblem2D problem) { + public void solve(ClassicalProblem2D problem) throws SolverException { prepare(problem); runTimeSequence(problem); } diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java index b41cb03..40927d7 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitCoupledSolver.java @@ -47,7 +47,7 @@ public ExplicitCoupledSolver(NumericProperty N, NumericProperty timeFactor) { status = RTECalculationStatus.NORMAL; } - private void prepare(ParticipatingMedium problem) { + private void prepare(ParticipatingMedium problem) throws SolverException { super.prepare(problem); var grid = getGrid(); @@ -55,7 +55,8 @@ private void prepare(ParticipatingMedium problem) { coupling.init(problem, grid); rte = coupling.getRadiativeTransferEquation(); fluxes = coupling.getRadiativeTransferEquation().getFluxes(); - + setCalculationStatus(fluxes.checkArrays()); + N = (int) grid.getGridDensity().getValue(); hx = grid.getXStep(); @@ -74,13 +75,9 @@ private void prepare(ParticipatingMedium problem) { @Override public void solve(ParticipatingMedium problem) throws SolverException { - this.prepare(problem); - status = coupling.getRadiativeTransferEquation().compute(getPreviousSolution()); + this.prepare(problem); + setCalculationStatus(coupling.getRadiativeTransferEquation().compute(getPreviousSolution())); runTimeSequence(problem); - - if (status != RTECalculationStatus.NORMAL) { - throw new SolverException(status.toString()); - } } @Override @@ -89,7 +86,7 @@ public boolean normalOperation() { } @Override - public void timeStep(int m) { + public void timeStep(int m) throws SolverException { pls = pulse(m); doIterations(getCurrentSolution(), nonlinearPrecision, m); } @@ -111,8 +108,9 @@ public void iteration(final int m) { } @Override - public void finaliseIteration(double[] V) { - status = rte.compute(V); + public void finaliseIteration(double[] V) throws SolverException { + FixedPointIterations.super.finaliseIteration(V); + setCalculationStatus(rte.compute(V)); } @Override @@ -121,7 +119,7 @@ public double phi(final int i) { } @Override - public void finaliseStep() { + public void finaliseStep() throws SolverException { super.finaliseStep(); coupling.getRadiativeTransferEquation().getFluxes().store(); } @@ -130,7 +128,7 @@ public RadiativeTransferCoupling getCoupling() { return coupling; } - public void setCoupling(RadiativeTransferCoupling coupling) { + public final void setCoupling(RadiativeTransferCoupling coupling) { this.coupling = coupling; this.coupling.setParent(this); } @@ -155,5 +153,11 @@ public DifferenceScheme copy() { public Class domain() { return ParticipatingMedium.class; } + + public void setCalculationStatus(RTECalculationStatus calculationStatus) throws SolverException { + this.status = calculationStatus; + if(status != RTECalculationStatus.NORMAL) + throw new SolverException(status.toString()); + } } diff --git a/src/main/java/pulse/problem/schemes/solvers/ExplicitLinearisedSolver.java b/src/main/java/pulse/problem/schemes/solvers/ExplicitLinearisedSolver.java index 3e0fc4c..5d3a6f9 100644 --- a/src/main/java/pulse/problem/schemes/solvers/ExplicitLinearisedSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/ExplicitLinearisedSolver.java @@ -49,6 +49,7 @@ public class ExplicitLinearisedSolver extends ExplicitScheme implements Solver { private RadiativeTransferSolver rte; - private Fluxes fluxes; private int N; private double hx; @@ -64,7 +58,7 @@ public MixedCoupledSolver(NumericProperty N, NumericProperty timeFactor, Numeric sigma = (double) def(SCHEME_WEIGHT).getValue(); } - private void prepare(ParticipatingMedium problem) { + private void prepare(ParticipatingMedium problem) throws SolverException { super.prepare(problem); var grid = getGrid(); @@ -73,31 +67,30 @@ private void prepare(ParticipatingMedium problem) { coupling.init(problem, grid); rte = coupling.getRadiativeTransferEquation(); - var U = getPreviousSolution(); - N = (int) grid.getGridDensity().getValue(); hx = grid.getXStep(); tau = grid.getTimeStep(); Bi1 = (double) problem.getProperties().getHeatLoss().getValue(); - - fluxes = rte.getFluxes(); - + var tridiagonal = new TridiagonalMatrixAlgorithm(grid) { @Override public double phi(int i) { + var fluxes = rte.getFluxes(); return A * fluxes.meanFluxDerivative(i) + B * (fluxes.meanFluxDerivative(i - 1) + fluxes.meanFluxDerivative(i + 1)); } @Override public double beta(final double f, final double phi, final int i) { + var U = getPreviousSolution(); return super.beta(f + ONE_MINUS_SIGMA * (U[i] - 2.0 * U[i - 1] + U[i - 2]) / HX2, TAU0_NP * phi, i); } - + @Override public void evaluateBeta(final double[] U) { + var fluxes = rte.getFluxes(); final double phiSecond = A * fluxes.meanFluxDerivative(1) + B * (fluxes.meanFluxDerivativeFront() + fluxes.meanFluxDerivative(2)); setBeta(2, beta(U[1] / tau, phiSecond, 2)); @@ -150,15 +143,7 @@ private void initConst(ParticipatingMedium problem) { public void solve(ParticipatingMedium problem) throws SolverException { this.prepare(problem); initConst(problem); - - setCalculationStatus(rte.compute(getPreviousSolution())); this.runTimeSequence(problem); - - var status = getCalculationStatus(); - if (status != RTECalculationStatus.NORMAL) { - throw new SolverException(status.toString()); - } - } @Override @@ -171,6 +156,7 @@ public double pulse(final int m) { @Override public double firstBeta(final int m) { + var fluxes = rte.getFluxes(); var U = getPreviousSolution(); final double phi = TAU0_NP * fluxes.fluxDerivativeFront(); return (_2TAUHX @@ -180,6 +166,7 @@ public double firstBeta(final int m) { @Override public double evalRightBoundary(final int m, final double alphaN, final double betaN) { + var fluxes = rte.getFluxes(); final double phi = TAU0_NP * fluxes.fluxDerivativeRear(); final var U = getPreviousSolution(); return (sigma * betaN + HX2_2TAU * U[N] + 0.5 * HX2 * phi @@ -229,4 +216,4 @@ public DifferenceScheme copy() { return new MixedCoupledSolver(grid.getGridDensity(), grid.getTimeFactor(), getTimeLimit()); } -} +} \ No newline at end of file diff --git a/src/main/java/pulse/problem/schemes/solvers/MixedLinearisedSolver.java b/src/main/java/pulse/problem/schemes/solvers/MixedLinearisedSolver.java index 2816684..979d50f 100644 --- a/src/main/java/pulse/problem/schemes/solvers/MixedLinearisedSolver.java +++ b/src/main/java/pulse/problem/schemes/solvers/MixedLinearisedSolver.java @@ -139,7 +139,7 @@ public double firstBeta(final int m) { } @Override - public void solve(ClassicalProblem problem) { + public void solve(ClassicalProblem problem) throws SolverException { this.prepare(problem); runTimeSequence(problem); } diff --git a/src/main/java/pulse/problem/statements/ClassicalProblem.java b/src/main/java/pulse/problem/statements/ClassicalProblem.java index 381e33a..72796cc 100644 --- a/src/main/java/pulse/problem/statements/ClassicalProblem.java +++ b/src/main/java/pulse/problem/statements/ClassicalProblem.java @@ -1,8 +1,21 @@ package pulse.problem.statements; +import java.util.List; +import java.util.Set; +import pulse.math.ParameterVector; +import pulse.math.Segment; +import pulse.math.transforms.StickTransform; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.solvers.ImplicitLinearisedSolver; +import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.model.ThermalProperties; +import pulse.properties.Flag; +import static pulse.properties.NumericProperties.def; +import static pulse.properties.NumericProperties.derive; +import pulse.properties.NumericProperty; +import static pulse.properties.NumericProperty.requireType; +import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.SOURCE_GEOMETRIC_FACTOR; import pulse.ui.Messages; /** @@ -12,16 +25,27 @@ */ public class ClassicalProblem extends Problem { + private double bias; + public ClassicalProblem() { super(); + bias = (double) def(SOURCE_GEOMETRIC_FACTOR).getValue(); setPulse(new Pulse()); } public ClassicalProblem(Problem p) { super(p); + bias = (double) def(SOURCE_GEOMETRIC_FACTOR).getValue(); setPulse(new Pulse(p.getPulse())); } + @Override + public Set listedKeywords() { + var set = super.listedKeywords(); + set.add(SOURCE_GEOMETRIC_FACTOR); + return set; + } + @Override public Class defaultScheme() { return ImplicitLinearisedSolver.class; @@ -52,4 +76,58 @@ public Problem copy() { return new ClassicalProblem(this); } + public NumericProperty getGeometricFactor() { + return derive(SOURCE_GEOMETRIC_FACTOR, bias); + } + + public void setGeometricFactor(NumericProperty bias) { + requireType(bias, SOURCE_GEOMETRIC_FACTOR); + this.bias = (double) bias.getValue(); + firePropertyChanged(this, bias); + } + + @Override + public void set(NumericPropertyKeyword type, NumericProperty value) { + super.set(type, value); + if (type == SOURCE_GEOMETRIC_FACTOR) { + setGeometricFactor(value); + } + } + + @Override + public void optimisationVector(ParameterVector output, List flags) { + + super.optimisationVector(output, flags); + + for (int i = 0, size = output.dimension(); i < size; i++) { + + var key = output.getIndex(i); + + if (key == SOURCE_GEOMETRIC_FACTOR) { + var bounds = Segment.boundsFrom(SOURCE_GEOMETRIC_FACTOR); + output.setParameterBounds(i, bounds); + output.setTransform(i, new StickTransform(bounds)); + output.set(i, bias); + } + + } + + } + + @Override + public void assign(ParameterVector params) throws SolverException { + super.assign(params); + for (int i = 0, size = params.dimension(); i < size; i++) { + + double value = params.get(i); + var key = params.getIndex(i); + + if (key == SOURCE_GEOMETRIC_FACTOR) { + setGeometricFactor(derive(SOURCE_GEOMETRIC_FACTOR, value)); + } + + } + + } + } diff --git a/src/main/java/pulse/problem/statements/DiathermicMedium.java b/src/main/java/pulse/problem/statements/DiathermicMedium.java index a34c744..b2aca28 100644 --- a/src/main/java/pulse/problem/statements/DiathermicMedium.java +++ b/src/main/java/pulse/problem/statements/DiathermicMedium.java @@ -93,7 +93,7 @@ public void assign(ParameterVector params) throws SolverException { break; case HEAT_LOSS: if (properties.areThermalPropertiesLoaded()) { - properties.emissivity(); + properties.calculateEmissivity(); final double emissivity = (double) properties.getEmissivity().getValue(); properties .setDiathermicCoefficient(derive(DIATHERMIC_COEFFICIENT, emissivity / (2.0 - emissivity))); diff --git a/src/main/java/pulse/problem/statements/NonlinearProblem.java b/src/main/java/pulse/problem/statements/NonlinearProblem.java index 3bfc634..5097ab3 100644 --- a/src/main/java/pulse/problem/statements/NonlinearProblem.java +++ b/src/main/java/pulse/problem/statements/NonlinearProblem.java @@ -1,26 +1,26 @@ package pulse.problem.statements; +import java.util.List; import static pulse.properties.NumericProperties.derive; import static pulse.properties.NumericPropertyKeyword.CONDUCTIVITY; import static pulse.properties.NumericPropertyKeyword.DENSITY; -import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS; import static pulse.properties.NumericPropertyKeyword.SPECIFIC_HEAT; import static pulse.properties.NumericPropertyKeyword.SPOT_DIAMETER; import static pulse.properties.NumericPropertyKeyword.TEST_TEMPERATURE; -import java.util.List; import java.util.Set; import pulse.input.ExperimentalData; import pulse.math.ParameterVector; import pulse.math.Segment; -import pulse.math.transforms.StandardTransformations; +import pulse.math.transforms.StickTransform; import pulse.problem.schemes.DifferenceScheme; import pulse.problem.schemes.ImplicitScheme; import pulse.problem.schemes.solvers.SolverException; import pulse.properties.Flag; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; +import static pulse.properties.NumericPropertyKeyword.LASER_ENERGY; import pulse.ui.Messages; public class NonlinearProblem extends ClassicalProblem { @@ -66,54 +66,54 @@ public NumericProperty getThermalConductivity() { return derive(CONDUCTIVITY, getProperties().thermalConductivity()); } + /** + * + * Does the same as super-class method plus updates the laser energy, if needed. + * @param params + * @throws pulse.problem.schemes.solvers.SolverException + * @see pulse.problem.statements.Problem.getPulse() + * + */ + @Override - public void optimisationVector(ParameterVector output, List flags) { - super.optimisationVector(output, flags); - int size = output.dimension(); - var properties = getProperties(); - - for (int i = 0; i < size; i++) { - - var key = output.getIndex(i); + public void assign(ParameterVector params) throws SolverException { + super.assign(params); + getProperties().calculateEmissivity(); - if (key == HEAT_LOSS) { + for (int i = 0, size = params.dimension(); i < size; i++) { - var bounds = new Segment(0.0, properties.maxBiot()); - final double Bi1 = (double) properties.getHeatLoss().getValue(); - output.setTransform(i, StandardTransformations.ABS); - output.set(i, Bi1); - output.setParameterBounds(i, bounds); + double value = params.inverseTransform(i); + NumericPropertyKeyword key = params.getIndex(i); + if (key == LASER_ENERGY) { + this.getPulse().setLaserEnergy(derive(key, value)); } } - } - + /** - * Assigns parameter values of this {@code Problem} using the optimisation - * vector {@code params}. Only those parameters will be updated, the types - * of which are listed as indices in the {@code params} vector. - * - * @param params the optimisation vector, containing a similar set of - * parameters to this {@code Problem} - * @throws SolverException - * @see listedTypes() + * + * Does the same as super-class method plus extracts the laser energy and stores it in the {@code output}, if needed. + * @param output + * @param flags + * @see pulse.problem.statements.Problem.getPulse() + * */ + @Override - public void assign(ParameterVector params) throws SolverException { - super.assign(params); - var p = getProperties(); - - for (int i = 0, size = params.dimension(); i < size; i++) { - - var key = params.getIndex(i); - - if (key == HEAT_LOSS) { + public void optimisationVector(ParameterVector output, List flags) { + super.optimisationVector(output, flags); + + for (int i = 0, size = output.dimension(); i < size; i++) { - p.setHeatLoss(derive(HEAT_LOSS, params.inverseTransform(i))); - p.emissivity(); + var key = output.getIndex(i); + if(key == LASER_ENERGY) { + var bounds = Segment.boundsFrom(LASER_ENERGY); + output.setParameterBounds(i, bounds); + output.setTransform(i, new StickTransform(bounds)); + output.set(i, (double) getPulse().getLaserEnergy().getValue()); } } diff --git a/src/main/java/pulse/problem/statements/ParticipatingMedium.java b/src/main/java/pulse/problem/statements/ParticipatingMedium.java index 2d60b1d..5674dcc 100644 --- a/src/main/java/pulse/problem/statements/ParticipatingMedium.java +++ b/src/main/java/pulse/problem/statements/ParticipatingMedium.java @@ -1,14 +1,11 @@ package pulse.problem.statements; -import static pulse.math.transforms.StandardTransformations.LOG; import static pulse.properties.NumericProperties.derive; -import static pulse.properties.NumericPropertyKeyword.NUMPOINTS; import java.util.List; import pulse.math.ParameterVector; import pulse.math.Segment; -import pulse.math.transforms.AtanhTransform; import pulse.math.transforms.StickTransform; import pulse.math.transforms.Transformable; import pulse.problem.schemes.DifferenceScheme; @@ -17,6 +14,8 @@ import pulse.problem.statements.model.ThermalProperties; import pulse.problem.statements.model.ThermoOpticalProperties; import pulse.properties.Flag; +import static pulse.properties.NumericPropertyKeyword.OPTICAL_THICKNESS; +import static pulse.properties.NumericPropertyKeyword.PLANCK_NUMBER; import pulse.ui.Messages; public class ParticipatingMedium extends NonlinearProblem { @@ -41,8 +40,8 @@ public void optimisationVector(ParameterVector output, List flags) { super.optimisationVector(output, flags); var properties = (ThermoOpticalProperties) getProperties(); - Segment bounds; - double value = 0; + Segment bounds = null; + double value; Transformable transform; for (int i = 0, size = output.dimension(); i < size; i++) { @@ -51,30 +50,28 @@ public void optimisationVector(ParameterVector output, List flags) { switch (key) { case PLANCK_NUMBER: - bounds = new Segment(1E-5, properties.maxNp()); + final double lowerBound = Segment.boundsFrom(PLANCK_NUMBER).getMinimum(); + bounds = new Segment(lowerBound, properties.maxNp()); value = (double) properties.getPlanckNumber().getValue(); - transform = new AtanhTransform(bounds); break; case OPTICAL_THICKNESS: value = (double) properties.getOpticalThickness().getValue(); - bounds = new Segment(1E-8, 1E5); - transform = LOG; - break; + bounds = Segment.boundsFrom(OPTICAL_THICKNESS); + break; case SCATTERING_ALBEDO: value = (double) properties.getScatteringAlbedo().getValue(); bounds = new Segment(0.0, 1.0); - transform = new StickTransform(bounds); break; case SCATTERING_ANISOTROPY: value = (double) properties.getScatteringAnisostropy().getValue(); bounds = new Segment(-1.0, 1.0); - transform = new StickTransform(bounds); break; default: continue; } + transform = new StickTransform(bounds); output.setTransform(i, transform); output.set(i, value); output.setParameterBounds(i, bounds); @@ -86,8 +83,9 @@ public void optimisationVector(ParameterVector output, List flags) { @Override public void assign(ParameterVector params) throws SolverException { super.assign(params); + var properties = (ThermoOpticalProperties) getProperties(); - + for (int i = 0, size = params.dimension(); i < size; i++) { var type = params.getIndex(i); @@ -100,17 +98,13 @@ public void assign(ParameterVector params) throws SolverException { case OPTICAL_THICKNESS: properties.set(type, derive(type, params.inverseTransform(i))); break; - case HEAT_LOSS: - case DIFFUSIVITY: - properties.emissivity(); - break; default: break; } } - + } @Override diff --git a/src/main/java/pulse/problem/statements/Problem.java b/src/main/java/pulse/problem/statements/Problem.java index 4b47ebd..d0e8adc 100644 --- a/src/main/java/pulse/problem/statements/Problem.java +++ b/src/main/java/pulse/problem/statements/Problem.java @@ -16,6 +16,7 @@ import pulse.math.Segment; import pulse.math.transforms.InvLenSqTransform; import pulse.math.transforms.StandardTransformations; +import static pulse.math.transforms.StandardTransformations.ABS; import pulse.math.transforms.StickTransform; import pulse.problem.laser.DiscretePulse; import pulse.problem.schemes.DifferenceScheme; @@ -24,7 +25,6 @@ import pulse.problem.schemes.solvers.SolverException; import pulse.problem.statements.model.ThermalProperties; import pulse.properties.Flag; -import static pulse.properties.NumericProperties.def; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; import static pulse.properties.NumericPropertyKeyword.DIFFUSIVITY; @@ -156,7 +156,7 @@ public HeatingCurve getHeatingCurve() { return curve; } - public Pulse getPulse() { + public final Pulse getPulse() { return pulse; } @@ -166,7 +166,7 @@ public Pulse getPulse() { * * @param pulse a {@code Pulse} object */ - public void setPulse(Pulse pulse) { + public final void setPulse(Pulse pulse) { this.pulse = pulse; pulse.setParent(this); } @@ -244,11 +244,13 @@ public void optimisationVector(ParameterVector output, List flags) { break; case MAXTEMP: final double signalHeight = (double) properties.getMaximumTemperature().getValue(); - output.set(i, signalHeight); + output.setTransform(i, ABS); output.setParameterBounds(i, new Segment(0.5 * signalHeight, 1.5 * signalHeight)); + output.set(i, signalHeight); break; case HEAT_LOSS: final double Bi = (double) properties.getHeatLoss().getValue(); + output.setParameterBounds(i, Segment.boundsFrom(HEAT_LOSS)); setHeatLossParameter(output, i, Bi); break; case TIME_SHIFT: @@ -257,22 +259,14 @@ public void optimisationVector(ParameterVector output, List flags) { output.setParameterBounds(i, new Segment(-magnitude, magnitude)); break; default: - continue; } } } - //TODO remove atanh transform and replace with abs protected void setHeatLossParameter(ParameterVector output, int i, double Bi) { - if (output.getTransform(i) == null) { - final double min = (double) def(HEAT_LOSS).getMinimum(); - final double max = (double) def(HEAT_LOSS).getMaximum(); - var bounds = new Segment(min, properties.areThermalPropertiesLoaded() ? properties.maxBiot() : max); - output.setTransform(i, StandardTransformations.ABS); - output.setParameterBounds(i, bounds); - } + output.setTransform(i, StandardTransformations.ABS); output.set(i, Bi); } @@ -286,6 +280,17 @@ protected void setHeatLossParameter(ParameterVector output, int i, double Bi) { @Override public void assign(ParameterVector params) throws SolverException { baseline.assign(params); + + List malformedList = params.findMalformedElements(); + + if(!malformedList.isEmpty()) { + StringBuilder sb = new StringBuilder("Cannot assign values: "); + malformedList.forEach(p -> + sb.append(String.format("%n %-25s", p.toString())) + ); + throw new SolverException(sb.toString()); + } + for (int i = 0, size = params.dimension(); i < size; i++) { double value = params.get(i); @@ -308,7 +313,6 @@ public void assign(ParameterVector params) throws SolverException { curve.set(TIME_SHIFT, derive(TIME_SHIFT, value)); break; default: - continue; } } @@ -390,11 +394,11 @@ public String toString() { return this.getClass().getSimpleName(); } - public ProblemComplexity getComplexity() { + public final ProblemComplexity getComplexity() { return complexity; } - public void setComplexity(ProblemComplexity complexity) { + public final void setComplexity(ProblemComplexity complexity) { this.complexity = complexity; } diff --git a/src/main/java/pulse/problem/statements/model/ThermalProperties.java b/src/main/java/pulse/problem/statements/model/ThermalProperties.java index 8cce847..1039f25 100644 --- a/src/main/java/pulse/problem/statements/model/ThermalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ThermalProperties.java @@ -1,6 +1,7 @@ package pulse.problem.statements.model; import static java.lang.Math.PI; +import java.util.List; import static pulse.input.InterpolationDataset.getDataset; import static pulse.input.InterpolationDataset.StandartType.HEAT_CAPACITY; import static pulse.properties.NumericProperties.def; @@ -9,10 +10,13 @@ import static pulse.properties.NumericPropertyKeyword.*; import java.util.Set; +import java.util.stream.Collectors; import pulse.input.ExperimentalData; import pulse.input.InterpolationDataset; import pulse.input.InterpolationDataset.StandartType; +import pulse.math.Segment; +import pulse.math.transforms.StickTransform; import pulse.problem.statements.Pulse2D; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; @@ -67,14 +71,20 @@ public ThermalProperties(ThermalProperties p) { fill(); } + public List findMalformedProperties() { + var list = this.numericData().stream() + .filter(np -> !np.validate()).collect(Collectors.toList()); + return list; + } + private void fill() { var rhoCurve = getDataset(StandartType.DENSITY); var cpCurve = getDataset(StandartType.HEAT_CAPACITY); if (rhoCurve != null) { - rhoCurve.interpolateAt(T); + rho = rhoCurve.interpolateAt(T); } if (cpCurve != null) { - cpCurve.interpolateAt(T); + cP = cpCurve.interpolateAt(T); } } @@ -270,15 +280,14 @@ public NumericProperty getThermalConductivity() { return derive(CONDUCTIVITY, thermalConductivity()); } - public void emissivity() { - setEmissivity(derive(EMISSIVITY, Bi * thermalConductivity() / (4. * Math.pow(T, 3) * l * STEFAN_BOTLZMAN))); - } - - public double maxBiot() { - double lambda = thermalConductivity(); - return 4.0 * STEFAN_BOTLZMAN * Math.pow(T, 3) * l / lambda; + public void calculateEmissivity() { + double newEmissivity = Bi * thermalConductivity() / (4. * Math.pow(T, 3) * l * STEFAN_BOTLZMAN); + var transform = new StickTransform(new Segment(0.01, 1.0)); + setEmissivity(derive(EMISSIVITY, + transform.transform(newEmissivity)) + ); } - + public double biot() { double lambda = thermalConductivity(); return 4.0 * emissivity * STEFAN_BOTLZMAN * Math.pow(T, 3) * l / lambda; @@ -329,7 +338,6 @@ public NumericProperty getEmissivity() { public void setEmissivity(NumericProperty e) { requireType(e, EMISSIVITY); this.emissivity = (double) e.getValue(); - setHeatLoss(derive(HEAT_LOSS, biot())); } @Override @@ -339,7 +347,12 @@ public String getDescriptor() { @Override public String toString() { - return "Show Details..."; + StringBuilder sb = new StringBuilder(getDescriptor()); + sb.append(":"); + sb.append(String.format("%n %-25s", this.getDiffusivity())); + sb.append(String.format("%n %-25s", this.getMaximumTemperature())); + sb.append(String.format("%n %-25s", this.getHeatLoss())); + return sb.toString(); } } diff --git a/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java b/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java index f6304e3..feafb90 100644 --- a/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java +++ b/src/main/java/pulse/problem/statements/model/ThermoOpticalProperties.java @@ -9,19 +9,11 @@ import static pulse.properties.NumericPropertyKeyword.SCATTERING_ALBEDO; import static pulse.properties.NumericPropertyKeyword.SCATTERING_ANISOTROPY; -import java.util.List; import java.util.Set; import pulse.input.ExperimentalData; import pulse.properties.NumericProperty; import pulse.properties.NumericPropertyKeyword; -import static pulse.properties.NumericPropertyKeyword.DENSITY; -import static pulse.properties.NumericPropertyKeyword.DIFFUSIVITY; -import static pulse.properties.NumericPropertyKeyword.HEAT_LOSS; -import static pulse.properties.NumericPropertyKeyword.MAXTEMP; -import static pulse.properties.NumericPropertyKeyword.SPECIFIC_HEAT; -import static pulse.properties.NumericPropertyKeyword.THICKNESS; -import pulse.properties.Property; public class ThermoOpticalProperties extends ThermalProperties { @@ -155,5 +147,17 @@ public void useTheoreticalEstimates(ExperimentalData c) { public String getDescriptor() { return "Thermo-Physical & Optical Properties"; } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(super.toString()); + sb.append(String.format("%n %-25s", this.getOpticalThickness())); + sb.append(String.format("%n %-25s", this.getPlanckNumber())); + sb.append(String.format("%n %-25s", this.getScatteringAlbedo())); + sb.append(String.format("%n %-25s", this.getScatteringAnisostropy())); + sb.append(String.format("%n %-25s", this.getSpecificHeat())); + sb.append(String.format("%n %-25s", this.getDensity())); + return sb.toString(); + } } diff --git a/src/main/java/pulse/properties/NumericProperties.java b/src/main/java/pulse/properties/NumericProperties.java index cdda622..cd5279e 100644 --- a/src/main/java/pulse/properties/NumericProperties.java +++ b/src/main/java/pulse/properties/NumericProperties.java @@ -42,15 +42,16 @@ public static boolean isValueSensible(NumericProperty property, Number val) { } double v = val.doubleValue(); - final double EPS = 1E-12; - - if (v > property.getMaximum().doubleValue() + EPS) { - return false; + boolean ok = true; + + if( !Double.isFinite(v) + || v > property.getMaximum().doubleValue() + EPS + || v < property.getMinimum().doubleValue() - EPS) { + ok = false; } - return v >= property.getMinimum().doubleValue() - EPS; - + return ok; } public static String printRangeAndNumber(NumericProperty p, Number value) { @@ -102,7 +103,9 @@ public static int compare(NumericProperty a, NumericProperty b) { * Searches for the default {@code NumericProperty} corresponding to * {@code keyword} in the list of pre-defined properties loaded from the * respective {@code .xml} file, and if found creates a new - * {@NumericProperty} which will replicate all field of the latter, but will + * { + * + * @NumericProperty} which will replicate all field of the latter, but will * set its value to {@code value}. * * @param keyword one of the constant {@code NumericPropertyKeyword}s diff --git a/src/main/java/pulse/properties/NumericPropertyKeyword.java b/src/main/java/pulse/properties/NumericPropertyKeyword.java index 65cdf46..c9e63cb 100644 --- a/src/main/java/pulse/properties/NumericPropertyKeyword.java +++ b/src/main/java/pulse/properties/NumericPropertyKeyword.java @@ -346,7 +346,14 @@ public enum NumericPropertyKeyword { * Levenberg-Marquardt damping ratio. A zero value presents pure Levenberg * damping. A value of 1 gives pure Marquardt damping. */ - DAMPING_RATIO; + DAMPING_RATIO, + + /** + * Determines how much weight is attributed to the front-face light source + * compared to rear face. Can be a number between zero and unity. + */ + + SOURCE_GEOMETRIC_FACTOR; public static Optional findAny(String key) { return Arrays.asList(values()).stream().filter(keys -> keys.toString().equalsIgnoreCase(key)).findAny(); diff --git a/src/main/java/pulse/search/direction/CompositePathOptimiser.java b/src/main/java/pulse/search/direction/CompositePathOptimiser.java index 99a243f..9b4c0bf 100644 --- a/src/main/java/pulse/search/direction/CompositePathOptimiser.java +++ b/src/main/java/pulse/search/direction/CompositePathOptimiser.java @@ -1,5 +1,6 @@ package pulse.search.direction; +import java.util.Arrays; import static pulse.properties.NumericProperties.compare; import java.util.List; @@ -70,6 +71,10 @@ public boolean iteration(SearchTask task) throws SolverException { // new set of parameters determined through search var candidateParams = parameters.sum(dir.multiply(step)); + if( Arrays.stream( candidateParams.getData() ).anyMatch(el -> !Double.isFinite(el) ) ) { + throw new SolverException("Illegal candidate parameters: not finite! " + p.getIteration()); + } + task.assign(new ParameterVector(parameters, candidateParams)); // assign new parameters double newCost = task.solveProblemAndCalculateCost(); // calculate the sum of squared residuals diff --git a/src/main/java/pulse/search/direction/LMOptimiser.java b/src/main/java/pulse/search/direction/LMOptimiser.java index 72c8b83..80bd5e5 100644 --- a/src/main/java/pulse/search/direction/LMOptimiser.java +++ b/src/main/java/pulse/search/direction/LMOptimiser.java @@ -1,5 +1,6 @@ package pulse.search.direction; +import java.util.Arrays; import static pulse.math.linear.SquareMatrix.asSquareMatrix; import static pulse.properties.NumericProperties.compare; import static pulse.properties.NumericProperties.def; @@ -76,8 +77,14 @@ public boolean iteration(SearchTask task) throws SolverException { var lmDirection = getSolver().direction(p); var candidate = parameters.sum(lmDirection); + + if( Arrays.stream( candidate.getData() ).anyMatch(el -> !Double.isFinite(el) ) ) { + throw new SolverException("Illegal candidate parameters: not finite! " + p.getIteration()); + } + task.assign(new ParameterVector( parameters, candidate)); // assign new parameters + double newCost = task.solveProblemAndCalculateCost(); // calculate the sum of squared residuals /* @@ -123,7 +130,11 @@ public void prepare(SearchTask task) throws SolverException { // the Jacobian is then used to calculate the 'gradient' Vector g1 = halfGradient(p); // g1 p.setGradient(g1); - + + if(Arrays.stream(g1.getData()).anyMatch(v -> !Double.isFinite(v))) { + throw new SolverException("Could not calculate objective function gradient"); + } + // the Hessian is then regularised by adding labmda*I var hessian = p.getNonregularisedHessian(); var damping = (levenbergDamping(hessian).multiply(dampingRatio) @@ -158,7 +169,7 @@ public void prepare(SearchTask task) throws SolverException { public RectangularMatrix jacobian(SearchTask task) throws SolverException { var residualCalculator = task.getCurrentCalculation().getOptimiserStatistic(); - + var p = ((LMPath) task.getIterativeState()); final var params = p.getParameters(); @@ -192,7 +203,7 @@ public RectangularMatrix jacobian(SearchTask task) throws SolverException { } } - + // revert to original params task.assign(params); diff --git a/src/main/java/pulse/search/direction/PathOptimiser.java b/src/main/java/pulse/search/direction/PathOptimiser.java index d033dc8..b11adc0 100644 --- a/src/main/java/pulse/search/direction/PathOptimiser.java +++ b/src/main/java/pulse/search/direction/PathOptimiser.java @@ -208,11 +208,11 @@ public static void setInstance(PathOptimiser selectedPathOptimiser) { selectedPathOptimiser.setParent(TaskManager.getManagerInstance()); } - protected DirectionSolver getSolver() { + protected final DirectionSolver getSolver() { return solver; } - protected void setSolver(DirectionSolver solver) { + protected final void setSolver(DirectionSolver solver) { this.solver = solver; } diff --git a/src/main/java/pulse/tasks/Calculation.java b/src/main/java/pulse/tasks/Calculation.java index 3863df1..7943c11 100644 --- a/src/main/java/pulse/tasks/Calculation.java +++ b/src/main/java/pulse/tasks/Calculation.java @@ -159,6 +159,13 @@ public void setScheme(DifferenceScheme scheme, ExperimentalData curve) { */ @SuppressWarnings({"unchecked", "rawtypes"}) public void process() throws SolverException { + var list = problem.getProperties().findMalformedProperties(); + if(!list.isEmpty()) { + StringBuilder sb = new StringBuilder("Illegal values:"); + for(NumericProperty np : list) + sb.append(String.format("%n %-25s", np)); + throw new SolverException(sb.toString()); + } ((Solver) scheme).solve(problem); } diff --git a/src/main/java/pulse/tasks/SearchTask.java b/src/main/java/pulse/tasks/SearchTask.java index d657875..717b7bc 100644 --- a/src/main/java/pulse/tasks/SearchTask.java +++ b/src/main/java/pulse/tasks/SearchTask.java @@ -51,6 +51,7 @@ import pulse.tasks.logs.CorrelationLogEntry; import pulse.tasks.logs.DataLogEntry; import pulse.tasks.logs.Details; +import static pulse.tasks.logs.Details.SOLVER_ERROR; import pulse.tasks.logs.Log; import pulse.tasks.logs.LogEntry; import pulse.tasks.logs.StateEntry; @@ -115,30 +116,32 @@ public SearchTask(ExperimentalData curve) { clear(); addListeners(); } - + /** - * Update the best state. The instance of this class stores two objects - * of the type IterativeState: the current state of the optimiser and - * the global best state. Calling this method will check if a new global - * best is found, and if so, this will store its parameters in the corresponding - * variable. This will then be used at the final stage of running the search task, - * comparing the converged result to the global best, and selecting whichever - * has the lowest cost. Such routine is required due to the possibility of - * some optimisers going uphill. + * Update the best state. The instance of this class stores two objects of + * the type IterativeState: the current state of the optimiser and the + * global best state. Calling this method will check if a new global best is + * found, and if so, this will store its parameters in the corresponding + * variable. This will then be used at the final stage of running the search + * task, comparing the converged result to the global best, and selecting + * whichever has the lowest cost. Such routine is required due to the + * possibility of some optimisers going uphill. */ - public void storeState() { - if(best == null || best.getCost() > path.getCost()) + if (best == null || best.getCost() > path.getCost()) { best = new IterativeState(path); + } } private void addListeners() { InterpolationDataset.addListener(e -> { - var p = current.getProblem().getProperties(); - if (p.areThermalPropertiesLoaded()) { - p.useTheoreticalEstimates(curve); + if (current.getProblem() != null) { + var p = current.getProblem().getProperties(); + if (p.areThermalPropertiesLoaded()) { + p.useTheoreticalEstimates(curve); + } } - }); + }); /** * Sets the difference scheme's time limit to the upper bound of the @@ -237,10 +240,7 @@ public void assign(ParameterVector searchParameters) { current.getProblem().assign(searchParameters); curve.getRange().assign(searchParameters); } catch (SolverException e) { - var status = FAILED; - status.setDetails(Details.PARAMETER_VALUES_NOT_SENSIBLE); - setStatus(status); - e.printStackTrace(); + notifyFailedStatus(e); } } @@ -285,15 +285,14 @@ public void run() { /* search cycle */ - /* sets an independent thread for manipulating the buffer */ + /* sets an independent thread for manipulating the buffer */ List> bufferFutures = new ArrayList<>(bufferSize); var singleThreadExecutor = Executors.newSingleThreadExecutor(); try { solveProblemAndCalculateCost(); } catch (SolverException e1) { - System.err.println("Failed on first calculation. Details:"); - e1.printStackTrace(); + notifyFailedStatus(e1); } final int maxIterations = (int) getInstance().getMaxIterations().getValue(); @@ -316,9 +315,7 @@ public void run() { finished = optimiser.iteration(this); } } catch (SolverException e) { - setStatus(FAILED); - System.err.println(this + " failed during execution. Details: "); - e.printStackTrace(); + notifyFailedStatus(e); break outer; } @@ -327,16 +324,16 @@ public void run() { fail.setDetails(MAX_ITERATIONS_REACHED); setStatus(fail); } - + //if global best is better than the converged value - if(best != null && best.getCost() < path.getCost()) { + if (best != null && best.getCost() < path.getCost()) { //assign the global best parameters assign(path.getParameters()); //and try to re-calculate try { solveProblemAndCalculateCost(); } catch (SolverException ex) { - Logger.getLogger(SearchTask.class.getName()).log(Level.SEVERE, null, ex); + notifyFailedStatus(ex); } } @@ -398,6 +395,13 @@ private void runChecks() { } } + + private void notifyFailedStatus(SolverException e1) { + var status = Status.FAILED; + status.setDetails(Details.SOLVER_ERROR); + status.setDetailedMessage(e1.getMessage()); + setStatus(status); + } public void addTaskListener(DataCollectionListener toAdd) { listeners.add(toAdd); @@ -441,41 +445,43 @@ public void setExperimentalCurve(ExperimentalData curve) { } } - + /** - * Will return {@code true} if status could be updated. + * Will return {@code true} if status could be updated. + * * @param status the status of the task - * @return {@code} true if status has been updated. {@code false} if - * the status was already set to {@code status} previously, or if it could - * not be updated at this time. + * @return {@code} true if status has been updated. {@code false} if the + * status was already set to {@code status} previously, or if it could not + * be updated at this time. * @see Calculation.setStatus() */ - public boolean setStatus(Status status) { Objects.requireNonNull(status); - + Status oldStatus = current.getStatus(); - boolean changed = current.setStatus(status) + boolean changed = current.setStatus(status) && (oldStatus != current.getStatus()); if (changed) { notifyStatusListeners(new StateEntry(this, status)); - } - + } + return changed; } /** *

- * Checks if this {@code SearchTask} is ready to be run. Performs basic - * check to see whether the user has uploaded all necessary data. If not, - * will create a status update with information about the missing data. + * Checks if this {@code SearchTask} is ready to be run.Performs basic check + * to see whether the user has uploaded all necessary data. If not, will + * create a status update with information about the missing data. *

* - * @return {@code READY} if the task is ready to be run, {@code DONE} if has - * already been done previously, {@code INCOMPLETE} if some problems exist. - * For the latter, additional details will be available using the - * {@code status.getDetails()} method. + * Status will be set to {@code READY} if the task is ready to be run, + * {@code DONE} if has already been done previously, {@code INCOMPLETE} if + * some problems exist. For the latter, additional details will be available + * using the {@code status.getDetails()} method. *

+ * + * @param updateStatus */ public void checkProblems(boolean updateStatus) { var status = current.getStatus(); @@ -617,7 +623,7 @@ public Calculation getCurrentCalculation() { public List getStoredCalculations() { return this.stored; } - + public void storeCalculation() { var copy = new Calculation(current); stored.add(copy); @@ -630,13 +636,14 @@ public void switchTo(Calculation calc) { var e = new TaskRepositoryEvent(TaskRepositoryEvent.State.TASK_MODEL_SWITCH, this.getIdentifier()); fireRepositoryEvent(e); } - + /** * Finds the best calculation by comparing those already stored by their * model selection statistics. - * @return the calculation showing the optimal value of the model selection statistic. + * + * @return the calculation showing the optimal value of the model selection + * statistic. */ - public Calculation findBestCalculation() { var c = stored.stream().reduce((c1, c2) -> c1.compareTo(c2) > 0 ? c2 : c1); return c.isPresent() ? c.get() : null; @@ -655,4 +662,4 @@ private void fireRepositoryEvent(TaskRepositoryEvent e) { } } -} \ No newline at end of file +} diff --git a/src/main/java/pulse/tasks/logs/Details.java b/src/main/java/pulse/tasks/logs/Details.java index 9c8f46a..acaf6df 100644 --- a/src/main/java/pulse/tasks/logs/Details.java +++ b/src/main/java/pulse/tasks/logs/Details.java @@ -56,7 +56,9 @@ public enum Details { * model selection criterion showed better result than already present. */ - BETTER_CALCULATION_RESULTS_THAN_PREVIOUSLY_OBTAINED; + BETTER_CALCULATION_RESULTS_THAN_PREVIOUSLY_OBTAINED, + + SOLVER_ERROR; @Override public String toString() { diff --git a/src/main/java/pulse/tasks/logs/StateEntry.java b/src/main/java/pulse/tasks/logs/StateEntry.java index f73282d..17333a6 100644 --- a/src/main/java/pulse/tasks/logs/StateEntry.java +++ b/src/main/java/pulse/tasks/logs/StateEntry.java @@ -41,6 +41,10 @@ public String toString() { if (status.getDetails() != NONE) { sb.append(" due to " + status.getDetails() + ""); } + if(status.getDetailedMessage().length() > 0) { + sb.append(" Details: "); + sb.append(status.getDetailedMessage()); + } sb.append(" at "); sb.append(getTime()); return sb.toString(); diff --git a/src/main/java/pulse/tasks/logs/Status.java b/src/main/java/pulse/tasks/logs/Status.java index 5520dbe..080f6ed 100644 --- a/src/main/java/pulse/tasks/logs/Status.java +++ b/src/main/java/pulse/tasks/logs/Status.java @@ -55,6 +55,7 @@ public enum Status { private final Color clr; private Details details = Details.NONE; + private String message = ""; Status(Color clr) { this.clr = clr; @@ -71,6 +72,14 @@ public Details getDetails() { public void setDetails(Details details) { this.details = details; } + + public String getDetailedMessage() { + return message; + } + + public void setDetailedMessage(String str) { + this.message = str; + } static String parse(String str) { var tokens = str.split("_"); diff --git a/src/main/java/pulse/ui/Launcher.java b/src/main/java/pulse/ui/Launcher.java index 1ee2635..6843a20 100644 --- a/src/main/java/pulse/ui/Launcher.java +++ b/src/main/java/pulse/ui/Launcher.java @@ -19,6 +19,9 @@ import com.alee.laf.WebLookAndFeel; import com.alee.skin.dark.WebDarkSkin; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; /** *

@@ -35,6 +38,8 @@ public class Launcher { private File errorLog; private final static boolean DEBUG = false; + private static final File LOCK = new File("pulse.lock"); + private Launcher() { if (!DEBUG) { arrangeErrorOutput(); @@ -47,28 +52,44 @@ private Launcher() { */ public static void main(String[] args) { new Launcher(); - splashScreen(); - - WebLookAndFeel.install(WebDarkSkin.class); - try { - UIManager.setLookAndFeel(new WebLookAndFeel()); - } catch (Exception ex) { - System.err.println("Failed to initialize LaF"); - } + + if (!LOCK.exists()) { - var newVersion = Version.getCurrentVersion().checkNewVersion(); + try { + LOCK.createNewFile(); + } catch (IOException ex) { + Logger.getLogger(Launcher.class.getName()).log(Level.SEVERE, "Unable to create lock file", ex); + } + + LOCK.deleteOnExit(); - /* Create and display the form */ - invokeLater(() -> { - getInstance().setLocationRelativeTo(null); - getInstance().setVisible(true); + splashScreen(); - if (newVersion != null) { - JOptionPane.showMessageDialog(null, "A new version of this software is available: " - + newVersion.toString() + "
Please visit the PULsE website for more details."); + WebLookAndFeel.install(WebDarkSkin.class); + try { + UIManager.setLookAndFeel(new WebLookAndFeel()); + } catch (Exception ex) { + System.err.println("Failed to initialize LaF"); } - }); + var newVersion = Version.getCurrentVersion().checkNewVersion(); + + /* Create and display the form */ + invokeLater(() -> { + getInstance().setLocationRelativeTo(null); + getInstance().setVisible(true); + + if (newVersion != null) { + JOptionPane.showMessageDialog(null, "A new version of this software is available: " + + newVersion.toString() + "
Please visit the PULsE website for more details."); + } + + }); + + } else { + System.out.println("An instance of PULsE is already running!"); + } + } private static void splashScreen() { diff --git a/src/main/resources/NumericProperty.xml b/src/main/resources/NumericProperty.xml index e5ca983..3e3180a 100644 --- a/src/main/resources/NumericProperty.xml +++ b/src/main/resources/NumericProperty.xml @@ -72,7 +72,7 @@ + maximum="10000" minimum="1" primitive-type="int" value="200"> + + + dimensionfactor="1.0" keyword="LASER_ENERGY" maximum="32.0" + minimum="0.01" value="5.0" primitive-type="double" discreet="false" + default-search-variable="false"/> n;IoSFIN&YY<;qn0EI+!P)J)GR5@MlR%~Qb@`~E)bTM1yhD9 zPEtt9QB+>v72YbBK%6eLUyjV;cPr+}34vh~7Q$X2N&t!fvl}!?M`Ti8Rf_pl*x1KH z7f}W$yKPs(p3@KliVLL1PnEp_CQ_9U=w~v!lL+YvU|~=6IYUIEI{|lbLcnicFJSu)EY)UIn-6zKEN3oO#&^xJ5?5XMsds{==;> zLPwvI1DZCz3RU}QUSZUXI3gEN)~xKBRqXIbkW?YKRr4vY;qG(~p9NBO+Xy>8>S9%( z9&5!fNt0?PJqwy^>DPEHkcd;i+v5)#ch+uqwP+vx6}ov3$j+HlsZe~-GNMo_G)Bja_R=2t>FMw+|{~znNVeytT_+s)0M=3!D#ItOZp55+Hfa-Z!a5?Y<0^Ug}b}?=3y+}dPV-)9f4i;$s#PX zlB=t?z??^`+XG+Uwhr}M-)DbcFlB&Ts~I9AKHE>c8laS8chXMDEF#XCfGVu8L@oZ& z0j)y3*CwZ+))%(|sr5y57_B84I@L3{>Eo&(!s;hEG;Itrn0uG0njKc++5|xOKs{Nxn?%5H2__CmilvfbMsw*ry$ z-bfwY1u(j_=c_t8+U=Qd+=a0B_CZy=hD;OrF%_JHQ5J_o07t8)9Kx($9&>K}Kz8%O z-d$^y4m zqc4$*e0JEhFwMRKVb4qhnyt zbe}~6+SOF<)B_7Txw<-o&3wJ_bPwt5;uy}+Zlhv_!Hg$A*@OQ0>tQiEw*rBp8bUSO zVCnMOBJ=vR@=F_D4UzM$g`zMHuRL#K54QqQ+MJlF2jR;Y|M?l}Ru9Te8>5r^NM`DS zh$_3AukeBT*_shwUuYmavSucsnfw2=8Vg@yLFU;G4 znIoDzaAQ?g>+o)|4x6wTk5vc0IVE5Y6$mrUYg!6p*u)iBFuAvdFo=KM3PfX2!C;~< z*@sDfh*Igth><0c+PelEyEbwyVXInlPmU@J)s|oyRy$aMu)-KWV;SBV+Yh5AQDIDc zP!_f=;#3$magp5C9?58qI^nk1wHQs^)malQvWeA>cg7FJ_;(hAEiEuJYSLoNpJWmE z%%2VP^1`?k$f;RJQE?anNhbOJ>WKM`WiZ<$k(&}DRj3V9*3WLFDcx(@R~paGJLKk{ zQwLj9B}7=##|nd)*)(0jjLn}Wg*+NtVK7e{?Sd(m0%+?Hq8@!iHh%}_pjq7tL|$pi zu<)0^Xosj$+EYQfIy(3}S~EYJ=J;y2(>#FP4wZZK*`B^_$EXt1@WZcBDqED&`He7_ z1RfDdd~_mWXHS+{CpNE+xE+>SV#;s;GnWykcYidO9I}JY*qwz2k(uaRGcMcFhwab( zSJ0_pS$7IC20MRTfoA_=1V5vAT$ z2T}Wu0O`R?RN|-!cK$iBd_ZkJGZAsINKZ<`L6wNk6Z&K9jMbPiwx^w?WApOTyNgl& zp8Ifc{a;id9%O(y8KFIf;>D%skbGzx=F=c?_~fM|g9h07Ia*xfVv_f{DT^>`!VpKY zfb1R%gmJj|=LwW=WsxNY>aC>ItSe8+2cwQ$`sYMIiS$J52(+WWV_`VDGyDJEw8#Y9 z>s}a+{5~2`nW)(hUJHZyxyHp>(A*RT@* zZu}$Qw?J$x7@!e9Ekf-36Kzh4o@tMcK=QM%DWc^>eq@L%KS1+Bv zy>nUwa!2I=HP;U*TqULEx*>(EKv;4dN2yAZSM&E4kkS&ok_zD^as2*WUj4a;X9U;; zr(&VbnzowD<7H0%rcb1d>#_<0BR<~y3~3?Qwin33`5zw$C@PRTtE$q}37b0eY}ACe zZ7-KAMa>meXdz0#vEFB0z4*Ha1pF2VQ?@a>1l0Cf2C-~2Cs#*C(zX52{g^%DYBoz2 zrJcU#oWk%IcLW@9bn|u9n$q@O3b9-Z7`|@<@!_p*^fm{fR6{T|1nJfZ=oWXjIrBoX zBjMfRwLoetvrIqRf`*RC{<<|(7+IfHM(9(tMxqY4k+?mDVm0J~VYD{G_@3yO&CBM_ zpB4l>7D&Y9^-i0pJ?VdFs30;wt&5QEpFynI!zZ4sFqs~S&BPu042ku|*k1Y50RfK% zQg;0aJBc^F4;8B&r0=0yIR@&xcR|x>l0A$kOW4>m$edP1_A370biW01?$2LjeGeNi zK@6i^AAKxaIvsX=u&yHOw%eXIdT&qL+PTf&waYK1(Ke#U>NanW&!=cRk)FDmu3=$J z#;_^B$YEUll%~(EK;rh!vMcL|OtsdRv94Je|NR=Ox_wE|H}F9%(6j)lB3OHCWjBt& zmG+dU7wkUS?48Qc7Ea&3&5^4zTOY4T4k@nzUXXz zZUy3~?_ncyeHwmy7n=LugsKFoK3}G$%W53;Mr>}bPbK^v!`QyAcg6({MkIY#w(yR? zJK7!NV)i)>%^`aqUQ5Fs_T7*2itZuX(`cZy`1&VSe_glfQmu4`^Tb1;+hrASIlH=Dz2h{6m{Pf~>9I%Wj#82?&cT z0TJB2j^!|y%E_uh=;zZ=|vSQXE^d3#?Jr1VL&;fYzi-V54 z$sBy$t+w>C_>zz9N9FWX(hoTy6EZy!NA(2v&@N3@sqLTHH$sPwS>}meb_?nHDza8j z1GW+%)+WnFtUq&D-X*JU*d1x}s{;GiLGv;-PgbMqH5t+dOM%4yIRRJu&9QkYdb)mv zHUL;)$FGyQzd+kaQ_L2&GpsupUFjaBW~hCXg=Y0t3B4^=r@y8v`|Pq|3F$hnOSEL# zR>-!eBX3)}M%J>h`gi|XzlFglooYzj)~{f;ZG;xQZwpj+ehSi0A6jDa@YnF?0_t@Q zlQ&goouV4_%S>o`ykMnd%03nuCDPa9&3M!IVDz@kI)?S+Hq)UHY_y)}{P{VC88mhX z2H65wpN{^Hc1JRA?v6Cf?o=SMZiwEM#LK%6x~xT0QErW&lc9; zh4&ro>{K8(+P{FfU*5OCVKUO`08ZZ7Fwq_@MS3KX)=ro5NHs)%cZzny)9zktff(te zpkNu+Y?Q2XH$E6#TM^5{2hX8y{mO2%j?Isor`$eUYPY*za9o@azt#G2#4i6p=A8E8 z{fPYd509~=4JCDGKZ>C&py5ga{dLZOZsxQ0ChOt@;}PO9hMYNGKe`7d)W&fV`kzc$ z2&Qjd!`kw){ka`TH$BFh;}t(`!StouWc`oI4-du^k~dzP74uEfohU9RYWtEO&|=U) zd}QUHvy^%dB-F>37>JLSihL zv^(U&Vgr+dZIOL_|+$WPjLq7#_Ho7 zYj^i?>(jPbniU2!uKRD>%C43_9s2b^hkgzYvej10Ezj8w-Dy|K+`hXN$n_T%pya#5 zWWLGmNnvN>$?Q0+dL!8mf!PdaF4_YmJvz>A57aW1cD|3ZYCHJ~yKUhT%p*mT`D5>f z5a%pgn7W!xA&1EgW}Iq;3u8&2LMqVFF0R?HV^=3&M$aXXAo*eg ze$%&$(e6>}j(98ea8h}zE=qWV3f!if$mGQfW13+2%r$g)h8eABxA;Gi^{sK8mE{oP zI(X1sbW4I?2e$%YAui4P8|4SLlLIFk}Yg{DnDZ1pHf68o}brmQN;Qr<-fvZ`5g=>Nw z3Z%3n_q-J1#zgBy-b5)x%R;88Kx`as;Ku!LD#Dk1ZF6>eX7(O{TW?NvbLS5J5eTS2 zZ0r(W8VB~{Cin=bcg`6qtpD$P|KPLc90Nsw%Hl^^KjiD*4!tpJ#ol)VWx(k<0=F{) zqkA`cQx+k%%HcRD&C86llW4pr zN*m>q-pNxH3RV3_0Qxy4lr9S}ecc!ahx^7H!&Ug}LHRYk@`JeC^ixMKZs!ODgn-j` zr!=HWmx!vrH1bMrxlr#o`LneU2SZ~Lc3+#CF5FXlxNy&b(zjJj6^HJBQ(37DbL+yS zPj?Fwaqef-px^n<>!6FVZa{aV;tm`E?+653L29pbzA4g?wW>r-Xoh;;2rSdlz8R*t zp7k-ELO?%eLLT`OLb2+yJ*R&(Pad`$@xFEpjK@~Hv8lB7KB(0Kj`Th2=Eog40=^Ik zsDf1gKHhq~hO=Z)sZ`L8J>@GNC%@3A4v=npL)k5BL1+1?X+>()ZC-Yw3-m?Z?K7~S z{u8Ridh51JD-(l(?z*D%pBPGq;P}%j4m9FBfZn%yM&1IDk(7wP_FY1omh}*4yJ9bI zqqK8ZTtTXT9d8fhXwi0k530t_+hQI4J=#N}p^mNup1J6R_KXl-Zp#se@)7GCPI4Ke5G z2KM_enX9LDCfojQwz*Qi)6C(V1W)vVr8HaJvf(|AQ9Hu9VYBh-*2Z}wrp?{|-fU;D+sg^ z*X7qK3hwT-h4b}PR6(lGt4iZ*cW^_WSr4&9HDCO_`e~lr1jE~wUAT@!x|Od1^b3FU z7|n2L9lZjwi&gGH*h`0z@m@W*aMkBhC3*6Un|n?idbX=?57x6oL+B4~##2kr!+2#s zxi_%;<%Pvoxmntl~t~ZB6mq=cI$}K$0V?-wIM{<7=eq zWNj85qR@1CO>yNgEgJ)|>qH1qC9^qDH%Cn)2X80)XH2ODRcIta-aL>UCQzFu93cbe zLAP*(Q%IJZ%0G0&xq)k)+;j5c?IWE;`g3_|j92XJo3(C&r@Q%<0Y8X7n-(D`R454tLjJHr|+Jk)|F0jC{YR8 zS2oJNs+wJ4*t5og(%EXH)pX-)bgkg*9n(Yd6I|^NO zna#50-Z&PXCvN3R&`Q68Ydx!%v(3qQ;^M(gUDp2LoIiCI!%6wnx>H9cc1@SBJAXZJ zw>Xg!keO_@6Og>ovAEbrYDOJd=V$E7%R#v)jeYd4hN&Jh6+~7!5;m{Jn}6JtNtgt2 zG!v}KyRJAm(kQX}tRNv5|AX1!&PMar-|minTr`h-3DYYzp}%q-`WLH06B+?k)h2XU zou7c@oksMLA7xjPaH=rr&>7)vI~0h3#rizn&*G;$WMqa?J*zW2Y<;?1@Uzh zj;928Y6(5jF*l{Wy9^(QE-p->%T%;W6`NOXTF^kkhSX|Bal1D{wshE@lN>ZQZEm)w zYkaQn5X0T_XAA52bPMO`8KB*#1kv8OkbOQ=fQ&toYs2Wo6FQH{eMsYx+|@B-bRC4x zIwCs@k;S1l)4j0PGqke~?LyW_x`KN8b_mfWT!UJURdtP*&Lrf9Bj5`GpA{sFcFbxn z?DG{qU_VLccIcd99!9;ME*nm!+n-}BVTx6Qbn%2!_|)fGKwG0RLdPw0a%b_n8YSI3 z*D(D3&O*Rv1-a4ZJ(QjHfEyyFWB)-_>uxu9e-8RpU&7Oe>Mxw5zwzry(2SlTJ4xrL z%f-6kE=GTF52UyHAnfhsvX7ZnA`cZL_3nW#Aq)PM^`C8B1ZJQAQd2DbW;a@l84PyD zPRW=Mlx)!fW7`zRNE(JLodIG*RLy1&t4&Y#V^U(U{A~M(=+>@+`mVOn zbnfNgnr+Q`g^my3`%kt*kZc)~o>k1rbS)A3c^|pavqtyEnAvXbce;aIIS7OWlZ)0z zoR{V1w}P<9&wYIq75h|o>uRMKUjq0>C{mc)b#NWq%PDF zh#HlB>|x7}<57B2l2)$3X%fb)%PptDQkefr^Xw?hG~lL+V)zgxZ>#xs@Mv_r(L1UbAVuarEBOjv3cUDxPq|Q&(p_M?y(0T&{3q0 zhP5cSYSw^CB_ig?F>0vP_CtOh=?c=W2@A*BwfhP(32vu6v%}(TW^9vCn-vk|Ya@c* zoGJ&s+xq=BLPzPr+-#>pv&zBD+fMg#kQB03E*l@)*h40!Kb|VzBQFfWBQ^qu=cCm` zJnB+e*`fN(FzLaJEgn`mm>KrEWjy39U$*o(pFC|FH_0sW{ZBv@gr)BCH^0hPOO$U> z%Pp}cn&e~0#!^E<_uSH1&$I=+Fqre3_CgiTJ~?i3gMj0?#kt{Al)?sFLFCNQ$IniQ z6)v7fx!&%xK}@2sv#$!;Twkyh`UZ`2v@fWj)6U~B90=sBf>_foRj4Fa6cb%^6yK_@ zyz{f5v&E#>`OW(whK3b%Qh5Bg0Rp+IAh!Ig)Ed}M(pO(Ni?V&X zk({Ign((l}!JoGg0zs)Dj(8X=#@j0ka}V9I6{W{KL8_>%;}smEq3NirQ)eQx%LBn- z$hmL?ydV(t3gQ}1R`WJEx8u00GtI#paP#0^RNM70!Vm3-kd|tvk!V1jS)&e4w7v%= z8q%CP9TUV2N1z}OaH=4R_TU6Pm9(`Xx;_0|7+5Dr zN1&)7AWM*DBNqm5fKb#B;OXTEctL<&X5`ex@~xX`4Y_p^}wy17)tR zK;)G_5S~t3Q%GS)vYrkhh#J~6twCm3Riu@>50^@l;!|vyyh;k^%MtL1fNeZ(uOJ?& zg5^khA*J?DLF}1tEq8o7@Re8H9p7muE9nLyBy$f!ZtX$wgCEGgf~Z1j`5w5^YzA#Q zVjI@E;RxhC0(n|Nw5g|1{jW}Ru!3cXlbeB#WHur4!Yb6DA3KQpOuIG6imjLT2=N&1 zXaw@4g0Pl9ep4^ntSAeEpaX2A3Rm&Z2O!dd;e}QUkWsc{(Bt3?IRXrU;8qYCN7Pzf z4`!n-U$&Z=rbalC+ileDzt*wVHzD~ zU4fjfHN!^W@RZW7!2I`{FrC||NVg>V*C~t9_o#Xr=?NYpsaKvEFAW8Qv?O zYa01GN5B&T%D+|*p=+(ELM^-R{*b2eNT^zmgXxba1F9Tu(a|)ox(nSPML*M?FJajB zo*RkHfu*VDa#l&s^4p|rpzvpo5Xf-_slB2yOg@wb)nQRk4?Ad2l&bS=7`~_s%~RB} z(+xqnF{;K-(XCcb!SqcN7}9QfWW{#Z*C9!Aw9h$@{@jrxpag*&Rgju%8u+Xn+D;!r ztUbUveyS8g@fyhbyb4{jHWwmwxpHe%kL`wVeiQVoAN9z}#45?p)o|oN+7-+VM<53X z1YAK%|MvH;Zkp}O% zH8h1BI_z&`&Ar!+6AedDe&aA)YQMnEk30BAU_KpPx-~Nj3numSO_)c%+jbp;P8Eg@ zjVt9*4Bpu)t{^do=X>O$uGkb}nT8(yyg91wrNzHL-?n?3Xj(Sw!+3(20gI)`>bB?U!`ApsQwm*8WX+X4yrVjZNLa zMxUcgH(e6sc%QqAm;Ke>Ew_wtE1l~*kgkx(jWMTR!-x<5kXd|4$CwuP+!1k#;z_#Z zflaj7%y%@5jH%faGajyGb8>#t*~Y^rt&-hrcsdgV{-&scl>B!h?f-CR&K3YQgO|B` z_;FD+83z23T|pQ*!$JQ6Ns`yj_zCUbTits;%pX=EDaE)tn~H1u<3hLGPYy7y^;$JV z&2UReUr)G(Er+hik&UG@af4Mr&Z*D&+kmc3JO&nHOL%m5b?h3a;EQ$*WPN|0@JOZa zXAU(IBbD4eLgB8^4YsWK*m0@0n5DFf7(Q(ieTuc@+jL!{W0j**6%l`)@$j*_5-WUH zR6$}7f8%-Z`}hil(YTON%~DggjV>RsiK>bIgKAVsPPqgK{8ck*8%&?Jatj!8X{Vc8 zP95qrtB(gt*ou$No+U;$3GgeOKJDn2(>Q#GGvVn;OXM4&7+vFLcj4Kd^*sjZ*2XgK z^XTcr(RkWBUPsrg#I)_{9vcIOgPC+frsq}J%|<{nIk;};B6L0mgk!HePWn$?)T zJ;jOPo}K$JjDEqCUeA4V9AZh&m9BR8UcmN+*qjEmjdVhU8=P*P(*wuHh`z3swm?eY@C+KDRG^D@c_eyLns`)QniAEN-@>&H7?4 zM(jR~Bb<~bri_A|txRICL^h(}T5Qkeqhc(K(%E2;Z zs~nD%VcDmP@auI6ul#xqA3jjZDc?59=Hy5(j^SPX*n1Wg--l!F^5iAEv6B7j!zE~I zy$2E)N`9!xqbg7na1pAuMb`ML2z}>tHWh6?^u#-xfLf5Z>m%%)GmcI$?Og#?qux$# zzFyRoTflI7yBmr2=5gqY*K~8wt%INr$NUM((wCdlSQkw0Pd7OIC@1&TceA?{@r~}r zgg&+1vf)osxCJ1G-OGNWzrJg_Wd`#Aej&-#qa@LqE#1C%?v);l+N;-uks>2JWJfxg&fRsKfNXZS8=;|zYfHVU>4JaDT18+gU z=_{naRu$n>=$O$EYlqbXZ%4+g{n9NSi;%%H?9P%TLib*4guJoM?&iy)edKEx-g2+d zCD*-%OM_SV3hLJ{kf~U@Kng8GompU3{+{dUL}b%?G%ywDGV)rl^*gCmTH;&iNoWto<3Cyp0G}YW(8lM#e zx-7TpU0F5O!=T$=I@F_of^J-6nJnY|E{J$@gWoQeji+vEJA{neWP63W?UT@Ln2WR* zt0D6JL$pIO%UFBl6h&GSWl|89&XBg@s@l0Q{GE zQ+{s|ZgCFe@6wMKVeL)HE)}7>jA2tZ;Qfh1QOYU=uF56~xTeDOfY&V8pPTjfRA<7|R9gR$l3EcW~h9+iJLW0U@MONb|Nbp{C~> z(5;^<+tXjIhluG%X&;!@D-$uIIFMxNg4l>$vSo7)`csIkhN|BLWe{-yUWBVyKjYw3#hc(({$E9Wb; zLT)o&^B+~vJ~J4Wj*ojNf9Yf)20T9m16+c>^y&~77yqyA9vzH!{>cix@6^4s2jUivfFA?`uOJ3bwZ=~@R&Oz;bgn+stj|Xybj+7BHzBT!8!uye zCiK6|g0{zKrv#aATy~>%BH%9^0WS#ntRS8i@(kyWxoz9_!rRT+xaTj$Ggoeb$-%sP zeYP{rqDv06OE|0X?%(#ss;28m*6~t2)OSpB-GbaVE=AG+u)snkyU-u%+ z^e=aScKQZ;io`RAa}}?hQ65{&w~k=M%-!}_lnid|!~s}Nrbx6#;6A?e#Nrv6ahgW@ zEi|D7re87-t?_oJVos!zJ_Uj{8ZS*;g#S&>9$2%j?QDyCQ8qp+h{`O`Mn$>zZ_lII zlxepIYGGZV+!)7{NSW!h%I}$5)}`}KQcg-qaa%0R!BURr$b#%D#?+Fxa;NUzfKiLi zz;Jvor9A@+#@~IT{_Qqj6_w{i!>~=4PH7*8U zD@#)938%1?(k?+L7KKFGJF}d0G5U~B1_PY6U+?{N3QaTjW8EzcZYAn5e_|hee>?#j ze@~FtbAk+C(AoK{ASP`Lq>N;zNaaSG9osH45R@Ckj&-yuudLNl!Zma5c%S?X%RYgo z-*Yl6I@pAabKIiJXV#S@Ax~(`w3U0Fr z?C-3lc#Rg+;;BfUrHb+xY3xN;s)3m!n#=T452j)ATcqXam}Oe=Z8fkL+7QLauTQ{q zdXI&l;UaV>CWrI-$0h8SH1}e`SFlzN_HNPTc%yGMTHU{i`7bwz?$8$If&HgdMWS-B zv^qk@-knTRi-CywV~&#>doi6l09B2<+}wRRWPb3BFYkc;jDP>)wutG-M8Ax-%TooB zQj|smS{6q8tpFiw5LlM19vLpp`Fy#@Lez_qd^Xs`BE>B z735a`3GR#wvCI6ox?{xD9$AZO!JG)V+e-#R{W~ zhF+iwF?+@m9F@Y5oa#M;GX__~=nsEJ>j&GQU#)Dq2_%`=O8EEWB#0rHPn-WF>fL%W zYC*!wYp!B?cbctqFhk2<#+$R&;A$eN0M&Q!C<0cW6@=Xt$U2g)pQ7RVRYh6Tul^bt zv)@rftAt2y@u(F_e{HZ^nq0%nX(n3z<%_Of^1G=J-g_t(t3%@t=j1M!GT7pviN(Be zeX>2ZahN~Z>PN0$TBU@Iz;S?IXY3%Gv;2Izu8N-3?QU}(uZg)cmSWZ`1CY39BQ40b zxBD@RBQN$YL^y|KZp_SaNy9vAm$2iIUT~DxFEd{jW>t z2vKL?9f>lhj)88=T1Tf`+cnhpP3>O8Z3Cq@#)jRSJjBpo!6yWOXU;7Hi z{Wt=41pHPIF){`sb?04Xd*x46w`3kd2GK|8Vob*lvah+ojpRWQgpEOUwVD=4+E`!S zwY!p=ggEY!9YfQ(pG{dp{Jl$c7MGNM5}+9JE5-9EJtDUB9Pw-!Xmx#_$0*5ndBCK#wYJ1WZ-b>lsdzmIsa1{M7xm}3Wv0= zLkmSZSR%gt><8@Ahaf2{6!YKghLJQy8ygh@F}^9@?-wr*j%l1Nk1qt8{$2XbZnPLP zSf0*h;oWDvn5*UJAyy>wXiUIL##WKuW4aaezGcZuEIevi|A?nyrp>tWV^hX#R_tZc z`<_mVg$oW`!TvSWZ7p{fW-%9*p!kPkXta?6zy^QoFPnHi_0eUa5tC z&keTRwq?zMg7z?c1G*}BereSu|zF<;!xobpA z5>Cdo#b=(|m;8VhgDe$~y{oG=$NX`?;l2AYeZ?`%f3>kg_sV%p=ZRmja^-PO2h=L7`yVLOrnxaiTg_qtvgWlCO z_~`GOIJAB(ipPW@qGCNX>DNf^iiE0wsk90n>%zy|YxACM@a&8=h$vqh*ADHX;}#zz zeevZo`D3!gI_oW{lLRT&qBD}VuLql|oi%L{>XPzOacvSN&f0{qx~+(Z3a)D}UKu-h zH0d|MXTF-%i@~VLi_wJkiuqf+abTNgR>c*BrQpoEW2n%srpLsj3><)%-`CM)omHTT zD*@fjWRG+O<#=x0zJiL{9@ex=z5}?_q!LwX~Yd%8V1e!`g$zm&O<_$8XvO z=%0?QU?r(0Kb`ovtbf>eu{D?lQ|j1WEzj~Ow}Ef8S*P)9?xxyKu{_OLq{r63Kw^tK-r-W zK|Fpqe*vpl8JCu?&)+zCY2QaNY!IJtIjCvZ6fcQ0zx)kBP4}V)l{06?5pYHz zPb!El3XyiEU0S_KepQ`6i!#p*hd@X2WN%5cQTl!g%)4Z1Bz*QYj8&`4#Il?3VBD7@ zkZT0;w1POM&QvKLmsb8MJF(L(G7>JKaC|wSjt=i&h5FJ@@J4Ybj5XiR*;+P}0r;B~~8SK~P0Y@(Knbr`_dq{5(3207rl$z!50o2-t@p z%|-`XPqaXBft^h2yg^)ionh}Lj}74ab)78G3@BU?HmD)07rl$aK|Fxy4K-Q zfZP`7(CvjPLkhyLZAH|DpAnO^4U#T{Hiyw!^<=E~ROBkF?C3&fM6w8>kw}efh}7~u zkY276jN0Plar?q+$vOYa5#R{i$q3vQEu1Rr24c@HLEPEzq0UNx+eM#L!p1x(Wt$K( zF-t}&!ZNlZj06_;WJji=)h3fR7RluwL}HEMFho=$PurfeY&ve^2yg^A0!0mhA}B!U zD!q$aQ0?esXi`toh{Ad4#xlMrJjEbi5S*2Wm{Z@#KbA5!VlhUP#nt+6A+_B7bS^#@ zD21oQcuF_|908>W6bS($>XT4m`;!Q}zQgGXA9){{(wr6|Ix~$}#I(yO`|r~zOX;FP z3Jg*4INxS5jN#=9QA&AII0762j(~Rr3atRqq@G5le;$V_?VNqRqNoZC@9W42ZPOBBfq}iSFGKQcgq4 zN}+|7Oj)?O%%Ww7&4EqMFU47LV_*5oM^j97oNc?5)=?;J+;9Xq0vv%tgg~JXAmP`y zqVm5_z^K>B&;(nP@n=N_Vgl@ z-us3;+Q`i=DYgVHYC<5?9t2gh=V)X>BM8B3Q=d0jiz*1wm7r?*vixI_u-j=RlMd!R zbAbc1X~2?VpN@0udlFXBMH% z-Z$kPR5=}OuoD(ll{-S!VLA}zJiTK&vKaX19SB{%gjlg{Pyq{+nV<@Rs_7Vjer$vz z>C+%>84uI(pJ{{=_?|H~T^4if3&bA(0*Q5=!}U5Z5?3Zdx#0+K1ULfui$J~#kkFfZ z5Wl5ASZ_Kff`d&R6)kHruoN_!2HUmk)s z5<0$imT}TJMhP>T3fJCtAt=Pk#pxDvRaG})#WR&R;Kxj_N5#R`L1agl+ zz6cPLL5JGwS^%UE5qO`Qrh>x4A@=)&ridyi&aqgoB{YLK(lzewVYqP$__{fmQlHRp zIMi+4Qp_h$2~}FC@0kS9PoqnL>4(lRq+O+9it|A=JTRx^ z2yg^A0=YmS&jm=-rL`#c?^A9He@fDovF%M#VA(;ZA#eelfbR-KCB)>cf)9Lk!2PACZQf%Lch&i_cr@O2p z!Rv2dnLA$Gg(JWb;0P#1AWsB{WHO-UipG`&;{Xk8?v zCt?0?o?Bzg7P3vtp8&gHRCUkmP&XZ^h-aP?F+tLEKGd{}%k)iq=#xm`DKz5EQ>gh< zJ)C@qgtsSm*q1__u>-EKUj zvwy>Q?YPIVW|JA_4e!FZ@m;7JKL&ND$sXzQ-I49$8ukxDPOXPw#R#WSl&`dyOw>i8nf7r%3V{({Myk^_!QAjQr>gYQF^8lE>z%zJkswq-f9{c_4s_}2Kb z&}Z%6glCprbPC*h=pc-!l|6FvlgI)GL?Tw}jF7R%Vf?ZcEwtbC zntn$Ql8K1_r8|yyTMd+~=jc^r?2<7Hi@*CBi&={4`kGmj1|lq|jX;rsqEILF;HnkY zscHc;3s87$TCoP->`Ag>CCJ}OW&oQ~lz}Yag^7!hV&yBSBk;~^y-+qJJ9NU){TR3C z0M;#ALqFi|etj^yVPG3vK74l(j${Qr@!5$Cf-PtQL^5O`etAdQqN_Z^ni5r^esX=j zDg6l%B_XzW4)}Y9U+Adqg?atEG%EQ3>H%LvtkTwRm^>$7yS7H}0+v1j!^w?`qGKbg zn!i4X)BQHn3BctPg~=}>e?9q|hqvREB`33!BN;K8D(R5zGy+;I3$Yg6yJ1v|;&SLolNaK=_x@r)G1!R5Ps$b^L&gq4 z=Q#HsO_|yaF@H)!Y+bk%^N$(u-?HVHA2I;LYbn?eWK6{HDZk3o?UA)xp|5c}zB$QS zq{3%{Qr2c#N2u{*JD7EBDCZ8Qr);Y$H--9N$4?!wI+kA`HZGo8& zrQ?NFR~)h>VcvcNo^7b0B07XBY}TE>ti=~Ql#L|IHfWBi{c9AqEZEcY>)hp*?@&-- zLc18dyI+er5yK|^Y9SLem_K=d-_~Q|J)yE`}`8@q~s~T6Q|u9*$SnC^4aETy1g?u zVEBZkPGwfUL1RqnUspba{eT17jvjw}oTdwr$&XCbm7XZQHgdwv#u>L=#&R z+xE>l=evKPy1Q!k?&`gsz3SPmocu}y;k=*ewdZw7X@SAkY2F2XfTq zl^TACoz)6c#B9I|Z1v?-!P6_Iqw!%OJs)j!uFX?Fe4T4-w zy1`d6XgQ(xL^XIlWBwFI`DA8bQe0v*XNg5;vsA?^96o)~*g6F)>0@l>)98yo(y3Svhjhy8kJXZxG%%xbIoo?Zyza7ik(e1_TmoWwtru&OHyE zBD~MABoEgE>sZHgfqdDIBz%M~2!mY@g`B^A@pNhk@@tmWtk>V9Y8MI3BQ#c}yA?x~r+%23cp3a+%eWs}IhjVyx+(Pe0?R&mQ%TRD3Hpmp@kMROXn^ zn=b>-I1dN#C>?%B2RB46wZh#8GUPRYjjHn~z z^D%4##SRbO2W#&y3STu60qj(?j$zIcOy+h> zLbi)*OWKUUC;R3N@-9YVm8~yU{O7mg91(2vb(NsNA@ci~pG=hRsy2B+8*8fFX;NgB&zi}iB@$T!pQa3_T|BrvJ@du7Cs1`;{@@F&MnVo&vAUoHF z6!1#j!#%bnjPx_TXE^nm4tmOk#+$gA?XYi^TQCX#p6h%SI)gR$P~-HfQf+ITCPxAn z##$xNJLG#!eB#8q=X+o*Enbg;D$A_7|79zwlG~&!Ye$!vf6;JrIg|3O@xSH zqD@o6X)VO-Mh}={-=G3Zg;?Ws7v^jspBtU$Ym(BWMTJ9}qP5!Jl~4mHoLQ*lye^_I7@T z??mV?1ul1q6ds9-Vx+L5$p7fI;9TCwq@TR036}NTb#PL=A&H@j;LdIX^mu1l0w|WM z-z}*XKDyD0MB(^ifJJ)={TlL;_pczoX;vnLDMiTSm zrw@51d$P#7vI|?}2&R_OdkB}d$%z2MYpUw`hswv44O`D*7J^v}nQ&vw5`%TpbwQch z&O&xy$gH}`jPDA1)~JQ3USBjX_W|#ItsW8uCpkU>kCGDs`xaPKmA!=}VX?9_QXC%g zl{a|Rl|(W^ocH}NhO&D$v5k%R$vJtwo~RA)4!0*#zdey0SE`Bq9$aSI{D7U}*CP22 zq;$T_>qX3ohL@9D(aJdt06mt zmp}iO7Bq4z9hE+Gh?ko^8sBtUqLowAfU2IAo(As|FKwZ_T8v= zvF_o3D?V7ys$)f4(}<|8R9$!?1VWY{^O`qmv?YW7FfLt=^-yUw<3AH?o;XM>o@Quc zD9l>(k#r~szH{PGShYHm%XqOi*L}|`m#jUrlTrycR1Zlk;5-6q^L|;kE*jkk_V}eqz1cp4JSx${fxwq@K$x5j&X%%i_T8 zlQh7NdQ@)Z+@6-r_G^J1aL>0n&*l=^wiun220~3!`uY z8i|m!3i?2cirLu#v*s~Lc%^HQ?$-@v|)^KBRt5i*_r7juoJ#pFcs z9+YOGDh@JRo#cZI^z*}lur*JnAw2Sz__wcuIV@t=sGXtm5gQ`12 z#}UXUj0EAI;3#A_@P+tI85Fxi8-uUTNSYZdQT?f?pHgQ24~ySl*C-fe-CPV!rqa+H zT@x=7jM}ZpzIffI_Fkn%AMX2;H8}cR(mO%#k}Ia{xCKLsJ9Cg;?H1UtTzR)w0^7(c zwVv;y>n4bLku54AM}Ck`7SqkN7&kC+i*}*fjHE5iq;CUtn13^@mKFqe9>Wksq2|oq z7=D;<9(ITTfaG9K>)m0eul?sanLv5|R7%|sct*hu6z0&%W7|mBUDi<$vin`XED#mV zs^Mw~=CN_j%JJpE$XaKzZ6ypMDK)t=^N;wrbq{|^yCb)&xk{b;P|soSnR4>3;R-=l zpsPQI$zlyxF+2i% z7eWGfkgskzcg-6X4|C0UymA)C`9pr?Dn5-BRAU=ui-NQ#GLLI)G1b6ezV;)4RsWqR zlaNvW@$F5@5aw#SCRxhm>#`IlGySPyKnal$ZXm+~C z`DVu~d?fhp9SddU*hDmr7BUJ5LD=F zc_^cs{|*mC{I^g279?4qn{6TUsQa0RH`_ta-R*Zd=Uq#5Gdxp1iF0JwnA}IrEYzDV zp}cs9+(Uz&hO$|}8K$mCN8f2(PWVgaE1B2|$Bu_P2qfm+1MN7t6fm@F_`VbE_piMY z0Qq+V(Cn-hTb0c?7!*S;4OT|$XNgA^JtK;xoNV~Z9CQS_<$8Uz(UUG!u06|4O2$dc zA2#ww)ZF2NUM>d<-+jViK52vKY+k6qN|!$>4WGf+E_$ckXeuVXu3+E(6_T9{<3CtY z6bS$Ta!4XI%b0k=Iz1_8S8qz%@9^sR1dUkf7~kXTr8ONfT{A11DA*Gn)a$uD*X#5{ zK7_SEyF#0Y4yk{D`$$WL!oRO#^B%1N*>fDPOsoZW#dqNeV1Z7RCT`l1aYP@=~vmtMSC40@yafTuxx^!xVN z4$sc@5A7YCUA)=y4eqIK3$klt4!z9&MA=661J zQU{;-P%VXE)*vffy2}RK^#9-yq9^^^f=z+z;xMm=m2V%YRMU)Vuy>g1Qc(HTXc(A* zV&c^VnLBugc?AX4tglYOAwofgtOFmvup#fxckFnp3schA#^kex(2T!^ski`_XjP`< zAXx2*+3oF54Xe6|&QrGc-2xAhrf2%Rl@iL-dKTBVHaz~hj`0=of=O%iNlVXkksB)I zFpHktm6GsLQbUXNc?ofbPi1bch8awqoQg?bgIKE*kMHQd7irEc@V@((3nXz)ac;+V z{pPP~@Jw}1>BC2ky&Io$AE`N?CI(}Y5Y^@darY99O5A>YU7GO?HfV3b-W)x5GXVeC z!2gCPR_J);UdX!cWlcCHqlM5wj=)%PuZ@s?kYn(0g3@}&Kjej(7^Z34i%IH85G?C$ zJy~EB1b~MEtv8m1#E7pR64RWB(>_W$*TLw?2UFO^$YRir=(U8)2$mnUuFUANShwSE za=c3qkv*{%0YbLga9#jG_MZH(x?n4IH&3VSp)sNu6BGDLEEW%thkuZ3%1TLCTuc$C z)hKtSwP?KzNLS4oUp|yMX{#V5`s-MI4aDvvN9(7}S$%C-C^B9I$Ns#1wYU@7g(nJ5D(ue|F-4P6)lmCf-pI_eaNVPs@mw6 zNQ)euwAeK@Ht<{Bu;oBT)Cz@kwo+0CZ0S^{g#9F&rR3-+758B3V~BSj-HoKpTr6(z zb*%~Bvz%--LC#qM>H$PI#tDPA*1ennEx2su6bE>F80y*P^ko06BKJRI?{eMQ_UZrq zijZIeJh-xT%!zC!fOD>_HNmdUuSZJj!;W64IFGT!$1e3x`R-Pk?Ok-rz z>V}R@&wh`+7v7(cR7O^!fpa(9#FWbor=Hr$5^KN6cU-r9Epv4|K;u0qx+gshAl8cm z+qeu1z*2?1)3-i7yqpz|5oWJapZD*Hdg@9~lP-yYz4er;<19LqhXT%i$a=gplk={~ zIrleQl$qp{fIRJ4gK=#xft9fk_^2}=r}ZYy`#4s-uTfZ^oBv}n8S|0x@B{Jw)Z|9s zYZ)izeVbttUwynff47-6E#(vHRjQXiL0qFgY&_BS`BFpHRR1%se0HwgmGgSAWEg9u z4-_YD;cwkG$KLe(Ns0k7Dvvf;PWoT{96RH&=(cF*C*aun-M!73Km_LI*C`zrETa}3 zr^1~ebtZ!o)ju6$RaT62NILHJ*HfNBB=!G z9c((B4wwZ?QzOYiQeQD0tY4J-S^TbsM%vc|Y7YL8cT==X{;cvh=D_NvbSznJ)2=v| zU=X)C?`Z2e-A8z^$cxe^wVlaF?a+VD;6*&di^kl%=ZE_SM0?I9e|576Wd9jdi&Zo@ zw{DR5L9%0nN{3M{gWqMMFB!d=p1+3^QO?%pYs$2yz4BMJx~%&F9iN~Vaa;a%*bk}T zyJecrTTz3qee}`X6T!C|Q`dQyQNNrWsR7G~1CUucpZ&LFh6qR z-FVo*DMsiQ7*zDb8Y*G&JK!w^e^>GzoAE(%AJJQ?ju?~H;FZo)9vgg9Ut1Q+w1{n# z>41(1m1xb|du|}kfY@ehp#X+!=m2(R&P8)68nnQjFLzo}MLbJJ%EJ~%&qj7r*Z!TP zU_V4na^P>e5i)Ut3WFiwzK_^a6iM2Gw{#}ceJG#s+>u!aT7qu9jZ{IrgzJtivjc-D z>{i{edh{+r@3WyuoUxb~^u!m3)CW6PK~^#w(pt_Uj(9IK?=~9j)K4 z3&JLUn?-3Yb-9Z-NWQCBBr_tbsqPwu@2YmhWcsQp6r#o~Dpw9cztPu;LL{kRiVZ$M z;Z8dFtdr2m3#Cvhk4v`hnp(G%VUKLrPpQ}7A8<~f7siKet@P2KTSH~*i3&S!hhMs7 zA|QL+KGKh^GXxq7amB^d+KX@rH$e^u2dsR_Fnyd*T$blbHL=_n&F|@wlmE84W)wgn zVH5M@?Sa_Q$NE3pXZYsa>_?O-=f^F?xf7o!*A-e_Ho_c9SvGw&V&~ZN*L8-m{$uHv1Ygg#n_uI?cg9-FqPp1@yMF z^gy>hym0k>yYbarFrTlnyQGf{>wQP3+gu2TFJ7o(80zDZy+a&Bp#$_e$85}Bao3?c#NFR8NDNcOB%*DE{ z6Ot6?8_sx{!r4X@>-1u+!lROfZ0Vc4Z7CG)3n(hb{C#isDe1%xVZ4_Ry0p7E==Ttv^aABKOn` z26Hc!o*927?>#aIfeFj=Q0*Y_<^_~M|p-{;N znJfz6k0dd~2>N0!!8|XmBRMjn5==rF-7cILZ#M4L5zzb{tcAOc8E}vpkG{UCz}zd# z-_^mCRG7xUr>p=8*(obphn!~I3S}ww7WLgNFs5?l{R%ErrWc!h$2&#H-5iuIxnU?Z zpS>Z}2aC~(;9VD{QpU09U5Ba-QuT4LC{a;mNjPQ+MMois!-v%v)I4PPW<{A|IO9wf z8lsu7=+11m{e&fF?vEqgw=1keC5ytGkgH}=Rz5(lMp!zpElDn@Vp{#;%9lQ(wijYS zf|9J-(`hgcm}B-12V=Ki70$-BqHpfhSt>umnyFCbJhNdjGT@9m$szu8g5_TYf{mV z9h<|eB!I;-F6)W7&$%_5d&7Gtgg9)3PQ1Liy^5zD6f~K81I>)z+Vsr+N#rgrn@et8 z!{C;;O1#;rUTJK(2lY)8<-M?>PK)b5bwM4c<3(3BP4FIHk-RrV5XL8v^98065*-Ll z;lKquxgMpikYw|qk-`kdhZIRGbqv&qFVu?j${^5iiiuIja=S$2gUE6)>K9xqNoEr^ z)T#+e?#A0{X<{y0iB%I5dRVR~v8~2$%W2k23+jCp6>|oK@oZ zvQb?(EK9mf7fraqBv5Dk+LX5Qd58Arn z{biN0k8yA7FP-j72EWLS8_8X22j}e@`1>S5koW0b?!O&M{pHCZWSiN;?Me~isH*Ju z7c>EdVLb<(f2uhk@8#y`R#7>CH$o~VZeTc{QzYuAzDG|m(O<2DZ&?=xD$oa&h|VA7 z#9LRl2b!i6w)h+fg$&ze1&5PcM5shhSmPwa&bOz_2`vwD_dgw2oX7w?g1(__?%=26 zynY2ko@j^grqR-gt^WH7BrtkzQFeU*EP^1Dqr*2KLPUr}h4dXl>6Id8x+_jdzsJQg zjX*8euVw?5q-=dIL&}wUNf>sN=b4YikFLeSYxaLv6}7}RZ+$r0opV)4*Ru?vWw!q(;D^EoickvK z0!5b0=H1>$-^O=@etuBOeD|||SMhMi?CusqLjI08*Adr#4L#bmhqfBp<1Tng{KHIa zkSLk1Rn~a(?!1riJNe1GXqw!Q6Drw$9wpf8Gpd|X+**>5z*vD2j7%AoxH{mQ?Y9zn zdi+b!By2;WVFT|%#Y7=M%aw2OysgH#9FQ3MYX=?YrwdDUigS`-bT@uCFY3$`xy!Uc zCN8-%-QO(klWN&oxp5bb9Zu%|%iX=U0J8dH>4#&td_!{wryGrofGU%N@YF9?yUnZ>N6-`39GW`%S@Bxdi*bqkJKjGK)OBcpym>oxT;j)C!FQL8fzkgS|C>%Sf_m@zqC2EpKTWaW z1m4~E#_hp!J((%p63eXlfRg5cvRlKpJjS%{M3|Yqaq;Ws<49haAAkbM)f7+mE0_-? zwk}Xb4<_y=fm2pb#9jwl9>NuYV+6MIh!Lf-|L29|0%;QjX{!Z7mdk{a<`0dSI79Zn zp|pHhK2EBlF)MUr(YrTb8$p})`4EJ-@-qdJ8aUJA+{MB(zqP6D$8ksv_PDSH)YY}z}W8?tB~xLJYku!N2J9p5E<VqQ`vemxV=fa> z7~v<;JPxm())BE~!J>pBIk9}duLTK2O3&a>E$zyf8pVG;mksC4X4G3k!8&v)hFE&c zgwW)N*vs^cHEjA~bl4@dTH%h8I1&H+8^5=pfgjEQ-XuwjqW#Ee>CtJmV~o+eBEHpy zv;;5c)HVbWr(uEixnq76Cg`!~3NFQevkDR#7)&k@a-)6GYg#W-()51hWf3!0z+?0|j3 zD)SGT77Qpx@CIYt0&7Gla$KRAB)4`fkH-^JI-Ku4EHpIwUG1Jyt~UG~IFBLS2Z@~5 z8a@V~FlyCQB(ujMv1ngG*YZ|SR=`PPi;wN!+y#XL64+KC9KALkvBXPb=L=@E+y-tJ zj-DN=VxyK2#Thd56H5(U>SqWB>=ptghe+XUlx8b7e%K-$N%sC+sZh5Y&|EL$NluNw z;R1GW<6;Kn4fv-c2GO0-$JXsoeAXF zeX+_F(i!}3=pgZeey|9`NwL`2CtxcLr%hFhAW?K&#KKKTB-sVpYyD0n6ma#PLZKdF zBzuD3=0q3GUsvI)DP2Q90Qa&L9iId_$IZYa^^M?f2iGg#uR!^P*`|R|1RZFDIpgjWMH9^2Ohko;UBR?Wgfyj*_7`3W zuB21TACm>MIjIs@g^ezV!-{e6Ol|_s?>YY&PMUB90uY<$ZQ*d8PQ5lX7KV!rTk+yk zYk8Xp8u}4eC80QYucw-!@CDN_4+oVcQg1Fq`y!$XU-9+@aW`Cx^oawjy0(a?xSrXh zqU?Jo%C+c;Dz}EEv-FbuZwlF`1$m(rvJ6*wp!bzBy7Y--rLJ>UJB4yxX0$Bb2rk0E zs6pxS1s_uE38bwpJZ-{>pyuOuO~kk#miz>6?FBvgyUcX(Q(JaiPHf7RzP|6yenuH`k1n**Gme$%Tl)e(YwA8cKCtvI=hTt-AcErRR678q$72p>=TC$o=mk z;2HBJ9_S zP|61|idL0FL<03dRXxsu{@cWXm>zQ^zutv8S^R=@T=Nk5m=OQV7fumEd9X;X<{-Hh zZzNL>ka;4hQqc!ba$!{iCV`M;NLAijVK1Wuw|1d1fy}g^nj63(;U`Lq$C;#gp6C(u zpG~1|^A4oP7^0#=v-N?pm87f*>W%O$GHmcvbd-nxejB;JQ9ba8)o&gio-I~P(g)0X zfd>6y@<=%53d>T7w@0Z;01z?9RIcCBh3%@PRO_7+=s24w95mur`%xrw{i1hjk zEbXb(M7a?}2Awyg;azaZv3p&x`HqgSpgt$#zcm317(n2I_lBoXex9nfW#!oB(#kucmsnP-f2 zBv5ERd2uk;{nW3qcc}l_#n^j;-O2?-a3YXgK5qnE-73xUgr9yg1{3konNgi2Z-se21Hb6C+c_F z5e;zB|Ci{n0{M`MK;$B}xx;V--CV+NLmgV9rOw&ljaz;JTL3hG+zN{BpVT-}I4EZG zUof>#FRCFvb^l5ha8W4H&4bN-xO}}$4}tQ?PKe3Tyz_&E6K;q;Dc@AmNy>Hn6SGhZ zAf6;b#&N!g>T|0BY_*vg%AAGC&S%Nyo(Zv?J4%KV-b^0KYx4$z@KbHMg!c0{0 zQMU(w=-G2hf$j^mRMx7Ji@g~W>`B5&LLu^x z;$dqjPutb5EhnBOPX11vw|Kxq>aF=Yc0oCm`8v%V8lRK0g)mE+bUf3E@$pkqkP$M)s@wgC3O- z*EJ(TB7o$Vt&kY5!gda=N)Kc}bVW-2^G8>I(ZU7>6^04oP`hvZhp%l}9;;bBtQ(7=>T6Jr1e#UtR_4 znm|u8Sf2NX74rJ@#H|vpHvr;NO^3qjWyFU9^FqqM-$`JqfD$e5ktO*Jth>-~6EE6v z>Ql<%iqOKxpbNi?7Zq%UF0;v37Y&VHoSOv%!XjjB9F)r7f^c^1Kj7bi=gxmH;lxJ0 zgl}*K%qk1omAu6aW#j}{P|y$V--s$H?X&Kp8doo!kWrMNmOE z42khP?$ZyeYMp;3HsVU`Op5v^cFOL|x{pEYhjYG1j+MPJH8_vufE^j#q5-2KXoouu zXS0vbXZSFuH`*IbdiN2}uLZ)#!ifH`E~_KrzU}I;NKU@CocYv`xzJ?JPB#U_U<>XF zq1cfWQO8GKy@$Lczd_XJLNU$ri)`mDSlrQ-v=%@kC0;_s^C*s{LNdK7Ndl4xAn=uw z6eTtf!KEmd=6+FpA>$-{{}5|D3G?4N;Cx}YA6j{|l{EgTQiyuZEQo_KVRnb6SH?FZUHMFW`dXG-1rH9c|& z-~ysw){wo+%M`;^fJ5L2tOn!yt`8YC&f&cIHeh>+eHw{wv-4orH-U}rcj zK}w!4zl7&hImHTrjPQ+jKEGxq37g5DLzU^2lgk=NjQ+^o$xcAQNiPRny$XFiGp4MF z%mkOK`ml?p4RCdV);e-cZFx^2s%FuBY_sWJ@@><~2YNsm9k2fC9j$nCvy-?q?|yhR z)^!>?45wxIo9u_s!yk`#sU$hrtdq_2lK0~O#bp|+Di&Su6R#h~^k5$Kj@CT;Y^tj$ zHgg}AS>;e4`!eqL))`emeZatnN8+>lvbDnE*s(#Dr`1PbxxPHG-1U&oEl473FA3f) z*eIZ%<&q7H)I^LrtnewsdEp5;yeO$2rBGD6HM_{|uq|z!8MO$;aQOo-{^r)ma29DT zwdSe$9Y=De5)ItuUnnODZ-Z7JCn3rT8Aw{_<6T-5Ynly!-iBRN z?cedH&TD>?C1bFTTc*Tb_Xot5SM{$3J+3<4tF=jl2px9<%^xAT!i#bA7jE$wD92Aq zWgJcFKXF3wN?n#8=R=MeFGg>pG5B!#o{4tR40o9?eVpKAo&*oiwKEzUrRHcR=ykX> z9xV8c&F%*PA~kaehp;Xpbs}Xk3jXH2CEFvS^B?@7g|}&Pn-_Y2kFWF}v~^85X9vB< zJ`DbP-7V|&z&v%8v8_N_S1K%(WF^74XQh_>$%wMp{=_~J5EzUB;AT86(o7bgBuG0T zh?OFd$G`@RZ`3cGhDi$`(W>jnpX}4+s~@ygb_`C=QcdQZN@L4Se_M6 z&qlm2-{{Q_(+Tg3$M@#?iyMyON7gJumxFP@j6pOIx63okOnoDL`}$qDqJOg=CP-P| z+MlUDCZ7FX_QKjOV$L|)txiBfIp1}I%=$TQ#x+Yp+IWR=f=2*lmIFr4rjPNi0M>=<80=7X9 zdlyDFnpA$k%38j9%fmeZ@1aJG4zkM$L*&TWiyRa#;Vh{j#g>eI2bB#uzk&cJVp%ve1Vw# z@hFw=Xie$YXMYmPNjTFrqo}val4t|0a{k-66D$(Qwf49bOE*vCTa=&R_~|$`sv(cH zU^H8Dj+FsqM9qc`$24^X|FGhJVm{RGr8DrS5J?N=@OGgfXS+0leH|aO$${b5Hm^Fe zuMrVX>!W+yw2V%Vn~{`u-2C;KaIWf#H{pro;7|uGE{YCJzQS>sQS$Pd)szc*GqU^v zHGfxe^{@--kQ6ryQZg^O2sEx{wW5SxhY`{2nw2CQ+lTlRb-c)@0(mZjS3M%$A6%0K8}sI0xF%=OFLcFD&-Y_S*{Dkq(n;}AF8 zacKqXMqgW*nijZk+lB5YuJ)g?yr)@~=vUAdN>&5wi*v2t+uf#mT2<<=rWLBFXlH&Q zmWwWP_6e~0LKaAbSoHZs8p@9_1ACC>ZN9mJ2^hm+?xZ z23uV{P7R^?ma4I})fv+?W)GpaRdJuzUIaA9+;f{yJGB=|l>&qGOIC5mJm78RbC;Gj zCzmB{RfRzas!JX3ra2+kv=;l4aAc=-LIzUW(v8=2K$Qkd$=Q{1eY_4Oq}arzv70ES z@r(CGb?WI?Nle%HHxy?Tntdj!gT8HKYKcNXrB_1ziLmd%5`v2ntEqeW)z+X0(YWtW zc4g|ihH6*WtVq_tygGa?UXk}yvRP^zDZkQatt;^Ik0tXO>es8#k z+IqtYP%c)>NWPbqFjoc*sL>#vTCMzxD2%abgU*RTN0$71i`&1(@XQ^gi9@?ADeb?B z^5Yu2m`-Dp?RgIc;=9Ar^@vYRUPopX%`7rd{oF>RA`ZA%>(syk+0iZ3<#U_Cc)oS? zZQo7_g@}SV9!jb4{%FCRp||uJAY{8}R(Z6x5#nnK#h1@Md4UC& z+~8%ReM0V!f>_%Q2p7NeiP9=gnFWZW==-5v#seOz@1+jbXQd=tq3&Kq-*zSH2BU5` zeLtYoKm`I5B!k2@O9&_Z<${$5*f1vS0HIUj3IPjN^AL3|u}^wL;s6b@kMd<+-H|t3 zI{313)9gy*SKm#+TScYNzo`wrO1x`5Pp23h28dp4yX`F|`0FK!Iw~NJRI`orA=`dQ zZ-o}0YceqIPrTp;T`p}OA2-CV=Z`^65gFW`NECfc=2(Q&K?w+z%)9oC88gaW6xG?DqZoS;wDMV z(c8?QX2bh6xCOUV3JMWR5HYCQ_((?6xUU&tI{QPEm^JwiYynPPzZAKY$No(NdDgGG zBzu69N_}ytYfq%t7iq!81BV(t1aEo5MFGn^W|hnHKHl1iy!IFKWUSY`+7B&C_x0$k zCFy%I(|ZGifR#sSOTq#+2v5=~DOIa7dV|%X`~c zmmL`Lp0Ka|Y}J!3Im~ZUXvg0}8g^*)}`n(BRyDXcMZq_0`=eL%++7#H0@w)U5e#0vXn)8Uj+5)u0J;F z7`+x3-MxoBUPTn@qqrJxYjr6$L6FzDLGS%&x>_(!^J&GBwPh5@D+~E(p0Mu=9lSUN zCIr5l`6eDquo&hZ_11S2uFzPJi>-h5ITR5+!zhyzt6PadCJ>_(8gm96 z63jxBD+PT7r!$Ey?wGY}`}c{lNK91vq2dBp)Jj9J$jNu?C9MyX1JYhSP` zAsrS&Nr$xZ&GR!}a#&zIX`|mZR^LZaUM<|*sO~$}V2;Hqo;8=SGsFenGp3yQf_{+x0=lh^mJ*~}(e7Pg-UHx%|0kh+R(|)ao zi;VIJrN~R3^)CylZ`9*5K2X>dAuiC`7In4hpt(@=2 z(7X>cdo;Z9$Mz@$7UI!ym#Jn;yGdY*0;ZL&b%s^Ak0y)s-+JELFe`9C?fxQzKa^xnkmQ{M-~6G5fVYO zAiGWB5YOM}hxkK0uZabPso5Hd^fr}m9-ardf9V=FgVQX+nKC3~gr6;CW%BVliOJLT z1lxzJAPRT1$Hs5COl;{ip=f z$A{7L4H>y1{({AO%maY)`CD~4O^q_5A zf=wEfD?8cX6ixlfvvy@ z+`Rj)+_oNML3zo~8dXq~45AvbWK(O!>NOf7{&c(Zd4?Vc=b- zS<|n$ClYKh9y$+rF+?TVJw_RjI%b{QFitLSQg}Z*B!GjwtDRuV-4K4tX}K+`I%hc+dX&VmG zERAh(?)-GYvkc=UW4*DAu+kd#JO>w;`j#AQ4aT3R%e;`%Z$XMXKYQlA&(>JGRK5He zy!cDbD%be*Iglz>F_?1s_->E?d%I%np1&@$A}OO~pi8H|2q{kBF`E)sN#^Tf@w5-@ zd_}jaJbjqI9eg^RN)QQXY}pUSgFpEbn=>G^nOZj@g!9O~0N?2WcQ&m|r=K_;JY$YB zmtW`-^$O=L`ZvD2;@SOV8C35T*xQ|;f36RKlz7t&A%caZqQ!Ev3OLN|mptVFhTI<< zH)8$-29#F&wNNwve4)nm#33~cON%An5=Ev0_0k~el61}Z&z9-Sd0GL)Ffn5~Yn$Uv z-1ToaXuxLSBNc2cd5wtPWWEY+U(H$wdKV+FI&a{w{>#NKelHFxeapFQ4d}53AZA*0 zA~k8V(9Z7hK+IliZ8^@|KL)^qNydEc5q}`Nq@QvpCDG*kInw#qGT?k~tru#5R~Vlt80Y0*Q7^K_ z`f^HH|7k!GclEj-2_o3~2;RA)iKPH#sY)8)^-xhK0n#V&;#&yx11{xYGFEA%n+&%; z)C_=HSpP1?qgT$Nkr;!3(nBp6Uw9MSKd;&0lR?$^HZsls&>o6BN& z@RCc94E?Y~x*m6`s0c@mKwem|Gtox2McbX0`h;p1{8vf1ju(mphgvr%7YnG5y%bS{ z{-TCv3Dv9=xS*Wg^wi*%MzP5*-RuJq-73zts8M$3sET?ZhG82Xxm3C_g!97b@|QcZ z=;=wkWQxBP+8K^4yRbBtnH_;(5bQSm{cSjp7Z`x_y8|duM+V$+%msYL)Jmj6Q~|y{ zi5ee#6)FT!F)!?E&Zx$o)d2|`ABKhl|GeU4Q_$31;!Y^CBDSWrD_sZ3rJX{JYJQ5A zAe1Sf*qQ#He*`^92P%;a4hc9bO~+J7`L%6>n!wyiD4|M*D^l_J3>77Ew}uz0!Z6&>|7e-n#0k#8gE&=#8|!A{ zuD50k;Ht(b_zxt6OHz_C?Kp+Hc55_ZAb@bA`QS46?Ew!B%sFJMz2LdiVT>JkHu|z= zxs;ig_*B(@2oHBA5Kl%Stakmww>v!dB`KPd#3Nym5Wrc&ihWH+lKn>L+#-=u=X=YJ zZ7SgU0IR~{T#X}j__m6y51ft01vH5FpHakd0`XvxUOq|u>4}7gF5wgBP>2C(;|2xC zC5EM)G@gk}pcfS=hz_A;>WrL^B&akn^(MfvR@A>=ceO&|UIt_1lR?-SScdMR{xzRP0|#VLSUw7etGk%Toy2P|m@aOk*V7z7d}SUa$-x z<>u(vo0luZj6Zk*RWhJ;W2P_c^DjSSZmUhp>y{(hA^#1edXn$f0Ft@aAg$Xzp()cC z?w2qh2T{Kwh6X)_g#wz3lOyppFoUQVxxxB;kfAt&*(ooKoq0~AmZKL+>2#}18nwZB zR#T_!%vwEdDk~OS5d^QoRGtOz1Btsl}A1&lT*$c;kM+l3SPnerh1gOKzR18MTdR zb-O{`PTnru^(dvj0ws+FO!a-m8pM_DUOfF!itlyL8^)F!`L-yfKb3?`Hp{!N(&=l= zz1qsPIMyDEdUV&2mC?osxn|MzXa`B2zWQ!H8)=7`A=6h+(cKy6o(K^&a2@g zGe9h(;#Wyk>tE~?93bR1!M_DW2>@%*uqR}va4)ADidHEq_CG0YpFb6$$j^+nnqsd< zpJ?l!Js_geBZN<(Ys1Ezd@iRL}=U&Gr0|!+_M0SR`5*W_Rr;2y-?x?FwVVv+1Jg zC534Fd5IY!X_=T*CTTg`b^U)_y=6dLOS3hMGq}6EySux)2M-$D-2#KVySux)Tae%m zL4!kZ_{cfWd+&X|zcYLG-d)|*RXtT}t*1m6FYt7$OQr#p3={?ow4k$cT7DHWxNR8* zW^YT3RiRS<*I}E?0WV@lI7|XIVgWO(Eg3-nBULB2gy-`m!lSi4Z=5H}<&@087>a?) zga@3G;v2%(4)-#4gcj5U7t0^bcq1(|h%`^wp|jjkHNy>8F9>Y54~^U`znFMLiASq} z_2T^OrDgG&MpLad^P6<(E>x0jV5b|2X^I!YJOUcmxMCNkcLRZ#0pzk_!c>*Kxcm9% zJhGXqW|B)%fBNa-Is3f*xp~5)*0(i=$qBCRU+5lru!)1s0@)`O@{Pu-~EDb zE}2`(JicSRVaAts6oU80g6-KMVX(K9NKF6+HP}$4F9@#L8}9Eu!)GhV8g>ha-`to6 zQXWgNFO6Xd6!h*A2lO0ZQedlrqaHr@AXR*?8imvodVpM}SKKb3Qk}3b};mhN85|{73p8ALm z44>(zwIr+0woU<;{4Eo|qH@7yi9jBvmtQov()lYD#0+Ios~ zf?(fOIbNrjtvf~M8rU-x5}gV)e++Kkm39m>BWp>E$=!x&H#;l5N)M3zWQ}EH1&*1SmNr?(zHb`5w?Gxy1wQ5fEh!oUFFsB@pn?p*!c4j=c8kt%?rc~QIl=j<1H7Kbq;%5leVaHj))&En!}{B zMP|#93kM2WS(QdTMAxh<(e&~!AyP?~638)H^P zJm{%ZB{Bcgy1iyt`&H(*2QKCy>oEOqWdlU33jSRxe4Uwt1!ZA6^Q-Z_kO+Y#2Dp2Z zu@Aywhkwe;?EO0f>M|I9ac8(#gM`1b6&(UfC39os`hekb*S{b0EjQXxielYJzP}cv z6Q!%2`3>8jN@6e66cp4FyDCT4Ad6U~{2Gb)jD^*K#~e)Y#E>!zBxpB z-61d-Vlm9vPsTNrZqSc6d|+7Z2WYN9cawlJF|35MY}%0CK4VP1UJIp{h_1)DtqIf9 zzf4H|#8od&`Mut(b?xWw*#=6p59XpS;iXA={fiz6Tn4K2KKO4Y+j&IRefXlH)!M9lCeQWqxz#6QhjnRVYY!*ju{E@Bvyhw7BlO?^Gy##68+soI^*W@+?6 z6>1S!ak&9uu#WqoKBlSFt#d(ol$L_UX;OBRDmD! zgjDW{Zc520E?oxIK@UE$@e|73YFtkbjSP=q5bgj=Y{94lop)rUs{eyBC2{am6b+fo zg0cUS{f3D!zu+WYK4if%Av&hw0E_guKg0z~(^Sze9};GyxSM-Q zL3F~$8;EbuxkhOMfi-V=?K7fZ59U%_TN91H#0Sb~Yd&OwxCh3}=!*no^k}jQ=afGX&Hx5C48nB>iaWK9>0(3+UD1oEz(FrmT`H*+lD*JT4B?yQQiBpub`P7K1VxUS^J!Woh} z6J7%&2kO}G`Y_*?{Q|+|)HKrV6Rv(7>Bo{@Njnn`)OoQe==~DWl>uPK5(V+l=s50) z>qfS(br6*m{Rf{RB0{_j2C;@*wA=}u-?>C1i#*-a_joUFd0Ch&su}`2l(vdHk^b9& zFffDxpv%^|LxU9M)Af?T1IjRlH=!wI=^{^96#7r!@r62s6O%BYJ;`nmrF|c14WY=E zW!-}#kWuCnAH2{4x@)KZg2wmfB<9mUXm%q^@WpZKHwsu$!O&MC$aX?Rq;>>&1t~f% zg`;Y6Uud6E2y!fYdXc`^X}XN&wgh7)Z(^X{?d`yK6UV*OCSqIsw1lVWKo<_9q-C*1 zFUVwus=!!3$rJe%@jaBmpJvOkh@8M3HX0o#x@k$E$~vsozvkD1g~bKs_i^2{2h|19 zG-H8Kg~vJ54v263hq3nib4E&QED-w4 z&xNnUfYST|la&9+jIo9liCO~=$)j}$%@O%lTAKU3zd2$Y;!BIM6Zn*wW)sMKw?c!? zhtO+bE8ab!DZ-}R79g41MDuFGTZH?oZZN2L>mUXE9X}}aKA++CRVe4(F_lxjxI|cR%*Bmm&G{po{cB3h6PowLyOq>OzjGwh0?M_o z73Cfv)ZYiNDVte$kpf#FA|*8pmv*y+tX|i`-E$#u?czT=(ArqrnS6f4aUW6h|;7aCGzL@9XocKlF)ej=KJ4Am#-qX)_Q3{}z>K4#v z@MX}x-R2jp@Yu=U0r#Urg8^s}0x)U8iyO%Ws0W@L<26i>>As!Jz$Xwf2kY8Rh zLc*}aArlD`RzJzmczQNk5yFTnf;yIPdoEK6Rv-M2XBG@_^bTHxd^kYLUCK-fB0*rH z0aDoQ$<|va(Vxs~>zSz6nw`{920pl^UkGTG5@l^G+lmTjc8RCxY>eyf5+%~uZIBKH zlomE+dM*V5b6@zh<8Afb0&dvEXD$s58Acw6J_2TCsKOM0nT!|xiO#KH-Ov;7?-O!| zN5A}%KGj%FR*JT-N5*OzOn%LAY+agNe=p98Tfb6LzRh~>BcuK^NUzE*tqPxfJp^Os z%L1N1+jYPe#X}QbQj0E2nVWTiucU$&p;t(`$%BJPTxvR^gG;0rcTCF;%Sko%hURv~ z#W!^|2+7C%bbZJ!ny+SXc^2ixT8}b%*Qb{)yAm`nVP%d&BK;^JB3jEDv;pMZLNTFo zsrTrxATR3gek|~pppxCTDz0P#@+Kk*ylz*#9WS(SW;|Sq2~lAJFL&x{3X1P#Cull9 z$Wm@jrhNoWzh`R2mWJjj@yO+|)cz?6CQjWyBeO=gUqVr!2WPgy1{J9O#Kzv8s^ZwB zW|8Wdz8)QIp{0WsPcOd_XsmN2kFVsOCKrmuLINt-p(L@tX;JjK0*Ldxgy$ad2Xt@4w1pxzTs_aR&Leg3(3 zxpS`_$!|3O1941fgsw`SW6G zX7-heRH93MVwg_W`-MP#(Za(U!LIS@6UquuD)a54h`zF}D``s&^|+gEHEsJ7V-1T8 zrD8oArfj=w;X-pwF$`pB&+~R8SEut2L&iH|$vQ#3W}6G!yR3Y`ex7kgCnIyTS*dig zf1H`@?#0KuqJfFU8bOc{Tk@qc?P)+F>*2QbjQ@x@{#nZQwh|fgDJDF!02g8Urw*63 zL}?z(*&y|VAgnK<%F$89<)&Il^}`BdKz2th!_HJvsqllx@GWK&rj+?on=MM&D?Yr} z%;PGRwRUG;K*!_o0uc4&fpsXjy%;Fl5CcN5IFjU&Q9>!AP%C>O;AoV@SBZ_%bkjgF zwBAkkTQP-i8p0p|*tAY%Ja9zK*TF^5nC~;J=TC>?VFDoa(`O(OKCUNDYP`jMRdh7l zzMd}KDtJEK%IsESHhM}T*Tr{l#}C(KQSM}T5R2e*(Y5e#28wFyg3z4vk4&w^>e%1c z4N&B1C1U^HPL#Z)T`Y0^<4HHOU+9-y^)AW(GMDRNVXHL+(TS4JUniK@+BA>_+)ecR z-l9t8!xD6H2#gpu&89PYNN8Nz&`wMX4*R5o7@>^8Tu%4A5AOYOQV(jWs}Ve$h`Cp3 zif|>)P$^D%UC*K-I?4@4Z1c{mq$Y=O;P5%a@!>&#s9Jyh($DL;FmvphqO}}nI*9sg z7}xeg_<2g&=*ksh`^=H|8!skTe5A}6kb8^qQ|+iGy$WJ9ly>7$qEES*a-R2L#ua>z zt3XtdxHzle%vg!1R25KKIPG2dEeqI6$t3wp6@}ZM0bFhSG}=)uW&hXglF3?{oh5dA zvQC3vMjx#`q0$yD9~|_q`04M;{Vur)6S6wAS6m<*;Jev&=pM zCwi!XU>TPP)yM%Lr@T}$DM-B5RJ#a#Z7^ZhR+4MIC)$}_MtScKQ`{Yry=I>k?hX|7 za_LkvfxD7`hv~mZ>MgV~I?7Q4rKH&|Ou6h$QysPFQi>voqSB{6#10X&4d7$PYS< zTm7?PDY%gUjDd;D2cp$mvTJiZfnYb=bdn8}R8o9z?GcCi{)XA}oDW&rl}=N1VxG+8 zQ2z8v8&_0AB7-sSOWyfDe8_gT=wzHM{yVFD{qRK+{jJYmEF|P7Oa%rxrL^kn199OS zINlKigs$DNieE$QKGVWpk~?s(W7NJvZ7!5rv!NNp&QNZsWkNYxi1Om;ts zgh1 zl#tn6aB(RyiHu%JUrAw~3v?3}di>?>r$s@NPPLnJnp}6)eazooP{Fy#TA3wr*50(Z z7jBIU_6SaV z{fu6CV0h;n+obb9IBHyo8~uQ~tc-|M;en`jYJq3RG^aX%oxHrL_2eJn;Ts>6pAYpc2G5jY8mscF1MTp!pS=QQYYo<>cZdAd~ITVK*KQ-c$LrEJ5rJIOqg1 zhP7+`;*V!yO*`-ou=TdTXz=n5`MRLlvGlMvBGqUg3xBQuPgw*r&tDn)G0G=6eSnodR-Cd2N$k5UF!R>JW0jQgk zyiqfaXZ>x}bGVC$$dCV7lZiV5pwoWB^1q#4&5iwDL@3T70=>Q&=Cfuel^p(&^b`LWkJGV3Z7mex zKltH+AmM!U&xe@$aOmsy5VCB~icP)F8qaifEA497uuGo^Ah+gtXV(-U(9mx~EB#yd zxrz|Z#~`L32Fqs6yjh_xLiB*3*z!O$UVra7Z+WS(3`@>e^wobf6P$9%U|hbqdaiat z`ke5*I1=SDNFV}T_@=8!>C@L7!N7@yZOLQ*^~f$_;|%1K}mahaL@B?K$DQFew|+C2?}IB`ir7NX9tte4Or=0_<<|) zb+qP2NmfbhaoeBN`kV^N_?wMv2dYoxr!YN6?>VMjN za-av~o8#FR`EBmMsycxEQ>t|ECUcO2iCgca_PqE9(=Yr^xeM)Zq2mfy*)wOl3#m&<@?YGC)oSHXywm~0_eb+X-Ce>wP{buom-WRS)eluZ9v}twa(w$XhAHZ zT3P)Ezx_Y={TH)-0}lEEpJHl;Ra2}f@WUzEdSX0 z->d6XJO-<*x@9dTh3p~-!G6bqj@yQ=%40Feezk-Y5I2Q-n!| zo73MXyA0%xy@{2TVDZ9uA8DHLzZush(jXbgT_F(kEtLzRL%*n@K9^bl7+wk^X&_}C zP7v011Y|H%ZN2`hAMtl7{bSZVOR_vm$RbON`s30Z1k5NMt1vFp820wpF4Xz}#oV77dQR$ao1iFEhIr&v{0yLpvQ1M{b0mW7 zV;}k0@X+diEwM3<&*vxD0GuZRT7=W#k>}Bnoo|V7Ep4T=_XI6nmO*?gTqOnvGOMxb zwzKVQgVF!6FT9KX$dfM=f>M}g8OkzW+y2MRz*#pYqn}G;z@{X4q)lvgq+-iRQbRr-(+fxES`iLjtZAn)f=EM*ULBI8JOT9HP?4g(hvdx< zAgh3$P^=Nx`!JaeF9nY&ZPV@~?9KfjLpgMkeh#3WUPnxmw?OTI?9Ybknr1Uf!9|Ks z7XzNdieGzF5cI4V#6h*5q=5Q(DT-L{%EC72sWNUjJoNbXh(gLk4#{_ zq+1Eio;yI)hypISSz8R?*jLo42t`hcxReif(9I;4C7gbNO4Dvas#5FW?0S*8Z-n); zP5nb~@z0%wYmj%qk%_L-Eo~GnpRLe^nn6@x&hq9dg0kOfXOmt4T4~(}7Y$;$>E9So zB|4^JbHFyM`*TnY5G*lIN}SxPs-<&*lpA7FnJgrmnBbdAqN6a4!9fy@fui$qQ5S6S z_g_{J&Kow3(1Q`N97F}W($}tQzvFHt>+$WW%l!IQ&$2%uc-*q&3Gs1W?R!Wp6*stRT zi{UE`*}GDMdi(jo48RzMU}&TB*qk(2P5b#2O=;Yj}kW2>)9eADuwMroE5W) z1tZ3Z3d(zVdgDg%>dHt5Cwbwh&ReC`5<#dkiB5EZno-VEL|UPTLb2IOj0a|a*V%oc ztBv}>aIu^EA#OHc$Q(9AQZCB0QT9wuK9Gha4r`cxjsAc30%ecCMz+zBX7?sa@B*Vy zoXJ=G1tpwHPM3(aBm#}6UpLu9Ap%a4)dD7w0={Q0La%`Yj0rL{gw~3C>W%G;xw0SfD1`G5#u; zqA_A&D?-%D1RO%%r@U95Pk6Kbqxn_{k1^7emK#WFE2wQ;foUb>p(q=5e5+-AkF@jv zW>RklWbvTv-q-umIMo^=d6araoW0ur3e^|-5Em<;KtVQw;@T?VYqVUkWXE*H`UKc% z0Rmfdv8B;G#VX};f+1M~%O8%d=6J~wEpnSylvUhAwAsIMvs`tn6YiRd&kj+fvED@;yhGC3s2^_pS1t@yqEPXh3Hc8=*I z-~x%iqoH;)hK$J0KyrQd^xRSTA$8R769HqrM$8LGbTtLJLI5P+g8@*aM@*3$_;iB^ zkT(qC-P=G0BmKq)N70>fQw{oDuG^z1b@a6_%LQtlLYJgn*b8SFNo2lwOs{n%uZTD* zT|%Ch(Q&j_xc*k|IAJiSP2(H4ftPl;W|O5J{=bK@zg$g!6M+k1h>J8(pfK#*+>ut^ zL49og1o^sQ>s{l_I8i!{t-FWGM_!eo5 zAtUiG$@ziOzsv1af}lvbXc79WGiLj+H9L^O#uJDBcZc~r!P=v=okJ6Usu7WtrWVeZ z<0zYN<^@8ZARI815VkjpK=WN_0xO~FCGB;8wdJ}HS|ax+HNRoUSBJX1!_{DGI5Q&W=w&!j)dUqR4;5eHn`%W~Q89sybiD zf4)SMF1`${xIor16bFRUi05jN$iUK^!0i?TK5~Tb2^-#l4u6=+6zRiU%&Ar=5}!N= zF!|=-aJW$1_vFM4b)z#mawVOT16>NzKl&|6M3Sh2|0iGnd|BZblr{!v;*7h`gT|G? z(3dv?D6w}$e#`ve7OgpJDG!)58>H_Q?}dv`4DtJ5PO#D)NEL@ngJrP6Uq5U;<>Ke^ zpc&uAI*>ko)Ys<6(Ygn9oZPa%7-mE9O5C~7m&!xGNCf0X2UQ%3fxOJoSRU@v5ufKm z(_BNyOLp+Htf+YHj*gI+JX0iXauE*C_{P%RQxbQJ%S{f)eHn1D_%C0PAP8Ky1|f|n zb*gXNo*UP6CjPn^5_o@gGd@P8H3uSG=E0<16il3C4X+|mNz~6-be3oLOfT<3UTZyj zO;cQ);4JMp#qlnuq*4W^*xJ>2GggsgtbS6%n-8?vu9__lnQ-!2oPc+0HfNRT@8Z)RM+ zOySTVBYr^qRGD`NmVXkfVwYLRF@}KsdhnT2*o~0IgQ=ep*2VZ-p%ZpSNAN(c_wRFN z?H1%rXp@`)FjmXTe|l4qztm&ZaCxAWAot)Vq^3D<{U0M9Fo8+9fedI03?v{H90Wd~ zIQmwOB6`&2H|3gXRDce3N{Ll%#PHrgdaw|?CjD$eHJOqPm5w<1>YFlZ`gOH!XiV~J zcqz%-6Dm2cf?U~Fwx$GOVYRM?a}hd`tt}kIXVXcgk8_9XyLVdcd9d|1|IUVF$9I9t zb+4Td6Y3L+kHr{>Rgn}<2#ORBrXc0qYbBu8r`JjEPagoMg}M)D;YNIpeOo_XQT+*^_2x^1s|^>(u{@Tcj&% zxzyO0zz)@G&J9XOB;NVo!}nijR*}pbLlB1O81GsP{K$VyzdS*w=Iy7Box@sfkaF?t zP}QD25sc^nk<*(;Mp##S_m`)mE(q) zb0*`z_LwbFaD(ZhsmK~#oP(6<=b3MY{%^)^L8?p+MVw$J zxl$cW-F(L2by0}(-bF{xS)^hmQ%m-V9)9Q&;YGdn#L{8rS&)tXD+h=sEbU)b2Hddpcyg`4QNlk>M8@c7YV=_31>_n%+cOSz z8y=PW0nw(^4co5=zru+`;q{JuJmq>Ih4|V;!cOobn z9WJA7Ept9=zgiePa_adI0tbXZ@^#EhfO<|x;+ASN5bTXamc|p6gu}FAmbbXyT03GX z)8K^oYdqD5TLTx(xQh&O2eWbxPOFczv-ctw&yZW>!#tPq0rw|@@rO!cfJim;cSSN( zQPX@o1*wOnqJ>j@8g&{W<@nxxuTX4MSXv#>br)l%9xhG z^BUWX;$tJy7ppZw&ApMiGk<*$k;8HDN;qQfU?gMXWm z7rPG5jUNudS_XwOpzrJX}Drf@igD6a#8cuqr^fZY4WHa$a76D znBEQk!loZaSW|v@xrI#Kq_JkpN_=`FnY4a`4`KRs3sJB@R>by}b+dA=RJz8i4YLMW z1+^knEWg(WgtlWda}y;2kE|61M~2z{19jg2mZZri1&TEG3IhcxuR()UVRDXH%M$cw zXe)_+3Aq1I`OZ&gfPKiqltlOuUTb1{QxP^PRc7q?@|@xwPLkLKf*Ul z)I8h$C6DWVS?U>!-is!tz#}d!fEnRdCa3zEE4dsGRwVGd z%iS;F=3ys(b$=MW_MB7yeEib&r2OQgJ)G#^Itta*cnCY+eGqJ(iuVCPp8*9-pN{Bj zN6vqALq9(Ubz~2qpkkc2qn0_UZE_`Fcv(sd@Y?Gk;EQ1(bZWPtEV>JU-fk1kLq?Yi zrg9`70)KLlO@l@1eFb~*2eIT3{|U4UKnho#*h9=O{22#3P0TOdx@i9lGEmRat{Z*> zm9g>1ytr^EQ0FsLmWYf|16;V^xgetg%m$J-Gp|p#s9k*S{NT%Fr97}45Yz>L`SPB zC!%(rVi9RvHf--QuG0vvouYCDFFW3Ioe`HJUmt2xb_1C*m zm~yHjSPJ#>Rla27n8}WG*!ztlSrF`%_!VlJ2j#d}w!^>&ezdw{1UTYry{vaHgr_se zVXh?5Po_-ar>C1juA$A4+nk@zAo{TJy=?JY`@;jy$$7u5L+1{n`(~IBH#yw)h_I_S>CsCneYYN< zYf8MFGwMO~4t_4Z=mhDw)g#tlK}^?Srb{Aku#dYL`*pd*MMCtJQzWG%iosF5=?$w^ z_JWB;*5)4OwYHs4^^^X-2g#V4O!CKGDZ}L0V!benQCDJ5(8134pt5z6$ZGn?ma6h| zDy_zEsO0DtUyY7xOtrJqB28|i*LdF&i%D&tPu-K4vcg{q9@7>p*A!m zjjfv2_0VDagd!x#1!fsq4vAJ{w#P$`K6K^tHljWsLikq;J%eyq1-nu4ne7(G0I4K5W#Z92MqSYj5F$4gM}i*!f_5v ze>}FapVa`cdWq%JClO(AygB#po0paC6xq^K<*lJRweb6|N{@qAW;FF~L*32|6I$TY zS0%=#m?T7GAZ6+94od*^>#DvMff9!OqZHB~ zc7owkJL#V7H=8f8zr$hXXj+X8gCTF7DB|kdICBDrE#KEocdZzMIJVN6||Vg_A{5=JHY z>%hm(HZhN9!Jj%Ft1 z=$2y|j%}v9WUd!Blb;{%vh_D7*gySkALV$=`Y5A8tc>%2)@whGmh4io6y_b=iarjxM*{4oSpceS4F?IT=IaxkuvSw znyG1UG>r1)0PhPsj7tpR=zf^*rz&xo{M1;u8Q+yYQ0{WkgNTZGn=1WgI{oXxn&gKa zHnOh-%&X!n(#&UAwzxxB9SB#eEUaRm8^)t0jnQ}uf66m*G9Y2OXRTj`kUnY^S^P!Q z^YSH{ge>45Ll;Qp2hMmA!h8T)wvY8g_Dm(a*ye*Gbw*x6!Rao@YuE%EHq9%)fey<# zw~A3VZ28?ikLa3~wyS^yIesXy8W|boQx@y~UG6KNsjGEi8&o@_Zh|mp&A$E-kDmw+ zgv-;)>%vzbe@&4v%DeXz{mv-E-qPSS-_Z--t49JyU_sHZ>inpeL#+?G%nTJo--zG=3&y=|s24~6VMW!%_Ao!E)ee~o z`_*YV#7QGG-t10Ur@~MKBp*Yg1AgNn=&#BB0fPxAe>??)9tou%yPt{bxRx|VcF+YiY#O8ht^+vc zYejCykJ>}@5`FeS0E6=%UF5#$rpvT;D)Bdxc5i^g!F#WSQg^IF_rr+P^e9lGC%(q zY&%v?bq`^)Y|I+0rGhDc;t0|s)B^*tHH*aX4e5NR6Lb32ly5z^no55Cf&lQrC+tP= z=yRqMwyX1prpQ&|Tav!QZR##3Q{Z>LEie6z794x~8BvDl{E42prnAS0=En_B%!^pf z71*Uemd#!WsJ@9Ql$vS2i4Y20Qv_#7nP^l&Xd@f^@TJ?E;lbvV22{&6RGOC~EnoJT zOLXFK?xkOSv0Bn_DeLyE2x2l=F`YYI^X>C?qQ(drk=1Nu+KN!|dDEkxNc}v-83B0xyYkwiO6RsRw)(Lux1jE53(cQ{lxPk4t_< z)_Vs{CtafBA+MsxV7bi+&lNc$47~{myl)ONwBZEvDmvf(?ygx%Qd`au=?3zmND%OD zU_RQonznW*A~vEyp84cjtr2?+3u2xmj=XDT zck=d4i-3kM#03{$`o~#=|*X^CsM@LIIA2D zAxD34+dTu~v{5xYMR|>%2s}Otzk&Lj3&`lCjncx6_v{^Xdz^>e9*QtaP(`(w(_Ta# zo;Lm@D?KoT3zHnDtxcs+R+lT4)7zQ&EeKC(|8Y;xz-#I#h|G$<_k%B-)MlZEa|@{% zssgCOjIRSbc+^PJ877MFhNQkrcJfPQBsO^G5GfzpI6hteiyqzvqHS09bk`(5$mHr> z^Ot9A_;@s9t~U$-W6v~wbs~RBLm@Xqt8l7&I_b1OB)FP@Ifr(DaagRHr&tS8$HHeNIF^|XN2}^jn4W@y( zaP!#y697>BWAsI?mgBBSJ!0CBNouJLrOhTph@W8(55k3r73|<7u@v(Y-JJ_GoK44l zNQmp>fxngXV%M|p<(Zzl!;g}Z`nqS&H|>7niiNbFC?Mit7ZN$_;f8SBQQyU@?2^Kh+=UPjmwSa28 z81EIOr6C^#H6IPUCX5=>pyEfIV^d!p_@;upjuD%S1TMjnnj#2FW6tHtMk*tH;87N) zyy*B*mcpXqrilFJJt)lAkSA;vVb`t~0By>2Prj>?tq(3X0|`_cGq+z+#wDK%D&@1n zbPCBM^q{+2o)PA5jlNQefw7>#kzV|j(}6K;jHYtP9-|-8a&ori2Ngf`A=OGSq=MHX z?Qjvgeks?doy*AI`wduIED^aCpN5ed;*ONc(RMH%s*1l}hoWTP_N7Ajl(l9n2Y_%R z>ETW!9`Z=Kv5gbs!(AJ^n7~FuWW!aIhE^2fdFkkQy#9)C{j~)M-%CYqwDny>TXHWg zLn95a;*95JY_Lc!Uq7TN!Z_Sg3@wanekgO-H?=+v_oeb-vJ2I(+XR+vwT5vpa0epAkasQ{nj(=vjaJI4CR~}8F zk&TOvoEHK_tyodgfy%?;){deMq#v&&?w#bP_+CWi><9%!1ZJ4=Ns<^Gh;8T2*qX06 zp;fnWNUsp-wd~zl&*Sf?rlO z$^h8#!G#viMu)T7fdL~_=y3Lg5qPibwpPsr%?2cuxV}eIBR21P*N1DBu5KlGqQWH# z83Vs<@lB;noHlKn-g(kwNV)bTE~J?^T#+cvg2AT-1Z!z3YA0FMqORm5f_FK(C@yqxf9<^-i@Od+JVHsttmdd^{*Fj6nppW`Pl%Q>ea>BWi zKg>-87KuupP<9GArd9&6*(=UXXs=pTw4-vgCq#jJn#Lb=uqQ07cIZA~l_`u?pBlSf zNdHv_x#90|tzFQA$%%EDpZ~HDokk|hz@OUS8@|md#Z7~U&(bV~dI5$>9g$AC_Z3lz z>z5HgWJK#810?$(vIA7H}{RV@|R?1*Z$XNwPLJ*NKNJIcg%Cg|ty|7@?mP zJK2ozUJlR*!0K2oRi(i&U4O8-zQ=Jsg3jpEe!cGD4sq}Doq__f`AGeCH*DLr?-jv& z=IF?m6~UOeCbREZ-933&bW8~NX+(OQ(cWnHEj!}KLSA}>c`R|v()$Lz-|a z#S@?d7UCP!$?+v}$9*98t71ZgeCt?eYMvvdyfiC0ocKXT&3nW{Ujf+tkal<}r|1|3 zA8^&5Ozg>Thwrr3y9JgiQ9C(>;3bT%Aahgdpr|OOP2xXPsZsPqFeNO)Zqak4=A1 zwdCR<6f0J^987B&}j^ze5H6Jq}OJtDxFX)ghEqJeX!FGA%bNyqE5R zFO(X1aA%Ul2_rjbBwehdeLKyheYS|;&WIZfF*_4tF!@zK0P3+5kj$O0`hV(T+aX&#HK+E}E$q>~J}vyDE_ z#B1fOMpis%hBL)fuMP$5xSI{SBEM$DP^UfNaBNN1@}GCflnFY^pKY zu;4Plq2yXI#+mX}9E&RO_5Pt>`^u$nDDx3oBr%k(lnIZ38bO5AQ{fR8$$k3)ed7(h znx<<$`8UPf9QfTxJ`yEg#M6E*y>Dlv9uNa|Gyh**?-*TK)2<80wr$(Sifx;nq+`32 ztk|~Aj%}-xj?=MiJDr?7`;5KM^X_ka>-QXURMouisx_{#O7pAn7*~$1zI_1csfmzgQbDqv zys=Z|F{iPZP(Vo#ro0PfRv{#&>F?-Bc>zTN-QnR~eOp-?=A@Tc$gErYHiIV7_hqWW zSC!deV%8iTw_q-QfuIgE1pX1tr`7w~;D&cF{jS6qm$aIl{S}1lY09{e7(}eWRt`d= zPY?VBrI>!{)w`V&J}~DMN`{SGv#IyUDI9iEw4Mu79o3@gdwiCMUA~`FT)0*ZIyx_R ztV6v+-Sa75r{D3=o78A zm$2wbwJ(*$HzDn<12zKPNBm2pQU;XJs?qB?`q5J?*|{E6_@kW8n4y zaS*&A4)cObw`EICQ(k=5$oA8h=(}A&IA(_emOWxqj42^R>r{M}TW^7A}v14u<-fe?&2rnpbvs87#oG#WVcE&?C+QNQX zU_m5$S4043Q_}`6*O6{3sR|0^;dl0_j+GojR=K0Ud?gST1KRh$4_D>fuNUJEVT68H5^z@%_VwgvieMHI!7fYc<2}2v*VW0^ykbzCsR9LQezr*CT~j$HN80!F(Ki~ydSpqd~k6& z3uG!_p}(h^seBN6`$Od9c?lFwdPSPt+))Y(HTrDL>FW81Rq%GATxa!;i>CFJaU|+h6wU+-Ul7d{l7}O+Q z;H)LBaYhEE9j9N4zuT8^nXxe&v|>n%3if<%^)8zNa-RC$%40KBJa5y}bwop;muzrI zQ$liNDrkAepNz_9w*0hqr_5-YZ1j8>a~q2r4Y@gmwfhCbmb&+TY+_9E*CLp?`Fw(g zl|YS5zvumIzg5klL4Q5r^Tg|_J9mKDzOn$vPoWVbK;JR#vuo2pT|EAl-PU_#?No*W zM1#mlSQ_zp)MfJ{5VNLR414JmA>2#{i^Geycv*g^;Kj2UM!IOt;P#e$WIAw0#ZDLx z5Ky5}A-NF~*`%iYt&HQeCglY^Np^zdQV)tiS5akboW*C(4CROSOrVUO5+RuI*BO@mA;T|mYVuGAmmVSvt`n}Qu?j;|BhJ1|;4m=c&& z<3?V*&MxqdyqxHxN=75~(QG&yJ9>dkGP}M|(ft#R9kfyyi4aGVJM$=Z$G>G9Z%3{@ zg&P9`=TeG1Z|1*F@ne25L&pzoX)e;%2gy3lLuiZ(f$x7w&*@C)HNj;LB7m$locsrf z`_}Ky^xGDHKq7TFER|sb?M#EK2K!@1oi@b_`?T4gzCcm?v_vpaB5=u_p1X;gortq6 zkp@!u-s|}`*2YpSu}pa%)wsr z`yg$`@dtAE@`k7z%u`M@5UpS{n z))=Y0%imuFmGihji6cag%6XjJoNA4iSC^=)fZz_(hTYtrg~bR<_)tTpfWvL zTP@tr%_zpS=tmXqqUldiSb|WR*Um5S%UrrhZ3?s;C?e^CayJ5%p|_`cT1>uji%09v zF}%7sDwONs`?7D$do7)-4RYpuc&#q2D2nYzG|`#mFvq2?=b3K1)a4#zmEVj%E^fk= zpC7)v$Rl`Z-#vK${LM-z_=gU(->huspq@y9ZMDXUdwW^}|C-JVZax#kEdOd&&tERo z(vx^ys1fgbG`M`*LU*3rKAl17|7P9D(EkLZuu5r`) zmPw?j#1k40y$>u3XHzhC*T0Dr^gH`Pxv@p^rU%=;+1c2PH#q_yd^v{qlCm>Hwnv%qlKU8m;jp?N?M&d3Gw;b2zI~BiCe6rfC0PLHg@~LHF5nH0Qb@Fq`E(k|yRvlr@=UrevK>d8-Oy$?@3}Cx56o4Z zh;|2E&af@E(vqnw+(v-yaL?!#dxwe?e8ol7B-b}aR%x!3;skT+rB}5>JJL9Jr$#iE zc1kW?3WJxMZba?8rG_ts+zmcqf6KPQ|r zS%p_=;@z%0>g$hAYCNlNW@+P#PM!i-%+Vq=M*?`@l?I!QFHKb&Hoq6uFwO$PEgOf+ z-I>n(s}_*Vhwv>a7OMR_%mhf6zvts|V9yPU!+4!2Tn%NoMs;7Z$%yl*&S@#{-;>*n z)dwIi%;yz;g;l3==dDa3WJ40F>5-?<{~-U!#pUv@CVGV*&Hp%MmHyaKW?lTb$=%e$ z5rd<^njHwmR-xpSV2XwUMG!?*z$6`3$$^cAENk_mZ@rierY~l8Guc=_f=b`zjC$sg zJrcVwfU#HvX5uy?-TwAKs*~V~F~OWMYJi$8>a=%!HnRR8xMWxpA>!oe zJHXl1M%oFgF(T_$EpdaizF8I_qV^`SmvK~RLNnQysR3QjS5ZgHy#J6|esdlwlLT$* z9gV*wTZLT^>s0@>$#r&3#N)7*lYkGL3nSoZm9M|^L7o1#r@TuRS0&;*za4+jSNkBz zGEjQ+oEgKB=1_TLCO3OUDR*d_rwau*=JT?iTfkIMlNb~RMF?a{GjK%+T`(-<34v&| zT7@SOQ!X+jN~4I)>qe}wjMr?C?CX<%PYGm(I|D(F;6xO&ansoGhPcNO2G5JpYFp-_ zk1a+QF-iA_C$z952}2(qmY!ykG*rQblWn3|!i~<|f^hCB(5%|twnFB{ruOu1Bbix) zMdD%&KDY8jcUckyYu%Iyqs>hu3VAV4SMY7qcr1EkaeA^LTQX z2q**RU_`@m<5-vPZ@AU})qjOQ3I2xLjS&+e=6l9ip~=5ry48CjxgdZLFu7rxxUUM#vZ)gOn4m9n4&r zQgwY$Q4+fEkM)_2EyPQAM(>+S8@0#U?SNo@2Z_Lj68DHX-5gNV@j;amxOivuh#z{8 zk=KlEFYd1*=-JX+4NHzVinc`?sii+&JfHx@4QR(p{nCOLY(ZQsCP($}F z@jA&)av%lr3{#OGY%)qco(PrKK;>!w2rvT4Iq}BsKxQ>^jIo#o&x~}c+t3ni(+|%z znRo2r*9A&=w0tKgi7$b^`6uQj$1t?>-7Ne%9=r|f3;|XwfDz)j^K|zh z#L9i4W7yx_5aMgp_0w|fvceb}%NnTg1G|2%iOa+>irmeF>Zn1WrY*%k7x2!Nl{7 z%fqICU57tz>m(q$Q#@I7CA17d6IfZxTEsXnoXVltJ*K&`9ZI7UQ!eZrVKTVVRKIa* z!}k8C8pP?JMPXg_qfvliEv$q`)s&Zz!kk0CT>;0ai(RPeklt$M!*wrn2M`OvxWl%h zE`2Y+2)^IdP$5@wa^zIk8bh>W2W5xyne2gEe8Mo>pTS$+c=k=vO%1uQdN=6uOIxK+ zOvo7u{7&~BY;Jfn>Fmpw|7*7mA0##(?E$_7I9L0t(s0oLmVGxC0b zm1oi+l5q_AsbhBa6R&EHJ0s@Rcd~!2Isq^g)#?XSu{XBnxPcG3O35{o>tK3w690nH zABPgy1^2nm(9C}I`rri$;|cB=c^z}s9S4r{$1B*NV*rAljM%WA?*@)h2@CSTug`ok zPF(l9T;MUW_03(3Me(+!Gi&>=Z}tETeoL#_6G=HYBsr`t1swDDwBurS(?$U-Xxn1;b~xs?1d}T5~LV3#03^@K<{Kubpt&- z!uK<^sA_d(XCL@-Hp^ovlgx+hcdCj{Ifv+&4ITnVMp7U%n}(n17H_=qr%n$+3<8j)4{Ed6b^Cc(1b=(w_CeAvdYFp-_@RkG5iwwNfO+2*XK_Z%yC4!98*98 zZeBLOPH{$OJk|pik&e&RBil{Q)=e$@*xCMfZ~9+?mYHT$D>o?dGh8r*$BH;QW6;&-U>7k4CHn^8)I`dHHRo_^-5jFs$!g-nE1m~>FEfM-Z7^1fmgbv| z@Zi9)^)6zv59C6IseRre6?fUx<-M8TWuV|0@$P|{3)XW*gR0Q2-3V_Ho8M?$1R^dc8092QmcN2GJ*pxMXfI zX|FmCX^#Q^M*J##Lo)rO#51eH+U4snFAjRI7$ldJ|vtFHp%(}*D>=p%MH zw$D+G&l!Jjd9^V9iA-kFN*-`DCfpZ*o3s=Y)lC>#Z9sYD!HtY0HIfaaOFU;

}zI zrXrnD{6}41n^r^F@iN0b33Eby=={$QSjLzXix0J-V9#`m@9)_aMwGauhpaB;OH zkt}VHfTMkjh(-Cmi+vAXFa?r6Q=t1rn4DI}dtXV^TN(KZ zcUMtBD+mG}MhxuiWnHbul0n>0m!tZdZOUwUdVnVD%67s=MrRoRfX`#!my_D;Xk$+- zdQStAmSSFiX_1yARW}H4$v!|#vwwC47r60`LQEXN+AX-P-nnTZuykDW!w+rq{fRU? z#enso2e*y39Blk8&g>zScPHAAXaivYS zILP)Y*q(HQ)mxo7`lO)0{@cX0Um;RuuGmc+zxP13d!9A6)!>#h{F@%O9H>lH>K^f% ztWA?CJd*7IICMFBASD_#EOqKIUuM34KXPNoBL(~pHyPESjJREiXQC?&jT;f-Bn z;$5!d1~7t+c4DKCHnfRw#<+AA-08T-a#PcLo?a)x&CLX8!bN<3cbwd=8}Dw+_7m;5 zH^Z*GOA%F@`{6)nD&t5aPDt3~?DdGFG<{Lnv(KRf?XEs#Lk+uBx=2UsPZQsVJ359G z4jj)td4cum{^i3%p`~Nb#9-~gy5wI8@;|&8q=F*Yp-pBBNgWHRD=APYQ0@ri)J(E3D2K=lr z#b{)7O$XvEZQKY@jaf45pgXo?uIAkHy36&v-$^v+{xTEem<5-=#ab;$)XGOE`&#GC z76eX>QPZ-I{KPl}t;#Nrlh`b%BiCTay=4E7uw~ zw5Al6wW1x4bcw@75BicpUr(VBH8ey|6N4DWei2-3yBd)a{Ze-p+L?zFH7lu4f2Q2| zYBHd}&icA8)OBs=r!c&PTGPJJLH+yE;f!m(i!1YtUzcSm%QtP(X0MkA%r54iGp|rL zsNtH_aqS5|II=3T19A~vg046_^eZnYyvQ<1zj3kCGU+Bc>Q?#YVlBRfDa25uB0&Lf zZ8ZcBw|+#}L%uLy?+)?89Su0$7N{w2JF1nF2R`_EMdA)0c8tGZd@+}DH$!N4Egn9#`A2v-D;>{FOZfmY5bLl;Ht<2cE<3VY~bx7 zpcnT}WO#g7T7HVU)2TgfO6jPt{G@WCPs_ny>XBZdxWSd6or1In`z$zobG~f%`pXMg2JFXv#bk z+%BHAP})$AXg*jh+y*%&O?hSUt0jULhTh{=Qsfw<{2X6PL#@TaP*+O?;2PC8=pQZ? z9Ki=`2aVfp%)3%W=dqOqGN@>G>!@I;87UgEIY^ZX-xyg*cvjIdywePAimE+rWAg~O zATAcgVs~QO%+q!gnr3NXvQQL7!4QCTPc`!7R(Onl5 ztXQ*3L`uRi=N@8pIir5m)t95k0G#r?4&}`Jxbrx!E$*|rDK9^&xvBML zdtSc=ch&PFuK*h-TSXBTb3)x$kSmDAmpBD}xYf-XDTbOnSr1rP7knoz#>TibBLDMQf*74Vok+h{Tb&{U3DBOT^|XAOa13KllN6|g~^T|>UhrI z1yMNmZ@CLk=SR+&3%l#~(R@DG9xWgGZ9hAv@#m6m4r1k#)joje(8lRy=&z&CGcFl3 zYTa*aZ^nv%jiYD-@`@()>JQbbTDEq&WDV1j%#3oNk)y|6LOe^jGh0fQo9A~iB(4uv zm>*;0s=bvKUp1_|*!oVrgNnBg!`ciHO26b-N`@wOwlX+lz!!BPuTX_6Lx>e?LUdFw zRO<}n(iaT%@OK)7zZ2co$vR1za58<6^mWbVr^02~;a|C~=C#^4l>~8U%cZqog&NNrUcW?agZ_7gd$eKmp@Dw8TJ!pyQ>wUvfd; znidfUM3`uUb7lB7{SZ%XaaG%`Hw!m44XhTuW&X;#I+YzCx;=oIH$cyQd_Wc(3A1`n zSTDLSs%6NxP^((QxsE=HCb2|0K`yx@)OUQ0J^>f*_H}joJ^HnSKyv(LCHsV#@{(Tp8!ODZ`<6(@;DKlp)dV|&kul?9@!5&ZXRmhg$h-=3=`E3(MsD*#|c& zZFM8-ZzeRJf$II@Pb8C>L~-@q0^6#;^Kt3oGhNZP&L=4!*g!Yy3&KtRl5%ES2wK`E zFn*w-a{AFoQrq>nyiYve|79H0VL?a|rPmlO-nF$gYQ+}L(J12INzIv$evtdZ$cVC_ zrX9G9$01D_j``DFY@u_7{;2yyv;{wmvbR1-O4~5n1q$k?KL}pc7YD$G6CRVK(V1Q6A8dSrYQo9zxX>tqky5@bXSbk zyv?A$LA6O*NE7<`bjuJ`k%_c`++_I+gB#(ZeMvesO*(T8Gd@)dXMdXZxA;-^yAY$P z-{Qj_pMgzwkgmn63q)vUJ3(gSW@{9C@AMMmLm+leBE=1S0m(M*CREe`zd&1b=m7X-ro6^(>>Y(t)L!l z=4)S%+4^!-gJH)}o9!j+c|L7yj8&1O82Cs*KtLL=E3mWlwDO04UH@9&XY&veD=m=U zo%7oAxk3^9JA$lCA~IJCD%R9s7_-JB%!Zkmr?XF58Q+d@?&M~%=61;u^15RAcw7zG zOvouKE7k;w*n8$=ZxbI!EmtI6OB>~#B^G_1@^jY1N*CZxOS^ik+z$kr7d&y*!VII# z!8s!V?x@da7l;(LmUt`y zFf7bloh^^CqO-)Mo6_yhk{7Hx8j!_G@-y#Sw^RXxDNA;&ef~1SFS?$yv5iW>ip}Ff zC{cFz*kLCeU3mWt$ZR=Y$c+w47J_xK_5gK=* zUc=e86eOD@z-9?lG=X@Sn-bSi`^d-;S@w4i|9iq#Q`c?<%I`r&EEZE&A$n(P(^X@D zUz{z!$@-R-erMxWwRVLvspkLcLy@*A!V&aABgEn;R>T4|LATs( zwC>F3d#YmMFssp9I0`-8`|1U|06-QSNn#asq$E?o*$X+w%3EfkDvIQUmKEa6`z!A~ zD5>%j{vW98Kb487W6>wtAC&iR+=)S{rcg3z3$xihRX1PKOS+xL1oBIRek}fFkZC$Vzb++kwL@iqm(0fF!2$nv~TAMBni76 zH1zN6TzU$jwG#Da%6!(K;&8u)(WL;i5IhNG7>R_%1Z?8OUTa{{GgxFd05*4m&6u>M zlz3|luc!It!HH4)ZmF07K`~L3ePe^XXZ;2w=G-junG6&wmtW!(gzE#a9{o%YUj(A_ zFh!Fhn_I%UuIh7Z$#;Q)SR_rHp}_gSCiK5n8{$Ts>T|ARuD!ccqX@ ziyH64pZ>9@u#$sV%?35eS$Yd0@&9O?@omBxXqJmaKj8}^ss!g({>ak`hxJM+RE2Ja zJDfyU8?H4Hd3xYC-xIyW;e9H=xrE}9Q>3EPG_1l9w^j~JqHj9$12*}?x97La29r!1 zznEbaaug#k=s#3ygr;f@{6BAzfCHE&c&Ol$vb6hfk&k_!?dE%fd>$hws$kTg1N{c& z&)c<*p0Bki>jI2Y6}-GMwUVtqY-Q_zxMT$3pgwXyX#a5}M?y#tM- zz;nMn0S90t$IA@s(BlDPiAaag^<9W4!j3pkxqY8t^W{h=dnPJ8gBMx}*)NOAB%jnbNwY5P7~OLcfjBY=kCSpC{PEAhHFEgLq|#Kqw@I5Ug!VHSx!D4m58{YjxbK0FWiI; zb!#$(F4gx3CY&RTp6r$6=`xMP=Vi5n&rnDzUyPijE!1qb`!9+;=nop@WRE5Hhm!M| z8IRKu?$e&u`A2=pKLtV0&Mrv$P;aIJmH@sBY5I+N_tu{cV}E!(dO(;X_jY`Z(IvpN zasw<;vBjMUX?pJ|D$KgU9`aEG#^gOxWY{3l7Km4&D}vYi*^11*MIMwmLYztfin|qm zv?}j!+;X^Xke&V>{6YdIQD(4qcp(3*Ww>Huv(W+!aUsXhBW4~QrNqUNDG;tAdJGYL ziOdOWvW0>aq-wY2y+NI2-Dbuy^r!y=DH|f_S(NcM&*6`Yfo9$@*9C3RRu;yDL@)Lp z{4dPy0UG`1icaePAMUphNvV&blQVT)9{^f0A2Pz!1ffNzMfZEKKghEF(-w=#GA%zI zlBw9qsWd-qqZ8T~@M(yHxf3`N3|lDOC-+Ty)q}t$wv?Q*6XAeEh>3_O{Cop`UMh@q zNY+84xpQ6rBr7>5+TYI^ri|9A`)a%1)63gdiX>GJk=&Udh0W`@l|ZnvNs?4_m_k6d=kIhO`-6ZsFCOb;I+N+YvN+X??Yp0 zzm(*LR0#1Y>hYCc8E2|a(_cT>9h3Y?AMJTVAE;|dyTiG3IWbV!d2sedpN7i;h}Fkz zx2lQPYmaw4ekCvx6izOW*W&WMIFp2!5Jn+ynq@ykFr_#B398~axcn(7s&!-i&*{JQ zXhc^8ShYYS&Co)gawLH?Tm3Gl%l5KAscNG9YRY;L`mVreuCt zCs4WRu%0%yAyT$Mwpwx@8HY5O_}2i5zS3^!5mh84KGer2Gtk7uO?QE8y4n5SCUnaXmEfYfI<-acA6SYu=n>REooQ zMKqe~3e2mHNQv~WeoKUCQe;5v`K{WX{CE@;D8RoV&@5A8=%5|FwP1=B+UDncvx30d7mMt!mKshA5u zA#fT^@=3CEkb6TT{m6OlWQ4zqaZ(CU0s5!z9JohhUAZrF$T}3X>!g~5N$@~~mQ=MJz#&tlw zGf{@#ouVEkE%;*?*R91p*<-^5WosCt)oQVG#QaERd;_zfuF&}^DXLo27oqRp^`TD; zp`8|ER+Ww%UW4Pm#=(HV{wW~0pM4;j0OEly#m8YGZm19@v!A;+ zhwrdK42PXmfJrFGVyXxR*QjMGQ;8K3p{0z6k7Su7!1+zvkqnb!uUcz7LsCt~bu=2f z)Xb1ODN-DJ-SI8v*vFUJP|q98-|Gwyiy33K#jiCK|E!MVSDt!(>p3<`(h)U3dg zxXa0sIel*1`ip1O`JKue_efc2%3(Fgt()4UlDn$Y?a;WHA|Hui0fHQiO~z>bZTI4( zzZ_LGfrE+059jjt*wEHfGRc#^kR(H9kQZ=?Rju>cbd@nE{`Sd$=jTL!!8~O;&p=5e zP(9y)^J1QKLkP`5n9yFxpJ1KURuk-&{rg9;4!eJ5r^SLuywPzmOnR;3MMaRf{t0+s z!ruX@>-YOwN4~m@^;9|s^nngrmSd7yJ;al3KdW3n)XZR~fFHkajS1=YZM00ZN_mlG7p*&k29`I|z1f^(g#xADpceA5WUcC$?P z0N0-jUu%_@hy~>?wt93r?O2Zj(`4_{Z(!tut|C^lsjV2CE0-Y zD|er^)wcd$ogxR?Va_lYH^S-~U2(-nXsV@Lb@-G%@fn&P-#FF<1DvKKtqRAhW9^#40nms^B!O zh16pq_KpIPpZ=q5U4#NmRU~9X5?dol?=u2IQfUImz7mSBZ#9t%*41FVcZRFiM1^t{ z@e#5kM5cJatYIjTO55v#-pteaU`8Aj>K<+-DveM*`sdRfB}RfkKX<6Q>m`2Bwd~)j zQwFNsTx%u+;ewox^oXRJkD*GrM63)gHYdIVI>j7bU>w&4r4P8N_QGYl;~T{~+SL8Z zi3g@&$thOY5>dE>F+2_ozuN>Ncc#stBO%OPBi3H!Ey-Fy7E4HE_1N^l&Aw|k+4uSO zCjAD~843729mK*@0%+BO7$pn#x8EpTAD$nZE72s9T0?myxE{rsr2E`ENb; zOuE#qt7n)dAE;mvhBqj(r-)vWD|Cpb10(!yvlZF8d}ea3ZhFnWIy?MxEJ3*6U64)W zJLWIa^JH{Ma9B>VuG*c^+J=bJE1amx73wtjJaD{my--0$+{3+?2Iz}Sb2b$y{O)Ce z-(Sngp>{&Dycds`jHCdPul4ZK-_7sDx#GV(5Hu^rBk-OfJ-CTrVS~zXaB=zn?35t# zEa71vrVY zvbjVRxZREB7xcKMdrZdXhNlVh0$m@!lbT&(Ms#C(wAvUJA^IoKZPa7h+-&Zc!fehy z&lduZzqJK^{%Auy`*>oHMZgQfwsfuXr_ObMoT4fz;t*=eedG9K(ibDj>ZNlK4^wBY zGWS^ceq;=Nob_N6ig-lC+_6D2LyChEn!3Lm8t9W33QR|xz7mFjbG9qpFi#HMKM?bK zQ;@FG9JmZg%a1)VYcFGHdB#1i!%Q0j*{PTy%`Y|R9jI?iaQkFecGBuJuz$DlST^M( zBU2vP5X5f{ONfzV_IP)uM1F;*zA-Zexs33rzArbY89kVn(QbG|klph(%NHYhOdD_W zWyL=L?-(riBE>e_ORM@y_7VIyG{+dvqE5njLeaC5Avci+aq1}Y! za5Xs^iR?d8Pb%j4w2TtW0{>lBRR=Wo%Z9+gLtg@D8+J^qc0K8+2EM!6?I6`VZc!xB zHWxqS#;ilFh2EVRt_C$bGnv*#r) zJmxm1tH5UFB{G-Bexhu1)ka7FF)m}T`W`Gj%o9^?RL?%>SrNG2^-uK?5QUex(q6i#*roIZ{-APQdSYq6w9n;=GOj#!qLY z)Q^HF6(`!^2I@K}C3<)6qAYPwAz2LE;I}RU@GlhBB2)0s-I~k8C=md$$N;}!&F{}* z&pV04W}C9|t!sX$!msrcUme51z!32x5+ip-BI+rcJdHGao^vR$!-4w_9MgY`)}CK%gvdLyRp9?XdW(vKt7l~+XklSgCCZ< zcPbYA><^6}?{PxbLZ~o!vLKMnn-N5=``S~n;OOiw3J`}t^A6h!^)r9pD(G2X+@mDV zLzp%)@WzDrvb>_MY_l-|Rhq{9v_SHJ+q^hP`wfvdZy7YBAY@z?Sc^q09brJ{p;Hbh9bU z+?OB_DftiJr`KemK8^p;+DJCDlgbL*b+eLE>|5M)y7oQN;JI1>>&{irwN1wHQ8UT^ z+jX&4!0(AtS--dQJd}je>EWPEM&|v7N^Yt&61@L=EfgU!reiY7P70jX5Z9+u*SMm9Zo9XgNFAOyrz!t=BeX8mo> zgYoheyLRKxE}JLlFY=kW`sRnWmZl%2;VT{0rEWJaUo67>ALKKvxbbxI5?b6CjPKyT z!s0?kg^fb@X2psVHA*OK$k%mF(1wv%-@6b(zx9f8nzL}k3Bo;Wi!{9`!7G!y8v#H#i!7j^UtAHH}_QIrg&#G?<3LeH3Js=8(N;~?XE7) zj0b;bO?;t8>ZCITxF}}C(!2TaS0VS@LIF>=l-KX?y%@I16qsB@rX%XdV+S>UPeRAY z1fkNo#^?6!xS20^B%a=Vm!S|in##5bw*>gD!Pr|mD8^_=7F#MD7GMoT-dJR;qyjxs zmbeC+|CQuH3BytP@h8#wrSTuX$2F3fo3ClFpGzN9T*pEyXR@FYefq)2`nUt;zT3rC zs&Gbu;qIP89vk_iV_v;uyU$aJaoVA&|J@~VSb-8;iNF5BEfGL5S?5--bI1Jlag$@@ z17tZ=&TP&_{Nnz@!x366f1ZV8vF%K3!13bu({_g&x$nn|77VY>Olu;VdWp9tDuP_9 z=(88?;;)#Qs?m_oYGTk*`x0ko$*CX6MPF9Y&fE&8J`a`$TL1Fm{zCuYAovKu2P7pV zF1_Wb6$cMg%yu4smpT4DfDY$f;nJF@%te)_Q$2Z))bm$28~FTkmP|)bu?yQBpI7mm z7Mpp3;hygE{}+?65B0bh##I)o*y1Mvp~`dL9!(}2Jdrv6Zi5{;qzIiorI)k<{WGjo zJ{0JPCk&oXvP$|MJu+ix=z2oKGONIUzKIUT)gG$Y8si{ddB6LW3$C_!;9??fr{xqI z#}y->sw$f8$})?*4+oi(6%TXvNBrO2t$g@1SRebN*~7g7{Qpjc_~%=lk{rEHAUPmWEym#y^#5sW@JdJHDOJxBY!&S5^m!kYcPsfp>yJ*%c*ne?q8O8D^*hot2 zWb7|#dR`PUHa+RHku2kdb*yQ6Oea=~DCtaV+~=&e*?3lz{uE z7LyaWao(hR*#D}K;{g~)s3G%fTvQ#Qcc^4Ue7k7k-IjZ)w)Y;0%5w&3jE8LM*Z^8E z`N+~z93qLLQSztQHKkR?baR!sUs$yVP!V+_W&oDU@6`R*#D_f+RUK~Vn_ zQUSn)-~pe5h;8TwZ*l@xo~;#b$n`}+l0xne0>kH{APQ907YBvrWuJoxCj4lTMyLSX z<&&}D4bLAH+=t%8cXhRMS0M>Ebb-=XV~hrb=L7-v5m8A7k%2 zIMAs8ZOAkk3o6_Q;_#pT{!rko#Icr&gI<4SdF-|z+%&0HqQ?Y!!eR9<{wA|x&?UM` zGue;O98j{`i{r}lm^&*QtB>1U5pG}X{lDo4kk5#qY$Jq&ftQepncKaS>?OQSMky!I z88QP$wQ63Rb5s_5QjT`EPsk|MEi)xcFz(7j@MPIwgU* zyJ8rdPvJKxza|PgydfXWxpy6{gb88^g6X)|a9o1D{_Jix>k5%X&lEtz0Ba zJxH$1s7S(M;=>0;!B=QFX%-zw-=Y17*|K&Vh$Z0X=uZiyF(jm!FiD)a#1(E0?^!y! zfZSq=%{O;cF=vp|WEW-ZeQ*qzglL&)d00#N)3BU+RJID$2s}iDDW)07ko-U|Mtm+f zO2Hx$Z%+{tsu>QaW;!*FXWoBL3HV1MfSsf|$|Gkr9oV>i2&xbb9clWivYrqYErLvG zDs&DHU(n%;8r#{@mrK@I#a;tYaFP}`l|@ij)l%Ivl@E%P;S>k;mZCle>qvN2NBAWb z9onMQHOp87Bmf6s*hG)~UWw(Kxr6^#q5e~0b^%|LTm`u=t;C23#~MGV0uuUt%pl=e z`vwma02SdovkA)Qm_%~CQBPfD>V&fpI=l1(imAw0X(}t1DWXLZ_yTNXd@_x0N4Hr2`Rl0pA#Xy*_OT_p5hjRlRBY%U@n%E1lif3 z3UCOpox%Sc$S*pgfH~JOtNhTb@{ICFqX&G-$>S9i(vGO-j}Zl}nB$cH<#Qm$p_%K- zjeekH=Ck}ux=5SjPhzgbT!axcg!Ax6^K;R=@iyvzEzkwZSLZbiihTD=3KyWpq*OkK zV(Bf`DpZO?W^V*TcSBrmQO}h6q%2Xcp-LAcXCoL#!x@$wQB` z!=&HhS|E%CiwLF`f5VjWy=?1rTKYLY|KJ6dtiu_t=aE vHNn2hT|Gi{SGU~u|6kkp|Lt5=n0H|_$Iz@qFmP3nub+&BqIiv{ap3;}{ry?{ literal 68380 zcmV*OKw-a$P)n;IoSFIN&Ya0JYDtp7P2oX6&63h|!~$L_g``a60%2*HKY57a zB!#3LL8Y}_;H`2A#OZvyWymakw_={05Ewds0qg~$1dxQkxoz6qISnD8xIn7^RLLt~B2@{2ekQX!iI9E;EbQ^#r-?{(C*Uql2>30K=nE@Q zYTNTpfm9kTG$W2Xxv>|GgNtEqZJ2Z!c9-hQD&tn27jd(;GcP*>vp~vh9d5@*U92+H zgE!dUk#qt&0z1T7K-nJ7HdcVf<$YnixI^|RzVBV!sy@=v#yOMnH$uQ`fmB$wd2841XwU)Y#4jLfT1_gV_`f(Kx_c1WhFwzx7*3_9bL{!aGrSRiFK z&=+jThDAI4-|QC$n_z_8_060{h4Shd2OoEzdDfX92cmWp~pVO{*aT(fPvfn6xq3Z};7mC*$whKKuLp$^GS8%@7&>**@Y`AMuXeNjoL8h&XEkDzm~8 zwfIK|bnnoJFw$e*!Zk=}ey2Q3YP0L(#6AUlpq@tjSVp#Nu_RP2DZmEXOdu zYRGG=uVa2IM`{g*S(`8+q`aetNBi587RUw2(pu442?ZJ^41cVK8*CmU&bV%>4A} zLiBlduvb~*ExcQSNP2Iij_v{&UE1?i93AcU%s1{r*t>h7DqdZtiTs!f&cP^)!y$m9 zRZ|vWgpjS~w0a=Bd0}KWcV=gW;rMQAfgPJH8y$NxGk?+`i^btD1DWxe-_7D^?ZEO@0l1 z{STx*T~+=?eUaTX`3mdE$M$tQx-t7q640)uQpfI?-_h08y$9uur+Y|e7sqgpb{iEd z3}!s}d2*A9J^XPi5Gbl4)TkLOU0x47ot>=w(uP$<P|Ezd+scLAhyTbaEfbOkEIBWq0!xK2SefGZM(x=qnDniL%OrZ3oWb?s^sFQS?`b zGx0?2FkeA)<>#S5a^GaJ6dBk4Xf2Rzekp@$BKn6DKAXE-<%R-F=_epWR3aPeo6^XY zU}U`{CI`k$Sb!PS+`TY&J3boTjQnhdRavd=#r_;Rei0t427Gf;z-%fIW}4No6vogA z%Sqn!veO33;#MFUg9-){eaSvd@&^Z#jeFD>aNb5V3AF%cDy}q z2*$m=2yAJAnUND0VctZGz-Rt*thLAD8@#`61#)uc5mXpTK$1zmzZznGXBo^kN#v%) zNabt6l=X`nX-c;m_Lasna}T=t=hVU0R0$E5^s&NVX4XUt$TokP6!K_ng~2>+wDTuh z3ZRWch`RI*+58=xgJyLr5P793!@^(wq8*}2X-5U+>geF_XwCd=n&Ye8PV)eEJ5=t` zXM6g#9i>W4!wx!oV^@pk?>vAj=hJ~I(2}!8w&<##7_$m``!eblcHzZqr;K>>}!f> zIguY3;tEiDpTS^1HnOYKrU8sP>yX=pP^H!4?1nuMG#VEl{^}0_#Wlo<5p7dEyLrEp zlj}?HI>@e^Z|`@xK;9kMrvKH8=Wg$u7J=MRIY7c~jCwhy`=vu9k*X327Br_WiZF#N?G0Y@C&d|gjXX?rb!ShhI~-?xVN z@D?|En}ZPF0PGlobn7^Di#pkyd7;>m@NV&1Ak~*zrk`y=Lq}(S-5M&4tj{VT^eI{+ zQHR?|+@3T9FI2bro5+-S)K6dwbf}&Tam# zU4AKzwh={Evw3@bK1JJ!^wiaK4GUv3hE857hq3uN#%FW$$kgiSRv>YEX4((yi0o*s zFJoP^GXDEDRCW50ps(+PTA*nGI*MTJt(Dm@8kgIB=7ZkiWy^$P#{33yAdth+25QGW zyEdK;f6J_zY$aOpyHofBZ>%1h$&QHAJzVz!#@A}RPBPk^vFHT=!FDzFY$9J~^aXjzg zNa$!c{QeF!_rD2M2|D_GiJC5}aXdF-b8~$v;qMs6_9ebOHgGT^>ASLpcLd(i?id%d z&vs}I+551X8uqa7ew1Il{9$};@i~&jMn~X(57)*uGZ61*jO#R`b4(HKJ~t;YNbUEKpYxHuI^{I<{R8=*r-FLfYuwuW?h6cx1>N z4S&t2Ue{20Q+3u!szED1f~NZmR(f{W$0DP|^i->x`@`FL4@T)JW1ieTawr5_44&xx z`8kFeIA$;g+5%Xgj{c5z=50Gs52~ez<)H&-QKw!dH(JN$$IVl2pDnf9-7h#UPKe)X z^*Caey)Sc4d+~lme*CA$Ski{jacDmWLs>w> z-7ujRj*-y+Wby*A^XAp9Eic=j+n&y*$5?Z`{HM*BzGSO>{-e^vgD{!ojcapbPlfp= zot-EyziwOn1DX#UfDf(w?KDBMI2DMR?Gnkda5kMe?J)}7_aD+R0~bl|k&+y(ylmcC z-%NvU;~%zHgv3}fX?Mtl#Reuz*$e`!RsWutM$dPsP+ouAvOjmshd*P+iw|KG?Y}K+ zjf@F6wdCP?{l1A!EcHY|q1z>S15EjnXoOt4)^lS(V>xtI1 z{x_feJ;fQQ8>^4At=-+ntxwx#X;v7_xbDAgBfDDuwC~#;?fW`7$W~h|w>)P#bf;Y@ zbNlXAAlF}*kCN{UmH8&OqXRn|PGrYnJvWl=5SYzy`hq<`(xYST_CPIDY3KV`>uD!n zVYe<=jJb4>WZsy&A;dY$7Is|Cb|A;KtLllTq)(wE(9xc}WVbQDUcrnWrM>zq^>8bY zth?@olxYbrH%x4>7!oQw*xZ#e_eGc5?FnDr0r_Pd`3K>(T4MO8d+}wL42<~xG=V+X zjmNuGkjF{B*qWn_-!4MiN3A>Jt<=Nl$Xhj0!W&fJwzG*$Uc4~6F@}A#ij-%V(UNwH z{}Wly8rKARQ3xS#^ z>mV1mDpyCb(^M$AY_b@4k$(C{Ei4h`@ULTY^(}e)A{SwmHjB$G9BjkP}gWFCGho@iOhOK-Qkg z&+}dIo`JEtdMyytv+L)1e)?=cN!gCR#7>CQ2b%7BWQzV&iB7H|~E^5x)3qo3q<9vsZuI zdUL9qJ9qGpKtKgzW0&~SSg;>A!AC&7WA+eX?SJR`1)nwN7$^c%7C*}8L%#m~;2SfS z?|COs2ArNFa62O~s#n7|Wf5Yl9FBw1yv#V$L>qo<7h=vWLHO0bp)sY>%nU7BG0jb6 z1hp0>btKY?w?cB|0Z5N)N@~w?B<=Dt8=UuTi~yUNWA!3WzdClrC*kkD(BAMpB(%+# z+3=Q31_-)zM5QlB)Vbw0XQ!trwkA$?SWUX5*xVR59D%$+Am|mO_~8#x?9j(~lUfI^ zs-&ZIzOJ&}C8f#`Ty4aAiFQD^B`634rGjWOE+XOgR*Dm(iHL^O@HwdKJxqsvXz9(H zF=fGY_(w=PKY%Gc+2cFH=cMB77_7_yA-?>g}po^*L=Fc5C0{##PxPsJL;e1o1V+5)aHJ};d zc_Xk)NBd^j!S$?c*zx>ieKp3pm{8 zjGG^K;0X9aAfO6T?fV3)dJSjEpi-%z9dpuGJWhV0Pwl6(?G0qNtofbfr=}$xt8V?W z6J4M$>aL%G{q&zuCDvQFU0RtK40P8OrT_R4Qi9{}RB@mY-vRW#k7wj902xV%Shx2g zTDPc+INKF_u2D)RtloSWaV1Njd9Bi}zBztZTtTXRo!~JNZI^eUYUI2v)+3>#V@NdA z(G_95bEL<5gYy?%5 ze9K7pwsEpI+u18Qs2h;q$%kRpwM{*lCgsppys-Fm_EnOuj01RAR6(lEtwQ5#cW^_W zSqrg5)c~T)Ce=K-5r!$2UAWn;ZcYk;AjfrpTC+WHNRIkE5~O zlcyuc5c+vKN*i69HCohDa15A3yWa{Dzu`4H>SS#e9U{?meob-ZFfAPovC9MqQ6;lE zP&Y?SBL{Dq{WB)lgeo)=A#d)_4il(N6ONDpbD>)>+$ki>O{E{Y;_QIcPVPB*@%E8U zr1NumYK)id?3=b~jHkQ$mIAI9HrfKa-ZyJ=w>{a19#*Hd?bYr12IZ4sFkb0Md=B~-t3VSP0aca8q^!*@ySH{@_Nz))zXb7X7~3Px(Z|EmYHR&m`KwFRw5wQg^z%l~ zKFB}k{eVShsc%D#7i+2KO-ew@mGgMvbI)btO@gra996tH#ZT8WHXWdBJov?sDqzxv znF{S6uus7|^7ChB`K{2tn|zHhqm}bdJ&pNvvQReW3=EyP91lG`(DF`y9mgOx$20$I zl&#Obp1aDynE0$9C0C8j)0_@e2QbjOg<;Po#evgCQziM# zp^{AAf%Mm#LVtKC^yC*(y(u)$|KO3I5M9b+KEe;I%uxmLr3%MW0z9>Zp6HmHQr?}1 z4M1lXCefuU*rkfi%TF zG2eeuT%-5+?z4huZ=BE8&*U+9fR5TQ(s)Apm>jwSfpEweRU6?m56eM>$l_3&>0Vf? z4(+UiJCQYluArX24MKDYH?|VR>$X>gbP{sI5%7h8&kB-7J7zWK_xcJSu%D!J|4x-M zG3xbn*>E!5{v2ZoQmiVZ3&)*;r#{yl+UkuEI(Dg(JB!!VDCyq0hT-pb76Lvi$c^6b zqRg}h+z>Gx{ST^|ce}a!bI||%B|Lqo{=zx>8^4|i&8Qi&lXRB4T&x@JV)O@gLwc(> z!lo>feax&9d8i<%cMot0S@5r{-)-w!F#EihnqbK{yU={}ATS*}C1XNRvUz)qX_e2&PoV#EgD95Ra z4o!AF%_>Ig0eGI?*=_F33>*Km(+Ah0-$Q6Y?w?FwDEnI~orIC|kD#LLLVu(z%zbMR zHhs5<>^e-MkGBF<5cV1~6o0c_j(dL+m^l8Sgy{ z{pR2O2F-k_H&ksOf+>@w*x6(@?C9)JNZxE8k}WwxL^Z2bBUcXg@`Y7>ovm^(H*X)@ zb1z$GfEW=~vspuH(Ubj{lo%vG+dN`<&RKU0lRFk8vT)hm_Qx?zHoI$BCZ(8h{-6X) z1tx`*HZy->FPpVUJe~97A;~G-Mm#Vp zyP8{9D{cJC(5(6yku$z>OJnAi;8zZ}Nc=b$({H&^unyjtWr1}M-I(OifsG;hjvL~n zO_p_M*xt#xBozcRjU%Rzl2$r$%v$R0D^_CZIpAiBfO+G($!h?hwE$Zy&h&ZO6y&ia zF|CD6QQo(o{eZpt5F}-VV&0ow@!tCjaV(uW%7U6c{sGj5S`1O6vX4D%*>N0eSllEm zK4Zeb1T|*v%|OzM~sc$XKr{feb}~-ea5aVjA%tbE%26A74++`v2K<`;}pwygxTl5)z32a@X3ruJ}Z`B zcp}kL1%7u$c31A(lV)uE#(CVsc37~kp83{c3s)MaeEG&8c>!@QxzB&k62Du%9L6)1 zTDL{Stt)bPOI|JGCQH}QXJYfjQ*i}hv7e)ltL$SBKp<75j)t`;w`x>}N+lxZ@KI{0 z)OIDmj&ucS*Mx=R%<8=bnFKd2&rDgo&5UUrYO^AuTrEV;{qa=!9(iFf9LSL z_4!Mnuiq#~`+^EO?L7X%fk4hGh&Am}`HFHyG0{aw2`%f$J3k9LTTFVL+q4g2Xjnlf zg~xvzAdssHV#~iut$|IGzWV$bli%6xY7`5&C)!YStQO zw;ywLra5>6ZXVc!syqKh_`!V;(o*d-5)G&`tJlWyR`;Mp1DaDOH9_2P1PTHHrwXEI z4^GfiNn0DD+tJU3@pXFt`K0CN;>BM%0{Ml2eEmUwMaJXd2oyC0WC_xA#Dd@r5Q-WC zJiQzNF9@*9jGVeyUg)?pN8pY{z+OQ-Z8OL#RI*ZTp!C({h`jtK!qaJM3LV&ytlA+2 zQA2yCHOLIBg0!;t;bJK|_!L_@uad&~as)ggU>lFyD~Ly`U^&vckobL*5qtVu%N^ei zeC1Vl$9LMvN@s%*lDQipw|1lWf%j!!K~y2NTz6b78%qB#h{~6Wk#}rYYZo3Luv32tvAs)jWjX<7M5Z3Z1Z0t#!6=h)% zbbyUi;VS&~07P0aJl}FYGRkxadK{b~M}Q#^+zLYDh?>9Fh1safm#rpdTAeSq^$Gcc z&%@mhkphAcUaA~{pdt{o3KDmGHcIT9nxl##(q=05W5!JVF!l??u;airf3AU)YLDUS zU6TU|%4sF(ZnqebwcuHxPVgCC~ zn9gocbhae=#5agNzZ6GFnsCDr2nGVVtRNC@Y|$p zpzvpo5Xf-_skOWkOg;_`s>7n79(ur@C{?FfFnm!7ny08`ryGKDV^ocvqFb$=g6W&a zFr?k|$cpW*uT7HVNbj>A{kbDYKnVglsvtF1)%RIBv>iWySgXHt{8TA~;?j9@_=s+(zhsebgf>6RRY@RK?*3X;&~e9Dy7l5O4)4_1`NrAW!ia73=Y9 zhwp_D8tc(t-G42NpH+f>)sxVaYX()XA3f3q;wV;Zi_norpqo|8DGjJ7* zpIY=L=`%8+{*-Q;diE6a%AKJuT*&B?$o#mPM?RHSb;R*OXFU3GM~;9u1Qb`0k~^nB za#qi4=Na;Y#A&pC3q``Y`4NW134*eThbMAHHM8gr3 z+b|Rt+s$|L;|{(Nm`AEhw`4}qeSbZC6Xucc)}2S8Q-z^@ql#W*ICAtXPF~MIhgP)_ zmb*D1r#KW>keEaBoC4Zj)D@aQEZxBNs_3b@mlps2nqsFl(X?#V2MGv$<&51erxqcm zA~aQLgXHnQ+*pL2Tj%DM69?N?Xm4R>kVeB8Ya!Waza;H~t|#lW_HD#7OE1W5Y{w03 z^f|h8(^ufj_e=2qR}W=-($v^4e)Xq5XTSd(DISgUTeO7*}UgVRb*8@0$C`0mij% z%Lb?sZYk;OSFU06!OL=FV@M}%unNdI^*Mj*-=(p~z+!9(kM6FHUE&mc(XN54@6Qt+ zsr3D@tC>eAxqF1dUEZ`>t{hBVsx4+I?E;2P+en{cP5CxmcBcue9Iw1F1hpMHFszXo zI$;qh${yq;MarEORgl<2-*_JUKE7OeOpcUPv(%Jrqss?uqH3alzbaLdlaACmvTH_e zh3V6lZec?%?r?L z3(xkb=P^jv*0nvl`*Sp&@{ZS$RVy%UTe`=_fZ-r$N@Lzz9g&U@qUq}|DkuMOf8L~b zS{iV06(Y;aZzKM<6Lab2t;F+}(6U~6r^>+y8qin0&w6*IL+~6~{Z5N>zGVS zJSy5#cKL?Fq}RFei8{UQ_wPmg2;4_l~_7O21%Aujjrw2C<~)N>{5}Phi^uY)S*# zL^>wLB)`m}%E8RsiGfuP7Dkr+s#s=qGIZV)hUBC4w+Z4ZR>qU`b2a;oFjNU%_-ogP%OdY*vX`~v~U44bP2HZlZ9C7;%0k|c5f4GUO|1b4}aR5gHR$=MBirz zBk;D`S8^-`CC$~UfW=^0BbSAFEq*doJp<4wx_|kbx z5kfbC>h!XFabh`U+A{llwfdeVo}C~7i_Z!oWF^rVa(8a(vUS`YeYgl!+8}FO6@0y?G4!;x*jdbL$|e z!!d8Xvh?Lf8|(Z@{b(TiqnzAV-&tf&c0$9uF}`;#`@7BJPg2XW>enxjsaU!|3N1pN zS+3u9$=M;0H$iLf9?kyl5Bz)lh8+6Lp8qgLiolFMmR2_qmaeoj_vul--2<-9Ztv`R zv}K^&}UGtc(TB(BQvQ(oL@fECiEm}wnC=6LRlJVy-CV%z#I@T#2y~gLk>BX zrjIX^gQN6d9#UqS>>rFjJ}U@xS#Hz2^3hligKmF`QIGlwy0ML9vW)jSBjU~Ve!EyU zp1P@R5i)k8?G@@aPeQkT4$@w%ipcj4(hkWmyPqeEinhlXUCynTeeKov#3YAq^IpA| z=I;Nd(2{)$*p=11@ts|r;B9;(maL1p&o;F48uDllbgULFhadI7?ebGy;Vw6R-UjK6 z(Deh80~=|ggp8l&N2Y;b87Kaic~gFG9&T|CHuz1o0{h zC;5JOvUZ5Eq-yH;2Wg!03&t)#jj6pV+oKyc&Am%)_YBbDvx1n}It6P+9E=#2vq7^E z7Gt?!-O4L{N_z*czO9C9=Mh3D3TfU}Ce-wJ1G=?yWPAFnbrCWB2<-#YdSxO;6bF(l zT@V|QOSWvzL4OL76)^YJ`!Iak8jrj^ew-dDogLYI<3lGd$AWKv#R3N}K^=kFPu6j8 z$(i+&_tp{K%D?nprA5qqcQswHp5PXtYvp`}R>*C}YyLyZqHd&s*Xib5N8?0KU9ETe z*T9=gwqoe+t&U{b70t;t@E4yI1V(p;Qe;_oM};mLYL5&qi-QV-LO16rgp9OQ3KmAB z{aj|+&&59Uv=?e2@-y$m^K{n1wH*d6mb zDoCIQlIQ5U>Of47X9mI2@o^93FP$tz|K|szze~`UULEY>;{IB?UQ^7cUtSrX6+|+* z3s_a@8aYmjP^KQFvj=h%-mrHat;c2?vEhR-ZC_z`NPD^(BEMjj#NDv*j;Fl1Q*sxM zfHML+F)y$r)YE9|i`|IFuTTKE%p{$gA? zr|z9S5Vvpy{2&l`1u=N4HGX2TdW$ioa`mBReLf1Iqra5732~*}cp1|(pDfWL4AyddDSf_PfUGn_r@wr$%BZ#QS-?sbZ1uG|8XgL&85Y-gHD^OU}< zr60Og5W~@(9%-|gaT`a#GXg# ziKh?cDqcIIJhqr`9mepFcG+W5GPt!92VgmoBB7eXy)e(vjMFqa-$D~gVCPHbq7|le zEanuOq)&mMjmAq87UF-CvIo{|YdhQGUX+c`3ZgO#v{6y+{oC_sHf7rFfm&D>C^yD2 zB~oTOt@3;3mUZ!*la!NEX#0cNnKlPYInE67=uM2NDR1RY-L)Pg7oLXU*d9uI2Ih~u zAH&`!32e|=X0HrM820w>G>a$Dxrc#ZYXosITGZ_N03NFoA+M8!&ksz486AE7X`;k?a(i+-Nhk?IHs~xiM^CL#y)cvtr6QI8$cM8RwIqVd*E(^nFeaM2bzw zIL9rjd}iG_vV43q(^l@G&o}2KGTp@;0Ys?N0G4tIiGd6s^%Kpw;iq~jC zErE*US*j?H(TTn2iq-Ma@Mbdo)B|al^cJ0Rbj&iX__iF-6RnA2#Mj4RI#uD8DHK3`x*aU>9&aJ$V9)Cw#!omky4X9N}9BK z9URFEjol_evJ>Q7v6;Jj+a!QC(8}ghsvOK%Du;xT^N(Vd)$Nn%3-R2`*svfg(ZV+Fa@Z@fF> zLhLf{t!@}Tbs5aMOj)7~8TSB&(Xmon7wePB3s5XJ4B8saFtJ;Cxr-G>7Y)5Y6=K$m z#W*5`Avx8124@Vaicuf@f>sZC!-c5yu9iv zrgx*+N(VEf+$Fp@a}};8(h;C~4jx6o>a&8dy8>B9()Cj`T)(O)Yx-ZmM#ikS711go zl3P4#h0%u>><;>5s(S8)!kco!yUF9C@*K zA;LKWV|ulUHnmitFM3UD;W3V2-p$u zTS3Ig7>LxJcbV;#KULl0xd<6ZAEApe9X-Il<^ng8>)bA-h>byYHJcPj+E`!SwY!p= zggEY!9Zl1@pG{s&{Jl$c7MGNM0-!EZ_5j_I0@pQ|k+fN{xi)91+jig_n%AqCqrk?~ zJD{%IG)H0cB9Pw-!Xmx#_(a6bp6C{T#wYI~WWX@XSLfiHIqy>%L_0VXB5PWgA%!M2 z-+uN3_Uc2Blog73Z+689nxc)33W1o=1n>1tkO#*!&ITJ_2sHh>0r`yS}dHu z|1$QiqHb&1yYSJYHQm-;Z0j558%NNrk`90TxCcL;(c|4-6)^ti^O#QYvjs<%9(%r0 zo!3-8`TiJ+l}Cjcq<4K7#Xg_tv4fM^@oq$|+vSvQ&57f5^|O;x z;dr4QTH?q_g(EGLibqEekkj(6^~{5jKIdsK_m$mtE?HuiHbW=Un#U_O(f7GQw8=l) zIIfvEu2|co)C@Z3`)mw5;v9upV;_{~^jRmh^}wEJLr=E;G5hiAc7~4p7!OuCMt!v% z)9Axy=FIg_eDr#MjG~GIRCQZ>v8`_mop6L|uYfK?`=Kj+H9HIfzH9_c-k6LBuKwYc zFUR$mF0lgoJlPpzC=R!^8#xvE`$tg)VM)62+UF=fd6Zk~jJj01^W!6gKJ$Xz&v+x5 z`Z3g06f%=M@{K~3qG2EWOCNp<`x?f4>oyJ6!j5i`-L%i2*iG&l(UOFdac%LL=eET^ zp!q;c#bfX4s?9KOEO2PgK1^SJ6!TtfsOnjF(3aZt{|DM>5f5K33olZ%F>!Sb0*X++d|!l znh9p8#qhQb#6pn&e^u1yNbNl||08yZ(bnDR@B~fYrk=!0@0>=jY8rg__e~sJyBft~ z!VpoRE*kf>9IFi_kujB4!DC(cczbQuqcxtLu?i98YT??!ouqD&j-^gmCX+uVORO{A zg2GXP6l>lIN!!+fP1Vkvwh(pb@KSMY5+=;th_E^>iH8cVYcF0IJ9sqi+h6WtdCOPR zx-l3zX%QOJUNL`bHx6v`%&NG8uoRqLa}?#<*6^5^lmYz_^T!&xtg|vSaV4O;ne36S zpd8Px*;`Oi+vBx$eQXX7Th|HL`#tQ>y_Qy!S($O7cUXI{_|h1I)NeW(+W`I3G3Bk! zRpqA>AD8tHTX?nxGkmc2>2b&xTaN*1U{;6h&;@M^sDcR83f~&{A&O0S(PO%kJKTq;IbT8=_%JYGl1G|? za7-OK8VC(5h^TL?I7ihB=ySO(@*Z%GGw zg-W=%Y;FF=$z!`C5C~cYv7vQ&!Ot*cWuj`=RxqWf*_`v_sn&+z=<2Nqr7;IL9D%$* zAm|l@B~n9+a_2VehMAN*RA7VnE0=qhxib?yvGKQ{~lspQGtl4hgy{S=sY$x^uT+1D^usU{Q4ZoY$YUyeYo5y;aD z;+Q&9#RObj@t5qxv|D6cxrmYn_91N3I%qGQCwJQ?FTqf%EHYcR!PQ1h5K*Q)DGLxC zS#di@Am0$kw+iBzBetiDefSyd*ZBcQFDHAX(;I)`2;@Hk_6lO#hMoUO;4yLp3NHe3 z1$qCeHht;LIq%05-g4q8%Vz}WK%|*PskUjm%jfucbQ}SW07rl$P{a|i4?&uaSPnPd-P0Bft^h2;>xj9kYiBb^+qZ#OcJaqX)Nh1ULd50gk{Oi-7A|herW&TcAU? z7pe>?2*0)kQRja_Owv|Jx(wPJM!M?BSnsLGRaTkmLS{s=2%(WkjckC_a@~<$wj+$% z;^cAr!fVMn|H~2J2;9jC+!igID(ePf&n!mVneU;_x&pV0KBce1&s7Ri+J!jc;+{O{$2yg_78UjU7fY4QX7dE5n zkx9^`o}dwh^U{rFd{KCcLB1e3D-kg#zmb0|Wo*P^j3|Sv_1;2i+51TeI~OR0r^I+l zI0762r3e%W0V3*?P=4E!2)n-B=?WitADPme79u(`jabCAODOa2(!4OvLoK_iVTf7?eI&J$F*$BFg!1@=eO z(86~#5LFpMiQ06}UqzThXyIEO3L&gmc0^gpkc=4+X`Mw%y$BQCyCHMf^&f*k0s^AeC-f?xzLuqLug8&TcbAlQ5#~cnU_SZ-T}OA_ z<6^#_D4e|6+Ps)_0#$x(43%1g3k|0utx``X8veo&;0SO83JQS&6(F&v7NXRiH{{Vq zZgxqrC1_C-0-@GGsG2@UBMTZq2wt1|yun&jL5QvhRg0J9AB%+DPAi#oFz=oN?4Lyg zmK6JRT%d_C84W12^LdE>J&(j1PvdIc*ZAUlpdfO3jsQm>s0b8@0Es!h5T*CLDes`l z>1cy#SX5Q&09E_xK$!FNj_L3s;Gef4bp8@zg*HJ2EKp{GDg>$~qXGJ{5ssu!gS2@Z zOviqq5lY~D#@KXO%+W6pd+ZA&)_xAxYrjZbnF!^EBft^h2;?sU`6@s{Z|+9I=6+zk z>6{1-HZ7&9+SMX7v{7D>Qq>*=b4+Cze;x>lT3mIBst~&`f)G*4Ync4<5VVmHI(!6? zej1Y=#(&;{wEt_!+kFE_1e@N9J@g5R9r+CBTh2#D>GlDH=A;|}jsQm>_Xy;R05KVK zsI{g!K<6O>?{m{sP*^y`zJJmbQF+BV7R$DPX3z$@#=RX3H%D=Co~uab?dhj z^T|^}l@{uICIa-+=u%+%p%V;gS814{JXUt?O1UkMLkx+;@vb{zR);AM%qckn9086% zE)d9b0TOj_HOl_`l-q$nC23ZdX$*DmC2WC13Ar1M5Ssv1zu$o$?w5P`axZ_^To~!6 zi77)u=@$s$@owP?#33XaszyO%>T`G zYmC`KwrSZDU>A(4?s*;RCLp=}4tP0;W^99S7;m-!NV~ z<}s|WV^VA{)3Q{YhhSE+-Vf$D=lVI7OMZ)0H+`L z9s0Q1zDV=M@9bYIF==CRz;Ow5uyf#``_Q?%=S>rH-&ugIS9=Ul+ci-m*P}!LMMfE52QhUruD$BmZF15EKt;v6(S{)-r6qsI$l6YPqj(Hw+0Rayun|edEG+u&7c624qw8yCP8@)+pf&=9EyqPOJqLbXZk?(Y@KFH@kB!S$ z;hWt_RuBdGTgePyQ;IV5CA=_UAyTY-1$6}8eyu0Ugk*=la%3OIF5HhbOIOhkxVvv} zjB4P%!DZiy75HdZx;@~1y}DpvqZ0OaX4$pjZ+y1yJhuJ0j($LV^G z0FewCNLbc^w&*G^Elr6kP(Qgg-wyo=5hWoue-8M2hF`p??S*;mJ2WbJAL{;JL#*7! zZov7XiC>=fO(S}V9SCfn0M5G|CTPpypaAFRx^7; zknYeHyYNtzZ-hA`>e!-idNyu%FXp!co}RD(I%;08ZCgv7!Xc=UJy3R{&(D{jl(m`G z5vu>#7G@nA%DIE-DMRW?O`v|{M^9dP>ZWQj66&opVbCj^Vl&bu97p`w%S+86$V z5E7B6>GRKAkOi9b+c?@yw!m#o$5VI{<)PY-_u%Nm2O$-qZNA0pG{;8|rQ?O4FFRyQ z!rXlZJli0$e~#wTHxg-Jh5~dt$Zfr zdt%5SQGL$Ao4VIDnv_ZK#N^o98jlZyA2!hDeqb|LeIS)EQLA+$l08)rTT<1MTJ+ac z++M;-Z_!_GN9R3wJXR5ws;(RqpJM%J3yr0;;7^a#09LOFIfgiVXR2`YG~E)WIWVqo z>&P#2!^mp2l2`T5qR_!ha>A? zHDRRF3=;pugFo~Q@h?2`C0S-=Jh+n+!Tp}(`ab0cO*cHubqg-8@;94Ujuu{PhAD@m zSFGvrNoJ`!)gEiAIaEILBkOndM80vBEe)yhRUf*KWmd@MEK=it0W8Ee)#rH6AwoN= z07rD#(IQ#)P(kz(L^9cG2~FYO1XzBxBrKZ zXCl)#CJr=w8fmMpygSti7_O+eUHnr(gCOQz2j7rwv%`7?ia(Fb57&jxB;hN<${2y+Pfgd_GN<{wD*eS+A6bOSTqRUyhx97`#lAw^4`r5$$zsi>2(wNK(V9TS(9=nx zN=%5qs9Kwo6__tXy`n%3O-O}DR&)^1yt!lU<&I&F&17FD{k&=&bWKnXp)3S`Ye*mk z?qIbMefM?51!1cUyi@RMf5q_e#kjNhrRtjS?{EH8R?zNn1T&*$2UmU`^;tHPUB-x@ zEk)Hl#&Qd|{lea46-fCQJ$;Wle&ljB!RvRdJl!&db}fSj2}$v~{x;z1>l1K?%#L1q zxIQt>b*35cHnSl^$4nN9tH0b#nQSIyIyYV1c`*fQUF+*SOaQFTHa*zg+JU=w-Y~e` zdUC?8QYMO&zXI+}>2^Kqi+qD_m`=35%DocN_Dj}8|B=#?Y_p9HN=i^qcpPCx=g~V8 zaRsh6dy1UdKeZQub|EIH=_G(K4$(7FrRr7aB|XV-o#FVseheRD&O7uzqdd1i%#rh& zME4oH_6gP1Yd~wm-NoE|LL08-V+#SdBilAtejFH2pMa+#!hw0b1PEYWpu6Y8DAu>EroV}PUoT5o-8m9yOsZ)r775ku)tRX`L*cbG zgAu806)cyg-6Us1tdxqL3m314CMan?eaDCVDj>*>5`@b!GN%ep^U`YCO<;ATZE(0V z>1H05-xYpajm(W6+PLNQbW$~*`_LA_f1Z`&loM2DOIc3`op5iU4VK#+(UgmP@#H_^ zQ{GN?%GCSDj#h8KeOB7n8FNr=m_H6FQb}-5-5x-i-@<*rJ#R|jaQR}2M<$*U*m!Rx>8!%gL0&!tyZU5+w^GuFk;00a02~mCdU1*T<2Xn3#7-R&B|YY z#tav41S5j1ZmoM-b|3NF^`fRryK^tmU7M06?P6b>1@zpjs=g}t`o zTAHIF=;EK)BtGP9ENJkfTJWexnwQ0HmBL-7E+R;)x)dO&V3}=iKyEIeN#Kj$Z~T~> z!21BIRym7Qz;Lq{)4`QvJqxK;GEFQ`x4g7;*EdmlAEURd+F?vSp<3DUVMOU1AN^T* zS*9(Clq=+X#OxgBY;En9!1z9fWk23Zy03Avm}%oYnww6Lb0eB|VgH=ALNr6V zN-l}9$Jemq+*oYnTbWLXLyFl6MK`J<)3RW(s7Mi+J!A@YPV6~;>1`~Lb!LtXPj)Yccyx_{LhW$73#JAO4{@I2Qbw%jS`2|jmr33R0j z*gK;E#ET4epu+asd=!jUcN$BRn!ceh^?3=DU;j#RJhoKho6og8`&a{%@*XL8`ei!( z3&QhNg0+O>sY*L+`bp{csAoGRq=TN#>B05e-FnCNrQBM1)Wws3;|sF8?mU{aJG6C> z>XRU}1uM&?r0+q3;`TkqgjQ+3F5fOn0Us;u+jILIjS@%U2$Geu$PA2SccGiSpzE7f zsYRpnv2=SA3;ieH`~aFGe5gjUPI0GYo==<@y@{p3`(3#ujI`xJ zSJKFZ_5&!y`1uQ@aIC8X^?a_>~3*AOn+y^Cid;=2yCAQ z=w6p>XdB_D3-vh=25-j4YZ;8kDI{Z!jMP(P_g9$(!@fVV`2ogH0-sP>1$M0ej7K<^ zmr3#I2hK9kbWzt_3gQ$im1}-?=I$MXYAq|5%k)Z%(n{ zavIUJ%1VB*(?{k0_HR5;_Z7j>knMCekyHyB0`ep4o9fifj6d2+Z zFUeh*v?94%Er9TH!yE{AX}v|smlBx2)=5S0`i+mvNc0(GG}A5MPm%}NSb#n&;+d+Y z?gIw|jjRRO;<7be@xZ%+q+F+I!n0JtNhQuH=egnueP-Sl z*o|~JB5ctl(t&W{SjuobNQUvWD`#wiX?&Cw0w+?JJDkw&`0V|gz#TavF~anw8}xhc zV;1=()0<7X+CsJC!w!1jg|e-Fl3Ty)uwk&;$7R2rAAWxzPWf(nNIG{)cpeRw@xyQJ zj<$sDx$kD84gj*VHm2AYCE4QkBPF@q{;z@IInF5i9jYmra>JMS8Qa!6AmK!(bcxLt z_|f6DP)_niGTrCsCqrZz_@d4;;lpU!f&uELnI~uH;x!5ng9G@!riz&-;xT!ppHqTYq@U5VLUmF-MYH8ryZ4(&4KgFJTjz6PSg&ygzu3 z9}6nSOn`@6<{Ljh#wRLRBwdQHWDOZb;gjrAizsoeo+1+`8bSF3$>6$6(8%6v4CJL@ z?3ptV8|~9d`}jCWnpsAT4OdycPlT9xb#S0RgFY35N8_+#U0k`_GFGbiVlM4;j_~{q zpBkL*_TWmrBY3--ttos^f%vh&N7TQ1h%s@j5ujn(!1V8f`CkH2N#$UacpEJ%PAMhR zg<#|Jj$a%yA#AfkZrJR}G87o6@iYa!Od>AC1mMV3!5P2#U>!U~17*Gv$j&~|lSERa zD7P?jRc2hJa9sBPm8k-KzkiKF%e>tvDVSzI?wwy)bvbRW66S18EYr=f^>~I)kYbTB zKQfCGUNm&FpptUD*XyV$7WthYXKnI!oYLn6yr#L6h^(;fg21D!${n4jBtgdlA)T$3 z9kTt@jdDx>`}_ICIxOXuV;~T8*)$lLO&^7>>9q7nW-`(tuX}$&56#zWBMhFkDY9&s zW)f3R|H`qDJ|Um(+3RICH}HML7-l=_Mr3e9_>~nVHK}*|xwgSM=|Yt;ZgWNM;$Igv z(2f0KMv}+r2^@SUj3rx-!LL!3aB%VD&SiUAb)-cJovli5zVl7fBI36YkUL zyt~k8^9DS+FG0OV8IK68eT4ZuLWRJ6DQEUrGqh-Pr_7y2T;EM%&13HyqAeBr$qPNpe#} zi=Wwu$v0!o9*n_vs?9fJBHeCuXCT?*VuIE6<;}UuNmLk;N$vdoSA{cPtdM!~o!2oe z9A686mu2xp-tyFub!){q>ZWM{hd#9kicCQ|*!qnFY@jxSZcl$h3`%j{vf5}0Ldkxn zfI<(zF&49X%~10A+(!EfoU4)=5*zy?)v4Y}MCmvV2jG!U6$?-~K=&70i)Rpe-H1dX zr2S@kd}>H{8sdH))=E92OGuA)N)>~pX&<@S9EH}mT8Fj$80!VC~3wCu2iEx6TmyM+)l>FM~HPf)=O8BC(q0%{{ z+8z8IRUFZb0Yzs?4CQNJu__2F@j`CdP_`O+RFc+sQyrOufeEY=cE%aX%RRZC@4|T+~THtyPzN1 z<+KN-Rj!?VsW0ZM32OnqY9)gcRQ!QJ$0XxsPPd(XFTek0{tq)=HQgMkXh5yus%GT2 zgQ|@_u{8Nkf$5&Hp`Q2Zh7}tkyhaeXLz$l7K(_EIC_QkfS95ims4fHzS#uZUkEu%U)?=X%;8+Xb+yk%e2Y2uV~QO-JOyy<-@zrvN6eh9co(nP03UK@lz$=k5y(SwgV0LUqN__W?JrRoRQge@XXt1(b=7} zTgOXPeWBl%1DSERx}PW|S#^aE;xpoRmtQn({;Vp|#-A?T8btSZE;v!iL)EQ*=wx(# zzTTI!xrJco-rIrQaz)DCqLJ77h<3XE9)Fs`Na?rerZ(T$2kv5A1i_UI%^ow_ZxH${ z{_Y6%gI4Hw&6Q+2B&b$V!kAbJ^SG!ZBKM!+=X&iZh9+51<*NyikZy|%4g7#MPU>+4 z%6-i0EcU3STQhypf#PpBYz*4n@s>{uenIA6?FzE5a7P)1V7nRS;q1_*L`HG5g<93~ zcH|7P#|Px=M+dye>Y=4#AzS5~Zqc~c9a{u}Y6R(4&FnfK=ll!v)%J%;e^wpQiY+|f z4ACQ4>xm%YGxX~?R4-;=9k|smx;}801$c^DCy4`QhWfyAH08&%^k%82+qj#{zsD|3$F_Y^(a~oGGE@oy5u0jHVC)auH`R zr72(up4j z0{pXM3z$6$OtrV@qmC43%eEl6=jsUKy8a z(9k?>)Qz(*9>Wx`O0(Hff-BSLxTR0rZ*FbEPMP(!C_$_H_55s30v?(** zMGj>hr&|K{5lDNRp%8t20-rUEp)J75gg%&uji9ar&k0bUI*quuvCFgxH4g0_XT+g| zsq)M4xMEMus5aqkbaFx#j(x2)Fu!`%vim}%akq;bu0--KIN+zACu%tJU&f;E0BdQC zI4uYo?KBEgn`v_vY!D%!0QNPC>UtlWCVG^^1`b$;&lyX|h@E{#A+~x_2i`!48a#A= z0SLFz#iSdHN>ap&P|YoP*c>HuTkK|9Yw=39=jj$W0I@nSYNe}zb~*JLPIN2DzV3PI zk`4gty}zcMl&A9L`(cZUsI=r`<8J~T_4S&2k)V6pBRL+&ld2;))Bk>;$V%90c1q2K zK)@tan*NjBluX9s$v(m7$ita$zEDE^w_gY1{lL;Jy|uEB6KU;&pVq&yx$~w331mT& zc4}fxMh%i0c}~i83ywwU2ouJ~g*wX`gT{8)#LYc@ioem#5CM!ll%w8)^UI~0>xf5Z zI$Z~N>(!lF*Q}bC0;=6)^+x zT2M%$XZS+3?XVRWr?%Wk!C|HlpEz<+HhVg}*@G7wC}b5+ z5|--!ap1w;6F4+WngG)_GNsRq^o<`R%F_8LWG$TH#n>RggzkO=w6rEH!{dE@*=%BW z4?i(S#ha*u(?NgF!D)5@B!aNu3P8H*>0hN9P{~W!M?C>51*MRSdfrIM^f-xf-%i05 zPzExQu=|iLZ+bCwO<^D~T!QN2JRTn&~j$anT__o#(f(Y!y0z?v?E|~ z{L75hIQQ_Ob#ri)Xkam)!(uf0Yi`Y9f#AUrE*cZgl#{1u_;w>iT7iP|yRT1V$;!_S znKrNBys>_rmd?@ST-e(B*O|tgV{-o#-OuAuB%Zgz4znUWe{dv5u@2?cz&8!k9^`i5 z+@5}3=)XP*4k_F?pbg`l;u?-J1NPlWySbHq-V zr+Zr7=u>fRQ%wUmdR$!`v_C$$HD?KaG#v}*6MVZ3b*@}9Q2Ki}zOUZ))Z4aYAg z%kTv;xKR(mcmPhvNHZGpOVZG0nEAi7zbK*Xz!Ae{8C!0~!6Xo;ple(ZKzJ(t)1Uzf zV{L%~59Rak@zA|DAy7Ai6pQ&G<6)GVhGe4=5X{wPRFxX4YCP1U`|bt)CzCfr98>_* zkuIr})6iLM051kLP;l2`PjF>TM(}?U_>|B+aOXfUMs8g;NIH^-n93YEv#g8AB3l1yLOOZuR$+!LUv<8L=0;Dg5N(<;BvCX zH92S^aC7~~2*qwgpkM;-K}3cpf)p#;W_?F^( z=#{_Np1`wNpS(94|3j&AAZj1s*W*KI1eUIN)^niYb5j4v|KBeXfTULHGZiQ&NJ$$5 z8nQ?~#X*RN{wuQlhcI*@)Q-Zi_cG?&S~i-+?oiYG0L1^$20v~XKcDBU-isowO#4eR zflN=^0`vkp+_r@bimbozgp%O7vfQw+pF*aX&9%}1U@(B6hzlV zH*p4`h4L}_zgDF|Y^VW=FdGmYPT2>tU!d|ZO8|tyNR-k?BJW#>I7~(Z5xHZ`SPsEp`7#gyd_K)BW*N_KZvDYL#U$PNk@6YLU{iohW^7XViTpvktWTy=p@71^z>$a-Fy4H+N}-7) zR*CW%aMSWm{~0qn2^67`|DY2;A%B5gbb^IFF`XCGzt5du33LI~<+LOthfJ8sr?kNN zW?0pTmq_iJ=O+KT!PlE`q;$C1{coV-L!ejy=F4(M+kf+dTkjEK*dq7tmMKIz4%&nP zU(YQpHMgvs2pcC`$r(5Dr(*y6l0qmVMIulj?h&=y0dXSF7iZQjGiux5Z3l%s+h5W` zU6CSM;5h9(b~<*?6u8vD15uSs^8dvb3Mfd<7ea$w(b@qYVzlJ`;3AlQLFDYAA*aBg zy?tdTg!SgtK!!ye-R+2cp`#6t{7M5%+41lrG|Q#3H56@_&2Irg>w}k5S(A=zGfUz_}P$qZDP}~ zsFO6I1BZnxXRu1=aD_(S5E>o1v|)JO3o#>ti5>R>1H)gJ7)#~8pzht27?QmK$#^f} zp!XC8t(fv2vYStG58yIwKlr8j?A5k-SpF9eEFiz;T)~L7vCt)MDjPQtgXLy0>k!n8 zASFwc*l>2=8GQLq@O3Ho_d zTr1Y!z?q@<-MQ{#2AORxpt(hH$n5_nyIceUgu&!8TROYtHt4@jJsc?_~MIB&$}}1 zdiqc%uY3Ok;!tRSK*G<@qW(VbxDG!u1Se6h(1#39{Umwg76K&_ATLfl>JisK zt~ea}<5ATzK=^C6(UFuhs@e#$3&T$J%sC zO7yQR{H}3H6oa{umB#V^=JD7Xh=Uq{JY1&L^ig`1F}j zoc&SY|4kE_ME~uMbS_lP$MwzPqYqstg>kV&`wZX)>}kD@)F>8Xr(72N{bbQ;t-w|9 z)p9L9Sb``|C}}P0Pt&y@d65Zbg}PhL7%}Fm-zQ?&<=L`S9UR~@t-3Y42F9>)U5@{C ze>XpYF38vy;cMcDH-1heyt|}>qAUNEOG^kV?rJ^Zh_g$6-^R+24m;YoupMEJY zD?*?LB5@FuMB)80j+1qdU{<89^K4MMCX)vkcn78wSp(15ML3b-GHwYw)ZhcW&EjX# zqlb_`hL%026e8%U&!^&X1n}J@3q*fYD60LHU}OC8b4Lb0zNMMk^4_;1Aaj9WEy&RiEC*mHLLDtsII|bfL6@^m$DhTWD{MY;L|I5kp&}9gvH;JU zhSJx-SBi*7RVYtI8nXX}>94E)o|$U6@$IuW{NFr{405O9o5=UTPr!Q9fJcSp4#84I zOx8FqnT^vo!x{PhTj-+?b3+r0XgtNT7mZkcQVO5^8@-WNY*6GD@Ew96@iSSmn{L=-krOM_GaLN zk~I+I(2oVne~Iq-kG@3I7wU_iU3g(!#2rKLTBReZ+h++?Z3oFj)B{Wv5;y1Z(LYcgMBILQO)HbQ=r+znEQ16ey8sTnkzCPB0K!Xix!5zDt<40QAs8){;}=c zfdE1G<3S`~6z>k9I+YP1uS!c*WG9|gEa~=!O1I?}e1ym< zfe3@Ozu+`2Dc`_5xnYa6+-|f5QRs_`(j2==3#^#jRuh^s|F^>vh(>^D17I}S5Gw1u z2I)ALQY|B0c#fV_B>Mo%7d0tEVjUv?x|6byXahRUpi_W?|;jE08NvVLAk$8Qe2URDY)Q>* z9FUQ%#NeEZU=d_d zM7F1GOwwofDb%AK5>Z|qXBgFsfmubJMGt)J@4tbwW-1f>2SeV0WcUGeY%Qqbe}Mc2 zT(3qHd6;)8&Kj>nQ`XV5;|vh$owe#-%%$T=`OD)WXm>I{>t4`5>?l~}mFxb@LU}rH zY?|T>9Z6+V#h#b$(Ed?Jia}6=>D9dOO<@ytVlrLiJJwU<! zv?ba!t?uwM%EGJL0thX_KJZ!+ZOf}QCH_pFrUG37``52}Ve>MDOK|0P(|& zHsuId$Zy@f*R9U=U*S_Wc$3KSH%3Rdqh#OHH9dgd@iczX%P?(*KxRpSAw|v~K?9ND z)H-KJuv@3nGrEV1jI{;LUVlZ)Sc0>$P{C-FrDaPv96CEZi;?dtc5ilK%;yQQ$&ZF& zGJ6kvk*tY`DB~e-eL>!nJ^tC2BROT<^I7c^#?^tE*F6bT5Ba4rCN-3SUpb&k-L*aW z0a!%_D<7LYP?XT=j-UndTDWopGO$h z^y%${zd-y|d}}NCTt;)M%8CQC&(BO)qa>{jPDN>R04dIJOt!KAoK20jcSL)BL@%GGw69)+o?R??&S$U z7$EL$^3K)KdRL!i@EqciZ+c-4^KDhLNy~u-Xq6IjEi z87kXcDwtS*^Hglyu}C(A7p>9sdDGC@sWQCWcQNb2{n~&|$O3lV#R@_%ukEW5 z;w4Qr)HJn@iQOFN`UfH%fU@h{C-#Aa3>6Ati4g7T8y{BCKN*BxW5O9tdLh@mQX-Xk zz8=?W1x9k&_u5_f^F>O^itwa20Zngmr-)(7LOuhfV>*TFcNlIu&EqzU_8H6cZ0pXY z5QckW)iEQkR~rkd3-eZbJp{T={cG>t{YznuuDYHb1=PKFTZ) z&2~Wwu}o(g5kIIk3m@h>Fd|E*LGc;3l~Gs6bXPTz+1_SQh;X1h&o*=hIZZ;wz`W%$ zko6nKi_*M3M|%RxE2hitt_06+p1rI0tLW^5j8wd4u_3E3bUTUzvo0414USBE16pjYG9_G!TA-no8w4xYyw`a* zWi-)U-!=ejkYHz8zLAKGTPO16MPB%^{v4c=ky8dl?1A(#qvEt#k+*St!wID@&;g=2LBp1! zjk2O)fh2sPHupe3+$VwhJ~}mAtMkTHVyW5hW!rCZb=c+l3f?|>Z;PqD@H%oj<9SJ{ zwHCYWY?mpZGT4enC^bA3Vl-hJf9}CCTh5Q(Ly^MNaMom`8ndA{lU4wcs0(l)(BLQP z#w1G1t6nJYX&NoJaP~8w7muy%#OOZ#i5}{u|B?Q!1HYVjc#}n&8Glo2P5(F^gl0@GrxW^2 z1}D~$qg$@0>n0HEtsj$RnIq;msrQJp0nzQy^x|KzNjO*8zpV&y`&x~JRD z%TH_N(J@(iSpb<4@!nu{iecXf5gQOf2MkNqicD#@dUNj0t(xVL1dZpB%Ts^Q#hO>u zPRJfjxz$#Y{}-h0v%)PA_uFdF5Y?PBz@2NzD(s3M<#f`J)%%bm^e|(_Iz%-QWKmV!UPLDW#J`~kD1zIz_7(Jz>NB}@cF=P ze}~P}XN(JuJX5cQn?dk0%(QmHo>m|nS63v305P{3byFHDD4vxdx$Jt79Y2%RKp{kQ z-e9-~1VTD(RNvQJm%eoPLz*2M&595zj?{oo9CoZVD_p>+ec$kDs`t^OgespNMnqig zd-qAhyke0GMsoZQmN$2<1C+-#v6rMPhl`1@;r{LEOEm_U`QNN188t!dS&{6y2JSXZ z8Ft1*IB_sTdzVm!KKG@w1VuF{<2NeqpnmMYorX>{nWKx59_q=y23}n2w)=Wt13CXD znlTWHN*s;Dp{-nR&Yc_1w83mxa5ItF5%P9ct(vvZN%K{pnX3~+Ny+iVZa1CZWN`SL z0Y&F<#G7ndL}oX=FW!Ka%&iR50m~bZ2y04c4pw1zr_^J<<(JUClVC-?H|H4&fAIk8 zTunebBo~b+k@UlSD07|2XG=-|BE$tgZ<-%S-%sdT!C~8^M*`+^b5`Tn=qVDm{*Wi{ zukj|BgB)$>gYp`*{-)S{#`o!uc~98n;N7e#&(Kn|mvEviHzKzIT$E}v^~-Y2;ThQM zWN1|=u(?Dp5H306l+xG_F{84$&aq!1u4o3BSdK7Nt9RtjRx@%CsFQth;7B^PvbN_svv zS29N~oaH{)m9nZ=icpo#atYRYQ--M4>;^U@zb<@sho&-#mlwS0@+?B*!~RHQPbQ#_ z7d6Z-{PK)ls>ydnKkjx$pI8dR*hV80hK@Q&e9A;P(~2;lq9~G6BEoq>8FzX!O(36AYAlu2jk8~3&Q$-jJ}NZ)%zp_L%K4}t^;jD+w)Xy8fawI1K&>fzTNd&% zcTUXpX(FfsK@G6<9s!1$Txji?dZ8a}_80v%ep*McAik9xEVRXk5mn&YvtT6cd6ujg z5kmHGXK*_{*iy$eFDx8iW$Mq_JIvm=6A7jUzu8$8QAWP(q3q)w(j!1p~Sb8&Bx2ar^GhR8+g zKso&2oHV^`m=nKwWu2DfEKwraK&aV0a316LGmp& zXP!oRO*3R3KA!4`SJ2HFJdIbei&qMuZBHXWveu04WmB-%A5dOPG3S=#0-X!=xHixR z-}kVsl&X}V|B*@;=I#oo!n)qtJBF4EO64-B6|>2qjswF^FZkjC$8wo{D?uoN0$ko4 zvg@U!%I`=xAQi!B z2AU4wuZq4vcJO`?_j|POCTQ+xy^=v2Kvb&4p-63G?>!Dmu}8930fG;0RhQ;1p4UTcK2QD(U9;YE2Yge570eMqO^{Vy3EIbBC>_4DRFp9+rk&K56l~ zn?84wWsbJR5sHH?90$7yX-$cG5{Q4S#y=A4o~vWkkgd z7<|6n2GU^wt2QGDcS!j7a><&enr|{YztD;u3#ZUA=Yx2i1@)R_xmT+gJ{9Bmx(PqI z^g~lbaJM~%uI3xPA=`QFBEX)-AXoHt__aJFf5tuNH{%aUhSCqZ)@7-Ma|a7PUR?h5 z*%ju$LoJdNsM1QRgY#GQ=rx;Mb?mbJu zg!`E!Us56aEsmxsMhOjLmJUm3OQClgiQeYRG4Dkw^65E zG@2emJ zKvfY)%WrxK#G`Cj>GT?wSvlc~bOwmRLARzjbU*Guk3$Wuxrz$lr3zf%en=vXH>8?bFW}rOhxvU2J(bU1P`- z9x2Gt$kf=?>XOCQh|c+)&cr;04>xIvU8=kTsFVbDMpx6*azV=p=XEDrx@9d5{kbhD z)51M`y9QM6wFhPmqc73O`RW-=mYvLw0FkXlpJM=>acmUB-b03-w0TPIviHhamU?T9 z+`RQ?Zq>-u{0pu*AKmP&T)DDliQt*D`{mi9I(A$XzB@%2Wwf&@+s$ybst44BFNhTQVaDQLBzDHc-l0abra)ieOWGFAg9 zv_-6_J_soKGz>j>TBE1k9OGYot7MNKikgl*>6bghPkR-cp9<=o4tCxq9|MH-=`r@t zjls1~=rgIhISODc3~o-N=AJX%_1MF_72f?rBIE3or4hTkRMoOo!D8ALG+aiM z2lCr`k!AZ}`H>}5l5g|6+4XZ@n-GesP=i2Rw0;5D7d%m1 zzzn7sg}V-U)$?0d*~k&)Xy%xF4Z~k?uX}l;QUSjhl&+`dm(Qq;cON|NiUc{BO$h0b zGdURpvhjS|3^*Y%^)WOX+=^6bimheq!DYG~k*4K%%zd7JhuLl{)r{HN8U&ov889o$ z%ZaIWdE03qH!IQ--(D@TNTiwGW;|L5S|=4xMYUK~7N0Mi)ckRBa%az8m9)@x=4!1= zncWY`{o`A;gFL+*GO0{s;rwI59&QlwE&MzcrPyu-Qmxk!N$SWi)mCMlr})KfbnOz6 z!s&>&2H!pBhJ-1%w!!2)22@&_WwzY`MTG(_{M!Pn>5?aWUJ?$U?bhR))IuC83=*O3 zLZ!mYBTq@x+b{@h74W{7Ok`k|)IV;OY{=zcIy}M3d!^;8V8M&{)al90$PhweNC;W@RmJfO$H`(n zDs6u+r;DiT%=GFFfB)1x`8+QHDL4)DQp@oBGb~W`ZsSc$4hN#@g!BOWuxjhKaD+b}io`ondy-uDVY0~^| zd&B=Y8%vbSlgDKTDC&UA7V#81mMrdg29`5bx3?UV8eEI=(BT}t2BHO(!Ci(Y%@nJ^ zlf0i~90n3GbjewlS`oJgy+huwc*z|Brca+^Oe&1Inz-W8zse$2ezf{J)G5`~-i?!> zQC9wSU?$;Wa{vQ&R_)1(H8(SzSG~Pzr0x@KcnhV17s!?n`vp^^O!5dp=Hn3D{^2n?}-9BinP)b zcOpX@n%ZY8^st?dQ#9ey*qCa=mDCLsSTyYVL~sh;F8Q40`r>N# zQdT|>7;HVh8E?Zd{~FmkE#P`9UP^HZh^;B17WalmtW3KRpCOHhBooZ% z#%iUBf05#mkaAjlk8GVW)}2|zOh13bEBd$nx$om@>oZ{Yo+^qAn5jIzSDB}b3K4)F zaTDJ{zS~#nJWgG-fs7dMIv5ep3%bTA8Z=a(Q-?H5Ck{)Nkcey>5y5g3`p@UG%{hTv z){os?5i5-k`kHu&N2HY568K&dD&D+_p#eV*L`Qw(QOdb1%=M(Af^u`zCA4V)f{)mp zc+clQA;;GT;o-g$xFBwZY5dLV!?{@W|b4VgtX`Sr+2?pbx(nL`! zwz*nLwGcotgM!^IJQ%=uVt(<(csiY4?{~!zb0@Gn7*>2~W+*i~1V+c-p+A!KpFZo0 zFwzPTr}c9!!>A+ZjIp^;sjwVuOFShm=qg0ufJ8dWV#%|BYG;{8?Twj4-mGIj%k1X# znIVCW?i@Efp;aH&SU?{UQKte)xumDxl!I*wZgm2^17#{wg2ZA(Wo4X7WD8cF73~_8 z2+%-?!F)jR<94sBnkg6Y+!}b%Vgzj;wwb1kc^XCDJwA2Gf9%MWKFFP}FN0m@#Pc!d z)lUO+#M1$R5Mj%Nb(`vpq?;|!Mdd=dj<1$K@M+lY_^iV8N}d%}#G$IBH?*0`2^h%F zHE}TnXjcxdtk?I0oR$QY3h5zhV=4_9r4_ zDyUi`z$kOdk+(gK!04~OsF(!c)7SV&C~*Qgz*p#hoWcm^e{EW|Q_PbePS2#VN>H+Y z#({3?F`W^{ZBZ7T{qoNO2#&nHagG53aSQ=KCJ1+~W90k(gq%P9R8XzHX8nIuonv?< z&DypnHdbugwryi#+qP}nwmGpqnRvp9ZEKQznP;AFzkB~&$Lg-5x^P!@*L_}B22HQ& zoLvb?uaxVHXH2-)KbcEF)PG7Ps-aJp98cpPG>+zc7VEL9O|tCVvmC z3yPK*sLul)REaw><%_yHK8;wt{hd@i*(`%fwSAls5}0hCSQzgA>81kv;ZA>$v>H8N zT`H&PZ}SUvH&FepSATSvrt59VU#O6LiqOL?ob@xX_2?*U!;{~x3<^v_ z+@dP&_Q(>v8G~7ZuEt8Y+0M37A3_sukeI{*m40`==ZSu0PpV8}!@D)+Ysmu*7pF&v z1(rGM(D}?S?ZawL%4H~V;<4*yL`;Dxh=h~JL?W6rFz$tv*Zt$;q%Hr{SD?Xu`lYxKWgSblzq`!wXpgh5c( zzj@{zB}O#T_Ap1Pqmrt%f37we$eX6xiz?9Vb>8DQ;ZD3^5_W zK1+3k9IQToAvfr8L^C`-izG{$K}mZ)q8kPt!od;-8N)&Emaeq@_?f;8nOqM~u1`)%av2kK&Bi!ZYtI~ZdQ+0#1$m1(%cbGUtrfGTJDY~mkjgW!X@VQE~JuSFFGBDGN(K7iE*C@ zh47IQ4UFG9!=NjF6tePr;-|3QdL)K4iGWigmIc_S^dr?qccP@5VK8<6G;hT5kZ$_C zGHM;;9-kOJjyaG-p)+K$#^Njng6*W@>J%AsV^6046?>1XvAD9jbK7W)bfiaSk+e|B zU2vz9%x~F==4ljYNJ5sQR@2{agRUAHB&T>8b+~L0Yraf==gt|{J6g}RB zWE-MR7@PQu25irhDRED5S@p0GUL^T)T(f6#F7Vt&gu1_vc|KtVAaUOhWToRyhD!@+ zVfn1C>*=3b1|q<6PghR75ktS&Y1XCMj^PsK!>XGwUZ&A@lo1Y)*CgD_8QkW+M9naj z=r0%R71ZXuPUqFR<6;*|)vOiX*M=--&w%do$QY=8YvT<)@f}|4+-0;CapFWs+I+($ z)pGs>0=3+{PUwt|78cuQEg>VTR<0P4F?C+`ec`k!zBW#O9KZvOI>Ll2n}&o!zPe=; zy6CfQ+VMGIRBF0GpoqtYSFqW@=Q64)(V^`Ctn$t7pGL&$s@EqiEsH}nY3pLQsz^B}-2@OWI()g-;JO+~H7fVY2gpT^3AKf4v)u%+-)5Y^1vAyrT?$j5}`hl~ynOzmRpP z!}`wDSmLK@X`KvE^0pOnXntu4+#`0S zN;c&DGYUDEk0F&!F=UE3<+Vd`#B+30Z%n(hLnKKF()*($b5_zCbEyz&*R2@p3h$@C z7!S7f zHNnTysFhj|BjiC7>pI2Y))p!>MIzEp+iWtsojq-nKby5?fZ|vr1r*_FArZxQb&Ep) z>s26I+yfsL+!)RWgN%iW227H}GKYSTGxSSR7)qV0`()^GZ*Rc13iqC+FzWji-OFyw z>dFQVnkes7;&kk9T60L27h1fgb+F!qE-P`+iXIWR|_&Tc??$H>FnVen}y`*+GE%EaaCx82$InfY}X8 zg4-7p@4H<;$Ct^rV*-AVx536bZs@JTXGELaa|B z8IT66=TS|W?=E8tjR>o^f8;PWc_Z<+@-90Y^3z(z&!~*4sn=Ls5FHly5D}o&#e`Ym z4$n6$S2bpS0QrxB1S2k)Q(k=k6uaUc??6#9GFsa5L!Ov02!l2E2jJ6=;IrJ&;}sMhXUGK4n zTvvuP*Y_ftM2750Swe!7g@2`b^?V`TiekSUJ#zCE-^nmaXSz+#DVJ(e!}wXz{>F% zH_vwTt`woDU%WGgZHHe~hbw0SJ4F${DLvgFM=h2zQ@5N8uFuf~b7=2UZSkh@yuAz) zX-bn*?hiu}4l~TR7EQvyIX&RgZ-u)J!*4KbLWHMqKlXJ^!zVBjo|D3Tnl(=7u|gQy z!l40wLn0VafQ6`Pb*nlL-UPokfqRfEDFA&q{2ZRQB zsXtpeU@nv~{fBq?G^~wBD1iuKuP6(4z6TDLS~Pfhq#vGF2bPSta$dA^XF%%rSWktN zI5uN+Tt)Ndv#57jl^V#THf?VI!35nw%lw@=$leP8nE-M7Zh-OK98AS?) z^_2s3vlH#+($2`@L4n*4z%(Tk9w925f;;cxg(=#>YAR(6h>ZNKe@4hWRpslq3a1Ua z+DYZy$tOKvGhs zB_~=yb4wia*%UMZe>)6Nt+MIn$F39lwl_bMBl~e4J?nh3(u|!uAPIv=X zr$m{p6G6?1hzxk#kWNaCJ3|r8XihDvdJ2;N%18zV3egO(*_I zgH#5Lb$=n;Q|f>h-h1G;V2^Jcw%%Q&c9xt&E6-|%f^^$G$xB}?h66+n%LROy_>z3Y2f|RPdr@^1R?IW&?Pu4u-h33xZxe}@vVn?px3dM? zO%VTDmxN)aY6nWmg-1{lAq9&aax@YbYWd8j#8UoSzrf&@YnVGWxp-HuAc3eTVX3n7 zetG*m&r0Ly;$8#K?5iS^}gTNj|b{X zXV)CRPz~Lt$S0+p$qsy8Yn#0zo5MwrQGkfTiE80Ja7p>_itc!@VJnVjeD0xy@;p^B zRh;M6y#wx`7>@^K1t5!^F+@&PGW`eace=z05K$>T@~|E;axv#&F0$%)E=L>@aJA7# zJB0!#SMb@16O(1DpNyQfULZvM8I0#N*W;cvkoBeH6+uy`kp~~00CT4R$dZXCIHS_} zr30DO)=98%y=ojN6_WLNJny;Lh&3o6%6*?it2<*Z4603D!r2XE&t}|Z4KTY+`_S(RUx>|;>T`nWa zUb)d^#(OT*den{e(XGGMg9EUDEIgUP`S}t3yjw9k6lmw>?kRqi)AAju{z`P^-`{`B zP3%bL%4(gM?{z!YXmc{Vs}`WRaR%&ZAngI=uY~BBL_DB=?Jx@x@GbO;Z_?32U z6jpREQ1u;sq3LLkHHX-tl60c*TT#W_!U5m_f$ap=P4Qzbc-DE*`Da6uSVQEW@-rOr3r*LQ>s z&JJp$Chw21I&gxNWGb=_$oQFX```pE?#jr3O@SvT_Mo<~sMi%WgcMuTJDi68o$y{A2z!I%8+KzZ{S zIBbNr9rf7q(<{JtSOl-H)bts7X|IYS@OA+O$RB;!#2sT0)N$1}zT{E#H2cBpp?-mx zhYJvRf(`hla{)ml!wQ&j_~J|G%!n^j(tLyINr~n!QU%60 zxjd^&t{cE2YGsP_Z&APb4WRfjn}p&|ZN^H*u@?cLEnK?mkWVIt#Be_Ct6XRdV5HDX zQUrquw&SRgJX6v3#1^h&5xEPZhUa;S3GXk5(ch>z6kp`1b$kRDGK%l6{P-`0FWFjV zs`cz%I2#0nyuw^iX=~J#%nVZTqznI;PO102mTY7l@bkUkO~6!G_1@T2EU`h(h<>Y3qr@5*#?WxD zcT=^P$a5CStwxlTd7-998#(1Aotp1|>PQ+6IE!ugOjE5N3}DCi!X4)K)w!k!jsSh@ zqR+RT2b{dXbpz9Hw2SG$+?WEjD+UXkyuInihO3Eql>yE*zCq_%%L_#?kt*ia+M>c* z$RSRpvp`a()(l292SxZeCX^sw%`(>zYF_3(Jq_;5Sx&SlZWqrdQb_tU?7DwW(yi?u z;fHu4sHc#l2ejrJZc@+EryshPfrr07Kp;NW=sz&(>gQA$Z!^%?T0b{DF)Jg9Vt%UQ zmsoH*q4(Om_TJ4=w))-^E?An*1LC-{b0I7UO&o6VX?2PkG7w$68|}U$50bDgg|U3S2y)2C%%@k|A_d;*q_;ixq%E%@c@_b@r%zY1GgU z5&X#Bx?e^|+&>hfqlan=>L$9kx#=I@9j*JyNO*KHxIy8*KITL}-{``A?8NVjzEc;P zUCP}V(IfF?^gzo0p#S_7P&Jv(CkZpKpygNmK2H^#(pSc^F%q3C z_R-Y)0@7KVJ+xjV%cy-UO7@<(-LE&%6&>ehww=*Q%Vloxc?yTjPQS*%^pv&Nh#FmtQ-Gf}e|Gsjfxx@OtBZ=j(z~a|g{Y;KLpuI2gjcoZ7wqe^Fu(0{ zMjb@tGqK>Cz89HabI_mqtFpi0RwLoAhwVGEh^f%xT&Uu7qJDgJGLkif$ID$3z{ujz z)HXX?qQ*FPF@-L8rD|R_aIRziG6A8%{km8j5IhgA#SAr;4`Q58PHaonHj8>@R2{OAg zHK4-oeqecg^6JK{iqXbdoh5u-%8d0$7rvuVJ}(u1wKzmw_mu3`b3X>+dSOqG;Vd7# zq;Ro4F+C#k`x>~Od;PL`=9r#lo}C6P{##SnT&K7k{wuOl7gFfH z&$!713>~;2Np}Y3x8WrWwm0&-!jN-}710o<&3At@*#rf%VuBTdtv9H9V|>D;$8K^a zMZ}N2%B~@fatj};y&=)?BA47ppY=|pArf>_In7jaJ5C8iJ72f;Ru*2Bq;-;G#H@9rKlrmnEp5NUWo6yo!i9B8{4Qzf ze|t1~f1BRM-CUpBb^Zt=E*ZXXrw{9_8!oZv)ipoaq7)%l$VD`Yp<+Tw8`OvkOiNGh4cg`<*0nL7h`XI*H2I0Hh>7g1 zwqu6-d6aiO;6fI6>{An%90RUr#TT|4Sb{5|S#$)t1=dC}A$z$)QVCNy&P>t`qc$it z4xaz;e$bzk5HbOcyoT(hpjama;>%AvAlBb95=ZKDcj{79Ly6MRJ`HAVD%PGjBVngwk1K|=pQcP^RKZxa2uFUBKv`Vf*#O_Y@Y3$ zzFZS{>j>Ih_zEZKh_g1!ps=2x+qb45v$G1b<9bDW!~}e9&z zKe#I#U|e;B`sKr`M3p64cu?WoI=>^1X#ol8OIoWhosm*}C}>&!J_c3u7km8Sny%R* zya2wsqm`)avCsL>!@>c3d`=HJ*p)C{vZD9N z*WsU=T&5f88BjB9dbW)K{iuVAAUtu;ynvP1f87~wpaQ%$89YCrQ#lREO|PyeHKA{9dNKg>fKot;=G01Vk3S#;`p$5ac3E? z?R5J05OF$&J;6W6MC*j{m!ANtedqlPN_a&DFLoKiUNzzXl;${zg_=F0a+r1%PC_}y8pbDHDf zUP;C81t3vD2?i><9j#Ua*Pjx1FvE6~8p0gFqx$EF2{%fh0FeOptv=Jm{vi#H;$zB4 z0k@Pp_i~x2!8p6dK3x*zI)lD9OrdcBHx~TIA2$yLj8A?YI5?mvGJ0U&E{)Lgm(0-g zJ4rrF1vGIxr)*N>635&pvyYAZ>+lqp6D%R@4@-XY$q|zZSbvknWrNPZ?Ie1C-bSR? zX)h8ZHdKL?)%YITBWP5{U1GC*^%v%)pn*h@0vhOJBvTn4Hn9TSzG)9lz2Y29t!cs3 zX%-1e)d98Nub(=j+DI5Y^SN{VLsR*y|8dGNphT0Hw4N{`PFVc_B)ji&2HU*7wys@N z72>LeuHM_fCwVX=vFZ4B44$@4@cu(MhYA#+5wKO0xc&sQxc$!Kf|F7zFL>m)7THB$ zK+Y-CajC#OyJPn*Oq|#C64Y`dWwfzM(iNXXDPzczPzt1*E zc>6Hb@_uj?eyju^h3E-;nd8^=VLLh>hR4`34#dj>hA8+V|YIPz{0UHdBN$1WmhPwDN$sxe9 z@)kse@q(2aa~G$f3$Go*(Ge+0;~}9n%}+^xUT}2~+i5|=jST8_eGlFc#`y!ph<3hX zL$36I0ZxAkIhoTBBqr%IWSkKv3xGw*BG$LS!o@H+qu{TJrijfeYt*duYZKv_t|m?< zi%t|Fjt+gg)cnH_#vrzzf0nx<(R`s#r3#M8`!c~F0UE|g4Rf6Kqi8QC3$kDhuy`m( zcz1#dXYzoOe#I_wH8=LwQ}1qLMfa^9nTW$4U)4 z2F=#$StC*Y)!09sVopSas8xez@dX@~R@004{uJ*pEfD#i^TdEP>;P^l|Mg#@<4%9I zA#Tut*nt{vw>ZmHydU`dq5?+$dd5Jr1Iw_rV}1U2dgW1%{}+<}^CVXx{xkxX>NF2| z^hE#aOZ*wO*uZQVgfQ_^B}H*`Ua$Qp;y=jQ8Ug>#gFiz>8&Ix6XzLd8RE|8g!51=& zV;x%y^z^rN{{F8Z?JSk`_bB?WQoujc;g26rJU-;ISv?Lhw;yfG>NQM+TLx)u^dJD; z_AiV3IRyS(_4lS^LIP+8b$a}^pDrou7GE46q@=$4BKz{nL=kCcBr$1oqW_JQqgecBo0BA)le|eE-srk8Zo&C3&a|!pJT8pGR9Jt zaKI3j>Z^_orff!;szh9&s~9*+)pJx9C4ebF6a@s9H}Zh_I{AzHf7;sr|K|CuSO8I| z|F|*y^q1oB{nu&fTI0ZE2N0zfR*|e)p)7CE)rBO;aV~@wLO>uNTp%GTRa0)g0SMsb z2RPXcWMDUk@WIW=+3_|li9=pe)hQ5B0NZbNm8;#lAo=f(^@4R`RH* zQV3lOA#@CS3`H8)V3^Q+Alv=+Pk{gL+7!n47ox@&<_s6+d9r}twxilo zc2bF~+QiRSRR z`d6L;7ufrYQh20DA*~Ww5r9w$TnPD*`0Pb3cyl$jKAE@~jw;DjA;UbLFpf1_DMt?k zrc5H}H&LAYSrCdAUWPE*!uaUE9!K{u^zGg@_}T#J>_aUL8%2awbTksJJ!vB*3t%{Z z$=t#U9Xt6dKr%imqUm3mlE0%k-U{^T3&~H!gJd9^047X=Vm-mB?|$a_4R8h#UInu{ zAa@l@sBkcLt-5`_ZXFG9O3U*yuP}l^Ofl~iERx~?CsV-0oWJl)@C;^h;#hR8Xcg)A zcW)IievDolDm)MK|4dD2?$4%83?n6pN29cvP#ntn4XyZ-O{hjb8Nn|}iRyWsJ!{;I z-}VL^h>e1=f&T!OE7}idmU0x}iW!8Z&>p z?*;{k?+X-BSKd<(M?*-ZW0`)RgbP{tN#`e$m29s5kd8aa%hJBVZqIxD;b=aHeRWK-FCPo zG!niQ=67u(4-MFK=KqD30B)$ydXEcQWJQCEY^(?FkRK539Jq4H)D}g((?mZRT;qJv zln5LH5qlVo52>HTHg_=wnW$u$5mg{9m3oic^5BVS={b%0bFWa*n~F6ASj zmceU$!z7H4c}@j2PPWcLH9Mj)JAchC(@i$tBB!Zfkcs) zgb3@#@-zgoXw6p-&;RNwiZ7qjXb9v;Xo>byH#O4GtS5|Y7g&Z&8e+8o`ZK1l+1x1IyCpg}+c6O#1E-^Cg@I9=*fDuoC(Bp<$jyrS472=2KW1Y3J1j@Q_z zZ5anA7k&_GN!lzF95)~2nc;Z<+=Ai6oRknw>4a25vP{3n_lrdL7s^}k_za0Ono>m! ztf_ibQV3Tx(~!%$E+(P2_AtE{m<08uejKE*;u~IvqKTwia!M3h%0+(T} zMp48PIxa<=WC>BIWCYdxPQcp;mOX$YW`~W@kb;gS&>)yB@Qh^DaW&Zmb(^xmp!W-2 zdlkA~Ffz6#Gacr8zR0ga(L+(8T5GXyRauHu)g-oRUeJ^#uLCH6(?r7ezj93f2KB9J z(XCiq|0Cvk#giLH6#jQk70fh=s1z0Ri(`Fi>>Cf*lObr-HKFR|TyR`BtyaYf^Uj4F z(}BoO*CEOGagDM?DoJ^*4qLoX=Xbkw`46agXq^a^gmI7n;E^yJVq~UTV&9K73YXu^ z>(v~H>{W5eZI-C^bMANl8}~j%RcyZy9V;uG{dPp*=0rHTbGqp(%2z#XT!ceis}(+E z3TC1uQ1SU9be)NKWF3&~jddWsMp%D~v;a5^{Sl9P;SC6aL`E3Gvebf|aZCl1jr;(J9PBYI8D2-TAo$V?weTwF22Xm;{r0?Ro*9Bb z+@%L2kyfm^W9IZ$8L$Um_}$ze$5SF6T-38+zCIBM7P>nlP(T5#l~BW#!q%*4=FeOf z>m5+vOKjrdX@i|prjuse2=NKrpi|oDtqBZgPAv#-;0)>CB8P6?>^TiE78I{Nuo(~k zNNW6N;O8R}+P}gJm@g8rIN(d2XNAus2Qeg3ZT!Y_|Ek8tBG7=va^#LE#QHKzc#~bN zU2lIPhd*$KkORZn1vw(Jn%QE*rSjneT(PjBu!u>MhYk=jgq}TAl}Ho{y9>E;ALxZp zsOj|f72#-tuiTv^(D?=AI&SQ1xon8l9q)kbFxo3}H>~H-G%_zy%j3_I`HUZph9u&_ z^gkP$J&b@g$1jDF{Bem8zXZ<~`Slt0F8hMexXis!Yg~iV{J$+BmY?uS~5;tqx0*7hBR61^H4i(5*0w2};-l z5J9c+1znrbfN)7NGayRos@%w%*YP{P`3BAwWXEOY`|1i4yYc7{k5(ev#ahJ}Tk>|?IVG8^pp{*ln2XWc+C zt?R4)l+$0g#(&$i4=7;G@XJ7Jb1?b2*zf*FfU6I-%mVh0yjtS#`tySJj2$c@tzeZ0 zUserM5MG}x@{254kjT*3v4f3ExKJo{1g33mdmUhR4k#Heeq#s;nT6ly#j{#baysn{ zoymC^tTh`}2`!ou&HOe=m#+&zMlb$#s%j1HTIfWpG{S z9_c|g`+x2HpQ8j4aQ*95>V@`Q{Mf1@%1t1?A2&Q6aaNx+ zi?_uEY3+hYfPc$6nv%`+@vN?1wv;$AQQ3HK_M#MWbm;5ApMyeN zvSXadKN}-dM6+4t(^0bh6*F9w3-zorwl^5Pm_O$mAo6Em$1Bfssf)EQM0-1&m}`{@ z9+s0DbR_;eEa6YLZJF-tndI`^um*>v|5@j5&;p29zbKaJ3H|m%MszU&fi0#>NhQ32 z#VuWwLe&5kD%$>3b;8)t)NMD*UHN`gc|hNn?g+ILQv{R~!l($b>in6Fpl+e1VQm6= zj~`k|XAue=G0p59#bVS-q*Mtsl)KqK1a5~-K5O98kw%IDK}6OK1yE-zFkLd&wq=3J zD9Ho_8zzJ%kmvr_PL&%ufQa!+0txegEv~@t>9(V9X{JxNwe`#KVkIR`xSxP@#~~aS zaYDRX0$37r%|dv|Wg$ykhy@&Ow0tkt)xKx(8B{5DS%ok!*a5Ddkogk_Tu-P#{NmHW zhCY2d!yYxgQd4!MGyTvzfB)SkPPkXk^!zQMDV9@}Ew1QF&5s?adAE4oTBGe1==8e2 z+p*6O60n8_BpK)D_cees3>JPBLR95q9nNS?t#!*|aP1n0kT4(>?Q?L;qSqUe?C zgqL}64|19a7m8j*GI`+$Y2*~2xML(BV?sJmD=umTS7+E{Lfr#eP4;IgGHh~?nQM!F z_Zr#POgpu!?Hia24PLU0b?>PUtn{iGOpNU1+kQt80a&&`Yu2DCCKynlp~o~{HRK{hLb0HWz*+oCqvGP(?|%MeL0tWyl6ZT~4HlVGbo|a4{b6^>^<|f$_My$IO!=~VCDB^CaeePsg4d;fqy-RCmU%svho~V(yK~AAOpgUQNn9896?7 zy3#Lb@UO>qCKvBwsPKSOR#vpWQ&i^Q>kL6gv(WCXg!vG56ncrS5ML+IYbO>P z!YJKN7@MO--fg4z{2?XvPP4ttPIPQqoZx6eEA`nP!Jp$rGP0^6Us#HFs;qA71VQyf zOEJY4mMZe%yJ7Hh60DVo22imNz5w4HWiS8%nb?s#E&S`36v+r2F&;=tYYr7H-9Ybq zj%~YLDystRTP_yT z)Tv`|J=@axMN{C*I$X%)JeeW8iqg{(j2Y~oqueeFw1V6-`f(=-E+-UxALl4?Y0mnk zK`x?nquY8vcY}tIj<5(MMa0?fqTxlg)7gP}bGE7z6*Y82tE;|od;&Rp>r~2`@gx+- z5nfkq#XgW2?zp)b*p^8s^0i|FjXzrTZA=IILsjYUj!;p<%Lx#kI=KfDo}dnFn%nC^ zEsj?>To@6+hFa2HCB-yGFukvqFuixMW5h3TBYV!*qSIp(s>&Y5{by=x%}z8y@jSs; zUQtxb7{tDca;XE;+E!A26n{l{izqzh$*#n}LAc-QB(U+z0cJsCr&?keDQ<9j2s}1N z|GNH6W`8H{Urbo(9{J_+=-0B5&J}xNXfRv|Ls;R0ofn+Ygk>j*A{>zU*DETiZMQR> z#9)b`;B|-q!@;YC5y&x5W1L?B{Nx=*1Gw`I6p`BQZ4OE9_c#HrasGS|y*BO9Oh!_2 z_Cw*44o6ER7#>j1hjKfUsG|2{uo-*(^I*>UXES$(qlw^E;ICrgAT&q0TRh1d!~U)utc9QH z52d@5W~MD>#%t#X3wch$rMCgmJBZ?wX-KgA&0}+`y~4chYjh6$VsY=hT#tItGuIiy z0>4Xd$V~1U@b-Y~*i@iK0ZD>SP_jIoUt13}=QVdC9xu4+RI}?PrA)zvmQM5*Cis1{ zpP+c9#);3Ru%29W{-V5AT0(_UfMMSgnIF{nI7A3zn;_bGyr`DC&FJ)^brvWTsRriq zw^IsV`3YgkZiF;SnnsmqS-tnrNYN$-d02d8W#=c=BuEyoTs93y?kNJ|$3B4d6uS=L zBH*oopyMrpk`^LAnAu8gr-c{gY^fb-73V(cBFyVxgm9!LGdxA&dxtQIAx)Z(b;L8` z7P~W#ZaONg8TOH5KPx26WjCN{8n+KdFHbBLprADC@%xe&HI;`6)**)Go8vfV*J}yb zfw1%75Vz^h62;nKww1fy@BL#5>t{>U9!@hV#e^!RhP$>~3`Z@5ED{!6L+^9*;&X~M zMUDBmi5Zk^;TgV27X|lRn-i+sWgd0Tast?P%#&d9@kML$U)|(C`zr(5fYjv$>84b& zpIFOX_q78p7m*~wDC)O|%qSh*d29%rE6yuN4$D_DPXLdCmuRpalhpCjjVNZ7E96=f zWnT#;FNF_L!V@74!Jjc$_~^dBs0M2ZP?UZ*3k#0s&AOgjttw|I$&isIYYEz*j?>@U zy%V%HtE#aO?tDm5QH`z6HZ(>}E~sxtGD+cbyq6m(+w<@1?+2=-le@Zc6wl|cFle87 zEV3Fb@xH8@tM`^}t+0$eij_?A94MO%r0wG@hd|q4fGh&$szgy0`_(UEJyz2zVd)7s z_^~qPjUmr-h$nnKP~*w-_MH<23zZ%Q1YvQ-;n&oPi%9u%eMb@n$`ahb&z)KXLsR~q zG%M#%B2@uuI2J-1WPs7ckf(I0gI(ZkS<_s7ihg&XRpg9aS;ygm_e;~D)$tL7QM+v*q{?gyOUkXiI=i^zsbtd(6{>46tU!gO5y3Sb~W zlIeDmWK7EX?@bW|b$8{KG#V6xA##nUmeUPOcG&RRMiih?{;<=7E(OR!GH8YXp|JsGB}@!|{N=jtQ#pQ9m%ci(rN`-SS;@ z;2g#7n%4Q&-=t~NGP1Ic4*{P0NJlUko&}AQ0H?faI=QeP@9g>5_rO#>hJ=U-L(0;G z1cl^M`TM26r1S8OIPJCoP5#yt#4WDy~l!E=Bnu6ymgW79C>i=-1+PC<%%>LnTF0YDP%`Y(dQ89}KC zSBccO9lFPk<)lg}lZxp3E{3$HvUh%-;6X{tml3TDfOnLNW26OUeLr~U_h-O;h^pH1 zxL}u1P1&REuTF8*Wx|pvQ4%I8{JQutLO1A*p>Gsyw?{Gd-Y}GSN$Ld@($9*GUCKs| zx+kzMaUwRVE|7f(s{$alV9rGzfCXt-V;ED`3#@;}pE1=@&_*gOx^|hC3;Y>T7teh2 zLBMHWOUfKuo6jV<40qJ3E?2CVllf{{U!Bg61*ok61~=A5 zYJf6{))9zLc!#k<6hs3O5;MzR^w4jR{ZRJ)o?0HiuPiLc0yZa$KeR)}yO(8Uh_Bz@ z|0p7(4PLmn7KvQQ-am@?^&zX917)!8W}!Xia>lEG zU|^|U3_d@MbgM*qmjj`CAUTHebI_X79Kb?SII$Ovtngva&aed@UWDhrHM|q%>>w5# zj-&eHbS!2Qsod8q6T_B?O~qZn*b#8#Yd|{z0#U@_$tU=Wwb+N2DFK^Og|0KYWX(~= z?HA1+I*u?SM`T<8n7QVADjmqCw_79NK-gdXgYoJ0exrK*dTD#7^}D-|6WbI^XyvQW zT&)rH&mHBToKHqH>JPS-{-iHtw9=I<&ty=Uh*S9Z;b`P?py<(^7NBz7-;g0DV>G2| zb>cpHu-80g``S%KO&17{+jN!q-q)k{R*m~B;Mf=S(*coyS)@s-gS~}p zOnqZHk4FgI+9|ST&Oj+%Q!w8UCQL>GDP-thQ!vtC)%5u^Z`u^%Y0Kh?RSbSkm(-C4 zHi5!n!)s86n<8lajm;!lt?nrCRfEd#pgV~#V2K$CRWOLQC2-S<4VbI=d^3dIS6wr#|)O+gfmEZ3H6yb4x@dkm1b1d|y*F7!q-&>?`EAS!F7F5aXs4i=w zRR{1r0uNd2HL^Q$z$Ka|S4HJ~Lc#Z2TKS!xSOY6I!Er07lFEqbD=*a-2vgONJ;dlz#Q>-`V?P_{NT*CUjj0LL{?y5U_h*@qNjeHy zX-h!wQoBsEU|zkMNa^x@JN0$zk_tek$B-6++`aS3x<2w18*&?LcIbA{six;S^{FID zHq{#r5FY>P#el0J)2Nt~-7#wTdBl`Op9M%@$;)P;{q9{a7XPOH=|H|JA#PK&tXJ3) zxcjm_tUt;}WW-1VJ)^d3g&_vv? zK&TdxJ2uikFcWA?@&Zp@Ukk8Y5LAjjAmd}?oD!2$K`GBMhVY09UxJZ?magx#LGx@C z;66Z{(>@QS)R(~{yH!Y^uXviUQUrwy+#YPd(p!I_T(U(bljvw^AFc z7HxKYb7!=t*44?S9B#QS@?4AZYNj(lFMV9!h#A<#e?Ws3#YjR5t7gc=IFYMGV$?)fT2SaD`DPf!vyzG*K7xJsQ*XTJ4a{MeCyh=ZQHi-#I`%O zZQEAIPP$_!9d&HmwrzfS_daLu_xFu){$HaOYSyY6*SzPv6PC|-0?3G$-@rp?X7M@n z(rx?ux!Mb`yjruBpL(hHfTW9lVSSBHp#uOHr=gu6-iEWoRQ;Y!(@xTQFlc%t)Nb)J z^7f=$E(n~HG$C=>5$@uriOD`_c{S4FPKmA`_DDEhQnWDjb?&^pkJ$B*44>&!PxN}OD9g~T?3kAuqB=t%pT|G13d6S+Z%*#A~bcHXI za^aFLCp1Dk!~y3$9Ci5QdWG!|c(XvXNOIRRUX57DD3rm+fkt4z>(UHbCn;ctEZlb$dy5UmpB6@V|t zy4yWMZ?WgxaXx=n1LU`P@b5$+6K&h1;lY<8&U%OUpIR-H>U~Ed@J#gg1C^m0xBB>E zIS*|u_zVMSQWyPBxrdvk&dT?2VOCVgE)(lo_I4Xl{6pyp!^jE06J{REyF_FH&(eRB z{o*bm@73kHr!DT^A_}<=@YFhsl<8Op#qsfeQL$FGS5G@eqHn0_dpOO4li41;V{e;4V zV;`>M4^Xor>4$xja+MZxN*>hGO@d#VvsMyTMgk?^h*hQ>ue=?AN0c9kX{I;)a7=O@`?9eKcQSVrO`x)Kv}NzZD@|g@gaS zSg&rbBF1oTf5Z=bn=;?)f{caFI`Lg;gQYb#H_%H6Wlp`lK0}mnoUyr#*ktyw@Z)UJ z^yoJ_&^`aPN(z;HRM*!TmzE>fhs}B(2_sr_Y`~`Qr?>O5B~flh=80$b1u;H@M>Aop z&iqB--rtEXjM{kBT3=+XXU?`aKy_c2_@TmeS|j0&pCl|*){5P?WW+ZY32*5WdTNko zGA89sK0KF9bhkW>C~bf7BBr4v>%w67mbrB96VCDg7HKGn2UW@k%)Kj2#+AgFC-{cb zu$t)-USsu{QT9VVEf-{`ftN_ZALq1ybH(P2!~?v~a8oc}`vGy7a6 zlKE$`*pmnxA;4NWOZI6Hxb#KM0g=c%0k?qC8_CzPQ*BB!IdTHs&EwYvKuHgVCOcyz zw*D%c{bJjbjwt_DC>{x&WA>2{wR~|luZ_VZE=|nkSg3jJsbn7qa#MVi zU0}r7csXg~$__qGnBzH?{+jh(7s@CsjN^?o!lYhX6skFk14BF?1_y^G+*ZAd&%-50 zDIsC_Oyg1C96Q+lt&CwxPs|Byxgh?(hvf&}u{ zHjrq#4VWZXgCC^d6vwE{2Kww|n4y+W+oN|8 zxbwK3K+0?9uoJUF9kDa0m2$W(vnp2sX1}RhNZ{_^rUpBUqO0?b8C8^$I$Z%-H<#9SFvIk{UP!%;Li0}s9;flkSCCUGw+ zS=lQTnnU&mA_ZJ&+}dEnuV?f!AqO84HYB#R_Lv%c9aIPD&rFV3>L1Vhq*=8|#GmeQ z2;+O;C9$QGn|CAgYUYi9^mqGtd?-4M_s1@95m-io5!%6pbrPKk zYH^CUtg&jD{8)lm4UCYU!|6seUX$r;Lo9DI*yaq)*~MLr4-J{Xs%4fj>;x-5lca72 z*1RJ!>U8ch@AhaU`qzg+kp`BVrBrP3W*Qb2H-i4Kz+~oyaB($vbiq$2h*k>5YM^-F zq8orV_FR^HkDF78h@UvZq&Wr&X?WZ@NuQ${%#a8oyqGil6-@kPx8P#d+6K_%Oy5m+ zL>RXJQi}zgc2-=DS!Ys7CF--02H2;4r38NPiVl!W9HsGZ@OHEwY9L2LX$P(eTdg;* z>hHNdy0x*mok})hMGpVIxoITK&?S0^d@h;&XpLDAG#ABLC0M@wqEEwW*ZRusYn#WxBGs}pczj7IDum{bf z^vdlb@k$Bn-?ln}YBS4RG0;kwrainzbWtOu;idJdLAK~%Z3g7~R;Uu`=*x-M8myEr^m1_<7lF2WRtq0VD=Btcb%*-;&R$zim(ru zTKfJ#=qB~Qz~h7;9feRype2DF-S4O%X-}hP%@Qi725YPtjqd3;5?dV^o6%}( z7MsXk9|!xKvwG(BXbbUkPn5FtqfnCTD(rp*N7KIRWWN3^E%^0pocEClp;FLo`Thio ztgy+8#SAY=OBW<`8HY%x9gi44=Vb{am`xJC{UWAxZ4-0m4%Jfy>%BX#VKz>REQZNZ z-J8cMv6pey-uGL}Cy2nZy?J}#L1W@on%|IMh5~V4$TAk9={r_jH^E5?8I$gV&EkUK zpdvsM|MupevX|dU3Z6nl%{%u>vYWGj-@d(Ep-pt;PZAU}M6W;xH%HBH^n-Q*1Uiv- zySe+bCx{Mo_aca%GxxRTCZCUWc!ymCjKfY;F?{j?GjO%cljp8_A~`y&;VB zAjlfjMin8MyL-!WkQAmuMYyo5DBq#W1yD!Qw|=Hh|1x9G$tW{KE*d^pyKlAZk5!V< zGDep~p&uq;cFjCDIVMHn0xbmk6@@ND5b!#Y6YdUy*HfXNCtBWol4PnCo*j)@7ltoq z{)Z-f8z)B0o!r0(paK@EHlRBu3Op2d$rn_#bV=n$@%kh*4x|Dh3Ku)%?0$OPn?}vF zG+SyH5R-Hle)BF94xd(71RDg(w|#X2$0@D(omlU2T@g8lYUiWO;H;#HWX&M#TkYxG7q{#oD%1Er0SCkHw!3 zMVZsMV#dzRbQJl$C7O>hqf8IlN^splo6n2t{k4&s;?<|}#fpFN5GS6%IyK<>hV}a0 zqlN<8_6c}t$9@3!8pIQ(f`vw0VXJ6KPd?CHm2jP-9TDP{O*Dyx^KV+6)dO*5m!{qp zLUhF2uw(Ld+D&I|n7>;LPFUL4en^@iZ9XGD!Pl%t}T?o4ZCQiXK-v z!M1*fipWEiZ*aBw&-^I-@6!mWwu?iqVl0_iNtPz-52?B41c`QFXB4Id0$lo}>i6qM zh{`F>!f83{x>ITQ8=_pV&wMpca5*pnA1^kDre~j$8v<7FujpU-`FZ$*BP>M%v*BD{ z>y90FhmIAiHkpw+_!0xgBZXX|nth&F{O14yw!GghWJk^RLu=aAh07*>NzT35s+I4N z2u0>L0;XrQO;^mBBFOK_+K8PPd2Wa7RxAq2m7pZK6A){Bc~wg^O{my62t+gP?HdYp zO)?L+un0J%VdLR0%+SJqUUR&T;^ehUx)hO;64GtbQQ3Yb@Z1>J!Cr@ZXaO^QuZOH_ z89mPVrdHO)P9@g;TdaNJ8YRJ(y-KBBK3K{IXsWX86;G~JjB-UUwBkzB0E!1{YIRwV z-T>nnviZ;x#nc0#T_NbIJejIYl>l#1eZCh}{k{lv-PVW)UU@?aX@MyEQo!;$+ba=| z*Q@dHwx%pDLD+P2gs{1D(S;A-KprL16HCq>;JE5px$1+rHkFEX^2picBXkgP-GVhv zfEg|HiQwZt2a=LH5~=Ap!gd+8jS_e^q{n*Le688uN*;#LO`2T2y8_P*U&^9hVZDKQ z)j!k~siXqgzdQ2VxPWoAJ97c@#@k6rMQ@n?XXy<>PfA%ZGwWnuXBAtL?7d`q$R897 z)_N}7HR|;3Q&O$r20Q;HqqTJ84s%U1GuhMlo5bP)LvGA28K_1c4uqF9VrC;nA&8I_ z|6}$`MNcr7TOrfjB*{^ca1r3vT{7u_%<=@y2o*w-1-c|jB$2rvkt<9im&8a;HA#)qJ(!`c z0g?TgVFgWXIK3T zoF0Ct!!Kb!s)CXmNcq-vwl5CC4D>O%*k$roij`q8@!{=_E_=PUfAStPUB(uRC;?;t zO3wAxa|D+jYl7f|x9Sy@EybUA;MkUWz3jY%x^)X0WH3~c=$pWz!<{P8d2jAvtIg|? znP~xzu>VWGFesj7j)}x&Mk9b@?5DqDohoSO=Ywpg-lEwat@5%nD1o^n-VWQQ9xzIZ z)nJzqtJU;JGPaDY3&~PTY?z@OAO&sK~c0V}{v!2SpV;N=|bR&$i^#U%Wm!YT3Z-L6ezt z>gE8^TL;xDkm7Ywqb>D75+7KC&&B&zkt1ZC4l1Wd$>VtYqLIx;#xI18mCQkzgOcg0 zuW2NDU3AVx3JOtlJb2%Cx`-2V;s&a{MIhk}&201y)1+LVM9+v^(+ zJeaicXjJOsBhu(`EDHo!iEcRH*Xp*HXQH3@W`a#8BScml6*U6X)F#a7n90rg%!k;hcjY0^56%pc%geZ8!fU( zU}WDuYR$CF`NI@rfXN{5%JkFE3o^!>e+dq&qW~4GL@_{USjfd>#Gy z@e^SI-AQCkwi~X8z?r4sHO?po?+0l3+1HPvpQfg~IR2P5AJP}-Bg67WJ#49Lz$Zpm zBkX_|?0-pnFQ5Y_ztgg>3R1#V{ILlo%?UU^vDTBvk%yxcXs(pU(?kQo2KdK_5i_8B zCe~1RFs>pCW)vmv!&Ce+ji#56o8Uv<<^h`!72-05if{@PRy)sBTH_#3sTTjI)BLK+p7?b>arh(Kg@!7U4M z7(a@%gbJNQ2+^CT;+f2s`2V=^54^zdoS}gg`z&LR;HQ`-%5oU)NUUgy!eQY zq&8~gg|!CV=v(xo1{^I!sXcGLY?$>5m|LEK#Qk+GGv-l4Gwbb#uj` zB&qt{+GB9#2OP8HG=}K>I!Dt$3T-2w)YF!Mu>aqSXNu>4FM=v-B)rjKavIVQG?Fkf zry%l_G-Q2|Q%PN-5z*|^Ie8J1V5?QSD;5r-CXovAo8YkZ(^G|uz^I^REli&zqf(FL z3NuR1;Q3*uFDux_JI1;oq}fT}uXe)Oj-ef7VCGpt-V}-q$JJ?9qVoKAhDMn?-D8(kMV#^JG>#NKi&V z>QmGDo|U^pnoT|z&*&AKO_;k=2#_@6*6Brt^h~NUEJ8f-d(sL~bu7@dmz-SlD z&`Ugf9(2J?Dk}qE5!t#A3#mjI_FlW}*NxM^svVAbzV{-~!n^1Y2RBS)b(<~CjQ(Sq zg35U$%)Z(I>ne9L({lR_WeXV{1rA7()@@V0jFeS;`wjVJecXl~hf%rUY**}S_!4M? zP@JD}Wa2zVx@ajh=})2xvJthf#;S_=aSd1D+ozVfB_r0U#3+;($@!P>oIQ80Xk$XQbmCRc@~CjXuzw9U&~Pd`NNe)<7Ne>v<5U z`Pj(f-svoi-N2Tgh?(W0oG!oJb`l903JcAuRGx4Gzcv>^?cf>E98x}<-nLPRg*={1+Yp4+;DRGAGMm+sTaGn*2BGm zo)I0X7<6Kg6{B0)$Y2N)Qu;bV@5_uRiY0G^wndi7lE6?$&u2F?<-6Y04qE-Xal6&$29YD` zHWE0Ilw%X59CE>m^l)?GK<3(8a(Vws!*K0S2iOcyQG-EOmko z0KGs~Y-D|I% z5dAxf{i6%&7#w~;^C*(^yT-2294^<(2Fi%ewr$LG`R2mP$1#&E;a>md#Mi^NidQcO z8K^)#iNxFBmC)R%dXJn=*A?zfMMm)uKfH?{D?Z|fxLCFDY2@5;c6!+UJ;{FRiG_TH zRPS!&<%S58lLuQE1TRMisoa#lKyD0t_{mF*5n4jJ^2K>jegY93HcGTqU9Ll8%UQL^ zFsOr=u7H1*ltB3X}*AA_<mqS>;s*0Yu2u_kBMS9f{LT~U30Izq z{46B;dPfJ+MmwfiT&yUk6NW`@BRKYQBR{tH)IL#zChW=#IPpd=lbbAI%vp8M%)!`= zd=f@^Zr{z-ns;wshr%Iv^LCSsx&N6lHIe7TKZ-D|9Ce$WK)IGDAXnj9@m;c==cv4! zL1N{s>I?T5&yEKY-!7`Mpx3>Zw>RSPxK_O-GwaQgRgUW}yg&}o{Dl)FTL760u#&@g zCs5?c3{?u2PVQ zSyG?-jI?|~Unr0=1Kl?eEK`d>6<;y#{w6Z$o7-?R5UIn=%w)7YQU6P-`x8u}+8s5A z2yA+y_LqDbG7K7$RgYb$MjVhJScv<_&{G$Pa&-GhXlfeHY-_yv1<@|m$a{Y6o97G| zIIKC$C!HgZ)NL^TzWyrF*(i38BPh?BqWV`ktUyXTm@xeb6n(XXNcgUco*_cK0f``7 z6uYdzHE$lBtlhU8a@rjYj)dSk4G}P&oGS+v^TPE7b;7q; zq{z;Wqa*Kr@1ATDI27Kx3}`L4nUP}n(~}J6WSc3XgT$5+YTouc+%q5Qj)+(| zAMx~UbOSga8MMxEyGD>LaKV1$pp-e?0>}r99r&7p)ftO6fOy}gFa+2yBF+#i!@Zu_ zpag{6elB+sq($E;-fVP@TzesUZkKHMWcbRpxfvlzmWZmVsv-4kuP|Ya+fi6ThOkOa zCvC`D8x9>7=c<;u2zHrAAQxV8mMdZYWnosn9r1+-hxq}TS3Hi@pqvg_36ms%@h09K zs9N_=z_5X^th}HfnCwkol4|;qb%nhc;124ge}jv0g>1Sccb-Kgl8(3|@ZFN%Z$!f; zUZcayo!DQzvK)Ri`Z`@6G-fz0N!C?M9^i(D7kAU%A2b~u4Nz;HYk`+C6hu7}icIxx z_Jdh(7CDNLMl(gI%rkeZ-sDLkPSg7%5of*xxQwd315Ieysd`YZWNNrxm+VFY_ooKy zH(04hepT{rhsUTvt?rXU`4e4xp;ar-Ck=n0ez3sYKp6fG8N;ksl8z48MvjUUsbMBO z*P5BZ_Z_0xeyBSiVEV+ekx3 zt}CRqMb$DZKl~!cTNlMlh(P?@SG32x7+~#+Z}kKu+vdnIn7a{f-br0r@r={!`sc?} z@ydtohFRvKxU+9KzVqJ^_mM^pzX4+iV*Mqa>4k$?{#>eMGzHKRGjP?bO{bNEEN zmv9%t<4xPotlIpGfvwQWMVPwPfna&Ya*URO(&AG;<+5wsX+gWzCVE~;G9|V z6)%tiDFH_1dKH#B_6TJj>XH);aUxpS;ctnSsOr}x6q}(yDOW3)6gBw%LtI%|h;f3w z$W>dwB{T8$if#y=FGig1G;sl;n((Q<)QBgPcV@qA+Z>bc@o;-0nNl07KuEL^M8FM!wdmJ5IXKIcBSsq2={S4&x^m3`6qH zn$8E!_H)@=O4{dk4_LE)j!x-?_+9ZFx3z3Y@Yk+L zG(rpOQWQCVT7Rtx_$zT*7|;#hq==PqB44YwaL!> zi)=QZFCL-Is`d3dSt&pBNi%f|x3tQ}(xHI4p1;UaGmtdVyeX$ZjRwa-#hB*R(4ak2 zD*dREQnCyKwFEJ|YM);1{qO`OZflz|I2Si|+4{mp4Y#FT`}mmUjWucIMdjFSyNP+h zyJK8H?>~pz-qovf%#ix$lWQIk(TuozubL_{H+KbzQ;y92J~HqXc*k|0vHskRrbc1l z;mhINGC&H<8LzL%0!9K07|Enf-;#SD?(5O85E}T`B?hW$ov+qNl5p%KesF9 zW5t(=v-b1u^&ZWqjuaL>#s7L=2y5peDA3XGUI)WP^{TT{ARSZni(6z#l7@bvyes`d zjY1=|Ub<496_p(d3Xv5L8d`*kQl(WZ{D@=UXfHl|I-HN>p~lk9Ighns^HZjmlw=;; z$cp-2qk7lHlr2nqxo=vJC-_2Et^!*MA3GFT@2FOw%PVlvoM0eJ(0e-rd}G^o@_q!J z>+?T1?W_T|?E~lzQ5uHOP%(Z*XOPPaTbT~K zy^?(eL!;H-Vn6aWPi2?8S1(|e6;QR0N9<`$A;q`2NjUEb4Ff7@&uz^vmCv}n8XSLNjTo>O()(y?Dx2;oUa_7X#myX20jX*SIVsFOPFqQTu%`*He8#0=#{FYP?!fNQ>dDsb`G zV0|)m;Q zgSBgd%j`rc8=<^@nB-GtMBQHF1YxceEDoEiJc6UQiu4!lm&J>#8_}+<+GY{(GlzA; zjO$K`uv%a)BZFL9x)_KzpNAq{cXGAVR!8QJBJa&$MCMa90LJbekYKu@N*P z1?hsv>i2b`*2K}&RG5|d^thCabwcS}zx|tWmz}h`dc~Iue_~s0X!Xi6UkdR0R+?CBvCW#oRJzQx?|qCP3=HxF-bW5lvDlT$*#jZ&KOw;IfLO>_y7dwJ{jxy=yqOk zO*jgXdt@y96BtyS^bSY4wlfM@vMXf?gq)tujrQx^C}lMDgfoK#Ts=%oFo?IJt9xpX z+Pp3H5vA{SXw|g)JFch2d4xVa&kLi%J-cu5N@Lr+SAOp_E~4}@-C;wu<4-;euAp|L z^MoNlGk9S?eY-j|T{;E8Tvl)?FFkN0<0&~sE8qBh5dPA2U-4MpCH_%26kR@P*EJ#R;~m6 zp^{n|-{*p2NvjCD69r14u2X9Z+6rAwb#v82{YBRmUaX8@v))9X$Gd)HMVP=a(54mj z;BDz@$-Fbm4UvAykf)32_G1fm6ntdne8Er2a9gR^0KK}FPz3E{z-9?VEP-&itpdkU z>)>ENO}3@G|M|rqgUW$lNsYurcn$W)q1tp8M`~u8A*IhL;a-S!MHlhaq&P&>MMPP$rUT+_1iATd z&`#if7iFKo5<-Cgi%t`MO3*k8ZPdWqFg6Dw0t)u1^wnVj2o|1dVGqj3i#U;1_i+}WHwK_4cqibH2mFLUU_ z(8r!{-uye6|Ic%f^M;onsCbWKq(5J&1rIOlD-Xeb2v?-C# zPu+TO=wK|9bMqO!V;hfLxINb)!~~%i+yqH>SqTP&R5tIr0`eS+iYO`@T}`&^jt|AA zS8r8hV#Wg#Dx7s?$J+GMmVQl8U>0VQgS-wHG9?{M(@_5~vx<@Hrj{$NlvpjO#2}}e ze=%m-vBAN8u`5-LS|yn16T%wAAB>n~VS5=!})v zBv`TOl-I)FhmfcFZ6m`>0rydif<$k0LU?7)UD$}hjTb~>hz;%7AgLybQ0lhM(=@s< z>xTPB*Ckc9BcY2Puq~H#ht)TO&Tk+hvfx7!^rFJgDUs+P{N|(pZIbCjIH-cB^vNVoRsa|xN=uPk=3qH9mPM>Aw*LK;CmPamD+@*t*2@gaP&1H|BK>* z&yX!J;h^Re7%vFIgUZ}{%vZ7iQh~d&P^6ln!R?AKn}R#{{er|E9@_EB&Q3{sv2IV| z#~MDMkz&nu^4RYg@}WRk!LO_%-5F>o!Ay=3w^AZk%02niRJh!E9hX1qG1NNc^+^6j zzVU#ykt_(|pqex4ob%;*Ve{k!^P`F}q$zbvFlG^MThw&meh*2Zpb(U}pt~MO z72A>9>LYS<{w}eYXlr|xk`NB4E2Qs>i=x{$Sclrz?EE89*W$RJo)FDX~ z%WF=U4{3>APrj7|v;FB;5g8XsvhjMQo3Ozg-);u)w}8d?p)yd7s7AaMmM~nzk0Be; zCO~v>RrD&Ao`s5F3#awI7uqDu z4L;x7nq9cLCJ09Wokmm6T!%F z)N5NRNZ|xgJQvqEi*M~dMiXt5<2}TBUBE?94auQI=5PS*f#E1bwiI~l9G}RuxK%*F z0)v!sQq3au3lP*M@Hz)UHb%qOpJP*Eib~53$F;spp7m42C+~cFU?jxba-O;kS5+&&=V&lO)Y}OTqej=EkX;9nF0sEr3~X--YaqOV39TI?a?*w$qx!4uP(J z6Q8p7K^0>bhDK3J=)J@DW7-`+X9kZLW)C`-Rw=J-gWyJ6ny8#91=t5k%Kj^s#JYk% zcbO523mRSPKBiPD=!kwY&^S|G5RksJlg?E~I0$BKA(octFM!=;Cm3$55v3%H!l}V% z-~SNb_s(2!tSvajZF zWwgVk^23&K)8mfF1k#_b!yV-pxXvCG7$iI1%KT57#7=Ws0zs8PVB^=_+%uhglVIuv(c^*j~|)4Sz<<`Ew9G3<$@eR=dmU&h9Rrbj~*(2 zceo2eeqK|B7mdX7x}ATStQmQJcY#qCbr076!I3Q<-+%FEvR5}Gwwet0EX}^}c0#+( zzFpAH%Y)^Y#}bh7ah|Vlu+p3IF~2XDM({{OxK5TJdgyYO8>9ehmS7$0(O&XNtLEn= zPvztDYMJQroK(<=Ro!aPw?%k}}1w}PeZr%@v=J?TI_!>5kQ zJe#tmw3LF=J$Ir+!`itb1(l8jgE!)7s{(2&AX`1hoA3{dTNE5X05h%^Ont`RXED`B z!hwu<2#4K2u}DNlzWs7b#s=VP%x*#9$;`+_K^mg=d_LiEZkh4BPMg@3+%t$XH%)IB zC=B|;;?(rnK@wCH2!98CYH>s*-0$(T0+xnw7RMGZn=yrC#0#peRu?#tlAOr@FHAhfF}2h)JkwtbrB!s{1Antl zM~t{k_c(*2i(>oWliANc5UvAK;56r9qY%~84VT)_JzK=B`CtaI6BnW7@iv_A07E;d z?46zeAB=}{P)6Gqg<8f0Uh@|)$(AWyNNKjgiwY|oLxZy6uwlDYv{jem;ke@XzSmTt z>9Iv7Tgn&a@8^bRnS?vI&H?WqVHY4&D1hPl)I;ljAH)n_?e;Vjf@@xsW{7~kz5a_| zbIBX#*IRv?wmHJ0G{|X>upAQ+QS$vj1EF3QF`lb1RnlPSV0`xmw$}4fuJV2keRt{G zp`p+<`}n-wXS(AZQufyp1K#4{?sS>5EWv?#AAn~1Purtq;w4t>QeFg0j-Y%l7BeL< zxE7oLtWAdCgf8nT@vyy%Fqo`H%j3KdNwtg6&amepsE7Z%LJQuqtes5iA813}#j~b_ zr4NLYZ9j`#4~)zZ=m6u5TLWC0eQOPAnb6J_wEEo6`Ixbt?@SRBi0qg!Ax`A_Tdssy zSE9+F%Y|`?gIY(+WLQAHgrgTuGsUmn`BY&ObhZ3!zK4Z)P#J4%XF~apnp|BHuc_=f zO1wWiAG+q(dH&hVM5J6?q;z~p%t}JXYqVgFJ(IusnJ+Xcb4Npa&KfyD^*cZ5J8fX* zZe8$N$%s4CxYLM9%KhEJGPmr?`5LyWDDqFZhy0dyazb~0`LfVXIjq>|SRQ#y8_;pJ z5eVuQ+JD>l#^!xR#5TE3w^unnh)$Inz#XGTA_wAdu|2m%N1S6X0~r?tT3?vQc9x(m z;Z--eHoocuZ+;B!b}5xT-v{GszD_=AO4};F4D6P9Ly|T(=U}Xnll;=>>Yy_sA}W?Z z@@d8-lE{m1uoPu|PJ$}f7#9LgO#F1HTw(;Cl#iKV4BbADKL&!$QL*0ch0udlwSBWEcM%6$Wdv2`ouW3AnDm|MP2+oSxmcMd z;UKQ0!5Eew9SwUwPo7rD5SmTs0$kty@f_FcK=eH|ojs_^7q<1w7VJj0Kmiu_K4~8! zy1QA*KU)cBC6pr>WSu8bbn>D6y!tIeMd2Cbk5_iQ&WfP4@T$?I6~o8Bu+iow%=_V( z{ChWNg#Ax3jfw6^Z`oZ2m**>&z;a{SIedhnlDE;Ut%`#)+MN0CGaN5zbQU1S%{9y( zHzp(mt2cPgOzS=ec&Itnc1JbOMBDDzbnyXo43j-gX&(8c#uJ+#1S?P$N@BC|$r3A$ zFk31FP5SnHo?k?wOEAA>v*va+q<1tNA801i5~6)pm5`S?xdcI_GJf=nBh5`qqC^l_ zu*_r|A1wZQWP!9^Q(CL`^4DrV+pu{Tx6P35(C5(D^q4DUozP~@r1|-^d4p{61P2wY z`G_dKS;Q!Egb6c+u8-Ye+>~M{)EH^i!mD)68%6b7hNh#quEbA zV|d15kbxTTeHaAl=^- zaNR@l^Rd%lPa*iqShBFh2#dy`j*i9sW&srt4Xya3UKEpq)cjFWB$2SUIm1$>gyZTma+66V&yIp8r1`9;W9+egS(7R@ZyXg=susH9F=eHlz7-I z2vd_VbseY*yx!(Xb4nP5hCA+={+QJyc@z4nj7IGXOcf^X^Oi>jkjGgU*5a^}{CY0! zLir-!aEX3o0yNZJ5SX8tbaU%3BHH11r=DaXoFsjqmj?~vDn0OU^W0#b>iCD;`sLl< zzl%y*$RMB;bP<5rFJ94O^PtXNE0B=`S#}Ve9kt0;Ebednr!;O0 z+8say$-sB2J)W%MrC(um1X5#4tQCrE0QPWhqeGug*lCQ1`9_LZaG97+&>VhsI9vMD z-=e4g<#Qm=zU0cu3fpHno{jI&*B7{fSbdmblm9RL2Wu&<>`5GVE)ZewW>To=1~_lD z^l&P!1FrMDTAN{Cu~`nK^`&w`uui`EQJ*quzTte_R z;Grjw-s9edjG)CH4Wd1xDP~bfZk71`CD>-&(U&g_f(r{vy|o+)a_OS`HzaI6rE7s- zgSfF#6*zBJe2yDJv*Q++L<_df2pL*lSBy$Y;PDYjkuq)HOhHG6b=XS!-I#nNAlwju z^~xgV#*pa_Ndr0zzp;kaNBTtLIc&vEqc72e^bi@DV5bFjF+4NP_rRvg?}b7-+kJ`O zq?C_uHM5171ZwHYLJ>k47mx#$-~@vG;YD&Z8Lq^)CtjLqoo8as`@Dyb4-CAsXTIIn zld)WTK;~wz6CLhPo&^edOM<>14-C5J@P&ED(BS&a4*3{zGZ@iw?^dqRJ&Zw zUgWlU-IaqLi0~%{8m*?t>YmKxkpSq5Q!n7`+cok;@}Mjf#n_(ap)v1|FK;6xqA

12Ay3&-%qlZ z$Nr=vi@Txf;+|4%`#~z+QKSyzo24WAKO1^Fk#hm=PXy^h2)Jt&=mL!@{Ev^?ic?Y< z=&sc4PaVV8@3DSzYLY0)fzTe_`IZ86RpRxyjyD6x)W#=F6HE7MV-r1L4pv@1$QQrlTj3S(5sS*2_yaL*?}u{Ic?{;2DDHRP7JR{M*~S98WqD|hS; zpabMucN=XloeSGMW+VCy^}V)d?r)qj-HCjh4kro!tFRq13iAURoph-0OmM1?`v$4Z zWPxg=8R@dpCDJJT*#8H@$E|)o4hLFhIAM_2G196}X_$%w>DM|aq_Cs_d>}ZfO+oXu z5icc(tTO-xPW-ZHD*v5lb#qTSev*5d<30-2jw4{9r@r~A&i3l!%%E=~>mO^Ad7|k8 zY$Opv$=&?e)6jd3&ETu3lIzcVT^J7Oq$msmqd`@Rk%MZxCxK%GyfDdJ13bG<=uBJ; z!1ZHLpDXlqP4O}bdMo`pik^?TP=FLyy064di`1~+Hg!aQ$n`Tpj;V|1e>o3$BU#9P zOyoMfH2j0g)`+HWuoH0B@xBA|_|aSGClj`w{a=V9eqJKWiXV7t!B!aHEd3d<(|_-2 zXtq2s5x)JA*^OaTKOg~{_3%9P#NVgKX)F*LqvDFf@Bc{;rIG*-FwKl)RZSh_i~WCy z7J0Bha>9Vi26UI`Q8e^@aWAzwd#?n)LK3pzb!n-E)GEx;MY>$DL-KwqRjot)b<=s) zAA@3YrA>{db-L1unFWs?-e4=c(wZE-`Y$(^|LehVj0WYx0}&D%6}A5GQ!Y1f%y_VG z?<{`j^X>`B+4+YKeTADiQ<-Gb9j)DmQZ)3Jbrg^>i)a(5^shW+z<4p-gK3jy2VUB*=HF20A)miD=gf{i-?-VmWNX!iqTO8GI zrXdXiO%V82Qqwx)Z~4Uk^lAPcNe$_E9ZqtBYIK7n;j8zIsx1j7o>3rV=rpfjNg1fR znq{;j&o&Oom~Xz{LUr&_XtIG(1a}2BZhf+7VkngJ-@X-#68SPrL`}7)B7KQZ<1$oj zcM1+)hbk>KAMjMBHzHiL%F)PQ6?7mFk&vzC41@wW-0qBF>zuiesHzx7dN}`rhWK!S z3sb(dhJy z9MC5Y3EHPn|1}a`V08>2%@|+>nS~te1R#PT{A7)~>Of?$R_}gWn<6qR{*$>>x!U~w zPlNERK;rRC3OZz``^8 zz_CDnhp!!4F2YsF*7777v6#T!BMfANaqv5fk_&>!psXv=mwwBHb$&Qc?ma_f7QcU- zb^cGk>_CPXAag(rL>_V?I%$o_OGI7RGx2vjm4G#SnS#-gFj1u`&s``i((YmltEe3% zJ~xtEyDmr;&qfk_!9BY7?Ef9aE9&4tI z>jc!FQ3&7O9Bfwdybs?Z{5$M0pd29jrRU*KIf|ERaCr&1UGnQeGF2$IUtZoED9pf2 zOQxL}f2bF(d^TW{;UgfFMqS3f6rThnJL~qf7OLT+z5knR#|6m!GbY7%LqygQ z$#(;jt_SG*ejlzp0q~`VitTz|)cF-H6IPPDXA*ITrYDs_#Zt*x;@0mn$dt6nHi84P zm!uJ)Kq2y)zwQBl;{W|s0gyu91*tG_DN%5V6}#bBaUGO-p;0<7ZReX1X%lDp+*u%X z2?JO=I|%3jKk7)Z`g&9=wvQu5FZmUPl&k6;la*GA(t3*2Y{hhDm7>AE{@W?)-(R;7 zF3z+2Wr+lo^9HlR_8yPBY?OYiLoE$+qf*qK(i-6&=V2#+<~>D)fRa_cRr`7(~~Tl8X!ZSAmVVUH)(aD-94WuA zmjpaCy7r&Wl~z^;RwtEYA?DyilESAq7@Ik%g=z&ZZoGGu_tu5H6)AD2-Fh4|&r5W~ z*$H%Tn)t=6Jte_ghkB}fDbO;H*Nd^*n^$?mkL&$dOm8n;DNL*f!6vP|YaPB7Stpx%>^ShjnuGqNZ zm2iGR_iFXy3<5{378eF@TP*kG4;zXX`y8Yd4oFTgW7xY=%T;NSYtheD>Fy2+tJ-+X zZ4M;?XLDHF@5HRQq~|!dO7PX@lLa=DncDx@G%Qh5GSpZ#>xN2?<3y{5?1bji(P~E* zd^>d{<%Nf9*Gj*awSNPz>M~gswXWuU%@$k1Vkmbo#o}FxAshSj923*cKi;F%PPgQM z8GFlLiHN&Q&DIfJht_iQ8Ols`cAsVI&BL|xKP#)pl?ih@J?`z8am8h7?No_rrvsio zbC^4uxBZim!}9hu*CqcstzY_Fh2u@6^QynuVSBDlKf-n4ztCj26&YQI!0R%OCzwfA zW$nLqoxiHfWW927?aB6Yk@X&>UeXdgH*X(TII3XScJ!9c{jD>m-Ur4&vtoYxl*GsDLmw2(G&TEg|!#_{9`0G+DIA24%a4tS~UJO3oBMUi%Eyr9gu`Sk@#t*hOvQe)FIcIhVuk zV7k%Vz{$KwjCGUOGpzC&?G1+W6Av}`NL{MUj4Jc&|6b?&N4+xtY1Z#kiRZjgYFAJp zUt-B}V&lp^6>)$58`>@2_F0XGWB1SYm$h+;?zwFLrekx0M(ct9%v>3!)lBmPWf*|K M)78&qol`;+0N~BM(EtDd