From c4164f26814ddc900de178f5cd95a5659909afe4 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sat, 29 May 2021 13:45:33 +0300 Subject: [PATCH] Merge legacy dataforge and migrate to git --- .gitignore | 4 + README.md | 9 + build.gradle | 65 + dataforge-control/build.gradle | 10 + dataforge-control/doc/States.vsdx | Bin 0 -> 29709 bytes .../hep/dataforge/control/ControlUtils.java | 22 + .../control/collectors/PointCollector.java | 100 + .../collectors/RegularPointCollector.java | 117 + .../control/collectors/ValueCollector.java | 36 + .../measurements/AbstractMeasurement.java | 157 + .../control/measurements/Measurement.java | 74 + .../measurements/MeasurementListener.java | 87 + .../measurements/RegularMeasurement.java | 46 + .../measurements/SimpleMeasurement.java | 142 + .../exceptions/ControlException.java | 54 + .../exceptions/MeasurementException.java | 50 + .../MeasurementInterruptedException.java | 50 + .../MeasurementNotReadyException.java | 40 + .../MeasurementTimeoutException.java | 40 + .../dataforge/exceptions/PortException.java | 49 + .../exceptions/PortLockException.java | 27 + .../hep/dataforge/control/DeviceManager.kt | 130 + .../control/connections/DeviceConnection.kt | 29 + .../control/connections/LoaderConnection.kt | 44 + .../dataforge/control/connections/Roles.kt | 17 + .../control/devices/AbstractDevice.kt | 199 + .../hep/dataforge/control/devices/Device.kt | 107 + .../control/devices/DeviceFactory.kt | 31 + .../dataforge/control/devices/DeviceHub.kt | 91 + .../control/devices/DeviceListener.kt | 45 + .../dataforge/control/devices/PortSensor.kt | 208 + .../hep/dataforge/control/devices/Sensor.kt | 259 + .../hep/dataforge/control/devices/Virtual.kt | 51 + .../hep/dataforge/control/ports/ComPort.kt | 162 + .../control/ports/GenericPortController.kt | 329 + .../hep/dataforge/control/ports/Port.kt | 200 + .../dataforge/control/ports/PortFactory.kt | 103 + .../hep/dataforge/control/ports/PortHelper.kt | 147 + .../hep/dataforge/control/ports/TcpPort.kt | 121 + .../dataforge/control/ports/VirtualPort.kt | 127 + .../hep.dataforge.context.PluginFactory | 1 + .../dataforge/control/ports/TcpPortTest.kt | 73 + dataforge-core/build.gradle | 8 + dataforge-core/dataforge-json/build.gradle | 22 + .../kotlin/hep/dataforge/io/JSONMetaReader.kt | 103 + .../kotlin/hep/dataforge/io/JSONMetaType.kt | 38 + .../kotlin/hep/dataforge/io/JSONMetaWriter.kt | 123 + .../hep.dataforge.io.envelopes.MetaType | 1 + .../hep/dataforge/io/JSONMetaTypeTest.kt | 53 + dataforge-core/docs/descriptors.md | 4 + dataforge-core/docs/logging.md | 22 + .../hep/dataforge/actions/ActionEvent.java | 31 + .../actions/ActionEventListener.java | 31 + .../hep/dataforge/actions/ActionManager.java | 141 + .../hep/dataforge/actions/ActionResult.java | 41 + .../actions/ActionStateListener.java | 23 + .../dataforge/actions/GeneratorAction.java | 52 + .../hep/dataforge/actions/GroupBuilder.java | 84 + .../dataforge/actions/ManyToOneAction.java | 170 + .../dataforge/actions/OneToManyAction.java | 67 + .../hep/dataforge/actions/OneToOneAction.java | 150 + .../connections/AutoConnectible.java | 44 + .../dataforge/connections/Connectible.java | 110 + .../hep/dataforge/connections/Connection.java | 66 + .../connections/ConnectionFactory.java | 37 + .../connections/ConnectionHelper.java | 163 + .../hep/dataforge/connections/ConnectsTo.java | 29 + .../connections/NamedValueListener.java | 31 + .../hep/dataforge/connections/RoleDef.java | 63 + .../hep/dataforge/connections/RoleDefs.java | 36 + .../dataforge/connections/ValueListener.java | 31 + .../hep/dataforge/description/NodeDef.java | 69 + .../hep/dataforge/description/NodeDefs.java | 34 + .../dataforge/description/TypedActionDef.java | 39 + .../hep/dataforge/description/ValueDef.java | 52 + .../hep/dataforge/description/ValueDefs.java | 34 + .../main/java/hep/dataforge/events/Event.java | 96 + .../hep/dataforge/events/EventBuilder.java | 97 + .../hep/dataforge/events/EventHandler.java | 25 + .../dataforge/events/EventTransmitter.java | 33 + .../AnonymousNotAlowedException.java | 42 + .../ChainPathNotSupportedException.java | 42 + .../exceptions/ConfigurationException.java | 51 + .../exceptions/ContentException.java | 54 + .../exceptions/ContextLockException.java | 32 + .../exceptions/DataFormatException.java | 42 + .../DescriptionNotFoundException.java | 42 + .../exceptions/DescriptorException.java | 42 + .../DuplicateDescriptionException.java | 45 + .../exceptions/EnvelopeFormatException.java | 46 + .../EnvelopeTargetNotFoundException.java | 57 + .../exceptions/NameNotFoundException.java | 74 + .../dataforge/exceptions/NamingException.java | 42 + .../NonEmptyMetaMorphException.java | 14 + .../exceptions/NotConnectedException.java | 32 + .../exceptions/NotDefinedException.java | 43 + .../exceptions/PathSyntaxException.java | 42 + .../TargetNotProvidedException.java | 48 + .../exceptions/ValueConversionException.java | 53 + .../hep/dataforge/goals/AbstractGoal.java | 148 + .../hep/dataforge/goals/GeneratorGoal.java | 41 + .../main/java/hep/dataforge/goals/Goal.java | 150 + .../hep/dataforge/goals/GoalListener.java | 29 + .../hep/dataforge/goals/MultiInputGoal.java | 181 + .../java/hep/dataforge/goals/PipeGoal.java | 56 + .../java/hep/dataforge/goals/StaticGoal.java | 33 + .../hep/dataforge/io/ColumnedDataReader.java | 86 + .../hep/dataforge/io/ColumnedDataWriter.java | 143 + .../main/java/hep/dataforge/io/IOUtils.java | 390 + .../java/hep/dataforge/io/LineIterator.java | 127 + .../hep/dataforge/io/LogbackConfigurator.java | 45 + .../hep/dataforge/io/MetaStreamReader.java | 87 + .../hep/dataforge/io/MetaStreamWriter.java | 44 + .../java/hep/dataforge/io/XMLMetaReader.java | 139 + .../java/hep/dataforge/io/XMLMetaWriter.java | 117 + .../hep/dataforge/io/history/Chronicle.java | 126 + .../hep/dataforge/io/history/History.java | 43 + .../java/hep/dataforge/io/history/Record.java | 107 + .../dataforge/meta/ConfigChangeListener.java | 57 + .../java/hep/dataforge/meta/Configurable.java | 46 + .../dataforge/meta/ConfigurableMergeRule.java | 100 + .../hep/dataforge/meta/Configuration.java | 205 + .../hep/dataforge/meta/CustomMergeRule.java | 66 + .../meta/DelegateConfigChangeListener.java | 72 + .../java/hep/dataforge/meta/JoinRule.java | 57 + .../hep/dataforge/meta/ListMergeRule.java | 30 + .../java/hep/dataforge/meta/MergeRule.java | 193 + .../main/java/hep/dataforge/meta/Meta.java | 250 + .../java/hep/dataforge/meta/MetaBuilder.java | 258 + .../java/hep/dataforge/meta/MetaNode.java | 251 + .../java/hep/dataforge/meta/MetaParser.java | 63 + .../java/hep/dataforge/meta/MetaProvider.java | 82 + .../java/hep/dataforge/meta/MetaUtils.java | 345 + .../hep/dataforge/meta/MutableMetaNode.java | 582 ++ .../java/hep/dataforge/meta/ReplaceRule.java | 53 + .../java/hep/dataforge/meta/SealedNode.java | 40 + .../dataforge/meta/SimpleConfigurable.java | 151 + .../java/hep/dataforge/meta/Template.java | 106 + .../hep/dataforge/names/AbstractNamedSet.java | 61 + .../dataforge/names/AlphanumComparator.java | 126 + .../dataforge/names/AnonymousNotAlowed.java | 32 + .../hep/dataforge/names/NameSetContainer.java | 32 + .../hep/dataforge/names/NamedMetaHolder.java | 65 + .../java/hep/dataforge/names/NamesUtils.java | 249 + .../java/hep/dataforge/providers/Path.java | 102 + .../hep/dataforge/providers/PathSegment.java | 94 + .../hep/dataforge/providers/Provider.java | 99 + .../hep/dataforge/providers/Providers.java | 119 + .../hep/dataforge/providers/Provides.java | 20 + .../dataforge/providers/ProvidesNames.java | 23 + .../dataforge/providers/SegmentedPath.java | 153 + .../hep/dataforge/states/MetaStateDef.java | 59 + .../hep/dataforge/states/MetaStateDefs.java | 36 + .../java/hep/dataforge/states/StateDef.java | 59 + .../java/hep/dataforge/states/StateDefs.java | 36 + .../java/hep/dataforge/tables/Adapters.java | 180 + .../hep/dataforge/tables/BasicAdapter.java | 65 + .../java/hep/dataforge/tables/Filtering.java | 168 + .../java/hep/dataforge/tables/MaskPoint.java | 70 + .../tables/NavigableValuesSource.java | 35 + .../dataforge/tables/ReadPointSetAction.java | 77 + .../dataforge/tables/SimpleValuesSource.java | 51 + .../hep/dataforge/tables/TableFormat.java | 74 + .../dataforge/tables/TableFormatBuilder.java | 205 + .../tables/TransformTableAction.java | 81 + .../hep/dataforge/tables/ValuesAdapter.java | 77 + .../hep/dataforge/tables/ValuesListener.java | 30 + .../dataforge/tables/VirtualDataPoint.java | 50 + .../dataforge/tables/XYPoissonAdapter.java | 60 + .../hep/dataforge/utils/ArgumentChecker.java | 59 + .../dataforge/utils/ContextMetaFactory.java | 36 + .../hep/dataforge/utils/DateTimeUtils.java | 33 + .../hep/dataforge/utils/GenericBuilder.java | 28 + .../hep/dataforge/utils/InvocationTarget.java | 37 + .../java/hep/dataforge/utils/MetaFactory.java | 17 + .../main/java/hep/dataforge/utils/Misc.java | 45 + .../hep/dataforge/utils/MultiCounter.java | 98 + .../java/hep/dataforge/utils/NamingUtils.java | 120 + .../java/hep/dataforge/utils/Optionals.java | 63 + .../dataforge/utils/ReferenceRegistry.java | 104 + .../kotlin/hep/dataforge/CoreExtensions.kt | 179 + .../src/main/kotlin/hep/dataforge/Misc.kt | 19 + .../src/main/kotlin/hep/dataforge/Named.kt | 57 + .../kotlin/hep/dataforge/NumberExtensions.kt | 56 + .../src/main/kotlin/hep/dataforge/Types.kt | 29 + .../src/main/kotlin/hep/dataforge/Utils.kt | 81 + .../kotlin/hep/dataforge/actions/Action.kt | 41 + .../hep/dataforge/actions/GenericAction.kt | 162 + .../kotlin/hep/dataforge/actions/KAction.kt | 284 + .../kotlin/hep/dataforge/cache/CachePlugin.kt | 208 + .../hep/dataforge/cache/DataCacheException.kt | 47 + .../hep/dataforge/cache/DefaultCache.kt | 300 + .../dataforge/cache/DefaultCacheManager.kt | 114 + .../dataforge/cache/DefaultCachingProvider.kt | 71 + .../dataforge/cache/MetaCacheConfiguration.kt | 42 + .../hep/dataforge/context/BasicPlugin.kt | 65 + .../hep/dataforge/context/Chronicler.kt | 90 + .../kotlin/hep/dataforge/context/Context.kt | 438 ++ .../hep/dataforge/context/ContextAware.kt | 57 + .../hep/dataforge/context/ContextBuilder.kt | 217 + .../hep/dataforge/context/ContextLock.kt | 66 + .../hep/dataforge/context/ExecutorPlugin.kt | 80 + .../hep/dataforge/context/FileReference.kt | 195 + .../kotlin/hep/dataforge/context/Global.kt | 160 + .../kotlin/hep/dataforge/context/Plugin.kt | 97 + .../kotlin/hep/dataforge/context/PluginDef.kt | 28 + .../hep/dataforge/context/PluginLoader.kt | 152 + .../hep/dataforge/context/PluginManager.kt | 192 + .../kotlin/hep/dataforge/context/PluginTag.kt | 128 + .../hep/dataforge/data/CheckedDataNode.kt | 50 + .../hep/dataforge/data/CustomDataFilter.kt | 156 + .../main/kotlin/hep/dataforge/data/Data.kt | 135 + .../kotlin/hep/dataforge/data/DataFactory.kt | 133 + .../kotlin/hep/dataforge/data/DataFilter.kt | 65 + .../kotlin/hep/dataforge/data/DataLoader.kt | 14 + .../kotlin/hep/dataforge/data/DataNode.kt | 351 + .../main/kotlin/hep/dataforge/data/DataSet.kt | 170 + .../kotlin/hep/dataforge/data/DataState.kt | 65 + .../kotlin/hep/dataforge/data/DataTree.kt | 294 + .../kotlin/hep/dataforge/data/DataUtils.kt | 222 + .../hep/dataforge/data/EmptyDataNode.kt | 26 + .../hep/dataforge/data/FileDataFactory.kt | 178 + .../hep/dataforge/data/FilteredDataNode.kt | 46 + .../kotlin/hep/dataforge/data/NamedData.kt | 78 + .../kotlin/hep/dataforge/data/NodeWrapper.kt | 49 + .../hep/dataforge/data/SmartDataLoader.kt | 39 + .../hep/dataforge/data/binary/Binary.kt | 95 + .../dataforge/data/binary/BufferedBinary.kt | 30 + .../hep/dataforge/data/binary/FileBinary.kt | 65 + .../hep/dataforge/data/binary/StreamBinary.kt | 36 + .../dataforge/description/ActionDescriptor.kt | 109 + .../hep/dataforge/description/Described.kt | 38 + .../hep/dataforge/description/Description.kt | 93 + .../description/DescriptorBuilder.kt | 205 + .../hep/dataforge/description/Descriptors.kt | 316 + .../dataforge/description/NodeDescriptor.kt | 190 + .../dataforge/description/ValueDescriptor.kt | 207 + .../main/kotlin/hep/dataforge/goals/Coal.kt | 162 + .../kotlin/hep/dataforge/goals/GoalGroup.kt | 56 + .../hep/dataforge/io/AbstractOutputManager.kt | 85 + .../kotlin/hep/dataforge/io/BufferChannel.kt | 67 + .../hep/dataforge/io/DirectoryOutput.kt | 81 + .../kotlin/hep/dataforge/io/MetaFileReader.kt | 119 + .../kotlin/hep/dataforge/io/OutputManager.kt | 146 + .../dataforge/io/envelopes/BinaryMetaType.kt | 63 + .../io/envelopes/DefaultEnvelopeReader.kt | 152 + .../io/envelopes/DefaultEnvelopeType.kt | 62 + .../io/envelopes/DefaultEnvelopeWriter.kt | 83 + .../hep/dataforge/io/envelopes/Envelope.kt | 134 + .../dataforge/io/envelopes/EnvelopeBuilder.kt | 148 + .../dataforge/io/envelopes/EnvelopeReader.kt | 83 + .../hep/dataforge/io/envelopes/EnvelopeTag.kt | 194 + .../dataforge/io/envelopes/EnvelopeType.kt | 103 + .../dataforge/io/envelopes/EnvelopeWriter.kt | 37 + .../io/envelopes/JavaObjectWrapper.kt | 76 + .../dataforge/io/envelopes/LazyEnvelope.kt | 46 + .../hep/dataforge/io/envelopes/MetaType.kt | 68 + .../dataforge/io/envelopes/SimpleEnvelope.kt | 51 + .../io/envelopes/TaglessEnvelopeType.kt | 216 + .../hep/dataforge/io/envelopes/Wrapper.kt | 71 + .../hep/dataforge/io/envelopes/XMLMetaType.kt | 47 + .../kotlin/hep/dataforge/io/output/Output.kt | 67 + .../hep/dataforge/io/output/StreamConsumer.kt | 35 + .../hep/dataforge/io/output/TextOutput.kt | 283 + .../kotlin/hep/dataforge/meta/KMetaBuilder.kt | 115 + .../kotlin/hep/dataforge/meta/Laminate.kt | 330 + .../hep/dataforge/meta/MetaDelegates.kt | 369 + .../kotlin/hep/dataforge/meta/MetaHolder.kt | 68 + .../kotlin/hep/dataforge/meta/MetaMorph.kt | 160 + .../main/kotlin/hep/dataforge/meta/Metoid.kt | 34 + .../hep/dataforge/names/CompositeName.kt | 134 + .../kotlin/hep/dataforge/names/EmptyName.kt | 76 + .../main/kotlin/hep/dataforge/names/Name.kt | 250 + .../kotlin/hep/dataforge/names/NameList.kt | 166 + .../kotlin/hep/dataforge/names/NameToken.kt | 123 + .../main/kotlin/hep/dataforge/states/State.kt | 397 + .../hep/dataforge/states/StateAnnotations.kt | 75 + .../kotlin/hep/dataforge/states/Stateful.kt | 221 + .../kotlin/hep/dataforge/tables/Column.kt | 69 + .../hep/dataforge/tables/ColumnFormat.kt | 94 + .../hep/dataforge/tables/ColumnTable.kt | 183 + .../kotlin/hep/dataforge/tables/ListColumn.kt | 113 + .../hep/dataforge/tables/ListOfPoints.kt | 82 + .../kotlin/hep/dataforge/tables/ListTable.kt | 196 + .../hep/dataforge/tables/MetaTableFormat.kt | 111 + .../main/kotlin/hep/dataforge/tables/Table.kt | 69 + .../kotlin/hep/dataforge/tables/Tables.kt | 219 + .../hep/dataforge/tables/ValuesParser.java | 17 + .../hep/dataforge/tables/ValuesReader.kt | 140 + .../hep/dataforge/tables/ValuesSource.kt | 40 + .../hep/dataforge/values/AbstractValue.kt | 85 + .../hep/dataforge/values/BinaryValue.kt | 33 + .../hep/dataforge/values/BooleanValue.kt | 97 + .../hep/dataforge/values/LateParseValue.kt | 41 + .../kotlin/hep/dataforge/values/LazyValue.kt | 48 + .../kotlin/hep/dataforge/values/ListValue.kt | 97 + .../hep/dataforge/values/MapValueProvider.kt | 40 + .../kotlin/hep/dataforge/values/NamedValue.kt | 76 + .../hep/dataforge/values/NumberValue.kt | 78 + .../hep/dataforge/values/StringValue.kt | 143 + .../hep/dataforge/values/SubstProvider.kt | 66 + .../kotlin/hep/dataforge/values/TimeValue.kt | 80 + .../main/kotlin/hep/dataforge/values/Value.kt | 314 + .../kotlin/hep/dataforge/values/ValueMap.kt | 218 + .../hep/dataforge/values/ValueProvider.kt | 165 + .../kotlin/hep/dataforge/values/ValueUtils.kt | 354 + .../kotlin/hep/dataforge/values/Values.kt | 97 + .../dataforge/workspace/AbstractWorkspace.kt | 98 + .../dataforge/workspace/ActiveWorkspace.kt | 73 + .../hep/dataforge/workspace/BasicWorkspace.kt | 88 + .../dataforge/workspace/DynamicWorkspace.kt | 80 + .../dataforge/workspace/FileBasedWorkspace.kt | 87 + .../hep/dataforge/workspace/Workspace.kt | 319 + .../dataforge/workspace/WorkspaceParser.kt | 37 + .../dataforge/workspace/tasks/AbstractTask.kt | 93 + .../hep/dataforge/workspace/tasks/KTask.kt | 198 + .../workspace/tasks/MultiStageTask.kt | 142 + .../hep/dataforge/workspace/tasks/PipeTask.kt | 24 + .../workspace/tasks/SingleActionTask.kt | 70 + .../hep/dataforge/workspace/tasks/Task.kt | 76 + .../dataforge/workspace/tasks/TaskModel.kt | 500 ++ .../workspace/templates/GatherTaskTemplate.kt | 44 + .../workspace/templates/TaskTemplate.kt | 10 + .../ch.qos.logback.classic.spi.Configurator | 1 + .../hep.dataforge.context.PluginFactory | 3 + .../services/hep.dataforge.data.DataLoader | 1 + .../hep.dataforge.io.envelopes.EnvelopeType | 2 + .../hep.dataforge.io.envelopes.MetaType | 2 + .../hep.dataforge.io.envelopes.Wrapper | 1 + .../hep.dataforge.workspace.WorkspaceParser | 0 .../dataforge/description/DescriptorsTest.kt | 39 + .../hep/dataforge/goals/AbstractGoalTest.kt | 80 + .../kotlin/hep/dataforge/io/XMLIOTest.groovy | 79 + .../io/envelopes/EnvelopeFormatSpec.groovy | 42 + .../io/envelopes/TaglessEnvelopeTest.kt | 62 + .../kotlin/hep/dataforge/kodex/CoalTest.kt | 69 + .../kotlin/hep/dataforge/kodex/KActionTest.kt | 90 + .../dataforge/kodex/ListAnnotationsTest.kt | 50 + .../kotlin/hep/dataforge/meta/LaminateTest.kt | 61 + .../hep/dataforge/meta/MetaMorphTest.kt | 44 + .../kotlin/hep/dataforge/meta/MetaTest.kt | 77 + .../hep/dataforge/meta/MetaUtilsTest.groovy | 89 + .../meta/MutableAnnotationSpec.groovy | 73 + .../kotlin/hep/dataforge/meta/TemplateTest.kt | 51 + .../kotlin/hep/dataforge/names/NameTest.kt | 56 + .../hep/dataforge/states/StateHolderTest.kt | 55 + .../kotlin/hep/dataforge/states/StateTest.kt | 45 + .../hep/dataforge/tables/TablesKtTest.kt | 30 + .../dataforge/values/ValueUtilsTest.groovy | 68 + .../hep/dataforge/workspace/WorkspaceTest.kt | 117 + .../src/test/resources/meta/template.xml | 13 + .../src/test/resources/meta/templateData.xml | 17 + dataforge-gui/build.gradle | 23 + dataforge-gui/dataforge-html/build.gradle | 6 + .../kotlin/hep/dataforge/io/HTMLOutput.kt | 251 + .../kotlin/hep/dataforge/io/HTMLOutputTest.kt | 39 + dataforge-gui/gui-demo/build.gradle | 21 + .../hep/dataforge/plots/demo/DemoApp.kt | 11 + .../hep/dataforge/plots/demo/DemoView.kt | 112 + .../dataforge/plots/demo/JFreeChartTest.kt | 30 + .../hep/dataforge/plots/demo/JFreeFXTest.java | 79 + .../hep/dataforge/plots/demo/PlotTest.kt | 84 + .../dataforge/plots/demo/SerializationTest.kt | 62 + .../dataforge/plots/demo/TableDisplayTest.kt | 26 + dataforge-gui/gui-workspace/build.gradle | 36 + .../fx/workspace/ContextEditorView.kt | 18 + .../kotlin/hep/dataforge/fx/ClipBoardTools.kt | 2 + .../main/kotlin/hep/dataforge/fx/FXPlugin.kt | 121 + .../main/kotlin/hep/dataforge/fx/KodexFX.kt | 181 + .../main/kotlin/hep/dataforge/fx/StatesFX.kt | 50 + .../hep/dataforge/fx/fragments/LogFragment.kt | 144 + .../hep/dataforge/fx/meta/ConfigEditor.kt | 165 + .../kotlin/hep/dataforge/fx/meta/ConfigFX.kt | 283 + .../dataforge/fx/meta/ConfigValueProperty.kt | 113 + .../hep/dataforge/fx/meta/MetaViewer.kt | 73 + .../hep/dataforge/fx/output/FXDisplay.kt | 78 + .../hep/dataforge/fx/output/FXDumbOutput.kt | 37 + .../dataforge/fx/output/FXOuptutManager.kt | 201 + .../hep/dataforge/fx/output/FXOutput.kt | 129 + .../hep/dataforge/fx/output/FXTextOutput.kt | 216 + .../hep/dataforge/fx/output/FXWebOutput.kt | 72 + .../hep/dataforge/fx/plots/PlotContainer.kt | 303 + .../hep/dataforge/fx/plots/PlotExtensions.kt | 33 + .../hep/dataforge/fx/table/MutableTable.kt | 13 + .../hep/dataforge/fx/table/MutableValues.kt | 43 + .../hep/dataforge/fx/table/TableDisplay.kt | 110 + .../dataforge/fx/test/FXOutputManagerTest.kt | 48 + .../hep/dataforge/fx/test/FXOutputPaneTest.kt | 43 + .../hep/dataforge/fx/test/FXWebViewTest.kt | 64 + .../hep/dataforge/fx/test/MetaEditorTest.kt | 81 + .../hep/dataforge/fx/test/MetaViewerTest.kt | 48 + .../dataforge/fx/values/ColorValueChooser.kt | 39 + .../fx/values/ComboBoxValueChooser.kt | 49 + .../dataforge/fx/values/TextValueChooser.kt | 102 + .../hep/dataforge/fx/values/ValueCallback.kt | 23 + .../hep/dataforge/fx/values/ValueChooser.kt | 89 + .../dataforge/fx/values/ValueChooserBase.kt | 69 + .../hep.dataforge.context.PluginFactory | 1 + dataforge-gui/src/main/resources/img/df.png | Bin 0 -> 54732 bytes .../dataforge/fx/output/FXWebOutputTest.kt | 50 + dataforge-maths/build.gradle | 5 + .../hep/dataforge/maths/DSNumberBenchmark.kt | 33 + .../hep/dataforge/maths/GridCalculator.java | 71 + .../hep/dataforge/maths/Interpolation.java | 91 + .../java/hep/dataforge/maths/MathUtils.java | 87 + .../hep/dataforge/maths/MatrixOperations.java | 151 + .../MultivariateUniformDistribution.java | 97 + .../java/hep/dataforge/maths/NamedMatrix.java | 187 + .../java/hep/dataforge/maths/NamedVector.java | 184 + .../hep/dataforge/maths/domains/Domain.java | 92 + .../maths/domains/HyperSquareDomain.java | 150 + .../dataforge/maths/domains/RangeDomain.java | 36 + .../maths/domains/UnconstrainedDomain.java | 111 + .../maths/domains/UnivariateDomain.java | 48 + .../maths/functions/FunctionCaching.java | 56 + .../maths/functions/FunctionFactories.java | 43 + .../maths/functions/MultiFactory.java | 37 + .../maths/functions/MultiFunction.java | 46 + .../maths/functions/UniFunction.java | 32 + .../functions/UnivariateSplineWrapper.java | 76 + .../hep/dataforge/maths/histogram/Bin.java | 49 + .../dataforge/maths/histogram/BinFactory.java | 8 + .../dataforge/maths/histogram/Histogram.java | 104 + .../maths/histogram/NamedHistogram.java | 87 + .../maths/histogram/SimpleHistogram.java | 66 + .../dataforge/maths/histogram/SquareBin.java | 111 + .../maths/histogram/UniformBinFactory.java | 40 + .../maths/histogram/UnivariateHistogram.java | 92 + .../maths/integration/CMIntegrand.java | 55 + .../integration/DistributionSampler.java | 67 + .../integration/GaussRuleIntegrator.java | 115 + .../IncompatibleIntegrandException.java | 28 + .../maths/integration/Integrand.java | 51 + .../maths/integration/Integrator.java | 67 + .../maths/integration/IntegratorFactory.java | 37 + .../IterativeUnivariateIntegrator.java | 91 + .../integration/MonteCarloIntegrand.java | 98 + .../integration/MonteCarloIntegrator.java | 122 + .../maths/integration/RiemanIntegrator.java | 90 + .../dataforge/maths/integration/Sample.java | 57 + .../dataforge/maths/integration/Sampler.java | 51 + .../SplitUnivariateIntegrator.java | 53 + .../integration/UnivariateIntegrand.java | 95 + .../integration/UnivariateIntegrator.java | 62 + .../kotlin/hep/dataforge/maths/chain/Chain.kt | 169 + .../dataforge/maths/expressions/DSNumber.kt | 127 + .../dataforge/maths/expressions/Expression.kt | 102 + .../hep/dataforge/maths/expressions/Field.kt | 147 + .../maths/extensions/MathExtensions.kt | 249 + .../maths/extensions/RealFieldExtensions.kt | 55 + .../maths/functions/FunctionLibrary.kt | 124 + .../hep.dataforge.context.PluginFactory | 1 + .../dataforge/maths/NamedMatrixTest.groovy | 21 + .../dataforge/maths/NamedVectorTest.groovy | 21 + .../maths/expressions/DSFieldTest.kt | 36 + .../maths/histogram/HistogramTest.groovy | 34 + .../MonteCarloIntegratorTest.groovy | 47 + .../integration/RiemanIntegratorTest.java | 43 + dataforge-plots/build.gradle | 13 + dataforge-plots/plots-jfc/build.gradle | 16 + .../dataforge/plots/jfreechart/FXPlotUtils.kt | 82 + .../plots/jfreechart/JFCDataWrapper.kt | 89 + .../plots/jfreechart/JFreeChartFrame.kt | 361 + .../plots/jfreechart/JFreeChartPlugin.kt | 45 + .../hep.dataforge.context.PluginFactory | 1 + dataforge-plots/plots-viewer/build.gradle | 32 + .../hep/dataforge/plots/viewer/PlotView.kt | 74 + .../hep/dataforge/plots/viewer/ViewerApp.kt | 31 + .../src/main/resources/fxml/PlotViewer.fxml | 23 + .../main/resources/fxml/styles/plotviewer.css | 7 + .../hep/dataforge/plots/AbstractPlotFrame.kt | 88 + .../kotlin/hep/dataforge/plots/FXPlotFrame.kt | 38 + .../main/kotlin/hep/dataforge/plots/Plot.kt | 126 + .../kotlin/hep/dataforge/plots/PlotBuilder.kt | 28 + .../hep/dataforge/plots/PlotDataAction.kt | 108 + .../kotlin/hep/dataforge/plots/PlotFrame.kt | 252 + .../kotlin/hep/dataforge/plots/PlotGroup.kt | 293 + .../hep/dataforge/plots/PlotListener.kt | 40 + .../kotlin/hep/dataforge/plots/PlotUtils.kt | 68 + .../kotlin/hep/dataforge/plots/Plottable.kt | 56 + .../hep/dataforge/plots/VirtualPlotFrame.kt | 34 + .../kotlin/hep/dataforge/plots/XYPlotFrame.kt | 96 + .../hep/dataforge/plots/data/AbstractPlot.kt | 65 + .../hep/dataforge/plots/data/DataPlot.kt | 127 + .../hep/dataforge/plots/data/DataPlotUtils.kt | 49 + .../hep/dataforge/plots/data/TimePlot.kt | 205 + .../dataforge/plots/data/XYFunctionPlot.kt | 181 + .../kotlin/hep/dataforge/plots/data/XYPlot.kt | 102 + .../hep/dataforge/plots/output/PlotOutput.kt | 45 + .../hep.dataforge.io.envelopes.Wrapper | 3 + .../plots/data/TimePlottableTest.groovy | 48 + dataforge-stat/build.gradle | 5 + dataforge-stat/dataforge-minuit/build.gradle | 5 + .../dataforge-minuit/licenseheader.txt | 8 + .../MINUIT/AnalyticalGradientCalculator.java | 69 + .../dataforge/MINUIT/CombinedMinimizer.java | 36 + .../MINUIT/CombinedMinimumBuilder.java | 55 + .../hep/dataforge/MINUIT/ContoursError.java | 140 + .../dataforge/MINUIT/DavidonErrorUpdator.java | 51 + .../dataforge/MINUIT/FunctionGradient.java | 73 + .../hep/dataforge/MINUIT/FunctionMinimum.java | 258 + .../dataforge/MINUIT/GradientCalculator.java | 40 + .../MINUIT/HessianGradientCalculator.java | 149 + .../MINUIT/InitialGradientCalculator.java | 124 + .../hep/dataforge/MINUIT/MinimumBuilder.java | 36 + .../hep/dataforge/MINUIT/MinimumError.java | 162 + .../dataforge/MINUIT/MinimumErrorUpdator.java | 33 + .../dataforge/MINUIT/MinimumParameters.java | 71 + .../hep/dataforge/MINUIT/MinimumSeed.java | 72 + .../MINUIT/MinimumSeedGenerator.java | 37 + .../hep/dataforge/MINUIT/MinimumState.java | 104 + .../java/hep/dataforge/MINUIT/MinosError.java | 202 + .../hep/dataforge/MINUIT/MinuitParameter.java | 299 + .../MINUIT/MnAlgebraicSymMatrix.java | 445 ++ .../hep/dataforge/MINUIT/MnApplication.java | 510 ++ .../java/hep/dataforge/MINUIT/MnContours.java | 300 + .../dataforge/MINUIT/MnCovarianceSqueeze.java | 108 + .../java/hep/dataforge/MINUIT/MnCross.java | 103 + .../java/hep/dataforge/MINUIT/MnEigen.java | 51 + .../main/java/hep/dataforge/MINUIT/MnFcn.java | 54 + .../hep/dataforge/MINUIT/MnFunctionCross.java | 400 + .../MINUIT/MnGlobalCorrelationCoeff.java | 78 + .../java/hep/dataforge/MINUIT/MnHesse.java | 371 + .../hep/dataforge/MINUIT/MnLineSearch.java | 212 + .../dataforge/MINUIT/MnMachinePrecision.java | 72 + .../java/hep/dataforge/MINUIT/MnMigrad.java | 145 + .../java/hep/dataforge/MINUIT/MnMinimize.java | 144 + .../java/hep/dataforge/MINUIT/MnMinos.java | 399 + .../java/hep/dataforge/MINUIT/MnParabola.java | 65 + .../dataforge/MINUIT/MnParabolaFactory.java | 66 + .../hep/dataforge/MINUIT/MnParabolaPoint.java | 38 + .../hep/dataforge/MINUIT/MnParameterScan.java | 121 + .../java/hep/dataforge/MINUIT/MnPlot.java | 404 + .../java/hep/dataforge/MINUIT/MnPosDef.java | 98 + .../java/hep/dataforge/MINUIT/MnPrint.java | 428 ++ .../java/hep/dataforge/MINUIT/MnScan.java | 196 + .../hep/dataforge/MINUIT/MnSeedGenerator.java | 115 + .../java/hep/dataforge/MINUIT/MnSimplex.java | 147 + .../java/hep/dataforge/MINUIT/MnStrategy.java | 286 + .../dataforge/MINUIT/MnUserCovariance.java | 149 + .../java/hep/dataforge/MINUIT/MnUserFcn.java | 38 + .../MINUIT/MnUserParameterState.java | 748 ++ .../dataforge/MINUIT/MnUserParameters.java | 392 + .../MINUIT/MnUserTransformation.java | 394 + .../java/hep/dataforge/MINUIT/MnUtils.java | 159 + .../MINUIT/ModularFunctionMinimizer.java | 62 + .../MINUIT/NegativeG2LineSearch.java | 83 + .../MINUIT/Numerical2PGradientCalculator.java | 136 + .../main/java/hep/dataforge/MINUIT/Range.java | 38 + .../hep/dataforge/MINUIT/ScanBuilder.java | 57 + .../hep/dataforge/MINUIT/ScanMinimizer.java | 40 + .../hep/dataforge/MINUIT/SimplexBuilder.java | 184 + .../dataforge/MINUIT/SimplexMinimizer.java | 45 + .../dataforge/MINUIT/SimplexParameters.java | 91 + .../MINUIT/SimplexSeedGenerator.java | 53 + .../MINUIT/SinParameterTransformation.java | 51 + .../SqrtLowParameterTransformation.java | 44 + .../MINUIT/SqrtUpParameterTransformation.java | 44 + .../MINUIT/VariableMetricBuilder.java | 148 + .../MINUIT/VariableMetricEDMEstimator.java | 33 + .../MINUIT/VariableMetricMinimizer.java | 46 + .../hep/dataforge/MINUIT/package-info.java | 16 + .../hep/dataforge/stat/fit/MINOSResult.java | 92 + .../hep/dataforge/stat/fit/MINUITFitter.java | 261 + .../hep/dataforge/stat/fit/MINUITPlugin.java | 110 + .../hep/dataforge/stat/fit/MINUITUtils.java | 152 + .../hep.dataforge.context.PluginFactory | 1 + .../dataforge/fitting/GaussianSpectrum.java | 116 + .../hep/dataforge/fitting/TestFitting.java | 87 + .../java/hep/dataforge/io/FittingIOUtils.java | 326 + .../java/hep/dataforge/io/PrintFunction.java | 178 + .../stat/fit/BasicIntervalEstimate.java | 93 + .../hep/dataforge/stat/fit/FitAction.java | 87 + .../hep/dataforge/stat/fit/FitHelper.java | 273 + .../hep/dataforge/stat/fit/FitResult.java | 216 + .../java/hep/dataforge/stat/fit/FitStage.java | 115 + .../java/hep/dataforge/stat/fit/FitState.java | 404 + .../java/hep/dataforge/stat/fit/Hessian.java | 65 + .../dataforge/stat/fit/IntervalEstimate.java | 64 + .../java/hep/dataforge/stat/fit/Param.java | 201 + .../java/hep/dataforge/stat/fit/ParamSet.java | 391 + .../stat/fit/UpperLimitGenerator.java | 65 + .../AnalyticalPosteriorLogLike.java | 100 + .../likelihood/BayesianConfidenceLimit.java | 163 + .../likelihood/ConfidenceLimitCalculator.java | 265 + .../dataforge/stat/likelihood/Gaussian.java | 92 + .../stat/likelihood/LogGaussian.java | 93 + .../stat/likelihood/LogLikelihood.java | 130 + .../likelihood/MarginalFunctionBuilder.java | 339 + .../likelihood/MultivariateDomainPrior.java | 93 + .../likelihood/MultivariateGaussianPrior.java | 88 + .../stat/likelihood/OneSidedUniformPrior.java | 113 + .../stat/likelihood/TwoSidedUniformPrior.java | 106 + .../dataforge/stat/models/AbstractModel.java | 126 + .../hep/dataforge/stat/models/Generator.java | 41 + .../dataforge/stat/models/HistogramModel.java | 188 + .../java/hep/dataforge/stat/models/Model.java | 139 + .../dataforge/stat/models/ModelFactory.java | 38 + .../dataforge/stat/models/ModelLibrary.java | 81 + .../stat/models/WeightedXYModel.java | 53 + .../hep/dataforge/stat/models/XYModel.java | 179 + .../stat/parametric/AbstractParametric.java | 65 + .../AbstractParametricBiFunction.java | 36 + .../AbstractParametricFunction.java | 44 + .../parametric/AbstractParametricValue.java | 27 + .../stat/parametric/DerivativeCalculator.java | 111 + .../stat/parametric/ParametricBiFunction.java | 52 + .../stat/parametric/ParametricFunction.java | 69 + .../ParametricMultiFunctionWrapper.java | 151 + .../stat/parametric/ParametricUtils.java | 170 + .../stat/parametric/ParametricValue.java | 55 + .../GaussianParameterGenerator.java | 82 + .../stat/simulation/ParameterGenerator.java | 46 + .../stat/simulation/SimulationUtils.java | 24 + .../hep/dataforge/stat/Distributions.kt | 44 + .../main/kotlin/hep/dataforge/stat/Kfit.kt | 83 + .../dataforge/stat/PolynomialDistribution.kt | 67 + .../main/kotlin/hep/dataforge/stat/Random.kt | 6 + .../kotlin/hep/dataforge/stat/Sampling.kt | 62 + .../stat/UniformRandomVectorGenerator.kt | 70 + .../kotlin/hep/dataforge/stat/fit/CMFitter.kt | 135 + .../hep/dataforge/stat/fit/FitManager.kt | 136 + .../kotlin/hep/dataforge/stat/fit/Fitter.kt | 55 + .../hep/dataforge/stat/fit/QOWFitter.kt | 261 + .../kotlin/hep/dataforge/stat/fit/QOWUtils.kt | 306 + .../hep.dataforge.context.PluginFactory | 2 + .../src/main/resources/upperlimit.txt | 62 + .../ConfidenceLimitCalculatorTest.java | 95 + .../MarginalFunctionBuilderTest.java | 120 + .../parametric/DerivativeCalculatorTest.java | 104 + dataforge-storage/build.gradle | 5 + .../kotlin/hep/dataforge/storage/Storage.kt | 208 + .../dataforge/storage/StorageConnection.kt | 87 + .../hep/dataforge/storage/StorageManager.kt | 106 + .../storage/files/FileCollectionLoader.kt | 75 + .../dataforge/storage/files/FileEnvelope.kt | 163 + .../dataforge/storage/files/FileStorage.kt | 215 + .../storage/files/FileTableLoader.kt | 326 + .../dataforge/storage/tables/TableLoader.kt | 95 + .../hep.dataforge.context.PluginFactory | 1 + .../hep.dataforge.storage.StorageElementType | 2 + .../hep.dataforge.storage/TableLoaderTest.kt | 110 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 58702 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 183 + gradlew.bat | 100 + grind/build.gradle | 13 + grind/grind-terminal/build.gradle | 53 + .../dataforge/grind/demo/WorkspaceDemo.groovy | 84 + .../dataforge/grind/helpers/PlotHelper.groovy | 150 + .../grind/helpers/PlotHelperFactory.groovy | 16 + .../grind/terminal/GrindTerminal.groovy | 311 + .../grind/terminal/RunDumbGrindShell.groovy | 16 + .../grind/terminal/RunGrindShell.groovy | 17 + .../grind/terminal/TerminalLogLayout.groovy | 81 + ...dataforge.grind.helpers.GrindHelperFactory | 1 + grind/groovymath/README.md | 19 + grind/groovymath/build.gradle | 6 + .../main/groovy/hep/dataforge/maths/GM.groovy | 47 + .../dataforge/maths/HistogramBuilder.groovy | 45 + .../maths/extensions/NumberExtension.groovy | 81 + .../extensions/RealMatrixExtension.groovy | 71 + .../extensions/RealVectorExtension.groovy | 89 + ...iateDifferentiableFunctionExtension.groovy | 65 + .../UnivariateFunctionExtension.groovy | 97 + .../hep/dataforge/maths/tables/GColumn.groovy | 190 + .../hep/dataforge/maths/tables/GRow.groovy | 13 + .../hep/dataforge/maths/tables/GTable.groovy | 175 + .../hep/dataforge/maths/tables/MapRow.groovy | 44 + .../dataforge/maths/tables/TableRow.groovy | 51 + .../dataforge/maths/tables/ValueType.groovy | 12 + ...rg.codehaus.groovy.runtime.ExtensionModule | 8 + .../groovy/hep/dataforge/maths/GMTest.groovy | 25 + .../extensions/FunctionExensionTest.groovy | 27 + .../extensions/NumberExtensionTest.groovy | 21 + .../extensions/RealMatrixExtensionTest.groovy | 22 + .../extensions/RealVectorExtensionTest.groovy | 24 + .../dataforge/maths/tables/GColumnTest.groovy | 25 + .../dataforge/maths/tables/GTableTest.groovy | 34 + .../groovy/hep/dataforge/grind/Grind.groovy | 172 + .../dataforge/grind/GrindMetaBuilder.groovy | 89 + .../hep/dataforge/grind/GrindShell.groovy | 164 + .../grind/extensions/CoreExtension.groovy | 358 + .../extensions/ExtensionInitializer.groovy | 62 + .../grind/helpers/AbstractHelper.groovy | 71 + .../grind/helpers/GrindHelper.groovy | 11 + .../grind/helpers/GrindHelperFactory.groovy | 8 + .../grind/helpers/MethodDescription.groovy | 12 + .../grind/helpers/WorkspaceHelper.groovy | 33 + .../helpers/WorkspaceHelperFactory.groovy | 16 + .../hep/dataforge/grind/misc/ReWrapper.groovy | 90 + .../grind/workspace/ContextSpec.groovy | 53 + .../grind/workspace/DataNodeSpec.groovy | 174 + .../grind/workspace/DefaultTaskLib.groovy | 197 + .../dataforge/grind/workspace/ExecSpec.groovy | 362 + .../grind/workspace/GrindWorkspace.groovy | 68 + .../workspace/GroovyWorkspaceParser.groovy | 50 + .../grind/workspace/WorkspaceSpec.groovy | 115 + .../grind/workspace/GrindTaskBuilder.kt | 71 + ...dataforge.grind.helpers.GrindHelperFactory | 1 + .../hep.dataforge.workspace.WorkspaceParser | 1 + ...rg.codehaus.groovy.runtime.ExtensionModule | 3 + .../grind/GrindMetaBuilderSpec.groovy | 63 + .../grind/GrindWorkspaceBuilderTest.groovy | 32 + .../hep/dataforge/grind/TestTask.groovy | 40 + .../dataforge/grind/WorkspaceSpecTest.groovy | 80 + .../grind/extensions/CoreExtensionTest.groovy | 29 + .../extensions/ValueExtensionTest.groovy | 32 + .../dataforge/grind/workspace/ExecTest.groovy | 77 + .../grind/workspace/WorkspaceTest.groovy | 62 + grind/src/test/resources/workspace/test.py | 11 + .../test/resources/workspace/workspace.groovy | 12 + numass-control/build.gradle | 78 + numass-control/control-room/build.gradle | 80 + .../resources/main/config/control-real.xml | 38 + .../build/resources/main/config/control.xml | 19 + .../control-room/build/scripts/control-room | 172 + .../build/scripts/control-room-devices | 172 + .../build/scripts/control-room-devices.bat | 84 + .../build/scripts/control-room.bat | 84 + .../resources/config/control-real.xml | 38 + .../production/resources/config/control.xml | 19 + .../inr/numass/control/BoardController.kt | 77 + .../kotlin/inr/numass/control/BoardView.kt | 103 + .../kotlin/inr/numass/control/ServerApp.kt | 25 + .../main/resources/config/control-real.xml | 38 + .../src/main/resources/config/control.xml | 19 + numass-control/cryotemp/build.gradle | 32 + .../inr/numass/control/cryotemp/PKT8App.kt | 40 + .../numass/control/cryotemp/PKT8Channel.kt | 72 + .../inr/numass/control/cryotemp/PKT8Device.kt | 330 + .../control/cryotemp/PKT8DeviceFactory.kt | 16 + .../numass/control/cryotemp/PKT8Display.kt | 216 + .../control/cryotemp/PKT8VirtualPort.kt | 70 + ...ep.dataforge.control.devices.DeviceFactory | 1 + .../main/resources/config-debug/devices.xml | 18 + .../src/main/resources/config/devices.xml | 31 + .../main/resources/fxml/PKT8Indicator.fxml | 42 + .../src/main/resources/fxml/PKT8Plot.fxml | 36 + numass-control/dante/build.gradle | 22 + .../inr/numass/control/dante/DanteClient.kt | 581 ++ .../inr/numass/control/dante/DanteTest.kt | 105 + numass-control/gun/build.gradle | 24 + .../kotlin/inr/numass/control/gun/EGun.kt | 52 + .../inr/numass/control/gun/EGunApplication.kt | 43 + .../kotlin/inr/numass/control/gun/EGunView.kt | 79 + .../inr/numass/control/gun/IT6800Device.kt | 203 + .../gun/src/main/resources/config/devices.xml | 9 + numass-control/magnet/build.gradle | 32 + .../inr/numass/control/magnet/LambdaHub.kt | 95 + .../inr/numass/control/magnet/LambdaMagnet.kt | 333 + .../control/magnet/LambdaPortController.kt | 79 + .../control/magnet/VirtualLambdaPort.kt | 145 + .../inr/numass/control/magnet/fx/MagnetApp.kt | 29 + .../numass/control/magnet/fx/MagnetDisplay.kt | 203 + .../inr/numass/control/magnet/test/CLITest.kt | 63 + .../numass/control/magnet/test/SetCurrent.kt | 50 + .../magnet/src/main/resources/debug.xml | 16 + .../src/main/resources/fxml/SingleMagnet.fxml | 113 + .../magnet/src/main/resources/logback.xml | 23 + .../control/magnet/VirtualLambdaPortTest.kt | 38 + numass-control/msp/build.gradle | 13 + ...CII Protocol User Manual SP1040016.100.pdf | 4933 ++++++++++++ numass-control/msp/docs/commands.htm | 489 ++ .../kotlin/inr/numass/control/msp/MspApp.kt | 41 + .../inr/numass/control/msp/MspDevice.kt | 413 + .../numass/control/msp/MspDeviceFactory.kt | 16 + .../inr/numass/control/msp/MspDisplay.kt | 217 + ...ep.dataforge.control.devices.DeviceFactory | 1 + .../msp/src/main/resources/config/devices.xml | 35 + .../msp/src/main/resources/fxml/MspView.fxml | 50 + .../inr/numass/control/msp/TestScanner.java | 38 + .../inr/numass/control/DeviceDisplayFX.kt | 128 + .../kotlin/inr/numass/control/FXExtensions.kt | 151 + .../control/NumassControlApplication.kt | 81 + .../inr/numass/control/NumassControlUtils.kt | 117 + .../numass/control/NumassStorageConnection.kt | 59 + .../inr/numass/control/StorageHelper.kt | 46 + numass-control/src/main/resources/img/df.png | Bin 0 -> 54732 bytes numass-control/vac/build.gradle | 20 + .../inr/numass/control/readvac/CM32Device.kt | 60 + .../inr/numass/control/readvac/ConsoleVac.kt | 62 + .../control/readvac/MKSBaratronDevice.kt | 56 + .../numass/control/readvac/MKSVacDevice.kt | 123 + .../control/readvac/MeradatVacDevice.kt | 94 + .../inr/numass/control/readvac/ReadVac.kt | 34 + .../control/readvac/ThyroContVacDevice.kt | 66 + .../control/readvac/VacCollectorDevice.kt | 152 + .../control/readvac/VacCollectorDisplay.kt | 140 + .../control/readvac/VacDeviceFactory.kt | 39 + .../inr/numass/control/readvac/VacDisplay.kt | 185 + .../kotlin/inr/numass/control/readvac/test.kt | 20 + ...ep.dataforge.control.devices.DeviceFactory | 1 + .../main/resources/config-test/devices.xml | 9 + .../vac/src/main/resources/config/devices.xml | 13 + .../inr/numass/control/readvac/TestMain.java | 45 + .../numass/control/readvac/TestRemote.java | 35 + .../numass/control/readvac/TimeShiftTest.java | 46 + numass-core/build.gradle | 12 + numass-core/docs/server_commands.md | 189 + numass-core/numass-data-api/build.gradle.kts | 19 + .../kotlin/inr/numass/data/api/MetaBlock.kt | 39 + .../kotlin/inr/numass/data/api/NumassBlock.kt | 116 + .../kotlin/inr/numass/data/api/NumassFrame.kt | 28 + .../kotlin/inr/numass/data/api/NumassPoint.kt | 135 + .../kotlin/inr/numass/data/api/NumassSet.kt | 88 + .../inr/numass/data/api/SignalProcessor.kt | 11 + .../inr/numass/data/api/SimpleNumassPoint.kt | 30 + .../numass-data-proto/build.gradle.kts | 46 + .../java/inr/numass/data/NumassProto.java | 4868 ++++++++++++ .../inr/numass/data/NumassEnvelopeType.kt | 140 + .../inr/numass/data/NumassFileEnvelope.kt | 52 + .../inr/numass/data/ProtoNumassPoint.kt | 177 + .../main/proto/inr/numas/numass-proto.proto | 33 + .../numass-signal-processing/build.gradle.kts | 19 + .../inr/numass/data/ChernovProcessor.kt | 100 + .../inr/numass/data/ChernovProcessorTest.kt | 26 + .../kotlin/inr/numass/NumassProperties.kt | 63 + .../kotlin/inr/numass/data/NumassDataUtils.kt | 123 + .../kotlin/inr/numass/data/SpectrumAdapter.kt | 104 + .../numass/data/analyzers/AbstractAnalyzer.kt | 105 + .../numass/data/analyzers/DebunchAnalyzer.kt | 33 + .../numass/data/analyzers/NumassAnalyzer.kt | 279 + .../numass/data/analyzers/SimpleAnalyzer.kt | 62 + .../inr/numass/data/analyzers/TimeAnalyzer.kt | 297 + .../inr/numass/data/legacy/NumassDatFile.kt | 233 + .../numass/data/storage/ClassicNumassPoint.kt | 112 + .../numass/data/storage/NumassDataFactory.kt | 47 + .../numass/data/storage/NumassDataLoader.kt | 194 + .../inr/numass/data/storage/NumassStorage.kt | 295 + .../services/hep.dataforge.data.DataLoader | 1 + .../hep.dataforge.io.envelopes.EnvelopeType | 1 + .../hep.dataforge.storage.StorageElementType | 1 + numass-main/build.gradle | 90 + .../groovy/inr/numass/LaunchGrindShell.groovy | 46 + .../src/main/groovy/inr/numass/RunTask.groovy | 15 + .../inr/numass/data/PointAnalyzer.groovy | 48 + .../numass/scripts/CountRateSummary.groovy | 23 + .../inr/numass/scripts/FindExIonRatio.groovy | 109 + .../numass/scripts/LossNormCalculation.groovy | 46 + .../numass/scripts/LossTailCalculation.groovy | 41 + .../groovy/inr/numass/scripts/OldTest.groovy | 104 + .../numass/scripts/PrintLossFunctions.groovy | 61 + .../inr/numass/scripts/ReadTextFile.groovy | 15 + .../inr/numass/scripts/ShowSpectrum.groovy | 65 + .../numass/scripts/ShowTransmission.groovy | 77 + .../numass/scripts/SignificanceTest.groovy | 110 + .../groovy/inr/numass/scripts/Simulate.groovy | 102 + .../inr/numass/scripts/SimulateGun.groovy | 67 + .../inr/numass/scripts/SimulatePileup.groovy | 157 + .../inr/numass/scripts/SterileDemo.groovy | 81 + .../numass/scripts/SystTransmission.groovy | 101 + .../inr/numass/scripts/Systematics.groovy | 99 + ...estExperimentalVariableLossSpectrum.groovy | 68 + .../scripts/TheoreticalLossFunction.groovy | 46 + .../inr/numass/scripts/TritiumTest.groovy | 119 + .../numass/scripts/models/TristanModel.groovy | 100 + .../scripts/underflow/ResponseFunction.groovy | 66 + .../numass/scripts/underflow/Underflow.groovy | 139 + .../src/main/java/inr/numass/Numass.java | 67 + .../numass/actions/AdjustErrorsAction.java | 87 + .../numass/actions/MonitorCorrectAction.java | 234 + .../actions/SubstractSpectrumAction.java | 65 + .../java/inr/numass/data/MonitorPoint.java | 74 + .../inr/numass/data/SpectrumGenerator.java | 188 + .../inr/numass/data/SpectrumInformation.java | 125 + .../java/inr/numass/data/package-info.java | 8 + .../java/inr/numass/models/BetaSpectrum.java | 247 + .../inr/numass/models/CustomNBkgSpectrum.java | 50 + .../numass/models/EmpiricalLossSpectrum.java | 75 + .../ExperimentalVariableLossSpectrum.java | 179 + .../src/main/java/inr/numass/models/FSS.java | 70 + .../numass/models/GaussSourceSpectrum.java | 91 + .../java/inr/numass/models/GunSpectrum.java | 155 + .../inr/numass/models/GunTailSpectrum.java | 89 + .../inr/numass/models/LossResConvolution.java | 44 + .../inr/numass/models/ModularSpectrum.java | 249 + .../numass/models/NamedSpectrumCaching.java | 231 + .../numass/models/RangedNamedSetSpectrum.java | 26 + .../inr/numass/models/ResolutionFunction.java | 134 + .../java/inr/numass/models/SimpleRange.java | 50 + .../java/inr/numass/models/SpectrumRange.java | 28 + .../java/inr/numass/models/Transmission.java | 44 + .../models/TransmissionConvolution.java | 85 + .../models/TransmissionInterpolator.java | 187 + .../numass/models/TritiumSpectrumCaching.java | 65 + .../numass/models/VariableLossSpectrum.java | 150 + .../java/inr/numass/utils/DataModelUtils.java | 67 + .../inr/numass/utils/ExpressionUtils.java | 58 + .../inr/numass/utils/NumassIntegrator.java | 46 + .../java/inr/numass/utils/OldDataReader.java | 191 + .../inr/numass/utils/UnderflowCorrection.java | 163 + .../main/kotlin/inr/numass/NumassPlugin.kt | 333 + .../src/main/kotlin/inr/numass/NumassUtils.kt | 305 + .../main/kotlin/inr/numass/WorkspaceTest.kt | 32 + .../inr/numass/actions/AnalyzeDataAction.kt | 36 + .../inr/numass/actions/MergeDataAction.kt | 126 + .../inr/numass/actions/PlotFitResultAction.kt | 75 + .../inr/numass/actions/SummaryAction.kt | 115 + .../inr/numass/actions/TimeAnalyzerAction.kt | 164 + .../inr/numass/actions/TimeSpectrumAction.kt | 136 + .../inr/numass/actions/TransformDataAction.kt | 221 + .../kotlin/inr/numass/data/NumassGenerator.kt | 138 + .../kotlin/inr/numass/data/PileUpSimulator.kt | 177 + .../kotlin/inr/numass/data/Visualization.kt | 87 + .../numass/data/analyzers/SmartAnalyzer.kt | 103 + .../kotlin/inr/numass/models/NBkgSpectrum.kt | 75 + .../models/NBkgSpectrumWithCorrection.kt | 62 + .../kotlin/inr/numass/models/NumassModels.kt | 152 + .../inr/numass/models/mc/BetaSampler.kt | 53 + .../inr/numass/models/misc/FunctionSupport.kt | 28 + .../kotlin/inr/numass/models/misc/Gauss.kt | 69 + .../inr/numass/models/misc/LossCalculator.kt | 435 ++ .../kotlin/inr/numass/models/misc/ModGauss.kt | 84 + .../inr/numass/models/misc/TailFunctions.kt | 41 + .../inr/numass/models/sterile/NumassBeta.kt | 225 + .../numass/models/sterile/NumassResolution.kt | 87 + .../models/sterile/NumassTransmission.kt | 84 + .../sterile/ParametricBiFunctionCache.kt | 39 + .../models/sterile/SterileNeutrinoSpectrum.kt | 169 + .../main/kotlin/inr/numass/scripts/Bunches.kt | 53 + .../kotlin/inr/numass/scripts/Correlation.kt | 72 + .../inr/numass/scripts/InversedChain.kt | 78 + .../inr/numass/scripts/InversedChainProto.kt | 58 + .../scripts/PileupIntensityDependency.kt | 117 + .../scripts/PileupIntensityDependencyGun.kt | 119 + .../inr/numass/scripts/PlotResiduals.kt | 79 + .../inr/numass/scripts/PointSliceSpectrum.kt | 72 + .../inr/numass/scripts/analysis/Run2017_05.kt | 20 + .../scripts/analysis/Run2017_05_frames.kt | 20 + .../inr/numass/scripts/analysis/Run2017_11.kt | 39 + .../inr/numass/scripts/analysis/Run2019_11.kt | 38 + .../inr/numass/scripts/analysis/Run2020_12.kt | 44 + .../scripts/models/DifferentialSpectrum.kt | 89 + .../numass/scripts/models/IntegralEndpoint.kt | 78 + .../numass/scripts/models/IntegralSpectrum.kt | 166 + .../numass/scripts/models/ResolutionDemo.kt | 57 + .../kotlin/inr/numass/scripts/models/demo.kt | 59 + .../scripts/threshold/FitAllWithPower.kt | 82 + .../numass/scripts/threshold/FitWithPower.kt | 71 + .../threshold/threshold-2017_05-CAMAC.kt | 71 + .../threshold/threshold-2019_11-CAMAC.kt | 80 + .../scripts/timeanalysis/AnalyzeDantePoint.kt | 72 + .../scripts/timeanalysis/AnalyzePoint.kt | 78 + .../numass/scripts/timeanalysis/Histogram.kt | 77 + .../scripts/timeanalysis/TestAnalyzer.kt | 59 + .../numass/scripts/timeanalysis/TestBunch.kt | 77 + .../timeanalysis/TestTimeAnalyzerLength.kt | 90 + .../numass/scripts/tristan/AnalyzeTristan.kt | 73 + .../scripts/tristan/FitTextTristanFiles.kt | 162 + .../inr/numass/scripts/tristan/ProtoPoint.kt | 44 + .../numass/scripts/tristan/TristanAnalyzer.kt | 51 + .../inr/numass/scripts/tristan/ViewPoint.kt | 37 + .../scripts/tristan/checkCorrelations.kt | 41 + .../numass/scripts/tristan/interpolateBump.kt | 35 + .../inr/numass/scripts/tristan/zeroPad.kt | 36 + .../inr/numass/scripts/utils/ScanTree.kt | 103 + .../inr/numass/scripts/utils/ScriptUtils.kt | 2 + .../inr/numass/subthreshold/Threshold.kt | 265 + .../numass/tasks/NumassFitScanSummaryTask.kt | 76 + .../inr/numass/tasks/NumassFitScanTask.kt | 82 + .../inr/numass/tasks/NumassFitSummaryTask.kt | 42 + .../kotlin/inr/numass/tasks/NumassTasks.kt | 536 ++ .../inr/numass/tasks/SpecialNumassTasks.kt | 55 + .../hep.dataforge.context.PluginFactory | 1 + numass-main/src/main/resources/data/FS.txt | 193 + .../src/main/resources/data/d2_17_1.xml | 61 + numass-main/src/main/resources/data/run23.cfg | 48 + .../main/resources/numass/models/transmission | 29 + .../src/test/java/inr/numass/NumassTest.java | 26 + .../numass/actions/PrepareDataActionTest.java | 32 + .../inr/numass/models/NumassSpectrumTest.java | 72 + .../java/inr/numass/models/PlotScatter.java | 42 + .../test/java/inr/numass/models/TestModels.kt | 93 + .../models/TestNeLossParametrisation.java | 71 + .../models/TransmissionInterpolatorTest.java | 41 + .../kotlin/inr/numass/NumassPluginTest.kt | 30 + numass-viewer/build.gradle | 30 + .../kotlin/inr/numass/viewer/AmplitudeView.kt | 198 + .../main/kotlin/inr/numass/viewer/Cache.kt | 48 + .../main/kotlin/inr/numass/viewer/HVView.kt | 87 + .../main/kotlin/inr/numass/viewer/MainView.kt | 202 + .../kotlin/inr/numass/viewer/PointInfoView.kt | 54 + .../inr/numass/viewer/SlowControlView.kt | 87 + .../kotlin/inr/numass/viewer/SpectrumView.kt | 154 + .../kotlin/inr/numass/viewer/StorageView.kt | 211 + .../main/kotlin/inr/numass/viewer/TimeView.kt | 173 + .../main/kotlin/inr/numass/viewer/Viewer.kt | 28 + .../inr/numass/viewer/test/ComponentTest.kt | 66 + .../kotlin/inr/numass/viewer/test/JFCTest.kt | 35 + .../inr/numass/viewer/test/StorageViewTest.kt | 11 + .../kotlin/inr/numass/viewer/test/TestApp.kt | 9 + .../src/main/resources/fxml/MainView.fxml | 56 + .../main/resources/fxml/NumassLoaderView.fxml | 80 + settings.gradle | 52 + site/SiteConfig.groovy | 61 + site/application.properties | 1 + site/build.gradle | 159 + site/content/apps.html | 13 + site/content/config/docs.yml | 12 + site/content/config/header.yml | 11 + site/content/docs.html | 5 + site/content/docs/0.1.0/release.md | 5 + site/content/docs/0.1.1/release.md | 11 + site/content/docs/0.4.0 - SNAPSHOT/release.md | 9 + site/content/docs/images/actions.png | Bin 0 -> 12382 bytes site/content/docs/images/meta.svg | 196 + .../images/metadata-processor-classic.png | Bin 0 -> 29987 bytes .../docs/images/metadata-processor.png | Bin 0 -> 20982 bytes site/content/docs/images/naming.svg | 100 + site/content/docs/images/tasks.png | Bin 0 -> 16424 bytes site/content/docs/shards/context/context.md | 17 + site/content/docs/shards/context/framework.md | 8 + .../content/docs/shards/context/navigation.md | 61 + site/content/docs/shards/context/plugins.md | 26 + .../docs/shards/control/connections.md | 0 site/content/docs/shards/control/control.md | 23 + .../docs/shards/control/measurements.md | 0 site/content/docs/shards/data_flow/actions.md | 11 + .../docs/shards/data_flow/data_flow.md | 10 + site/content/docs/shards/data_flow/tasks.md | 11 + .../docs/shards/envelopes/binary_meta.md | 31 + .../docs/shards/envelopes/default_format.md | 37 + .../shards/envelopes/envelope_description.md | 18 + .../docs/shards/envelopes/envelopes.md | 19 + .../docs/shards/envelopes/tagless_format.md | 25 + .../docs/shards/extensions/extensions.md | 9 + site/content/docs/shards/extensions/grind.md | 12 + site/content/docs/shards/extensions/kodex.md | 10 + site/content/docs/shards/how_to_read.md | 17 + .../content/docs/shards/meta/configuration.md | 21 + .../content/docs/shards/meta/hidden_values.md | 13 + site/content/docs/shards/meta/meta-data.md | 33 + site/content/docs/shards/meta/meta-layers.md | 22 + .../docs/shards/meta/meta_structure.md | 42 + site/content/images/df_icon.ico | Bin 0 -> 16958 bytes site/content/images/sms_logo.png | Bin 0 -> 20051 bytes site/content/index.md | 121 + site/content/misc.html | 81 + site/content/modules.html | 71 + site/content/news.html | 5 + site/content/news/02_16_update.md | 15 + site/content/news/08_17_update.md | 7 + site/content/news/lomonosov_2015.markdown | 8 + site/content/news/site_start.html | 23 + site/content/releases.html | 9 + site/content/robots.txt | 7 + site/content/sitemap.xml | 11 + .../classes/hep/dataforge/site/DFTagLib.class | Bin 0 -> 3931 bytes .../classes/hep/dataforge/site/Launcher.class | Bin 0 -> 452 bytes .../hep/dataforge/site/ResourceMapper.class | Bin 0 -> 9196 bytes .../groovy/hep/dataforge/site/DFTagLib.groovy | 55 + .../groovy/hep/dataforge/site/Launcher.java | 18 + .../hep/dataforge/site/ResourceMapper.groovy | 178 + site/theme/config.rb | 30 + .../fonts/glyphicons-halflings-regular.eot | Bin 0 -> 20127 bytes .../fonts/glyphicons-halflings-regular.svg | 288 + .../fonts/glyphicons-halflings-regular.ttf | Bin 0 -> 45404 bytes .../fonts/glyphicons-halflings-regular.woff | Bin 0 -> 23424 bytes .../fonts/glyphicons-halflings-regular.woff2 | Bin 0 -> 18028 bytes .../theme/images/apple-touch-icon-114x114.png | Bin 0 -> 10684 bytes .../theme/images/apple-touch-icon-120x120.png | Bin 0 -> 10000 bytes .../theme/images/apple-touch-icon-144x144.png | Bin 0 -> 14277 bytes .../theme/images/apple-touch-icon-152x152.png | Bin 0 -> 16593 bytes .../theme/images/apple-touch-icon-180x180.png | Bin 0 -> 18337 bytes site/theme/images/apple-touch-icon-57x57.png | Bin 0 -> 4482 bytes site/theme/images/apple-touch-icon-72x72.png | Bin 0 -> 5809 bytes site/theme/images/apple-touch-icon-76x76.png | Bin 0 -> 6531 bytes site/theme/images/apple-touch-icon.png | Bin 0 -> 4482 bytes site/theme/images/df_logo.png | Bin 0 -> 12640 bytes site/theme/images/favicon.ico | Bin 0 -> 34494 bytes site/theme/images/gears_animated.gif | Bin 0 -> 11477 bytes site/theme/includes/footer.html | 17 + site/theme/includes/header.html | 17 + site/theme/includes/in_progress.html | 10 + site/theme/includes/navbar.html | 32 + site/theme/javascripts/bootstrap.js | 2363 ++++++ site/theme/javascripts/bootstrap.min.js | 7 + site/theme/javascripts/npm.js | 13 + site/theme/layouts/default.html | 25 + site/theme/layouts/docs.html | 39 + site/theme/layouts/news.html | 20 + site/theme/layouts/releases.html | 45 + site/theme/stylesheets/bootstrap.css | 6800 +++++++++++++++++ site/theme/stylesheets/bootstrap.min.css | 5 + site/theme/stylesheets/navbar-fixed-top.css | 4 + site/theme/stylesheets/override.css | 49 + 1087 files changed, 117263 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 build.gradle create mode 100644 dataforge-control/build.gradle create mode 100644 dataforge-control/doc/States.vsdx create mode 100644 dataforge-control/src/main/java/hep/dataforge/control/ControlUtils.java create mode 100644 dataforge-control/src/main/java/hep/dataforge/control/collectors/PointCollector.java create mode 100644 dataforge-control/src/main/java/hep/dataforge/control/collectors/RegularPointCollector.java create mode 100644 dataforge-control/src/main/java/hep/dataforge/control/collectors/ValueCollector.java create mode 100644 dataforge-control/src/main/java/hep/dataforge/control/measurements/AbstractMeasurement.java create mode 100644 dataforge-control/src/main/java/hep/dataforge/control/measurements/Measurement.java create mode 100644 dataforge-control/src/main/java/hep/dataforge/control/measurements/MeasurementListener.java create mode 100644 dataforge-control/src/main/java/hep/dataforge/control/measurements/RegularMeasurement.java create mode 100644 dataforge-control/src/main/java/hep/dataforge/control/measurements/SimpleMeasurement.java create mode 100644 dataforge-control/src/main/java/hep/dataforge/exceptions/ControlException.java create mode 100644 dataforge-control/src/main/java/hep/dataforge/exceptions/MeasurementException.java create mode 100644 dataforge-control/src/main/java/hep/dataforge/exceptions/MeasurementInterruptedException.java create mode 100644 dataforge-control/src/main/java/hep/dataforge/exceptions/MeasurementNotReadyException.java create mode 100644 dataforge-control/src/main/java/hep/dataforge/exceptions/MeasurementTimeoutException.java create mode 100644 dataforge-control/src/main/java/hep/dataforge/exceptions/PortException.java create mode 100644 dataforge-control/src/main/java/hep/dataforge/exceptions/PortLockException.java create mode 100644 dataforge-control/src/main/kotlin/hep/dataforge/control/DeviceManager.kt create mode 100644 dataforge-control/src/main/kotlin/hep/dataforge/control/connections/DeviceConnection.kt create mode 100644 dataforge-control/src/main/kotlin/hep/dataforge/control/connections/LoaderConnection.kt create mode 100644 dataforge-control/src/main/kotlin/hep/dataforge/control/connections/Roles.kt create mode 100644 dataforge-control/src/main/kotlin/hep/dataforge/control/devices/AbstractDevice.kt create mode 100644 dataforge-control/src/main/kotlin/hep/dataforge/control/devices/Device.kt create mode 100644 dataforge-control/src/main/kotlin/hep/dataforge/control/devices/DeviceFactory.kt create mode 100644 dataforge-control/src/main/kotlin/hep/dataforge/control/devices/DeviceHub.kt create mode 100644 dataforge-control/src/main/kotlin/hep/dataforge/control/devices/DeviceListener.kt create mode 100644 dataforge-control/src/main/kotlin/hep/dataforge/control/devices/PortSensor.kt create mode 100644 dataforge-control/src/main/kotlin/hep/dataforge/control/devices/Sensor.kt create mode 100644 dataforge-control/src/main/kotlin/hep/dataforge/control/devices/Virtual.kt create mode 100644 dataforge-control/src/main/kotlin/hep/dataforge/control/ports/ComPort.kt create mode 100644 dataforge-control/src/main/kotlin/hep/dataforge/control/ports/GenericPortController.kt create mode 100644 dataforge-control/src/main/kotlin/hep/dataforge/control/ports/Port.kt create mode 100644 dataforge-control/src/main/kotlin/hep/dataforge/control/ports/PortFactory.kt create mode 100644 dataforge-control/src/main/kotlin/hep/dataforge/control/ports/PortHelper.kt create mode 100644 dataforge-control/src/main/kotlin/hep/dataforge/control/ports/TcpPort.kt create mode 100644 dataforge-control/src/main/kotlin/hep/dataforge/control/ports/VirtualPort.kt create mode 100644 dataforge-control/src/main/resources/META-INF/services/hep.dataforge.context.PluginFactory create mode 100644 dataforge-control/src/test/kotlin/hep/dataforge/control/ports/TcpPortTest.kt create mode 100644 dataforge-core/build.gradle create mode 100644 dataforge-core/dataforge-json/build.gradle create mode 100644 dataforge-core/dataforge-json/src/main/kotlin/hep/dataforge/io/JSONMetaReader.kt create mode 100644 dataforge-core/dataforge-json/src/main/kotlin/hep/dataforge/io/JSONMetaType.kt create mode 100644 dataforge-core/dataforge-json/src/main/kotlin/hep/dataforge/io/JSONMetaWriter.kt create mode 100644 dataforge-core/dataforge-json/src/main/resources/META-INF/services/hep.dataforge.io.envelopes.MetaType create mode 100644 dataforge-core/dataforge-json/src/test/kotlin/hep/dataforge/io/JSONMetaTypeTest.kt create mode 100644 dataforge-core/docs/descriptors.md create mode 100644 dataforge-core/docs/logging.md create mode 100644 dataforge-core/src/main/java/hep/dataforge/actions/ActionEvent.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/actions/ActionEventListener.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/actions/ActionManager.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/actions/ActionResult.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/actions/ActionStateListener.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/actions/GeneratorAction.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/actions/GroupBuilder.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/actions/ManyToOneAction.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/actions/OneToManyAction.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/actions/OneToOneAction.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/connections/AutoConnectible.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/connections/Connectible.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/connections/Connection.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/connections/ConnectionFactory.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/connections/ConnectionHelper.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/connections/ConnectsTo.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/connections/NamedValueListener.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/connections/RoleDef.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/connections/RoleDefs.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/connections/ValueListener.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/description/NodeDef.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/description/NodeDefs.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/description/TypedActionDef.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/description/ValueDef.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/description/ValueDefs.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/events/Event.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/events/EventBuilder.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/events/EventHandler.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/events/EventTransmitter.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/exceptions/AnonymousNotAlowedException.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/exceptions/ChainPathNotSupportedException.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/exceptions/ConfigurationException.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/exceptions/ContentException.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/exceptions/ContextLockException.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/exceptions/DataFormatException.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/exceptions/DescriptionNotFoundException.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/exceptions/DescriptorException.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/exceptions/DuplicateDescriptionException.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/exceptions/EnvelopeFormatException.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/exceptions/EnvelopeTargetNotFoundException.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/exceptions/NameNotFoundException.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/exceptions/NamingException.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/exceptions/NonEmptyMetaMorphException.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/exceptions/NotConnectedException.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/exceptions/NotDefinedException.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/exceptions/PathSyntaxException.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/exceptions/TargetNotProvidedException.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/exceptions/ValueConversionException.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/goals/AbstractGoal.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/goals/GeneratorGoal.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/goals/Goal.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/goals/GoalListener.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/goals/MultiInputGoal.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/goals/PipeGoal.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/goals/StaticGoal.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/io/ColumnedDataReader.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/io/ColumnedDataWriter.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/io/IOUtils.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/io/LineIterator.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/io/LogbackConfigurator.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/io/MetaStreamReader.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/io/MetaStreamWriter.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/io/XMLMetaReader.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/io/XMLMetaWriter.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/io/history/Chronicle.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/io/history/History.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/io/history/Record.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/meta/ConfigChangeListener.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/meta/Configurable.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/meta/ConfigurableMergeRule.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/meta/Configuration.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/meta/CustomMergeRule.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/meta/DelegateConfigChangeListener.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/meta/JoinRule.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/meta/ListMergeRule.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/meta/MergeRule.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/meta/Meta.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/meta/MetaBuilder.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/meta/MetaNode.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/meta/MetaParser.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/meta/MetaProvider.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/meta/MetaUtils.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/meta/MutableMetaNode.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/meta/ReplaceRule.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/meta/SealedNode.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/meta/SimpleConfigurable.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/meta/Template.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/names/AbstractNamedSet.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/names/AlphanumComparator.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/names/AnonymousNotAlowed.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/names/NameSetContainer.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/names/NamedMetaHolder.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/names/NamesUtils.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/providers/Path.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/providers/PathSegment.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/providers/Provider.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/providers/Providers.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/providers/Provides.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/providers/ProvidesNames.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/providers/SegmentedPath.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/states/MetaStateDef.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/states/MetaStateDefs.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/states/StateDef.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/states/StateDefs.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/tables/Adapters.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/tables/BasicAdapter.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/tables/Filtering.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/tables/MaskPoint.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/tables/NavigableValuesSource.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/tables/ReadPointSetAction.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/tables/SimpleValuesSource.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/tables/TableFormat.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/tables/TableFormatBuilder.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/tables/TransformTableAction.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/tables/ValuesAdapter.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/tables/ValuesListener.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/tables/VirtualDataPoint.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/tables/XYPoissonAdapter.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/utils/ArgumentChecker.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/utils/ContextMetaFactory.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/utils/DateTimeUtils.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/utils/GenericBuilder.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/utils/InvocationTarget.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/utils/MetaFactory.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/utils/Misc.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/utils/MultiCounter.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/utils/NamingUtils.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/utils/Optionals.java create mode 100644 dataforge-core/src/main/java/hep/dataforge/utils/ReferenceRegistry.java create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/CoreExtensions.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/Misc.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/Named.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/NumberExtensions.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/Types.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/Utils.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/actions/Action.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/actions/GenericAction.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/actions/KAction.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/cache/CachePlugin.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/cache/DataCacheException.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/cache/DefaultCache.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/cache/DefaultCacheManager.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/cache/DefaultCachingProvider.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/cache/MetaCacheConfiguration.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/context/BasicPlugin.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/context/Chronicler.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/context/Context.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/context/ContextAware.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/context/ContextBuilder.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/context/ContextLock.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/context/ExecutorPlugin.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/context/FileReference.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/context/Global.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/context/Plugin.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/context/PluginDef.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/context/PluginLoader.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/context/PluginManager.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/context/PluginTag.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/data/CheckedDataNode.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/data/CustomDataFilter.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/data/Data.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/data/DataFactory.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/data/DataFilter.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/data/DataLoader.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/data/DataNode.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/data/DataSet.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/data/DataState.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/data/DataTree.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/data/DataUtils.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/data/EmptyDataNode.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/data/FileDataFactory.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/data/FilteredDataNode.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/data/NamedData.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/data/NodeWrapper.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/data/SmartDataLoader.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/data/binary/Binary.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/data/binary/BufferedBinary.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/data/binary/FileBinary.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/data/binary/StreamBinary.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/description/ActionDescriptor.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/description/Described.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/description/Description.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/description/DescriptorBuilder.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/description/Descriptors.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/description/NodeDescriptor.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/description/ValueDescriptor.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/goals/Coal.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/goals/GoalGroup.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/io/AbstractOutputManager.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/io/BufferChannel.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/io/DirectoryOutput.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/io/MetaFileReader.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/io/OutputManager.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/BinaryMetaType.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/DefaultEnvelopeReader.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/DefaultEnvelopeType.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/DefaultEnvelopeWriter.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/Envelope.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/EnvelopeBuilder.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/EnvelopeReader.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/EnvelopeTag.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/EnvelopeType.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/EnvelopeWriter.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/JavaObjectWrapper.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/LazyEnvelope.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/MetaType.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/SimpleEnvelope.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/TaglessEnvelopeType.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/Wrapper.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/XMLMetaType.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/io/output/Output.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/io/output/StreamConsumer.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/io/output/TextOutput.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/meta/KMetaBuilder.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/meta/Laminate.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/meta/MetaDelegates.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/meta/MetaHolder.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/meta/MetaMorph.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/meta/Metoid.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/names/CompositeName.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/names/EmptyName.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/names/Name.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/names/NameList.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/names/NameToken.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/states/State.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/states/StateAnnotations.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/states/Stateful.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/tables/Column.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/tables/ColumnFormat.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/tables/ColumnTable.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/tables/ListColumn.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/tables/ListOfPoints.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/tables/ListTable.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/tables/MetaTableFormat.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/tables/Table.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/tables/Tables.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/tables/ValuesParser.java create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/tables/ValuesReader.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/tables/ValuesSource.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/values/AbstractValue.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/values/BinaryValue.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/values/BooleanValue.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/values/LateParseValue.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/values/LazyValue.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/values/ListValue.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/values/MapValueProvider.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/values/NamedValue.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/values/NumberValue.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/values/StringValue.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/values/SubstProvider.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/values/TimeValue.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/values/Value.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/values/ValueMap.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/values/ValueProvider.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/values/ValueUtils.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/values/Values.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/workspace/AbstractWorkspace.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/workspace/ActiveWorkspace.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/workspace/BasicWorkspace.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/workspace/DynamicWorkspace.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/workspace/FileBasedWorkspace.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/workspace/Workspace.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/workspace/WorkspaceParser.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/workspace/tasks/AbstractTask.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/workspace/tasks/KTask.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/workspace/tasks/MultiStageTask.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/workspace/tasks/PipeTask.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/workspace/tasks/SingleActionTask.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/workspace/tasks/Task.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/workspace/tasks/TaskModel.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/workspace/templates/GatherTaskTemplate.kt create mode 100644 dataforge-core/src/main/kotlin/hep/dataforge/workspace/templates/TaskTemplate.kt create mode 100644 dataforge-core/src/main/resources/META-INF/services/ch.qos.logback.classic.spi.Configurator create mode 100644 dataforge-core/src/main/resources/META-INF/services/hep.dataforge.context.PluginFactory create mode 100644 dataforge-core/src/main/resources/META-INF/services/hep.dataforge.data.DataLoader create mode 100644 dataforge-core/src/main/resources/META-INF/services/hep.dataforge.io.envelopes.EnvelopeType create mode 100644 dataforge-core/src/main/resources/META-INF/services/hep.dataforge.io.envelopes.MetaType create mode 100644 dataforge-core/src/main/resources/META-INF/services/hep.dataforge.io.envelopes.Wrapper create mode 100644 dataforge-core/src/main/resources/META-INF/services/hep.dataforge.workspace.WorkspaceParser create mode 100644 dataforge-core/src/test/kotlin/hep/dataforge/description/DescriptorsTest.kt create mode 100644 dataforge-core/src/test/kotlin/hep/dataforge/goals/AbstractGoalTest.kt create mode 100644 dataforge-core/src/test/kotlin/hep/dataforge/io/XMLIOTest.groovy create mode 100644 dataforge-core/src/test/kotlin/hep/dataforge/io/envelopes/EnvelopeFormatSpec.groovy create mode 100644 dataforge-core/src/test/kotlin/hep/dataforge/io/envelopes/TaglessEnvelopeTest.kt create mode 100644 dataforge-core/src/test/kotlin/hep/dataforge/kodex/CoalTest.kt create mode 100644 dataforge-core/src/test/kotlin/hep/dataforge/kodex/KActionTest.kt create mode 100644 dataforge-core/src/test/kotlin/hep/dataforge/kodex/ListAnnotationsTest.kt create mode 100644 dataforge-core/src/test/kotlin/hep/dataforge/meta/LaminateTest.kt create mode 100644 dataforge-core/src/test/kotlin/hep/dataforge/meta/MetaMorphTest.kt create mode 100644 dataforge-core/src/test/kotlin/hep/dataforge/meta/MetaTest.kt create mode 100644 dataforge-core/src/test/kotlin/hep/dataforge/meta/MetaUtilsTest.groovy create mode 100644 dataforge-core/src/test/kotlin/hep/dataforge/meta/MutableAnnotationSpec.groovy create mode 100644 dataforge-core/src/test/kotlin/hep/dataforge/meta/TemplateTest.kt create mode 100644 dataforge-core/src/test/kotlin/hep/dataforge/names/NameTest.kt create mode 100644 dataforge-core/src/test/kotlin/hep/dataforge/states/StateHolderTest.kt create mode 100644 dataforge-core/src/test/kotlin/hep/dataforge/states/StateTest.kt create mode 100644 dataforge-core/src/test/kotlin/hep/dataforge/tables/TablesKtTest.kt create mode 100644 dataforge-core/src/test/kotlin/hep/dataforge/values/ValueUtilsTest.groovy create mode 100644 dataforge-core/src/test/kotlin/hep/dataforge/workspace/WorkspaceTest.kt create mode 100644 dataforge-core/src/test/resources/meta/template.xml create mode 100644 dataforge-core/src/test/resources/meta/templateData.xml create mode 100644 dataforge-gui/build.gradle create mode 100644 dataforge-gui/dataforge-html/build.gradle create mode 100644 dataforge-gui/dataforge-html/src/main/kotlin/hep/dataforge/io/HTMLOutput.kt create mode 100644 dataforge-gui/dataforge-html/src/test/kotlin/hep/dataforge/io/HTMLOutputTest.kt create mode 100644 dataforge-gui/gui-demo/build.gradle create mode 100644 dataforge-gui/gui-demo/src/main/kotlin/hep/dataforge/plots/demo/DemoApp.kt create mode 100644 dataforge-gui/gui-demo/src/main/kotlin/hep/dataforge/plots/demo/DemoView.kt create mode 100644 dataforge-gui/gui-demo/src/main/kotlin/hep/dataforge/plots/demo/JFreeChartTest.kt create mode 100644 dataforge-gui/gui-demo/src/main/kotlin/hep/dataforge/plots/demo/JFreeFXTest.java create mode 100644 dataforge-gui/gui-demo/src/main/kotlin/hep/dataforge/plots/demo/PlotTest.kt create mode 100644 dataforge-gui/gui-demo/src/main/kotlin/hep/dataforge/plots/demo/SerializationTest.kt create mode 100644 dataforge-gui/gui-demo/src/main/kotlin/hep/dataforge/plots/demo/TableDisplayTest.kt create mode 100644 dataforge-gui/gui-workspace/build.gradle create mode 100644 dataforge-gui/gui-workspace/src/main/kotlin/hep/dataforge/fx/workspace/ContextEditorView.kt create mode 100644 dataforge-gui/src/main/kotlin/hep/dataforge/fx/ClipBoardTools.kt create mode 100644 dataforge-gui/src/main/kotlin/hep/dataforge/fx/FXPlugin.kt create mode 100644 dataforge-gui/src/main/kotlin/hep/dataforge/fx/KodexFX.kt create mode 100644 dataforge-gui/src/main/kotlin/hep/dataforge/fx/StatesFX.kt create mode 100644 dataforge-gui/src/main/kotlin/hep/dataforge/fx/fragments/LogFragment.kt create mode 100644 dataforge-gui/src/main/kotlin/hep/dataforge/fx/meta/ConfigEditor.kt create mode 100644 dataforge-gui/src/main/kotlin/hep/dataforge/fx/meta/ConfigFX.kt create mode 100644 dataforge-gui/src/main/kotlin/hep/dataforge/fx/meta/ConfigValueProperty.kt create mode 100644 dataforge-gui/src/main/kotlin/hep/dataforge/fx/meta/MetaViewer.kt create mode 100644 dataforge-gui/src/main/kotlin/hep/dataforge/fx/output/FXDisplay.kt create mode 100644 dataforge-gui/src/main/kotlin/hep/dataforge/fx/output/FXDumbOutput.kt create mode 100644 dataforge-gui/src/main/kotlin/hep/dataforge/fx/output/FXOuptutManager.kt create mode 100644 dataforge-gui/src/main/kotlin/hep/dataforge/fx/output/FXOutput.kt create mode 100644 dataforge-gui/src/main/kotlin/hep/dataforge/fx/output/FXTextOutput.kt create mode 100644 dataforge-gui/src/main/kotlin/hep/dataforge/fx/output/FXWebOutput.kt create mode 100644 dataforge-gui/src/main/kotlin/hep/dataforge/fx/plots/PlotContainer.kt create mode 100644 dataforge-gui/src/main/kotlin/hep/dataforge/fx/plots/PlotExtensions.kt create mode 100644 dataforge-gui/src/main/kotlin/hep/dataforge/fx/table/MutableTable.kt create mode 100644 dataforge-gui/src/main/kotlin/hep/dataforge/fx/table/MutableValues.kt create mode 100644 dataforge-gui/src/main/kotlin/hep/dataforge/fx/table/TableDisplay.kt create mode 100644 dataforge-gui/src/main/kotlin/hep/dataforge/fx/test/FXOutputManagerTest.kt create mode 100644 dataforge-gui/src/main/kotlin/hep/dataforge/fx/test/FXOutputPaneTest.kt create mode 100644 dataforge-gui/src/main/kotlin/hep/dataforge/fx/test/FXWebViewTest.kt create mode 100644 dataforge-gui/src/main/kotlin/hep/dataforge/fx/test/MetaEditorTest.kt create mode 100644 dataforge-gui/src/main/kotlin/hep/dataforge/fx/test/MetaViewerTest.kt create mode 100644 dataforge-gui/src/main/kotlin/hep/dataforge/fx/values/ColorValueChooser.kt create mode 100644 dataforge-gui/src/main/kotlin/hep/dataforge/fx/values/ComboBoxValueChooser.kt create mode 100644 dataforge-gui/src/main/kotlin/hep/dataforge/fx/values/TextValueChooser.kt create mode 100644 dataforge-gui/src/main/kotlin/hep/dataforge/fx/values/ValueCallback.kt create mode 100644 dataforge-gui/src/main/kotlin/hep/dataforge/fx/values/ValueChooser.kt create mode 100644 dataforge-gui/src/main/kotlin/hep/dataforge/fx/values/ValueChooserBase.kt create mode 100644 dataforge-gui/src/main/resources/META-INF/services/hep.dataforge.context.PluginFactory create mode 100644 dataforge-gui/src/main/resources/img/df.png create mode 100644 dataforge-gui/src/test/kotlin/hep/dataforge/fx/output/FXWebOutputTest.kt create mode 100644 dataforge-maths/build.gradle create mode 100644 dataforge-maths/src/jmh/kotlin/hep/dataforge/maths/DSNumberBenchmark.kt create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/GridCalculator.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/Interpolation.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/MathUtils.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/MatrixOperations.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/MultivariateUniformDistribution.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/NamedMatrix.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/NamedVector.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/domains/Domain.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/domains/HyperSquareDomain.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/domains/RangeDomain.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/domains/UnconstrainedDomain.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/domains/UnivariateDomain.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/functions/FunctionCaching.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/functions/FunctionFactories.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/functions/MultiFactory.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/functions/MultiFunction.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/functions/UniFunction.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/functions/UnivariateSplineWrapper.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/histogram/Bin.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/histogram/BinFactory.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/histogram/Histogram.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/histogram/NamedHistogram.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/histogram/SimpleHistogram.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/histogram/SquareBin.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/histogram/UniformBinFactory.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/histogram/UnivariateHistogram.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/integration/CMIntegrand.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/integration/DistributionSampler.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/integration/GaussRuleIntegrator.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/integration/IncompatibleIntegrandException.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/integration/Integrand.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/integration/Integrator.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/integration/IntegratorFactory.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/integration/IterativeUnivariateIntegrator.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/integration/MonteCarloIntegrand.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/integration/MonteCarloIntegrator.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/integration/RiemanIntegrator.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/integration/Sample.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/integration/Sampler.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/integration/SplitUnivariateIntegrator.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/integration/UnivariateIntegrand.java create mode 100644 dataforge-maths/src/main/java/hep/dataforge/maths/integration/UnivariateIntegrator.java create mode 100644 dataforge-maths/src/main/kotlin/hep/dataforge/maths/chain/Chain.kt create mode 100644 dataforge-maths/src/main/kotlin/hep/dataforge/maths/expressions/DSNumber.kt create mode 100644 dataforge-maths/src/main/kotlin/hep/dataforge/maths/expressions/Expression.kt create mode 100644 dataforge-maths/src/main/kotlin/hep/dataforge/maths/expressions/Field.kt create mode 100644 dataforge-maths/src/main/kotlin/hep/dataforge/maths/extensions/MathExtensions.kt create mode 100644 dataforge-maths/src/main/kotlin/hep/dataforge/maths/extensions/RealFieldExtensions.kt create mode 100644 dataforge-maths/src/main/kotlin/hep/dataforge/maths/functions/FunctionLibrary.kt create mode 100644 dataforge-maths/src/main/resources/META-INF/services/hep.dataforge.context.PluginFactory create mode 100644 dataforge-maths/src/test/java/hep/dataforge/maths/NamedMatrixTest.groovy create mode 100644 dataforge-maths/src/test/java/hep/dataforge/maths/NamedVectorTest.groovy create mode 100644 dataforge-maths/src/test/java/hep/dataforge/maths/expressions/DSFieldTest.kt create mode 100644 dataforge-maths/src/test/java/hep/dataforge/maths/histogram/HistogramTest.groovy create mode 100644 dataforge-maths/src/test/java/hep/dataforge/maths/integration/MonteCarloIntegratorTest.groovy create mode 100644 dataforge-maths/src/test/java/hep/dataforge/maths/integration/RiemanIntegratorTest.java create mode 100644 dataforge-plots/build.gradle create mode 100644 dataforge-plots/plots-jfc/build.gradle create mode 100644 dataforge-plots/plots-jfc/src/main/kotlin/hep/dataforge/plots/jfreechart/FXPlotUtils.kt create mode 100644 dataforge-plots/plots-jfc/src/main/kotlin/hep/dataforge/plots/jfreechart/JFCDataWrapper.kt create mode 100644 dataforge-plots/plots-jfc/src/main/kotlin/hep/dataforge/plots/jfreechart/JFreeChartFrame.kt create mode 100644 dataforge-plots/plots-jfc/src/main/kotlin/hep/dataforge/plots/jfreechart/JFreeChartPlugin.kt create mode 100644 dataforge-plots/plots-jfc/src/main/resources/META-INF/services/hep.dataforge.context.PluginFactory create mode 100644 dataforge-plots/plots-viewer/build.gradle create mode 100644 dataforge-plots/plots-viewer/src/main/kotlin/hep/dataforge/plots/viewer/PlotView.kt create mode 100644 dataforge-plots/plots-viewer/src/main/kotlin/hep/dataforge/plots/viewer/ViewerApp.kt create mode 100644 dataforge-plots/plots-viewer/src/main/resources/fxml/PlotViewer.fxml create mode 100644 dataforge-plots/plots-viewer/src/main/resources/fxml/styles/plotviewer.css create mode 100644 dataforge-plots/src/main/kotlin/hep/dataforge/plots/AbstractPlotFrame.kt create mode 100644 dataforge-plots/src/main/kotlin/hep/dataforge/plots/FXPlotFrame.kt create mode 100644 dataforge-plots/src/main/kotlin/hep/dataforge/plots/Plot.kt create mode 100644 dataforge-plots/src/main/kotlin/hep/dataforge/plots/PlotBuilder.kt create mode 100644 dataforge-plots/src/main/kotlin/hep/dataforge/plots/PlotDataAction.kt create mode 100644 dataforge-plots/src/main/kotlin/hep/dataforge/plots/PlotFrame.kt create mode 100644 dataforge-plots/src/main/kotlin/hep/dataforge/plots/PlotGroup.kt create mode 100644 dataforge-plots/src/main/kotlin/hep/dataforge/plots/PlotListener.kt create mode 100644 dataforge-plots/src/main/kotlin/hep/dataforge/plots/PlotUtils.kt create mode 100644 dataforge-plots/src/main/kotlin/hep/dataforge/plots/Plottable.kt create mode 100644 dataforge-plots/src/main/kotlin/hep/dataforge/plots/VirtualPlotFrame.kt create mode 100644 dataforge-plots/src/main/kotlin/hep/dataforge/plots/XYPlotFrame.kt create mode 100644 dataforge-plots/src/main/kotlin/hep/dataforge/plots/data/AbstractPlot.kt create mode 100644 dataforge-plots/src/main/kotlin/hep/dataforge/plots/data/DataPlot.kt create mode 100644 dataforge-plots/src/main/kotlin/hep/dataforge/plots/data/DataPlotUtils.kt create mode 100644 dataforge-plots/src/main/kotlin/hep/dataforge/plots/data/TimePlot.kt create mode 100644 dataforge-plots/src/main/kotlin/hep/dataforge/plots/data/XYFunctionPlot.kt create mode 100644 dataforge-plots/src/main/kotlin/hep/dataforge/plots/data/XYPlot.kt create mode 100644 dataforge-plots/src/main/kotlin/hep/dataforge/plots/output/PlotOutput.kt create mode 100644 dataforge-plots/src/main/resources/META-INF/services/hep.dataforge.io.envelopes.Wrapper create mode 100644 dataforge-plots/src/test/groovy/hep/dataforge/plots/data/TimePlottableTest.groovy create mode 100644 dataforge-stat/build.gradle create mode 100644 dataforge-stat/dataforge-minuit/build.gradle create mode 100644 dataforge-stat/dataforge-minuit/licenseheader.txt create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/AnalyticalGradientCalculator.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/CombinedMinimizer.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/CombinedMinimumBuilder.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/ContoursError.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/DavidonErrorUpdator.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/FunctionGradient.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/FunctionMinimum.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/GradientCalculator.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/HessianGradientCalculator.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/InitialGradientCalculator.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MinimumBuilder.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MinimumError.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MinimumErrorUpdator.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MinimumParameters.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MinimumSeed.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MinimumSeedGenerator.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MinimumState.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MinosError.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MinuitParameter.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MnAlgebraicSymMatrix.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MnApplication.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MnContours.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MnCovarianceSqueeze.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MnCross.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MnEigen.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MnFcn.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MnFunctionCross.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MnGlobalCorrelationCoeff.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MnHesse.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MnLineSearch.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MnMachinePrecision.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MnMigrad.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MnMinimize.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MnMinos.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MnParabola.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MnParabolaFactory.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MnParabolaPoint.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MnParameterScan.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MnPlot.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MnPosDef.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MnPrint.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MnScan.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MnSeedGenerator.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MnSimplex.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MnStrategy.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MnUserCovariance.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MnUserFcn.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MnUserParameterState.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MnUserParameters.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MnUserTransformation.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/MnUtils.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/ModularFunctionMinimizer.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/NegativeG2LineSearch.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/Numerical2PGradientCalculator.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/Range.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/ScanBuilder.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/ScanMinimizer.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/SimplexBuilder.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/SimplexMinimizer.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/SimplexParameters.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/SimplexSeedGenerator.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/SinParameterTransformation.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/SqrtLowParameterTransformation.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/SqrtUpParameterTransformation.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/VariableMetricBuilder.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/VariableMetricEDMEstimator.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/VariableMetricMinimizer.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/MINUIT/package-info.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/stat/fit/MINOSResult.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/stat/fit/MINUITFitter.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/stat/fit/MINUITPlugin.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/java/hep/dataforge/stat/fit/MINUITUtils.java create mode 100644 dataforge-stat/dataforge-minuit/src/main/resources/META-INF/services/hep.dataforge.context.PluginFactory create mode 100644 dataforge-stat/dataforge-minuit/src/test/java/hep/dataforge/fitting/GaussianSpectrum.java create mode 100644 dataforge-stat/dataforge-minuit/src/test/java/hep/dataforge/fitting/TestFitting.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/io/FittingIOUtils.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/io/PrintFunction.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/fit/BasicIntervalEstimate.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/fit/FitAction.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/fit/FitHelper.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/fit/FitResult.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/fit/FitStage.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/fit/FitState.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/fit/Hessian.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/fit/IntervalEstimate.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/fit/Param.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/fit/ParamSet.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/fit/UpperLimitGenerator.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/likelihood/AnalyticalPosteriorLogLike.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/likelihood/BayesianConfidenceLimit.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/likelihood/ConfidenceLimitCalculator.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/likelihood/Gaussian.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/likelihood/LogGaussian.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/likelihood/LogLikelihood.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/likelihood/MarginalFunctionBuilder.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/likelihood/MultivariateDomainPrior.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/likelihood/MultivariateGaussianPrior.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/likelihood/OneSidedUniformPrior.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/likelihood/TwoSidedUniformPrior.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/models/AbstractModel.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/models/Generator.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/models/HistogramModel.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/models/Model.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/models/ModelFactory.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/models/ModelLibrary.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/models/WeightedXYModel.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/models/XYModel.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/parametric/AbstractParametric.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/parametric/AbstractParametricBiFunction.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/parametric/AbstractParametricFunction.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/parametric/AbstractParametricValue.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/parametric/DerivativeCalculator.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/parametric/ParametricBiFunction.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/parametric/ParametricFunction.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/parametric/ParametricMultiFunctionWrapper.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/parametric/ParametricUtils.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/parametric/ParametricValue.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/simulation/GaussianParameterGenerator.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/simulation/ParameterGenerator.java create mode 100644 dataforge-stat/src/main/java/hep/dataforge/stat/simulation/SimulationUtils.java create mode 100644 dataforge-stat/src/main/kotlin/hep/dataforge/stat/Distributions.kt create mode 100644 dataforge-stat/src/main/kotlin/hep/dataforge/stat/Kfit.kt create mode 100644 dataforge-stat/src/main/kotlin/hep/dataforge/stat/PolynomialDistribution.kt create mode 100644 dataforge-stat/src/main/kotlin/hep/dataforge/stat/Random.kt create mode 100644 dataforge-stat/src/main/kotlin/hep/dataforge/stat/Sampling.kt create mode 100644 dataforge-stat/src/main/kotlin/hep/dataforge/stat/UniformRandomVectorGenerator.kt create mode 100644 dataforge-stat/src/main/kotlin/hep/dataforge/stat/fit/CMFitter.kt create mode 100644 dataforge-stat/src/main/kotlin/hep/dataforge/stat/fit/FitManager.kt create mode 100644 dataforge-stat/src/main/kotlin/hep/dataforge/stat/fit/Fitter.kt create mode 100644 dataforge-stat/src/main/kotlin/hep/dataforge/stat/fit/QOWFitter.kt create mode 100644 dataforge-stat/src/main/kotlin/hep/dataforge/stat/fit/QOWUtils.kt create mode 100644 dataforge-stat/src/main/resources/META-INF/services/hep.dataforge.context.PluginFactory create mode 100644 dataforge-stat/src/main/resources/upperlimit.txt create mode 100644 dataforge-stat/src/test/java/hep/dataforge/stat/likelihood/ConfidenceLimitCalculatorTest.java create mode 100644 dataforge-stat/src/test/java/hep/dataforge/stat/likelihood/MarginalFunctionBuilderTest.java create mode 100644 dataforge-stat/src/test/java/hep/dataforge/stat/parametric/DerivativeCalculatorTest.java create mode 100644 dataforge-storage/build.gradle create mode 100644 dataforge-storage/src/main/kotlin/hep/dataforge/storage/Storage.kt create mode 100644 dataforge-storage/src/main/kotlin/hep/dataforge/storage/StorageConnection.kt create mode 100644 dataforge-storage/src/main/kotlin/hep/dataforge/storage/StorageManager.kt create mode 100644 dataforge-storage/src/main/kotlin/hep/dataforge/storage/files/FileCollectionLoader.kt create mode 100644 dataforge-storage/src/main/kotlin/hep/dataforge/storage/files/FileEnvelope.kt create mode 100644 dataforge-storage/src/main/kotlin/hep/dataforge/storage/files/FileStorage.kt create mode 100644 dataforge-storage/src/main/kotlin/hep/dataforge/storage/files/FileTableLoader.kt create mode 100644 dataforge-storage/src/main/kotlin/hep/dataforge/storage/tables/TableLoader.kt create mode 100644 dataforge-storage/src/main/resources/META-INF/services/hep.dataforge.context.PluginFactory create mode 100644 dataforge-storage/src/main/resources/META-INF/services/hep.dataforge.storage.StorageElementType create mode 100644 dataforge-storage/src/test/kotlin/hep.dataforge.storage/TableLoaderTest.kt create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 grind/build.gradle create mode 100644 grind/grind-terminal/build.gradle create mode 100644 grind/grind-terminal/src/main/groovy/hep/dataforge/grind/demo/WorkspaceDemo.groovy create mode 100644 grind/grind-terminal/src/main/groovy/hep/dataforge/grind/helpers/PlotHelper.groovy create mode 100644 grind/grind-terminal/src/main/groovy/hep/dataforge/grind/helpers/PlotHelperFactory.groovy create mode 100644 grind/grind-terminal/src/main/groovy/hep/dataforge/grind/terminal/GrindTerminal.groovy create mode 100644 grind/grind-terminal/src/main/groovy/hep/dataforge/grind/terminal/RunDumbGrindShell.groovy create mode 100644 grind/grind-terminal/src/main/groovy/hep/dataforge/grind/terminal/RunGrindShell.groovy create mode 100644 grind/grind-terminal/src/main/groovy/hep/dataforge/grind/terminal/TerminalLogLayout.groovy create mode 100644 grind/grind-terminal/src/main/resources/META-INF/services/hep.dataforge.grind.helpers.GrindHelperFactory create mode 100644 grind/groovymath/README.md create mode 100644 grind/groovymath/build.gradle create mode 100644 grind/groovymath/src/main/groovy/hep/dataforge/maths/GM.groovy create mode 100644 grind/groovymath/src/main/groovy/hep/dataforge/maths/HistogramBuilder.groovy create mode 100644 grind/groovymath/src/main/groovy/hep/dataforge/maths/extensions/NumberExtension.groovy create mode 100644 grind/groovymath/src/main/groovy/hep/dataforge/maths/extensions/RealMatrixExtension.groovy create mode 100644 grind/groovymath/src/main/groovy/hep/dataforge/maths/extensions/RealVectorExtension.groovy create mode 100644 grind/groovymath/src/main/groovy/hep/dataforge/maths/extensions/UnivariateDifferentiableFunctionExtension.groovy create mode 100644 grind/groovymath/src/main/groovy/hep/dataforge/maths/extensions/UnivariateFunctionExtension.groovy create mode 100644 grind/groovymath/src/main/groovy/hep/dataforge/maths/tables/GColumn.groovy create mode 100644 grind/groovymath/src/main/groovy/hep/dataforge/maths/tables/GRow.groovy create mode 100644 grind/groovymath/src/main/groovy/hep/dataforge/maths/tables/GTable.groovy create mode 100644 grind/groovymath/src/main/groovy/hep/dataforge/maths/tables/MapRow.groovy create mode 100644 grind/groovymath/src/main/groovy/hep/dataforge/maths/tables/TableRow.groovy create mode 100644 grind/groovymath/src/main/groovy/hep/dataforge/maths/tables/ValueType.groovy create mode 100644 grind/groovymath/src/main/resources/META-INF/services/org.codehaus.groovy.runtime.ExtensionModule create mode 100644 grind/groovymath/src/test/groovy/hep/dataforge/maths/GMTest.groovy create mode 100644 grind/groovymath/src/test/groovy/hep/dataforge/maths/extensions/FunctionExensionTest.groovy create mode 100644 grind/groovymath/src/test/groovy/hep/dataforge/maths/extensions/NumberExtensionTest.groovy create mode 100644 grind/groovymath/src/test/groovy/hep/dataforge/maths/extensions/RealMatrixExtensionTest.groovy create mode 100644 grind/groovymath/src/test/groovy/hep/dataforge/maths/extensions/RealVectorExtensionTest.groovy create mode 100644 grind/groovymath/src/test/groovy/hep/dataforge/maths/tables/GColumnTest.groovy create mode 100644 grind/groovymath/src/test/groovy/hep/dataforge/maths/tables/GTableTest.groovy create mode 100644 grind/src/main/groovy/hep/dataforge/grind/Grind.groovy create mode 100644 grind/src/main/groovy/hep/dataforge/grind/GrindMetaBuilder.groovy create mode 100644 grind/src/main/groovy/hep/dataforge/grind/GrindShell.groovy create mode 100644 grind/src/main/groovy/hep/dataforge/grind/extensions/CoreExtension.groovy create mode 100644 grind/src/main/groovy/hep/dataforge/grind/extensions/ExtensionInitializer.groovy create mode 100644 grind/src/main/groovy/hep/dataforge/grind/helpers/AbstractHelper.groovy create mode 100644 grind/src/main/groovy/hep/dataforge/grind/helpers/GrindHelper.groovy create mode 100644 grind/src/main/groovy/hep/dataforge/grind/helpers/GrindHelperFactory.groovy create mode 100644 grind/src/main/groovy/hep/dataforge/grind/helpers/MethodDescription.groovy create mode 100644 grind/src/main/groovy/hep/dataforge/grind/helpers/WorkspaceHelper.groovy create mode 100644 grind/src/main/groovy/hep/dataforge/grind/helpers/WorkspaceHelperFactory.groovy create mode 100644 grind/src/main/groovy/hep/dataforge/grind/misc/ReWrapper.groovy create mode 100644 grind/src/main/groovy/hep/dataforge/grind/workspace/ContextSpec.groovy create mode 100644 grind/src/main/groovy/hep/dataforge/grind/workspace/DataNodeSpec.groovy create mode 100644 grind/src/main/groovy/hep/dataforge/grind/workspace/DefaultTaskLib.groovy create mode 100644 grind/src/main/groovy/hep/dataforge/grind/workspace/ExecSpec.groovy create mode 100644 grind/src/main/groovy/hep/dataforge/grind/workspace/GrindWorkspace.groovy create mode 100644 grind/src/main/groovy/hep/dataforge/grind/workspace/GroovyWorkspaceParser.groovy create mode 100644 grind/src/main/groovy/hep/dataforge/grind/workspace/WorkspaceSpec.groovy create mode 100644 grind/src/main/kotlin/hep/dataforge/grind/workspace/GrindTaskBuilder.kt create mode 100644 grind/src/main/resources/META-INF/services/hep.dataforge.grind.helpers.GrindHelperFactory create mode 100644 grind/src/main/resources/META-INF/services/hep.dataforge.workspace.WorkspaceParser create mode 100644 grind/src/main/resources/META-INF/services/org.codehaus.groovy.runtime.ExtensionModule create mode 100644 grind/src/test/groovy/hep/dataforge/grind/GrindMetaBuilderSpec.groovy create mode 100644 grind/src/test/groovy/hep/dataforge/grind/GrindWorkspaceBuilderTest.groovy create mode 100644 grind/src/test/groovy/hep/dataforge/grind/TestTask.groovy create mode 100644 grind/src/test/groovy/hep/dataforge/grind/WorkspaceSpecTest.groovy create mode 100644 grind/src/test/groovy/hep/dataforge/grind/extensions/CoreExtensionTest.groovy create mode 100644 grind/src/test/groovy/hep/dataforge/grind/extensions/ValueExtensionTest.groovy create mode 100644 grind/src/test/groovy/hep/dataforge/grind/workspace/ExecTest.groovy create mode 100644 grind/src/test/groovy/hep/dataforge/grind/workspace/WorkspaceTest.groovy create mode 100644 grind/src/test/resources/workspace/test.py create mode 100644 grind/src/test/resources/workspace/workspace.groovy create mode 100644 numass-control/build.gradle create mode 100644 numass-control/control-room/build.gradle create mode 100644 numass-control/control-room/build/resources/main/config/control-real.xml create mode 100644 numass-control/control-room/build/resources/main/config/control.xml create mode 100644 numass-control/control-room/build/scripts/control-room create mode 100644 numass-control/control-room/build/scripts/control-room-devices create mode 100644 numass-control/control-room/build/scripts/control-room-devices.bat create mode 100644 numass-control/control-room/build/scripts/control-room.bat create mode 100644 numass-control/control-room/out/production/resources/config/control-real.xml create mode 100644 numass-control/control-room/out/production/resources/config/control.xml create mode 100644 numass-control/control-room/src/main/kotlin/inr/numass/control/BoardController.kt create mode 100644 numass-control/control-room/src/main/kotlin/inr/numass/control/BoardView.kt create mode 100644 numass-control/control-room/src/main/kotlin/inr/numass/control/ServerApp.kt create mode 100644 numass-control/control-room/src/main/resources/config/control-real.xml create mode 100644 numass-control/control-room/src/main/resources/config/control.xml create mode 100644 numass-control/cryotemp/build.gradle create mode 100644 numass-control/cryotemp/src/main/kotlin/inr/numass/control/cryotemp/PKT8App.kt create mode 100644 numass-control/cryotemp/src/main/kotlin/inr/numass/control/cryotemp/PKT8Channel.kt create mode 100644 numass-control/cryotemp/src/main/kotlin/inr/numass/control/cryotemp/PKT8Device.kt create mode 100644 numass-control/cryotemp/src/main/kotlin/inr/numass/control/cryotemp/PKT8DeviceFactory.kt create mode 100644 numass-control/cryotemp/src/main/kotlin/inr/numass/control/cryotemp/PKT8Display.kt create mode 100644 numass-control/cryotemp/src/main/kotlin/inr/numass/control/cryotemp/PKT8VirtualPort.kt create mode 100644 numass-control/cryotemp/src/main/resources/META-INF/services/hep.dataforge.control.devices.DeviceFactory create mode 100644 numass-control/cryotemp/src/main/resources/config-debug/devices.xml create mode 100644 numass-control/cryotemp/src/main/resources/config/devices.xml create mode 100644 numass-control/cryotemp/src/main/resources/fxml/PKT8Indicator.fxml create mode 100644 numass-control/cryotemp/src/main/resources/fxml/PKT8Plot.fxml create mode 100644 numass-control/dante/build.gradle create mode 100644 numass-control/dante/src/main/kotlin/inr/numass/control/dante/DanteClient.kt create mode 100644 numass-control/dante/src/main/kotlin/inr/numass/control/dante/DanteTest.kt create mode 100644 numass-control/gun/build.gradle create mode 100644 numass-control/gun/src/main/kotlin/inr/numass/control/gun/EGun.kt create mode 100644 numass-control/gun/src/main/kotlin/inr/numass/control/gun/EGunApplication.kt create mode 100644 numass-control/gun/src/main/kotlin/inr/numass/control/gun/EGunView.kt create mode 100644 numass-control/gun/src/main/kotlin/inr/numass/control/gun/IT6800Device.kt create mode 100644 numass-control/gun/src/main/resources/config/devices.xml create mode 100644 numass-control/magnet/build.gradle create mode 100644 numass-control/magnet/src/main/kotlin/inr/numass/control/magnet/LambdaHub.kt create mode 100644 numass-control/magnet/src/main/kotlin/inr/numass/control/magnet/LambdaMagnet.kt create mode 100644 numass-control/magnet/src/main/kotlin/inr/numass/control/magnet/LambdaPortController.kt create mode 100644 numass-control/magnet/src/main/kotlin/inr/numass/control/magnet/VirtualLambdaPort.kt create mode 100644 numass-control/magnet/src/main/kotlin/inr/numass/control/magnet/fx/MagnetApp.kt create mode 100644 numass-control/magnet/src/main/kotlin/inr/numass/control/magnet/fx/MagnetDisplay.kt create mode 100644 numass-control/magnet/src/main/kotlin/inr/numass/control/magnet/test/CLITest.kt create mode 100644 numass-control/magnet/src/main/kotlin/inr/numass/control/magnet/test/SetCurrent.kt create mode 100644 numass-control/magnet/src/main/resources/debug.xml create mode 100644 numass-control/magnet/src/main/resources/fxml/SingleMagnet.fxml create mode 100644 numass-control/magnet/src/main/resources/logback.xml create mode 100644 numass-control/magnet/src/test/kotlin/inr/numass/control/magnet/VirtualLambdaPortTest.kt create mode 100644 numass-control/msp/build.gradle create mode 100644 numass-control/msp/docs/ASCII Protocol User Manual SP1040016.100.pdf create mode 100644 numass-control/msp/docs/commands.htm create mode 100644 numass-control/msp/src/main/kotlin/inr/numass/control/msp/MspApp.kt create mode 100644 numass-control/msp/src/main/kotlin/inr/numass/control/msp/MspDevice.kt create mode 100644 numass-control/msp/src/main/kotlin/inr/numass/control/msp/MspDeviceFactory.kt create mode 100644 numass-control/msp/src/main/kotlin/inr/numass/control/msp/MspDisplay.kt create mode 100644 numass-control/msp/src/main/resources/META-INF/services/hep.dataforge.control.devices.DeviceFactory create mode 100644 numass-control/msp/src/main/resources/config/devices.xml create mode 100644 numass-control/msp/src/main/resources/fxml/MspView.fxml create mode 100644 numass-control/msp/src/test/java/inr/numass/control/msp/TestScanner.java create mode 100644 numass-control/src/main/kotlin/inr/numass/control/DeviceDisplayFX.kt create mode 100644 numass-control/src/main/kotlin/inr/numass/control/FXExtensions.kt create mode 100644 numass-control/src/main/kotlin/inr/numass/control/NumassControlApplication.kt create mode 100644 numass-control/src/main/kotlin/inr/numass/control/NumassControlUtils.kt create mode 100644 numass-control/src/main/kotlin/inr/numass/control/NumassStorageConnection.kt create mode 100644 numass-control/src/main/kotlin/inr/numass/control/StorageHelper.kt create mode 100644 numass-control/src/main/resources/img/df.png create mode 100644 numass-control/vac/build.gradle create mode 100644 numass-control/vac/src/main/kotlin/inr/numass/control/readvac/CM32Device.kt create mode 100644 numass-control/vac/src/main/kotlin/inr/numass/control/readvac/ConsoleVac.kt create mode 100644 numass-control/vac/src/main/kotlin/inr/numass/control/readvac/MKSBaratronDevice.kt create mode 100644 numass-control/vac/src/main/kotlin/inr/numass/control/readvac/MKSVacDevice.kt create mode 100644 numass-control/vac/src/main/kotlin/inr/numass/control/readvac/MeradatVacDevice.kt create mode 100644 numass-control/vac/src/main/kotlin/inr/numass/control/readvac/ReadVac.kt create mode 100644 numass-control/vac/src/main/kotlin/inr/numass/control/readvac/ThyroContVacDevice.kt create mode 100644 numass-control/vac/src/main/kotlin/inr/numass/control/readvac/VacCollectorDevice.kt create mode 100644 numass-control/vac/src/main/kotlin/inr/numass/control/readvac/VacCollectorDisplay.kt create mode 100644 numass-control/vac/src/main/kotlin/inr/numass/control/readvac/VacDeviceFactory.kt create mode 100644 numass-control/vac/src/main/kotlin/inr/numass/control/readvac/VacDisplay.kt create mode 100644 numass-control/vac/src/main/kotlin/inr/numass/control/readvac/test.kt create mode 100644 numass-control/vac/src/main/resources/META-INF/services/hep.dataforge.control.devices.DeviceFactory create mode 100644 numass-control/vac/src/main/resources/config-test/devices.xml create mode 100644 numass-control/vac/src/main/resources/config/devices.xml create mode 100644 numass-control/vac/src/test/java/inr/numass/control/readvac/TestMain.java create mode 100644 numass-control/vac/src/test/java/inr/numass/control/readvac/TestRemote.java create mode 100644 numass-control/vac/src/test/java/inr/numass/control/readvac/TimeShiftTest.java create mode 100644 numass-core/build.gradle create mode 100644 numass-core/docs/server_commands.md create mode 100644 numass-core/numass-data-api/build.gradle.kts create mode 100644 numass-core/numass-data-api/src/main/kotlin/inr/numass/data/api/MetaBlock.kt create mode 100644 numass-core/numass-data-api/src/main/kotlin/inr/numass/data/api/NumassBlock.kt create mode 100644 numass-core/numass-data-api/src/main/kotlin/inr/numass/data/api/NumassFrame.kt create mode 100644 numass-core/numass-data-api/src/main/kotlin/inr/numass/data/api/NumassPoint.kt create mode 100644 numass-core/numass-data-api/src/main/kotlin/inr/numass/data/api/NumassSet.kt create mode 100644 numass-core/numass-data-api/src/main/kotlin/inr/numass/data/api/SignalProcessor.kt create mode 100644 numass-core/numass-data-api/src/main/kotlin/inr/numass/data/api/SimpleNumassPoint.kt create mode 100644 numass-core/numass-data-proto/build.gradle.kts create mode 100644 numass-core/numass-data-proto/gen/main/java/inr/numass/data/NumassProto.java create mode 100644 numass-core/numass-data-proto/src/main/kotlin/inr/numass/data/NumassEnvelopeType.kt create mode 100644 numass-core/numass-data-proto/src/main/kotlin/inr/numass/data/NumassFileEnvelope.kt create mode 100644 numass-core/numass-data-proto/src/main/kotlin/inr/numass/data/ProtoNumassPoint.kt create mode 100644 numass-core/numass-data-proto/src/main/proto/inr/numas/numass-proto.proto create mode 100644 numass-core/numass-signal-processing/build.gradle.kts create mode 100644 numass-core/numass-signal-processing/src/main/kotlin/inr/numass/data/ChernovProcessor.kt create mode 100644 numass-core/numass-signal-processing/src/test/kotlin/inr/numass/data/ChernovProcessorTest.kt create mode 100644 numass-core/src/main/kotlin/inr/numass/NumassProperties.kt create mode 100644 numass-core/src/main/kotlin/inr/numass/data/NumassDataUtils.kt create mode 100644 numass-core/src/main/kotlin/inr/numass/data/SpectrumAdapter.kt create mode 100644 numass-core/src/main/kotlin/inr/numass/data/analyzers/AbstractAnalyzer.kt create mode 100644 numass-core/src/main/kotlin/inr/numass/data/analyzers/DebunchAnalyzer.kt create mode 100644 numass-core/src/main/kotlin/inr/numass/data/analyzers/NumassAnalyzer.kt create mode 100644 numass-core/src/main/kotlin/inr/numass/data/analyzers/SimpleAnalyzer.kt create mode 100644 numass-core/src/main/kotlin/inr/numass/data/analyzers/TimeAnalyzer.kt create mode 100644 numass-core/src/main/kotlin/inr/numass/data/legacy/NumassDatFile.kt create mode 100644 numass-core/src/main/kotlin/inr/numass/data/storage/ClassicNumassPoint.kt create mode 100644 numass-core/src/main/kotlin/inr/numass/data/storage/NumassDataFactory.kt create mode 100644 numass-core/src/main/kotlin/inr/numass/data/storage/NumassDataLoader.kt create mode 100644 numass-core/src/main/kotlin/inr/numass/data/storage/NumassStorage.kt create mode 100644 numass-core/src/main/resources/META-INF/services/hep.dataforge.data.DataLoader create mode 100644 numass-core/src/main/resources/META-INF/services/hep.dataforge.io.envelopes.EnvelopeType create mode 100644 numass-core/src/main/resources/META-INF/services/hep.dataforge.storage.StorageElementType create mode 100644 numass-main/build.gradle create mode 100644 numass-main/src/main/groovy/inr/numass/LaunchGrindShell.groovy create mode 100644 numass-main/src/main/groovy/inr/numass/RunTask.groovy create mode 100644 numass-main/src/main/groovy/inr/numass/data/PointAnalyzer.groovy create mode 100644 numass-main/src/main/groovy/inr/numass/scripts/CountRateSummary.groovy create mode 100644 numass-main/src/main/groovy/inr/numass/scripts/FindExIonRatio.groovy create mode 100644 numass-main/src/main/groovy/inr/numass/scripts/LossNormCalculation.groovy create mode 100644 numass-main/src/main/groovy/inr/numass/scripts/LossTailCalculation.groovy create mode 100644 numass-main/src/main/groovy/inr/numass/scripts/OldTest.groovy create mode 100644 numass-main/src/main/groovy/inr/numass/scripts/PrintLossFunctions.groovy create mode 100644 numass-main/src/main/groovy/inr/numass/scripts/ReadTextFile.groovy create mode 100644 numass-main/src/main/groovy/inr/numass/scripts/ShowSpectrum.groovy create mode 100644 numass-main/src/main/groovy/inr/numass/scripts/ShowTransmission.groovy create mode 100644 numass-main/src/main/groovy/inr/numass/scripts/SignificanceTest.groovy create mode 100644 numass-main/src/main/groovy/inr/numass/scripts/Simulate.groovy create mode 100644 numass-main/src/main/groovy/inr/numass/scripts/SimulateGun.groovy create mode 100644 numass-main/src/main/groovy/inr/numass/scripts/SimulatePileup.groovy create mode 100644 numass-main/src/main/groovy/inr/numass/scripts/SterileDemo.groovy create mode 100644 numass-main/src/main/groovy/inr/numass/scripts/SystTransmission.groovy create mode 100644 numass-main/src/main/groovy/inr/numass/scripts/Systematics.groovy create mode 100644 numass-main/src/main/groovy/inr/numass/scripts/TestExperimentalVariableLossSpectrum.groovy create mode 100644 numass-main/src/main/groovy/inr/numass/scripts/TheoreticalLossFunction.groovy create mode 100644 numass-main/src/main/groovy/inr/numass/scripts/TritiumTest.groovy create mode 100644 numass-main/src/main/groovy/inr/numass/scripts/models/TristanModel.groovy create mode 100644 numass-main/src/main/groovy/inr/numass/scripts/underflow/ResponseFunction.groovy create mode 100644 numass-main/src/main/groovy/inr/numass/scripts/underflow/Underflow.groovy create mode 100644 numass-main/src/main/java/inr/numass/Numass.java create mode 100644 numass-main/src/main/java/inr/numass/actions/AdjustErrorsAction.java create mode 100644 numass-main/src/main/java/inr/numass/actions/MonitorCorrectAction.java create mode 100644 numass-main/src/main/java/inr/numass/actions/SubstractSpectrumAction.java create mode 100644 numass-main/src/main/java/inr/numass/data/MonitorPoint.java create mode 100644 numass-main/src/main/java/inr/numass/data/SpectrumGenerator.java create mode 100644 numass-main/src/main/java/inr/numass/data/SpectrumInformation.java create mode 100644 numass-main/src/main/java/inr/numass/data/package-info.java create mode 100644 numass-main/src/main/java/inr/numass/models/BetaSpectrum.java create mode 100644 numass-main/src/main/java/inr/numass/models/CustomNBkgSpectrum.java create mode 100644 numass-main/src/main/java/inr/numass/models/EmpiricalLossSpectrum.java create mode 100644 numass-main/src/main/java/inr/numass/models/ExperimentalVariableLossSpectrum.java create mode 100644 numass-main/src/main/java/inr/numass/models/FSS.java create mode 100644 numass-main/src/main/java/inr/numass/models/GaussSourceSpectrum.java create mode 100644 numass-main/src/main/java/inr/numass/models/GunSpectrum.java create mode 100644 numass-main/src/main/java/inr/numass/models/GunTailSpectrum.java create mode 100644 numass-main/src/main/java/inr/numass/models/LossResConvolution.java create mode 100644 numass-main/src/main/java/inr/numass/models/ModularSpectrum.java create mode 100644 numass-main/src/main/java/inr/numass/models/NamedSpectrumCaching.java create mode 100644 numass-main/src/main/java/inr/numass/models/RangedNamedSetSpectrum.java create mode 100644 numass-main/src/main/java/inr/numass/models/ResolutionFunction.java create mode 100644 numass-main/src/main/java/inr/numass/models/SimpleRange.java create mode 100644 numass-main/src/main/java/inr/numass/models/SpectrumRange.java create mode 100644 numass-main/src/main/java/inr/numass/models/Transmission.java create mode 100644 numass-main/src/main/java/inr/numass/models/TransmissionConvolution.java create mode 100644 numass-main/src/main/java/inr/numass/models/TransmissionInterpolator.java create mode 100644 numass-main/src/main/java/inr/numass/models/TritiumSpectrumCaching.java create mode 100644 numass-main/src/main/java/inr/numass/models/VariableLossSpectrum.java create mode 100644 numass-main/src/main/java/inr/numass/utils/DataModelUtils.java create mode 100644 numass-main/src/main/java/inr/numass/utils/ExpressionUtils.java create mode 100644 numass-main/src/main/java/inr/numass/utils/NumassIntegrator.java create mode 100644 numass-main/src/main/java/inr/numass/utils/OldDataReader.java create mode 100644 numass-main/src/main/java/inr/numass/utils/UnderflowCorrection.java create mode 100644 numass-main/src/main/kotlin/inr/numass/NumassPlugin.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/NumassUtils.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/WorkspaceTest.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/actions/AnalyzeDataAction.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/actions/MergeDataAction.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/actions/PlotFitResultAction.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/actions/SummaryAction.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/actions/TimeAnalyzerAction.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/actions/TimeSpectrumAction.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/actions/TransformDataAction.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/data/NumassGenerator.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/data/PileUpSimulator.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/data/Visualization.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/data/analyzers/SmartAnalyzer.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/models/NBkgSpectrum.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/models/NBkgSpectrumWithCorrection.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/models/NumassModels.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/models/mc/BetaSampler.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/models/misc/FunctionSupport.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/models/misc/Gauss.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/models/misc/LossCalculator.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/models/misc/ModGauss.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/models/misc/TailFunctions.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/models/sterile/NumassBeta.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/models/sterile/NumassResolution.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/models/sterile/NumassTransmission.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/models/sterile/ParametricBiFunctionCache.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/models/sterile/SterileNeutrinoSpectrum.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/Bunches.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/Correlation.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/InversedChain.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/InversedChainProto.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/PileupIntensityDependency.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/PileupIntensityDependencyGun.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/PlotResiduals.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/PointSliceSpectrum.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/analysis/Run2017_05.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/analysis/Run2017_05_frames.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/analysis/Run2017_11.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/analysis/Run2019_11.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/analysis/Run2020_12.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/models/DifferentialSpectrum.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/models/IntegralEndpoint.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/models/IntegralSpectrum.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/models/ResolutionDemo.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/models/demo.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/threshold/FitAllWithPower.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/threshold/FitWithPower.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/threshold/threshold-2017_05-CAMAC.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/threshold/threshold-2019_11-CAMAC.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/timeanalysis/AnalyzeDantePoint.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/timeanalysis/AnalyzePoint.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/timeanalysis/Histogram.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/timeanalysis/TestAnalyzer.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/timeanalysis/TestBunch.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/timeanalysis/TestTimeAnalyzerLength.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/tristan/AnalyzeTristan.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/tristan/FitTextTristanFiles.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/tristan/ProtoPoint.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/tristan/TristanAnalyzer.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/tristan/ViewPoint.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/tristan/checkCorrelations.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/tristan/interpolateBump.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/tristan/zeroPad.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/utils/ScanTree.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/scripts/utils/ScriptUtils.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/subthreshold/Threshold.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/tasks/NumassFitScanSummaryTask.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/tasks/NumassFitScanTask.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/tasks/NumassFitSummaryTask.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/tasks/NumassTasks.kt create mode 100644 numass-main/src/main/kotlin/inr/numass/tasks/SpecialNumassTasks.kt create mode 100644 numass-main/src/main/resources/META-INF/services/hep.dataforge.context.PluginFactory create mode 100644 numass-main/src/main/resources/data/FS.txt create mode 100644 numass-main/src/main/resources/data/d2_17_1.xml create mode 100644 numass-main/src/main/resources/data/run23.cfg create mode 100644 numass-main/src/main/resources/numass/models/transmission create mode 100644 numass-main/src/test/java/inr/numass/NumassTest.java create mode 100644 numass-main/src/test/java/inr/numass/actions/PrepareDataActionTest.java create mode 100644 numass-main/src/test/java/inr/numass/models/NumassSpectrumTest.java create mode 100644 numass-main/src/test/java/inr/numass/models/PlotScatter.java create mode 100644 numass-main/src/test/java/inr/numass/models/TestModels.kt create mode 100644 numass-main/src/test/java/inr/numass/models/TestNeLossParametrisation.java create mode 100644 numass-main/src/test/java/inr/numass/models/TransmissionInterpolatorTest.java create mode 100644 numass-main/src/test/kotlin/inr/numass/NumassPluginTest.kt create mode 100644 numass-viewer/build.gradle create mode 100644 numass-viewer/src/main/kotlin/inr/numass/viewer/AmplitudeView.kt create mode 100644 numass-viewer/src/main/kotlin/inr/numass/viewer/Cache.kt create mode 100644 numass-viewer/src/main/kotlin/inr/numass/viewer/HVView.kt create mode 100644 numass-viewer/src/main/kotlin/inr/numass/viewer/MainView.kt create mode 100644 numass-viewer/src/main/kotlin/inr/numass/viewer/PointInfoView.kt create mode 100644 numass-viewer/src/main/kotlin/inr/numass/viewer/SlowControlView.kt create mode 100644 numass-viewer/src/main/kotlin/inr/numass/viewer/SpectrumView.kt create mode 100644 numass-viewer/src/main/kotlin/inr/numass/viewer/StorageView.kt create mode 100644 numass-viewer/src/main/kotlin/inr/numass/viewer/TimeView.kt create mode 100644 numass-viewer/src/main/kotlin/inr/numass/viewer/Viewer.kt create mode 100644 numass-viewer/src/main/kotlin/inr/numass/viewer/test/ComponentTest.kt create mode 100644 numass-viewer/src/main/kotlin/inr/numass/viewer/test/JFCTest.kt create mode 100644 numass-viewer/src/main/kotlin/inr/numass/viewer/test/StorageViewTest.kt create mode 100644 numass-viewer/src/main/kotlin/inr/numass/viewer/test/TestApp.kt create mode 100644 numass-viewer/src/main/resources/fxml/MainView.fxml create mode 100644 numass-viewer/src/main/resources/fxml/NumassLoaderView.fxml create mode 100644 settings.gradle create mode 100644 site/SiteConfig.groovy create mode 100644 site/application.properties create mode 100644 site/build.gradle create mode 100644 site/content/apps.html create mode 100644 site/content/config/docs.yml create mode 100644 site/content/config/header.yml create mode 100644 site/content/docs.html create mode 100644 site/content/docs/0.1.0/release.md create mode 100644 site/content/docs/0.1.1/release.md create mode 100644 site/content/docs/0.4.0 - SNAPSHOT/release.md create mode 100644 site/content/docs/images/actions.png create mode 100644 site/content/docs/images/meta.svg create mode 100644 site/content/docs/images/metadata-processor-classic.png create mode 100644 site/content/docs/images/metadata-processor.png create mode 100644 site/content/docs/images/naming.svg create mode 100644 site/content/docs/images/tasks.png create mode 100644 site/content/docs/shards/context/context.md create mode 100644 site/content/docs/shards/context/framework.md create mode 100644 site/content/docs/shards/context/navigation.md create mode 100644 site/content/docs/shards/context/plugins.md create mode 100644 site/content/docs/shards/control/connections.md create mode 100644 site/content/docs/shards/control/control.md create mode 100644 site/content/docs/shards/control/measurements.md create mode 100644 site/content/docs/shards/data_flow/actions.md create mode 100644 site/content/docs/shards/data_flow/data_flow.md create mode 100644 site/content/docs/shards/data_flow/tasks.md create mode 100644 site/content/docs/shards/envelopes/binary_meta.md create mode 100644 site/content/docs/shards/envelopes/default_format.md create mode 100644 site/content/docs/shards/envelopes/envelope_description.md create mode 100644 site/content/docs/shards/envelopes/envelopes.md create mode 100644 site/content/docs/shards/envelopes/tagless_format.md create mode 100644 site/content/docs/shards/extensions/extensions.md create mode 100644 site/content/docs/shards/extensions/grind.md create mode 100644 site/content/docs/shards/extensions/kodex.md create mode 100644 site/content/docs/shards/how_to_read.md create mode 100644 site/content/docs/shards/meta/configuration.md create mode 100644 site/content/docs/shards/meta/hidden_values.md create mode 100644 site/content/docs/shards/meta/meta-data.md create mode 100644 site/content/docs/shards/meta/meta-layers.md create mode 100644 site/content/docs/shards/meta/meta_structure.md create mode 100644 site/content/images/df_icon.ico create mode 100644 site/content/images/sms_logo.png create mode 100644 site/content/index.md create mode 100644 site/content/misc.html create mode 100644 site/content/modules.html create mode 100644 site/content/news.html create mode 100644 site/content/news/02_16_update.md create mode 100644 site/content/news/08_17_update.md create mode 100644 site/content/news/lomonosov_2015.markdown create mode 100644 site/content/news/site_start.html create mode 100644 site/content/releases.html create mode 100644 site/content/robots.txt create mode 100644 site/content/sitemap.xml create mode 100644 site/out/production/classes/hep/dataforge/site/DFTagLib.class create mode 100644 site/out/production/classes/hep/dataforge/site/Launcher.class create mode 100644 site/out/production/classes/hep/dataforge/site/ResourceMapper.class create mode 100644 site/src/main/groovy/hep/dataforge/site/DFTagLib.groovy create mode 100644 site/src/main/groovy/hep/dataforge/site/Launcher.java create mode 100644 site/src/main/groovy/hep/dataforge/site/ResourceMapper.groovy create mode 100644 site/theme/config.rb create mode 100644 site/theme/fonts/glyphicons-halflings-regular.eot create mode 100644 site/theme/fonts/glyphicons-halflings-regular.svg create mode 100644 site/theme/fonts/glyphicons-halflings-regular.ttf create mode 100644 site/theme/fonts/glyphicons-halflings-regular.woff create mode 100644 site/theme/fonts/glyphicons-halflings-regular.woff2 create mode 100644 site/theme/images/apple-touch-icon-114x114.png create mode 100644 site/theme/images/apple-touch-icon-120x120.png create mode 100644 site/theme/images/apple-touch-icon-144x144.png create mode 100644 site/theme/images/apple-touch-icon-152x152.png create mode 100644 site/theme/images/apple-touch-icon-180x180.png create mode 100644 site/theme/images/apple-touch-icon-57x57.png create mode 100644 site/theme/images/apple-touch-icon-72x72.png create mode 100644 site/theme/images/apple-touch-icon-76x76.png create mode 100644 site/theme/images/apple-touch-icon.png create mode 100644 site/theme/images/df_logo.png create mode 100644 site/theme/images/favicon.ico create mode 100644 site/theme/images/gears_animated.gif create mode 100644 site/theme/includes/footer.html create mode 100644 site/theme/includes/header.html create mode 100644 site/theme/includes/in_progress.html create mode 100644 site/theme/includes/navbar.html create mode 100644 site/theme/javascripts/bootstrap.js create mode 100644 site/theme/javascripts/bootstrap.min.js create mode 100644 site/theme/javascripts/npm.js create mode 100644 site/theme/layouts/default.html create mode 100644 site/theme/layouts/docs.html create mode 100644 site/theme/layouts/news.html create mode 100644 site/theme/layouts/releases.html create mode 100644 site/theme/stylesheets/bootstrap.css create mode 100644 site/theme/stylesheets/bootstrap.min.css create mode 100644 site/theme/stylesheets/navbar-fixed-top.css create mode 100644 site/theme/stylesheets/override.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..4bc31799 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +build/ +.gradle/ +.idea/ +out/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..4e13f328 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# Purpose # + +This repository contains tools and utilities for [Trotsk nu-mass](http://www.inr.ru/~trdat/) experiment. + +# Set-up # + +This project build using [DataForge](http://www.inr.ru/~nozik/dataforge/) framework. Currently in order to compile numass tools, one need to download dataforge gradle project [here](https://bitbucket.org/Altavir/dataforge). If both projects (numass and dataforge) are in the same directory, everything will work out of the box, otherwise, one needs to edit `gradle.properties` file in the root of numass project and set `dataforgePath` to the relative path of dataforge directory. + +It is intended to fix this problem with public maven repository later. \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..24526447 --- /dev/null +++ b/build.gradle @@ -0,0 +1,65 @@ +buildscript { + ext.kotlin_version = "1.4.30" + repositories { + mavenCentral() + jcenter() + } + + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +plugins{ + id 'org.openjfx.javafxplugin' version '0.0.9' apply false +} + +allprojects { + apply plugin: 'java' + apply plugin: "org.jetbrains.kotlin.jvm" + + group = 'inr.numass' + version = '1.0.0' + + [compileJava, compileTestJava]*.options*.encoding = 'UTF-8' + + repositories { + mavenCentral() + jcenter() + } + + dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + compile 'org.jetbrains:annotations:16.0.2' + testImplementation group: 'junit', name: 'junit', version: '4.+' + + //Spock dependencies. To be removed later + testCompile 'org.codehaus.groovy:groovy-all:2.5.+' + testCompile "org.spockframework:spock-core:1.2-groovy-2.5" + } + + compileKotlin { + kotlinOptions { + jvmTarget = "1.8" + javaParameters = true + freeCompilerArgs += [ + '-Xjvm-default=enable', + "-progressive", + "-Xuse-experimental=kotlin.Experimental" + ] + } + } + + compileTestKotlin { + kotlinOptions { + jvmTarget = "1.8" + javaParameters = true + freeCompilerArgs += [ + '-Xjvm-default=enable', + "-progressive", + "-Xuse-experimental=kotlin.Experimental" + ] + } + } +} \ No newline at end of file diff --git a/dataforge-control/build.gradle b/dataforge-control/build.gradle new file mode 100644 index 00000000..c3b47469 --- /dev/null +++ b/dataforge-control/build.gradle @@ -0,0 +1,10 @@ + +description = 'dataforge-control' + +dependencies { + // Adding dependencies here will add the dependencies to each subproject. + compile project(':dataforge-core') + //TODO consider removing storage dependency + compile project(':dataforge-storage') + compile 'org.scream3r:jssc:2.8.0' +} \ No newline at end of file diff --git a/dataforge-control/doc/States.vsdx b/dataforge-control/doc/States.vsdx new file mode 100644 index 0000000000000000000000000000000000000000..5981c89659cd4577484993f57513698d01ff6367 GIT binary patch literal 29709 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𝔡iJkk<>Y$#fjh4X>FxN{hFTrBCxG}WEy0hTA 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 literal 0 HcmV?d00001 diff --git a/dataforge-control/src/main/java/hep/dataforge/control/ControlUtils.java b/dataforge-control/src/main/java/hep/dataforge/control/ControlUtils.java new file mode 100644 index 00000000..be996f09 --- /dev/null +++ b/dataforge-control/src/main/java/hep/dataforge/control/ControlUtils.java @@ -0,0 +1,22 @@ +package hep.dataforge.control; + +import hep.dataforge.control.devices.Device; +import hep.dataforge.io.envelopes.Envelope; +import hep.dataforge.meta.Meta; + +/** + * Created by darksnake on 11-Oct-16. + */ +public class ControlUtils { + public static String getDeviceType(Meta meta){ + return meta.getString("type"); + } + + public static String getDeviceName(Meta meta){ + return meta.getString("name",""); + } + + public static Envelope getDefaultDeviceResponse(Device device, Envelope request){ + throw new UnsupportedOperationException("Not implemented"); + } +} diff --git a/dataforge-control/src/main/java/hep/dataforge/control/collectors/PointCollector.java b/dataforge-control/src/main/java/hep/dataforge/control/collectors/PointCollector.java new file mode 100644 index 00000000..28ea8e60 --- /dev/null +++ b/dataforge-control/src/main/java/hep/dataforge/control/collectors/PointCollector.java @@ -0,0 +1,100 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.control.collectors; + +import hep.dataforge.tables.ValuesListener; +import hep.dataforge.utils.DateTimeUtils; +import hep.dataforge.values.Value; +import hep.dataforge.values.ValueFactory; +import hep.dataforge.values.ValueMap; + +import java.time.Instant; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A class to dynamically collect measurements from multi-channel devices and + * bundle them into DataPoints. The collect method is called when all values are + * present. + * + * @author Alexander Nozik + */ +public class PointCollector implements ValueCollector { + + private final List names; + private final ValuesListener consumer; + private final Map valueMap = new ConcurrentHashMap<>(); + //TODO make time averaging? + + public PointCollector(ValuesListener consumer, Collection names) { + this.names = new ArrayList<>(names); + this.consumer = consumer; + } + + public PointCollector(ValuesListener consumer, String... names) { + this.names = Arrays.asList(names); + this.consumer = consumer; + } + + @Override + public void put(String name, Value value) { + valueMap.put(name, value); + if (valueMap.keySet().containsAll(names)) { + collect(); + } + } + + @Override + public void put(String name, Object value) { + valueMap.put(name, ValueFactory.of(value)); + if (valueMap.keySet().containsAll(names)) { + collect(); + } + } + + /** + * Could be used to force collect even if not all values are present + */ + @Override + public void collect() { + collect(DateTimeUtils.now()); + } + + public synchronized void collect(Instant time) { + ValueMap.Builder point = new ValueMap.Builder(); + + point.putValue("timestamp", time); + valueMap.entrySet().forEach((entry) -> { + point.putValue(entry.getKey(), entry.getValue()); + }); + + // filling all missing values with nulls + names.stream().filter((name) -> (!point.build().hasValue(name))).forEach((name) -> { + point.putValue(name, ValueFactory.NULL); + }); + + consumer.accept(point.build()); + valueMap.clear(); + } + + @Override + public void clear() { + valueMap.clear(); + } + + + +} diff --git a/dataforge-control/src/main/java/hep/dataforge/control/collectors/RegularPointCollector.java b/dataforge-control/src/main/java/hep/dataforge/control/collectors/RegularPointCollector.java new file mode 100644 index 00000000..694fd4f9 --- /dev/null +++ b/dataforge-control/src/main/java/hep/dataforge/control/collectors/RegularPointCollector.java @@ -0,0 +1,117 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.control.collectors; + +import hep.dataforge.utils.DateTimeUtils; +import hep.dataforge.values.Value; +import hep.dataforge.values.ValueFactory; +import hep.dataforge.values.ValueMap; +import hep.dataforge.values.Values; + +import java.time.Duration; +import java.time.Instant; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; + +/** + * An averaging DataPoint collector that starts timer on first put operation and + * forces collection when timer expires. If there are few Values with same time + * during this period, they are averaged. + * + * @author Alexander Nozik + */ +public class RegularPointCollector implements ValueCollector { + + private final Map> values = new ConcurrentHashMap<>(); + private final Consumer consumer; + private final Duration duration; + private Instant startTime; + /** + * The names that must be in the dataPoint + */ + private List names = new ArrayList<>(); + private Timer timer; + + public RegularPointCollector(Duration duration, Consumer consumer) { + this.consumer = consumer; + this.duration = duration; + } + + public RegularPointCollector(Duration duration, Collection names, Consumer consumer) { + this(duration, consumer); + this.names = new ArrayList<>(names); + } + + @Override + public void collect() { + collect(DateTimeUtils.now()); + } + + public synchronized void collect(Instant time) { + if(!values.isEmpty()) { + ValueMap.Builder point = new ValueMap.Builder(); + + Instant average = Instant.ofEpochMilli((time.toEpochMilli() + startTime.toEpochMilli()) / 2); + + point.putValue("timestamp", average); + + for (Map.Entry> entry : values.entrySet()) { + point.putValue(entry.getKey(), entry.getValue().stream().mapToDouble(Value::getDouble).sum() / entry.getValue().size()); + } + + // filling all missing values with nulls + for (String name : names) { + if (!point.build().hasValue(name)) { + point.putValue(name, ValueFactory.NULL); + } + } + + startTime = null; + values.clear(); + consumer.accept(point.build()); + } + } + + @Override + public synchronized void put(String name, Value value) { + if (startTime == null) { + startTime = DateTimeUtils.now(); + timer = new Timer(); + timer.schedule(new TimerTask() { + @Override + public void run() { + collect(); + } + }, duration.toMillis()); + } + + if (!values.containsKey(name)) { + values.put(name, new ArrayList<>()); + } + values.get(name).add(value); + } + + private void cancel() { + if (timer != null && startTime != null) { + timer.cancel(); + } + } + + @Override + public void clear() { + values.clear(); + + } + + public void stop() { + cancel(); + clear(); + startTime = null; + } + + +} diff --git a/dataforge-control/src/main/java/hep/dataforge/control/collectors/ValueCollector.java b/dataforge-control/src/main/java/hep/dataforge/control/collectors/ValueCollector.java new file mode 100644 index 00000000..5c767405 --- /dev/null +++ b/dataforge-control/src/main/java/hep/dataforge/control/collectors/ValueCollector.java @@ -0,0 +1,36 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.control.collectors; + +import hep.dataforge.values.Value; +import hep.dataforge.values.ValueFactory; + +/** + * A collector of values which listens to some input values until condition + * satisfied then pushes the result to external listener. + * + * @author Alexander Nozik + */ +public interface ValueCollector { + + void put(String name, Value value); + + default void put(String name, Object value) { + put(name, ValueFactory.of(value)); + } + + /** + * Send current cached result to listener. Could be used to force collect + * even if not all values are present. + */ + void collect(); + + /** + * Clear currently collected data + */ + void clear(); + +} diff --git a/dataforge-control/src/main/java/hep/dataforge/control/measurements/AbstractMeasurement.java b/dataforge-control/src/main/java/hep/dataforge/control/measurements/AbstractMeasurement.java new file mode 100644 index 00000000..97b7658a --- /dev/null +++ b/dataforge-control/src/main/java/hep/dataforge/control/measurements/AbstractMeasurement.java @@ -0,0 +1,157 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.control.measurements; + +import hep.dataforge.exceptions.MeasurementException; +import hep.dataforge.utils.DateTimeUtils; +import kotlin.Pair; +import org.slf4j.LoggerFactory; + +import java.time.Instant; +import java.util.function.Consumer; + +/** + * A boilerplate code for measurements + * + * @author Alexander Nozik + */ +@Deprecated +public abstract class AbstractMeasurement implements Measurement { + + // protected final ReferenceRegistry> listeners = new ReferenceRegistry<>(); + protected Pair lastResult; + protected Throwable exception; + private MeasurementState state; + + protected MeasurementState getMeasurementState() { + return state; + } + + protected void setMeasurementState(MeasurementState state) { + this.state = state; + } + + /** + * Call after measurement started + */ + protected void afterStart() { + setMeasurementState(MeasurementState.PENDING); + notifyListeners(it -> it.onMeasurementStarted(this)); + } + + /** + * Call after measurement stopped + */ + protected void afterStop() { + setMeasurementState(MeasurementState.FINISHED); + notifyListeners(it -> it.onMeasurementFinished(this)); + } + + /** + * Reset measurement to initial state + */ + protected void afterPause() { + setMeasurementState(MeasurementState.OK); + notifyListeners(it -> it.onMeasurementFinished(this)); + } + + protected synchronized void onError(String message, Throwable error) { + LoggerFactory.getLogger(getClass()).error("Measurement failed with error: " + message, error); + setMeasurementState(MeasurementState.FAILED); + this.exception = error; + notify(); + notifyListeners(it -> it.onMeasurementFailed(this, error)); + } + + /** + * Internal method to notify measurement complete. Uses current system time + * + * @param result + */ + protected final void result(T result) { + result(result, DateTimeUtils.now()); + } + + /** + * Internal method to notify measurement complete + * + * @param result + */ + protected synchronized void result(T result, Instant time) { + this.lastResult = new Pair<>(result, time); + setMeasurementState(MeasurementState.OK); + notify(); + notifyListeners(it -> it.onMeasurementResult(this, result, time)); + } + + protected void updateProgress(double progress) { + notifyListeners(it -> it.onMeasurementProgress(this, progress)); + } + + protected void updateMessage(String message) { + notifyListeners(it -> it.onMeasurementProgress(this, message)); + } + + protected final void notifyListeners(Consumer consumer) { + getDevice().forEachConnection(MeasurementListener.class, consumer); + } + + @Override + public boolean isFinished() { + return state == MeasurementState.FINISHED; + } + + @Override + public boolean isStarted() { + return state == MeasurementState.PENDING || state == MeasurementState.OK; + } + + @Override + public Throwable getError() { + return this.exception; + } + + protected synchronized Pair get() throws MeasurementException { + if (getMeasurementState() == MeasurementState.INIT) { + start(); + LoggerFactory.getLogger(getClass()).debug("Measurement not started. Starting"); + } + while (state == MeasurementState.PENDING) { + try { + //Wait for result could cause deadlock if called in main thread + wait(); + } catch (InterruptedException ex) { + throw new MeasurementException(ex); + } + } + if (this.lastResult != null) { + return this.lastResult; + } else if (state == MeasurementState.FAILED) { + throw new MeasurementException(getError()); + } else { + throw new MeasurementException("Measurement failed for unknown reason"); + } + } + + @Override + public Instant getTime() throws MeasurementException { + return get().getSecond(); + } + + @Override + public T getResult() throws MeasurementException { + return get().getFirst(); + } + + protected enum MeasurementState { + INIT, //Measurement not started + PENDING, // Measurement in process + OK, // Last measurement complete, next is planned + FAILED, // Last measurement failed + FINISHED, // Measurement finished or stopped + } + +} diff --git a/dataforge-control/src/main/java/hep/dataforge/control/measurements/Measurement.java b/dataforge-control/src/main/java/hep/dataforge/control/measurements/Measurement.java new file mode 100644 index 00000000..c4d9e200 --- /dev/null +++ b/dataforge-control/src/main/java/hep/dataforge/control/measurements/Measurement.java @@ -0,0 +1,74 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.control.measurements; + +import hep.dataforge.control.devices.Device; +import hep.dataforge.exceptions.MeasurementException; + +import java.time.Instant; + +/** + * A general representation of ongoing or completed measurement. Could be + * regular. + * + * @author Alexander Nozik + */ +@Deprecated +public interface Measurement { + + Device getDevice(); + + /** + * Begin the measurement + */ + void start(); + + /** + * Stop the measurement + * + * @param force force stop if measurement in progress + * @throws MeasurementException + */ + boolean stop(boolean force) throws MeasurementException; + + /** + * Measurement is started + * + * @return + */ + boolean isStarted(); + + /** + * Measurement is complete or stopped and could be recycled + * + * @return + */ + boolean isFinished(); + + /** + * Get the time of the last measurement + * + * @return + * @throws MeasurementException + */ + Instant getTime() throws MeasurementException; + + /** + * Get last measurement result or wait for measurement to complete and + * return its result. Synchronous call. + * + * @return + * @throws MeasurementException + */ + T getResult() throws MeasurementException; + + /** + * Last thrown exception. Null if no exceptions are thrown + * + * @return + */ + Throwable getError(); +} diff --git a/dataforge-control/src/main/java/hep/dataforge/control/measurements/MeasurementListener.java b/dataforge-control/src/main/java/hep/dataforge/control/measurements/MeasurementListener.java new file mode 100644 index 00000000..052ce382 --- /dev/null +++ b/dataforge-control/src/main/java/hep/dataforge/control/measurements/MeasurementListener.java @@ -0,0 +1,87 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.control.measurements; + +import hep.dataforge.exceptions.MeasurementException; + +import java.time.Instant; + +/** + * A listener for device measurements + * + * @author Alexander Nozik + */ +@Deprecated +public interface MeasurementListener { + + /** + * Measurement started. Ignored by default + * @param measurement + */ + default void onMeasurementStarted(Measurement measurement){ + + } + + /** + * Measurement stopped. Ignored by default + * @param measurement + */ + default void onMeasurementFinished(Measurement measurement){ + + } + + /** + * Measurement result obtained + * @param measurement + * @param result + */ + void onMeasurementResult(Measurement measurement, Object result, Instant time); + + /** + * Measurement failed with exception + * @param measurement + * @param exception + */ + void onMeasurementFailed(Measurement measurement, Throwable exception); + + /** + * Measurement failed with message + * @param measurement + * @param message + */ + default void onMeasurementFailed(Measurement measurement, String message){ + onMeasurementFailed(measurement, new MeasurementException(message)); + } + + /** + * Measurement progress updated. Ignored by default + * @param measurement + * @param progress + */ + default void onMeasurementProgress(Measurement measurement, double progress) { + + } + + /** + * Measurement progress message updated. Ignored by default + * @param measurement + * @param message + */ + default void onMeasurementProgress(Measurement measurement, String message) { + + } + +} diff --git a/dataforge-control/src/main/java/hep/dataforge/control/measurements/RegularMeasurement.java b/dataforge-control/src/main/java/hep/dataforge/control/measurements/RegularMeasurement.java new file mode 100644 index 00000000..f2405e0f --- /dev/null +++ b/dataforge-control/src/main/java/hep/dataforge/control/measurements/RegularMeasurement.java @@ -0,0 +1,46 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.control.measurements; + +import java.time.Duration; + +/** + * + * @author Alexander Nozik + */ +@Deprecated +public abstract class RegularMeasurement extends SimpleMeasurement { + + private boolean stopFlag = false; + + @Override + protected void finishTask() { + if (stopFlag || (stopOnError() && getMeasurementState() == MeasurementState.FAILED)) { + afterStop(); + } else { + startTask(); + } + } + + @Override + public boolean stop(boolean force) { + if (isFinished()) { + return false; + } else if (force) { + return interruptTask(force); + } else { + stopFlag = true; + return true; + } + } + + protected boolean stopOnError() { + return true; + } + + protected abstract Duration getDelay(); + +} diff --git a/dataforge-control/src/main/java/hep/dataforge/control/measurements/SimpleMeasurement.java b/dataforge-control/src/main/java/hep/dataforge/control/measurements/SimpleMeasurement.java new file mode 100644 index 00000000..09f687ed --- /dev/null +++ b/dataforge-control/src/main/java/hep/dataforge/control/measurements/SimpleMeasurement.java @@ -0,0 +1,142 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.control.measurements; + +import hep.dataforge.exceptions.MeasurementException; +import hep.dataforge.utils.DateTimeUtils; +import kotlin.Pair; +import org.slf4j.LoggerFactory; + +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.FutureTask; +import java.util.concurrent.TimeUnit; + +/** + * A simple one-time measurement wrapping FutureTask. Could be restarted + * + * @author Alexander Nozik + */ +@Deprecated +public abstract class SimpleMeasurement extends AbstractMeasurement { + + private FutureTask> task; + + /** + * invalidate current task. New task will be created on next getFuture call. + * This method does not guarantee that task is finished when it is cleared + */ + private void clearTask() { + task = null; + } + + /** + * Perform synchronous measurement + * + * @return + * @throws Exception + */ + protected abstract T doMeasure() throws Exception; + + @Override + public synchronized void start() { + //PENDING do we need executor here? + //Executors.newSingleThreadExecutor().submit(getTask()); + if (!isStarted()) { + afterStart(); + startTask(); + } else { + LoggerFactory.getLogger(getClass()).warn("Alredy started"); + } + } + + @Override + public synchronized boolean stop(boolean force) { + if (isStarted()) { + afterStop(); + return interruptTask(force); + } else { + return false; + } + } + + protected boolean interruptTask(boolean force) { + if (task != null) { + if (task.isCancelled() || task.isDone()) { + task = null; + return true; + } else { + return task.cancel(force); + } + } else { + return false; + } + } + + protected ThreadGroup getThreadGroup() { + return null; + } + + protected Duration getMeasurementTimeout() { + return null; + } + + protected String getThreadName() { + return "measurement thread"; + } + + protected void startTask() { + Runnable process = () -> { + Pair res; + try { + Duration timeout = getMeasurementTimeout(); + task = buildTask(); + task.run(); + if (timeout == null) { + res = task.get(); + } else { + res = task.get(getMeasurementTimeout().toMillis(), TimeUnit.MILLISECONDS); + } + + + if (res != null) { + result(res.getFirst(), res.getSecond()); + } else { + throw new MeasurementException("Empty result"); + } + } catch (Exception ex) { + onError("failed to start measurement task", ex); + } + clearTask(); + finishTask(); + }; + new Thread(getThreadGroup(), process, getThreadName()).start(); + } + + /** + * Reset measurement task and notify listeners + */ + protected void finishTask() { + afterStop(); + } + + private FutureTask> buildTask() { + return new FutureTask<>(() -> { + try { + T res = doMeasure(); + if (res == null) { + return null; + } + Instant time = DateTimeUtils.now(); + return new Pair<>(res, time); + } catch (Exception ex) { + onError("failed to report measurement results", ex); + return null; + } + }); + } + +} diff --git a/dataforge-control/src/main/java/hep/dataforge/exceptions/ControlException.java b/dataforge-control/src/main/java/hep/dataforge/exceptions/ControlException.java new file mode 100644 index 00000000..c25c020a --- /dev/null +++ b/dataforge-control/src/main/java/hep/dataforge/exceptions/ControlException.java @@ -0,0 +1,54 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.exceptions; + +/** + * + * @author Alexander Nozik + */ +public class ControlException extends Exception { + + /** + * Creates a new instance of ControlException without detail + * message. + */ + public ControlException() { + } + + /** + * Constructs an instance of ControlException with the + * specified detail message. + * + * @param msg the detail message. + */ + public ControlException(String msg) { + super(msg); + } + + public ControlException(String message, Throwable cause) { + super(message, cause); + } + + public ControlException(Throwable cause) { + super(cause); + } + + protected ControlException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + + +} diff --git a/dataforge-control/src/main/java/hep/dataforge/exceptions/MeasurementException.java b/dataforge-control/src/main/java/hep/dataforge/exceptions/MeasurementException.java new file mode 100644 index 00000000..50501512 --- /dev/null +++ b/dataforge-control/src/main/java/hep/dataforge/exceptions/MeasurementException.java @@ -0,0 +1,50 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.exceptions; + +/** + * + * @author Alexander Nozik + */ +public class MeasurementException extends ControlException { + + /** + * Creates a new instance of MeasurementException without + * detail message. + */ + public MeasurementException() { + } + + /** + * Constructs an instance of MeasurementException with the + * specified detail message. + * + * @param msg the detail message. + */ + public MeasurementException(String msg) { + super(msg); + } + + public MeasurementException(String message, Throwable cause) { + super(message, cause); + } + + public MeasurementException(Throwable cause) { + super(cause); + } + + +} diff --git a/dataforge-control/src/main/java/hep/dataforge/exceptions/MeasurementInterruptedException.java b/dataforge-control/src/main/java/hep/dataforge/exceptions/MeasurementInterruptedException.java new file mode 100644 index 00000000..51a1c9e2 --- /dev/null +++ b/dataforge-control/src/main/java/hep/dataforge/exceptions/MeasurementInterruptedException.java @@ -0,0 +1,50 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.exceptions; + +/** + * + * @author Alexander Nozik + */ +public class MeasurementInterruptedException extends MeasurementException { + + /** + * Creates a new instance of MeasurementInterruptedException + * without detail message. + */ + public MeasurementInterruptedException() { + } + + /** + * Constructs an instance of MeasurementInterruptedException + * with the specified detail message. + * + * @param msg the detail message. + */ + public MeasurementInterruptedException(String msg) { + super(msg); + } + + public MeasurementInterruptedException(String message, Throwable cause) { + super(message, cause); + } + + public MeasurementInterruptedException(Throwable cause) { + super(cause); + } + + +} diff --git a/dataforge-control/src/main/java/hep/dataforge/exceptions/MeasurementNotReadyException.java b/dataforge-control/src/main/java/hep/dataforge/exceptions/MeasurementNotReadyException.java new file mode 100644 index 00000000..548a3622 --- /dev/null +++ b/dataforge-control/src/main/java/hep/dataforge/exceptions/MeasurementNotReadyException.java @@ -0,0 +1,40 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.exceptions; + +/** + * + * @author Alexander Nozik + */ +public class MeasurementNotReadyException extends MeasurementException { + + /** + * Creates a new instance of MeasurementNotReadyException + * without detail message. + */ + public MeasurementNotReadyException() { + } + + /** + * Constructs an instance of MeasurementNotReadyException with + * the specified detail message. + * + * @param msg the detail message. + */ + public MeasurementNotReadyException(String msg) { + super(msg); + } +} diff --git a/dataforge-control/src/main/java/hep/dataforge/exceptions/MeasurementTimeoutException.java b/dataforge-control/src/main/java/hep/dataforge/exceptions/MeasurementTimeoutException.java new file mode 100644 index 00000000..4faa3666 --- /dev/null +++ b/dataforge-control/src/main/java/hep/dataforge/exceptions/MeasurementTimeoutException.java @@ -0,0 +1,40 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.exceptions; + +/** + * + * @author Alexander Nozik + */ +public class MeasurementTimeoutException extends MeasurementException { + + /** + * Creates a new instance of MeasurementTimeoutException + * without detail message. + */ + public MeasurementTimeoutException() { + } + + /** + * Constructs an instance of MeasurementTimeoutException with + * the specified detail message. + * + * @param msg the detail message. + */ + public MeasurementTimeoutException(String msg) { + super(msg); + } +} diff --git a/dataforge-control/src/main/java/hep/dataforge/exceptions/PortException.java b/dataforge-control/src/main/java/hep/dataforge/exceptions/PortException.java new file mode 100644 index 00000000..0a8f4cd3 --- /dev/null +++ b/dataforge-control/src/main/java/hep/dataforge/exceptions/PortException.java @@ -0,0 +1,49 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.exceptions; + +/** + * + * @author Alexander Nozik + */ +public class PortException extends ControlException { + + /** + * Creates a new instance of PortException without detail + * message. + */ + public PortException() { + } + + /** + * Constructs an instance of PortException with the specified + * detail message. + * + * @param msg the detail message. + */ + public PortException(String msg) { + super(msg); + } + + public PortException(String message, Throwable cause) { + super(message, cause); + } + + public PortException(Throwable cause) { + super(cause); + } + +} diff --git a/dataforge-control/src/main/java/hep/dataforge/exceptions/PortLockException.java b/dataforge-control/src/main/java/hep/dataforge/exceptions/PortLockException.java new file mode 100644 index 00000000..680abaf8 --- /dev/null +++ b/dataforge-control/src/main/java/hep/dataforge/exceptions/PortLockException.java @@ -0,0 +1,27 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.exceptions; + +public class PortLockException extends RuntimeException { + + public PortLockException() { + } + + public PortLockException(String msg) { + super(msg); + } + +} diff --git a/dataforge-control/src/main/kotlin/hep/dataforge/control/DeviceManager.kt b/dataforge-control/src/main/kotlin/hep/dataforge/control/DeviceManager.kt new file mode 100644 index 00000000..6d91910a --- /dev/null +++ b/dataforge-control/src/main/kotlin/hep/dataforge/control/DeviceManager.kt @@ -0,0 +1,130 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.control + +import hep.dataforge.connections.Connection +import hep.dataforge.context.* +import hep.dataforge.control.devices.Device +import hep.dataforge.control.devices.DeviceFactory +import hep.dataforge.control.devices.DeviceHub +import hep.dataforge.exceptions.ControlException +import hep.dataforge.meta.Meta +import hep.dataforge.names.Name +import java.util.* + +/** + * A plugin for creating and using different devices + * Created by darksnake on 11-Oct-16. + */ +@PluginDef(name = "devices", info = "Management plugin for devices an their interaction") +class DeviceManager : BasicPlugin(), DeviceHub { + + /** + * Registered devices + */ + private val _devices = HashMap() + + /** + * the list of top level devices + */ + val devices: Collection = _devices.values + + override val deviceNames: List + get() = _devices.entries.flatMap { entry -> + if (entry.value is DeviceHub) { + (entry.value as DeviceHub).deviceNames.map { it -> entry.key.plus(it) } + } else { + listOf(entry.key) + } + } + + + fun add(device: Device) { + val name = Name.ofSingle(device.name) + if (_devices.containsKey(name)) { + logger.warn("Replacing existing device in device manager!") + remove(name) + } + _devices[name] = device + } + + fun remove(name: Name) { + Optional.ofNullable(this._devices.remove(name)).ifPresent { it -> + try { + it.shutdown() + } catch (e: ControlException) { + logger.error("Failed to stop the device: " + it.name, e) + } + } + } + + + fun buildDevice(deviceMeta: Meta): Device { + val factory = context + .findService(DeviceFactory::class.java) { it.type == ControlUtils.getDeviceType(deviceMeta) } + ?: throw RuntimeException("Can't find factory for given device type") + val device = factory.build(context, deviceMeta) + + deviceMeta.getMetaList("connection").forEach { connectionMeta -> device.connectionHelper.connect(context, connectionMeta) } + + add(device) + return device + } + + override fun optDevice(name: Name): Optional { + return when { + name.isEmpty() -> throw IllegalArgumentException("Can't provide a device with zero name") + name.length == 1 -> Optional.ofNullable(_devices[name]) + else -> Optional.ofNullable(_devices[name.first]).flatMap { hub -> + if (hub is DeviceHub) { + (hub as DeviceHub).optDevice(name.cutFirst()) + } else { + Optional.empty() + } + } + } + } + + override fun detach() { + _devices.values.forEach { it -> + try { + it.shutdown() + } catch (e: ControlException) { + logger.error("Failed to stop the device: " + it.name, e) + } + } + super.detach() + } + + override fun connectAll(connection: Connection, vararg roles: String) { + this._devices.values.forEach { device -> device.connect(connection, *roles) } + } + + override fun connectAll(context: Context, meta: Meta) { + this._devices.values.forEach { device -> device.connectionHelper.connect(context, meta) } + } + + class Factory : PluginFactory() { + + override val type: Class + get() = DeviceManager::class.java + + override fun build(meta: Meta): Plugin { + return DeviceManager() + } + } +} diff --git a/dataforge-control/src/main/kotlin/hep/dataforge/control/connections/DeviceConnection.kt b/dataforge-control/src/main/kotlin/hep/dataforge/control/connections/DeviceConnection.kt new file mode 100644 index 00000000..2d639668 --- /dev/null +++ b/dataforge-control/src/main/kotlin/hep/dataforge/control/connections/DeviceConnection.kt @@ -0,0 +1,29 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.control.connections + +import hep.dataforge.connections.Connection +import hep.dataforge.control.devices.Device + +abstract class DeviceConnection : Connection { + + private var _device: Device? = null + val device: Device + get() = _device ?: throw RuntimeException("Connection closed") + + override fun isOpen(): Boolean { + return this._device != null + } + + override fun open(device: Any) { + this._device = Device::class.java.cast(device) + } + + override fun close() { + this._device = null + } + +} diff --git a/dataforge-control/src/main/kotlin/hep/dataforge/control/connections/LoaderConnection.kt b/dataforge-control/src/main/kotlin/hep/dataforge/control/connections/LoaderConnection.kt new file mode 100644 index 00000000..f17b23c0 --- /dev/null +++ b/dataforge-control/src/main/kotlin/hep/dataforge/control/connections/LoaderConnection.kt @@ -0,0 +1,44 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.control.connections + +import hep.dataforge.connections.Connection +import hep.dataforge.context.Context +import hep.dataforge.context.ContextAware +import hep.dataforge.context.launch +import hep.dataforge.storage.tables.MutableTableLoader +import hep.dataforge.tables.ValuesListener +import hep.dataforge.values.Values + +/** + * + * @author Alexander Nozik + */ +class LoaderConnection(private val loader: MutableTableLoader) : Connection, ValuesListener, ContextAware { + + override val context: Context + get() = loader.context + + override fun accept(point: Values) { + launch { + loader.append(point) + } + } + + override fun isOpen(): Boolean { + return true + } + + override fun open(`object`: Any) { + + } + + @Throws(Exception::class) + override fun close() { + loader.close() + } + +} diff --git a/dataforge-control/src/main/kotlin/hep/dataforge/control/connections/Roles.kt b/dataforge-control/src/main/kotlin/hep/dataforge/control/connections/Roles.kt new file mode 100644 index 00000000..2d474abc --- /dev/null +++ b/dataforge-control/src/main/kotlin/hep/dataforge/control/connections/Roles.kt @@ -0,0 +1,17 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.control.connections + +/** + * + * @author Alexander Nozik + */ +object Roles { + const val DEVICE_LISTENER_ROLE = "deviceListener" + const val MEASUREMENT_LISTENER_ROLE = "measurementListener" + const val STORAGE_ROLE = "storage" + const val VIEW_ROLE = "view" +} diff --git a/dataforge-control/src/main/kotlin/hep/dataforge/control/devices/AbstractDevice.kt b/dataforge-control/src/main/kotlin/hep/dataforge/control/devices/AbstractDevice.kt new file mode 100644 index 00000000..e963a2ce --- /dev/null +++ b/dataforge-control/src/main/kotlin/hep/dataforge/control/devices/AbstractDevice.kt @@ -0,0 +1,199 @@ +/* + * Copyright 2017 Alexander Nozik. + * + * 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 hep.dataforge.control.devices + +import hep.dataforge.connections.Connection +import hep.dataforge.connections.ConnectionHelper +import hep.dataforge.context.Context +import hep.dataforge.context.Global +import hep.dataforge.control.connections.Roles +import hep.dataforge.control.devices.Device.Companion.INITIALIZED_STATE +import hep.dataforge.description.ValueDef +import hep.dataforge.events.Event +import hep.dataforge.events.EventHandler +import hep.dataforge.exceptions.ControlException +import hep.dataforge.listAnnotations +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaHolder +import hep.dataforge.names.AnonymousNotAlowed +import hep.dataforge.states.* +import hep.dataforge.values.ValueType +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import java.time.Duration +import java.util.concurrent.* + +/** + * + * + * State has two components: physical and logical. If logical state does not + * coincide with physical, it should be invalidated and automatically updated on + * next request. + * + * + * @author Alexander Nozik + */ +@AnonymousNotAlowed +@StateDef( + value = ValueDef( + key = INITIALIZED_STATE, + type = [ValueType.BOOLEAN], + def = "false", + info = "Initialization state of the device" + ), writable = true +) +abstract class AbstractDevice(override final val context: Context = Global, meta: Meta) : MetaHolder(meta), Device { + + final override val states = StateHolder() + + val initializedState: ValueState = valueState(INITIALIZED_STATE) { old, value -> + if (old != value) { + if (value.boolean) { + init() + } else { + shutdown() + } + } + } + + /** + * Initialization state + */ + val initialized by initializedState.booleanDelegate + + private var stateListenerJob: Job? = null + + private val _connectionHelper: ConnectionHelper by lazy { ConnectionHelper(this) } + + override fun getConnectionHelper(): ConnectionHelper { + return _connectionHelper + } + + /** + * A single thread executor for this device. All state changes and similar work must be done on this thread. + * + * @return + */ + protected val executor: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor { r -> + val res = Thread(r) + res.name = "device::$name" + res.priority = Thread.MAX_PRIORITY + res.isDaemon = true + res + } + + init { + //initialize states + javaClass.listAnnotations(StateDef::class.java, true).forEach { + states.init(ValueState(it)) + } + javaClass.listAnnotations(MetaStateDef::class.java, true).forEach { + states.init(MetaState(it)) + } + } + + @Throws(ControlException::class) + override fun init() { + logger.info("Initializing device '{}'...", name) + states.update(INITIALIZED_STATE, true) + stateListenerJob = context.launch { + val flow = states.changes() + flow.collect { + onStateChange(it.first, it.second) + } + } + } + + @Throws(ControlException::class) + override fun shutdown() { + logger.info("Shutting down device '{}'...", name) + forEachConnection(Connection::class.java) { c -> + try { + c.close() + } catch (e: Exception) { + logger.error("Failed to close connection", e) + } + } + states.update(INITIALIZED_STATE, false) + stateListenerJob?.cancel() + executor.shutdown() + } + + + override val name: String + get() = meta.getString("name", type) + + protected fun runOnDeviceThread(runnable: () -> Unit): Future<*> { + return executor.submit(runnable) + } + + protected fun callOnDeviceThread(callable: () -> T): Future { + return executor.submit(callable) + } + + protected fun scheduleOnDeviceThread(delay: Duration, runnable: () -> Unit): ScheduledFuture<*> { + return executor.schedule(runnable, delay.toMillis(), TimeUnit.MILLISECONDS) + } + + protected fun repeatOnDeviceThread( + interval: Duration, + delay: Duration = Duration.ZERO, + runnable: () -> Unit + ): ScheduledFuture<*> { + return executor.scheduleWithFixedDelay(runnable, delay.toMillis(), interval.toMillis(), TimeUnit.MILLISECONDS) + } + + + /** + * Override to apply custom internal reaction of state change + */ + protected open fun onStateChange(stateName: String, value: Any) { + forEachConnection(Roles.DEVICE_LISTENER_ROLE, DeviceListener::class.java) { + it.notifyStateChanged(this, stateName, value) + } + } + + override val type: String + get() = meta.getString("type", "unknown") + + protected fun updateState(stateName: String, value: Any?) { + states.update(stateName, value) + } +} + + +val Device.initialized: Boolean + get() { + return if (this is AbstractDevice) { + this.initialized + } else { + this.states + .filter { it.name == INITIALIZED_STATE } + .filterIsInstance(ValueState::class.java).firstOrNull()?.value?.boolean ?: false + } + } + +fun Device.notifyError(message: String, error: Throwable? = null) { + logger.error(message, error) + forEachConnection(DeviceListener::class.java) { + it.evaluateDeviceException(this, message, error) + } +} + +fun Device.dispatchEvent(event: Event) { + forEachConnection(EventHandler::class.java) { it -> it.pushEvent(event) } +} \ No newline at end of file diff --git a/dataforge-control/src/main/kotlin/hep/dataforge/control/devices/Device.kt b/dataforge-control/src/main/kotlin/hep/dataforge/control/devices/Device.kt new file mode 100644 index 00000000..f1333f52 --- /dev/null +++ b/dataforge-control/src/main/kotlin/hep/dataforge/control/devices/Device.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.control.devices + +import hep.dataforge.Named +import hep.dataforge.connections.AutoConnectible +import hep.dataforge.connections.Connection.EVENT_HANDLER_ROLE +import hep.dataforge.connections.Connection.LOGGER_ROLE +import hep.dataforge.connections.RoleDef +import hep.dataforge.connections.RoleDefs +import hep.dataforge.context.ContextAware +import hep.dataforge.control.connections.Roles.DEVICE_LISTENER_ROLE +import hep.dataforge.control.connections.Roles.VIEW_ROLE +import hep.dataforge.events.EventHandler +import hep.dataforge.exceptions.ControlException +import hep.dataforge.meta.Metoid +import hep.dataforge.states.Stateful +import org.slf4j.Logger + + +/** + * The Device is general abstract representation of any physical or virtual + * apparatus that can interface with data acquisition and control system. + * + * + * The device has following important features: + * + * + * * + * **States:** each device has a number of states that could be + * accessed by `getState` method. States could be either stored as some + * internal variables or calculated on demand. States calculation is + * synchronous! + * + * * + * **Listeners:** some external class which listens device state + * changes and events. By default listeners are represented by weak references + * so they could be finalized any time if not used. + * * + * **Connections:** any external device connectors which are used + * by device. The difference between listener and connection is that device is + * obligated to notify all registered listeners about all changes, but + * connection is used by device at its own discretion. Also usually only one + * connection is used for each single purpose. + * + * + * + * @author Alexander Nozik + */ +@RoleDefs( + RoleDef(name = DEVICE_LISTENER_ROLE, objectType = DeviceListener::class, info = "A device listener"), + RoleDef(name = LOGGER_ROLE, objectType = Logger::class, unique = true, info = "The logger for this device"), + RoleDef(name = EVENT_HANDLER_ROLE, objectType = EventHandler::class, info = "The listener for device events"), + RoleDef(name = VIEW_ROLE) +) +interface Device : AutoConnectible, Metoid, ContextAware, Named, Stateful { + + /** + * Device type + * + * @return + */ + val type: String + + @JvmDefault + override val logger: Logger + get() = optConnection(LOGGER_ROLE, Logger::class.java).orElse(context.logger) + + /** + * Initialize device and check if it is working but do not start any + * measurements or issue commands. Init method could be called only once per + * MeasurementDevice object. On second call it throws exception or does + * nothing. + * + * @throws ControlException + */ + @Throws(ControlException::class) + fun init() + + /** + * Release all resources locked during init. No further work with device is + * possible after shutdown. The init method called after shutdown can cause + * exceptions or incorrect work. + * + * @throws ControlException + */ + @Throws(ControlException::class) + fun shutdown() + + + companion object { + const val INITIALIZED_STATE = "initialized" + } +} diff --git a/dataforge-control/src/main/kotlin/hep/dataforge/control/devices/DeviceFactory.kt b/dataforge-control/src/main/kotlin/hep/dataforge/control/devices/DeviceFactory.kt new file mode 100644 index 00000000..58374888 --- /dev/null +++ b/dataforge-control/src/main/kotlin/hep/dataforge/control/devices/DeviceFactory.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.control.devices + +import hep.dataforge.utils.ContextMetaFactory + +/** + * Created by darksnake on 06-May-17. + */ +interface DeviceFactory : ContextMetaFactory { + /** + * The type of the device factory. One factory can supply multiple device classes depending on configuration. + * + * @return + */ + val type: String +} diff --git a/dataforge-control/src/main/kotlin/hep/dataforge/control/devices/DeviceHub.kt b/dataforge-control/src/main/kotlin/hep/dataforge/control/devices/DeviceHub.kt new file mode 100644 index 00000000..1c4c2677 --- /dev/null +++ b/dataforge-control/src/main/kotlin/hep/dataforge/control/devices/DeviceHub.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.control.devices + +import hep.dataforge.connections.Connection +import hep.dataforge.context.Context +import hep.dataforge.meta.Meta +import hep.dataforge.names.Name +import hep.dataforge.providers.Provider +import hep.dataforge.providers.Provides +import hep.dataforge.providers.ProvidesNames +import java.util.* +import java.util.stream.Stream + +/** + * A hub containing several devices + */ +interface DeviceHub : Provider { + + val deviceNames: List + + fun optDevice(name: Name): Optional + + @Provides(DEVICE_TARGET) + fun optDevice(name: String): Optional { + return optDevice(Name.of(name)) + } + + @ProvidesNames(DEVICE_TARGET) + fun listDevices(): Stream { + return deviceNames.stream().map{ it.toString() } + } + + fun getDevices(recursive: Boolean): Stream { + return if (recursive) { + deviceNames.stream().map { it -> optDevice(it).get() } + } else { + deviceNames.stream().filter { it -> it.length == 1 }.map { it -> optDevice(it).get() } + } + } + + /** + * Add a connection to each of child devices + * + * @param connection + * @param roles + */ + fun connectAll(connection: Connection, vararg roles: String) { + deviceNames.stream().filter { it -> it.length == 1 } + .map>{ this.optDevice(it) } + .map{ it.get() } + .forEach { it -> + if (it is DeviceHub) { + (it as DeviceHub).connectAll(connection, *roles) + } else { + it.connect(connection, *roles) + } + } + } + + fun connectAll(context: Context, meta: Meta) { + deviceNames.stream().filter { it -> it.length == 1 } + .map>{ this.optDevice(it) } + .map{ it.get() } + .forEach { it -> + if (it is DeviceHub) { + (it as DeviceHub).connectAll(context, meta) + } else { + it.connectionHelper.connect(context, meta) + } + } + } + + companion object { + const val DEVICE_TARGET = "device" + } +} diff --git a/dataforge-control/src/main/kotlin/hep/dataforge/control/devices/DeviceListener.kt b/dataforge-control/src/main/kotlin/hep/dataforge/control/devices/DeviceListener.kt new file mode 100644 index 00000000..e4a3c6d4 --- /dev/null +++ b/dataforge-control/src/main/kotlin/hep/dataforge/control/devices/DeviceListener.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.control.devices + +import hep.dataforge.connections.Connection + +/** + * A listener that listens to device state change initialization and shut down + * + * @author Alexander Nozik + */ +interface DeviceListener: Connection { + + /** + * Notify that state of device is changed. + * + * @param device + * @param name the name of the state + * @param state + */ + fun notifyStateChanged(device: Device, name: String, state: Any) + + /** + * + * @param device + * @param message + * @param exception + */ + fun evaluateDeviceException(device: Device, message: String, exception: Throwable?) { + + } +} diff --git a/dataforge-control/src/main/kotlin/hep/dataforge/control/devices/PortSensor.kt b/dataforge-control/src/main/kotlin/hep/dataforge/control/devices/PortSensor.kt new file mode 100644 index 00000000..00709e70 --- /dev/null +++ b/dataforge-control/src/main/kotlin/hep/dataforge/control/devices/PortSensor.kt @@ -0,0 +1,208 @@ +/* + * Copyright 2017 Alexander Nozik. + * + * 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. + */ + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.control.devices + +import hep.dataforge.context.Context +import hep.dataforge.control.devices.PortSensor.Companion.CONNECTED_STATE +import hep.dataforge.control.devices.PortSensor.Companion.DEBUG_STATE +import hep.dataforge.control.ports.GenericPortController +import hep.dataforge.control.ports.PortFactory +import hep.dataforge.description.NodeDef +import hep.dataforge.description.ValueDef +import hep.dataforge.description.ValueDefs +import hep.dataforge.events.EventBuilder +import hep.dataforge.exceptions.ControlException +import hep.dataforge.meta.Meta +import hep.dataforge.nullable +import hep.dataforge.states.* +import hep.dataforge.useValue +import hep.dataforge.values.ValueType.BOOLEAN +import hep.dataforge.values.ValueType.NUMBER +import java.time.Duration + +/** + * A Sensor that uses a Port to obtain data + * + * @param + * @author darksnake + */ +@StateDefs( + StateDef( + value = ValueDef( + key = CONNECTED_STATE, + type = [BOOLEAN], + def = "false", + info = "The connection state for this device" + ), writable = true + ), + //StateDef(value = ValueDef(name = PORT_STATE, info = "The name of the port to which this device is connected")), + StateDef( + value = ValueDef( + key = DEBUG_STATE, + type = [BOOLEAN], + def = "false", + info = "If true, then all received phrases would be shown in the log" + ), writable = true + ) +) +@MetaStateDef( + value = NodeDef( + key = "port", + descriptor = "method::hep.dataforge.control.ports.PortFactory.build", + info = "Information about port" + ), writable = true +) +@ValueDefs( + ValueDef(key = "timeout", type = arrayOf(NUMBER), def = "400", info = "A timeout for port response in milliseconds") +) +abstract class PortSensor(context: Context, meta: Meta) : Sensor(context, meta) { + + private var _connection: GenericPortController? = null + protected val connection: GenericPortController + get() = _connection ?: throw RuntimeException("Not connected") + + val connected = valueState(CONNECTED_STATE, getter = { connection.port.isOpen }) { old, value -> + if (old != value) { + logger.info("State 'connect' changed to $value") + connect(value.boolean) + } + update(value) + } + + var debug by valueState(DEBUG_STATE) { old, value -> + if (old != value) { + logger.info("Turning debug mode to $value") + setDebugMode(value.boolean) + } + update(value) + }.booleanDelegate + + var port by metaState(PORT_STATE, getter = { connection.port.toMeta() }) { old, value -> + if (old != value) { + setupConnection(value) + } + update(value) + }.delegate + + private val defaultTimeout: Duration = Duration.ofMillis(meta.getInt("timeout", 400).toLong()) + + init { +// meta.useMeta(PORT_STATE) { +// port = it +// } + meta.useValue(DEBUG_STATE) { + updateState(DEBUG_STATE, it.boolean) + } + } + + private fun setDebugMode(debugMode: Boolean) { + //Add debug listener + if (debugMode) { + connection.apply { + onAnyPhrase("$name[debug]") { phrase -> logger.debug("Device {} received phrase: \n{}", name, phrase) } + onError("$name[debug]") { message, error -> + logger.error( + "Device {} exception: \n{}", + name, + message, + error + ) + } + } + } else { + connection.apply { + removePhraseListener("$name[debug]") + removeErrorListener("$name[debug]") + } + } + updateState(DEBUG_STATE, debugMode) + } + + private fun connect(connected: Boolean) { + if (connected) { + try { + if (_connection == null) { + logger.debug("Setting up connection using device meta") + val initialPort = meta.optMeta(PORT_STATE).nullable + ?: meta.optString(PORT_STATE).nullable?.let { PortFactory.nameToMeta(it) } + ?: Meta.empty() + setupConnection(initialPort) + } + connection.open() + this.connected.update(true) + } catch (ex: Exception) { + notifyError("Failed to open connection", ex) + this.connected.update(false) + } + } else { + _connection?.close() + _connection = null + this.connected.update(false) + } + } + + protected open fun buildConnection(meta: Meta): GenericPortController { + val port = PortFactory.build(meta) + return GenericPortController(context, port) + } + + private fun setupConnection(portMeta: Meta) { + _connection?.close() + this._connection = buildConnection(portMeta) + setDebugMode(debug) + updateState(PORT_STATE, portMeta) + } + + @Throws(ControlException::class) + override fun shutdown() { + super.shutdown() + connected.set(false) + } + + protected fun sendAndWait(request: String, timeout: Duration = defaultTimeout): String { + return connection.sendAndWait(request, timeout) { true } + } + + protected fun sendAndWait( + request: String, + timeout: Duration = defaultTimeout, + predicate: (String) -> Boolean + ): String { + return connection.sendAndWait(request, timeout, predicate) + } + + protected fun send(message: String) { + connection.send(message) + dispatchEvent( + EventBuilder + .make(name) + .setMetaValue("request", message) + .build() + ) + } + + companion object { + const val CONNECTED_STATE = "connected" + const val PORT_STATE = "port" + const val DEBUG_STATE = "debug" + } +} diff --git a/dataforge-control/src/main/kotlin/hep/dataforge/control/devices/Sensor.kt b/dataforge-control/src/main/kotlin/hep/dataforge/control/devices/Sensor.kt new file mode 100644 index 00000000..01852f12 --- /dev/null +++ b/dataforge-control/src/main/kotlin/hep/dataforge/control/devices/Sensor.kt @@ -0,0 +1,259 @@ +/* + * Copyright 2017 Alexander Nozik. + * + * 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. + */ + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.control.devices + +import hep.dataforge.context.Context +import hep.dataforge.control.devices.Sensor.Companion.MEASUREMENT_MESSAGE_STATE +import hep.dataforge.control.devices.Sensor.Companion.MEASUREMENT_META_STATE +import hep.dataforge.control.devices.Sensor.Companion.MEASUREMENT_PROGRESS_STATE +import hep.dataforge.control.devices.Sensor.Companion.MEASUREMENT_RESULT_STATE +import hep.dataforge.control.devices.Sensor.Companion.MEASUREMENT_STATUS_STATE +import hep.dataforge.control.devices.Sensor.Companion.MEASURING_STATE +import hep.dataforge.description.NodeDef +import hep.dataforge.description.ValueDef +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaMorph +import hep.dataforge.meta.buildMeta +import hep.dataforge.states.* +import hep.dataforge.values.ValueType +import kotlinx.coroutines.InternalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.launch +import kotlinx.coroutines.time.delay +import java.time.Duration +import java.time.Instant + +/** + * A device with which could perform of one-time or regular measurements. Only one measurement is allowed at a time + * + * @author Alexander Nozik + */ +@ValueDef( + key = "resultBuffer", + type = [ValueType.NUMBER], + def = "100", + info = "The size of the buffer for results of measurements" +) +@StateDefs( + StateDef( + value = ValueDef( + key = MEASURING_STATE, + type = [ValueType.BOOLEAN], + info = "Shows if this sensor is actively measuring" + ), writable = true + ), + StateDef( + ValueDef( + key = MEASUREMENT_STATUS_STATE, + enumeration = Sensor.MeasurementState::class, + info = "Shows if this sensor is actively measuring" + ) + ), + StateDef(ValueDef(key = MEASUREMENT_MESSAGE_STATE, info = "Current message")), + StateDef(ValueDef(key = MEASUREMENT_PROGRESS_STATE, type = [ValueType.NUMBER], info = "Current progress")) +) +@MetaStateDefs( + MetaStateDef( + value = NodeDef(key = MEASUREMENT_META_STATE, info = "Configuration of current measurement."), + writable = true + ), + MetaStateDef(NodeDef(key = MEASUREMENT_RESULT_STATE, info = "The result of the last measurement in Meta form")) +) +abstract class Sensor(context: Context, meta: Meta) : AbstractDevice(context, meta) { + + private val coroutineContext = executor.asCoroutineDispatcher() + + protected var job: Job? = null + + val resultState = metaState(MEASUREMENT_RESULT_STATE) + + /** + * The result of last measurement + */ + val result: Meta by resultState.delegate + + /** + * The error from last measurement + */ + val error: Meta by metaState(MEASUREMENT_ERROR_STATE).delegate + + /** + * Current measurement configuration + */ + var measurement by metaState(MEASUREMENT_META_STATE) { old: Meta?, value: Meta -> + startMeasurement(old, value) + update(value) + }.delegate + + /** + * true if measurement in process + */ + val measuring = valueState(MEASURING_STATE) { value -> + if (value.boolean) { + startMeasurement(null, measurement) + } else { + stopMeasurement() + } + update(value) + } + + /** + * Current state of the measurement + */ + val measurementState by valueState(MEASUREMENT_STATUS_STATE).enumDelegate() + + var message by valueState(MEASUREMENT_MESSAGE_STATE).stringDelegate + + var progress by valueState(MEASUREMENT_PROGRESS_STATE).doubleDelegate + + override fun shutdown() { + stopMeasurement() + super.shutdown() + } + + /** + * Start measurement with current configuration if it is not in progress + */ + fun measure() { + if (!measuring.booleanValue) { + measuring.set(true) + } + } + + /** + * Notify measurement state changed + */ + protected fun notifyMeasurementState(state: MeasurementState) { + updateState(MEASUREMENT_STATUS_STATE, state.name) + when (state) { + MeasurementState.NOT_STARTED -> updateState(MEASURING_STATE, false) + MeasurementState.STOPPED -> updateState(MEASURING_STATE, false) + MeasurementState.IN_PROGRESS -> updateState(MEASURING_STATE, true) + MeasurementState.WAITING -> updateState(MEASURING_STATE, true) + } + } + + /** + * Set active measurement using given meta + * @param oldMeta Meta of previous active measurement. If null no measurement was set + * @param newMeta Meta of new measurement. If null, then clear measurement + * @return actual meta for new measurement + */ + protected abstract fun startMeasurement(oldMeta: Meta?, newMeta: Meta) + + /** + * stop measurement with given meta + */ + protected open fun stopMeasurement() { + synchronized(this) { + job?.cancel() + notifyMeasurementState(MeasurementState.STOPPED) + } + } + + protected fun measurement(action: suspend () -> Unit) { + job = context.launch { + notifyMeasurementState(MeasurementState.IN_PROGRESS) + action.invoke() + notifyMeasurementState(MeasurementState.STOPPED) + } + } + + protected fun scheduleMeasurement(interval: Duration, action: suspend () -> Unit) { + job = context.launch { + delay(interval) + notifyMeasurementState(MeasurementState.IN_PROGRESS) + action.invoke() + notifyMeasurementState(MeasurementState.STOPPED) + } + } + + @InternalCoroutinesApi + protected fun regularMeasurement(interval: Duration, action: suspend () -> Unit) { + job = context.launch { + while (true) { + notifyMeasurementState(MeasurementState.IN_PROGRESS) + action.invoke() + notifyMeasurementState(MeasurementState.WAITING) + delay(interval) + } + }.apply { + invokeOnCompletion(onCancelling = true, invokeImmediately = true) { + notifyMeasurementState(MeasurementState.STOPPED) + } + } + } + + protected fun notifyResult(value: Any, timestamp: Instant = Instant.now()) { + val result = buildMeta("result") { + RESULT_TIMESTAMP to timestamp + when (value) { + is Meta -> setNode(RESULT_VALUE, value) + is MetaMorph -> setNode(RESULT_VALUE, value.toMeta()) + else -> RESULT_VALUE to value + } + } + updateState(MEASUREMENT_RESULT_STATE, result) + forEachConnection(SensorListener::class.java){ + it.reading(this,value) + } + } + + protected fun notifyError(value: Any, timestamp: Instant = Instant.now()) { + val result = buildMeta("error") { + RESULT_TIMESTAMP to timestamp + if (value is Meta) { + setNode(RESULT_VALUE, value) + } else { + RESULT_VALUE to value + } + } + updateState(MEASUREMENT_ERROR_STATE, result) + } + + enum class MeasurementState { + NOT_STARTED, // initial state, not started + IN_PROGRESS, // in progress + WAITING, // waiting on scheduler + STOPPED // stopped + } + + companion object { + const val MEASURING_STATE = "measurement.active" + const val MEASUREMENT_STATUS_STATE = "measurement.state" + const val MEASUREMENT_META_STATE = "measurement.meta" + const val MEASUREMENT_RESULT_STATE = "measurement.result" + const val MEASUREMENT_ERROR_STATE = "measurement.error" + const val MEASUREMENT_MESSAGE_STATE = "measurement.message" + const val MEASUREMENT_PROGRESS_STATE = "measurement.progress" + + const val RESULT_SUCCESS = "success" + const val RESULT_TIMESTAMP = "timestamp" + const val RESULT_VALUE = "value" + + } +} + +interface SensorListener{ + fun reading(sensor: Sensor, any:Any) +} diff --git a/dataforge-control/src/main/kotlin/hep/dataforge/control/devices/Virtual.kt b/dataforge-control/src/main/kotlin/hep/dataforge/control/devices/Virtual.kt new file mode 100644 index 00000000..61b3cfb1 --- /dev/null +++ b/dataforge-control/src/main/kotlin/hep/dataforge/control/devices/Virtual.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2017 Alexander Nozik. + * + * 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 hep.dataforge.control.devices + +import hep.dataforge.context.Context +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import java.time.Duration +import java.time.Instant +import java.util.* + +private val VIRTUAL_SENSOR_TYPE = "@test" + +private val generator = Random() + +class VirtualSensor(context: Context) : Sensor(context, Meta.empty()) { + override fun startMeasurement(oldMeta: Meta?, newMeta: Meta) { + if (oldMeta !== newMeta) { + val delay = Duration.parse(newMeta.getString("duration", "PT0.2S")) + val mean = newMeta.getDouble("mean", 1.0) + val sigma = newMeta.getDouble("sigma", 0.1) + + measurement { + Thread.sleep(delay.toMillis()) + val value = generator.nextDouble() * sigma + mean + MetaBuilder("result").setValue("value", value).setValue("timestamp", Instant.now()) + } + } + } + + + override val type: String + get() { + return VIRTUAL_SENSOR_TYPE + } + +} \ No newline at end of file diff --git a/dataforge-control/src/main/kotlin/hep/dataforge/control/ports/ComPort.kt b/dataforge-control/src/main/kotlin/hep/dataforge/control/ports/ComPort.kt new file mode 100644 index 00000000..723e4fe7 --- /dev/null +++ b/dataforge-control/src/main/kotlin/hep/dataforge/control/ports/ComPort.kt @@ -0,0 +1,162 @@ +/* + * Copyright 2017 Alexander Nozik. + * + * 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 hep.dataforge.control.ports + +import hep.dataforge.exceptions.PortException +import hep.dataforge.meta.Meta +import hep.dataforge.meta.buildMeta +import jssc.SerialPort +import jssc.SerialPort.* +import jssc.SerialPortEventListener +import jssc.SerialPortException +import kotlinx.coroutines.launch +import java.io.IOException + +/** + * @author Alexander Nozik + */ +class ComPort(val address: String, val config: Meta) : Port() { + + override val name: String = if (address.startsWith("com::")) { + address + } else { + "com::$address" + } + + // private static final int CHAR_SIZE = 1; + // private static final int MAX_SIZE = 50; + private val port: SerialPort by lazy { + SerialPort(name) + } + + private val serialPortListener = SerialPortEventListener { event -> + if (event.isRXCHAR) { + val chars = event.eventValue + try { + val bytes = port.readBytes(chars) + receive(bytes) + } catch (ex: IOException) { + throw RuntimeException(ex) + } catch (ex: SerialPortException) { + throw RuntimeException(ex) + } + } + } + + override val isOpen: Boolean + get() = port.isOpened + + + override fun toString(): String { + return name + } + + @Throws(PortException::class) + override fun open() { + try { + if (!port.isOpened) { + port.apply { + openPort() + val baudRate = config.getInt("baudRate", BAUDRATE_9600) + val dataBits = config.getInt("dataBits", DATABITS_8) + val stopBits = config.getInt("stopBits", STOPBITS_1) + val parity = config.getInt("parity", PARITY_NONE) + setParams(baudRate, dataBits, stopBits, parity) + addEventListener(serialPortListener) + } + } + } catch (ex: SerialPortException) { + throw PortException("Can't open the port", ex) + } + + } + + @Throws(PortException::class) + fun clearPort() { + try { + port.purgePort(PURGE_RXCLEAR or PURGE_TXCLEAR) + } catch (ex: SerialPortException) { + throw PortException(ex) + } + + } + + @Throws(Exception::class) + override fun close() { + port.let { + it.removeEventListener() + if (it.isOpened) { + it.closePort() + } + } + super.close() + } + + @Throws(PortException::class) + public override fun send(message: ByteArray) { + if (!isOpen) { + open() + } + launch { + try { + logger.debug("SEND: $message") + port.writeBytes(message) + } catch (ex: SerialPortException) { + throw RuntimeException(ex) + } + } + } + + override fun toMeta(): Meta = buildMeta { + "type" to "com" + "name" to this@ComPort.name + "address" to address + update(config) + } + + companion object { + + /** + * Construct ComPort with default parameters: + * + * + * Baud rate: 9600 + * + * + * Data bits: 8 + * + * + * Stop bits: 1 + * + * + * Parity: non + * + * @param portName + */ + @JvmOverloads + fun create(portName: String, baudRate: Int = BAUDRATE_9600, dataBits: Int = DATABITS_8, stopBits: Int = STOPBITS_1, parity: Int = PARITY_NONE): ComPort { + return ComPort(portName, buildMeta { + setValue("type", "com") + putValue("name", portName) + putValue("baudRate", baudRate) + putValue("dataBits", dataBits) + putValue("stopBits", stopBits) + putValue("parity", parity) + }) + } + } +} + diff --git a/dataforge-control/src/main/kotlin/hep/dataforge/control/ports/GenericPortController.kt b/dataforge-control/src/main/kotlin/hep/dataforge/control/ports/GenericPortController.kt new file mode 100644 index 00000000..f15db13e --- /dev/null +++ b/dataforge-control/src/main/kotlin/hep/dataforge/control/ports/GenericPortController.kt @@ -0,0 +1,329 @@ +/* + * Copyright 2017 Alexander Nozik. + * + * 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 hep.dataforge.control.ports + +import hep.dataforge.context.Context +import hep.dataforge.context.ContextAware +import hep.dataforge.context.Global +import hep.dataforge.exceptions.ControlException +import hep.dataforge.exceptions.PortException +import hep.dataforge.utils.ReferenceRegistry +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.ByteArrayOutputStream +import java.nio.charset.Charset +import java.time.Duration +import java.util.concurrent.CompletableFuture +import java.util.concurrent.TimeUnit + + +/** + * A port controller helper that allows both synchronous and asynchronous operations on port + * @property port the port associated with this controller + */ +open class GenericPortController( + override val context: Context, + val port: Port, + private val phraseCondition: (String) -> Boolean = { it.endsWith("\n") } +) : PortController, AutoCloseable, ContextAware { + + + constructor(context: Context, port: Port, delimiter: String) : this(context, port, { it.endsWith(delimiter) }) + + private val waiters = ReferenceRegistry() + private val listeners = ReferenceRegistry() + private val exceptionListeners = ReferenceRegistry() + private val buffer = ByteArrayOutputStream(); + + + fun open() { + try { + port.holdBy(this) + if (!port.isOpen) { + port.open() + } + } catch (e: PortException) { + throw RuntimeException("Can't hold the port $port by generic handler", e) + } + } + + override val logger: Logger + get() = LoggerFactory.getLogger("${context.name}.$port") + + override fun accept(byte: Byte) { + synchronized(port) { + buffer.write(byte.toInt()) + val string = buffer.toString("UTF8") + if (phraseCondition(string)) { + acceptPhrase(string) + buffer.reset() + } + } + } + + private fun acceptPhrase(message: String) { + waiters.forEach { waiter -> waiter.acceptPhrase(message) } + listeners.forEach { listener -> listener.acceptPhrase(message) } + } + + override fun error(errorMessage: String, error: Throwable) { + exceptionListeners.forEach { it -> + context.executors.defaultExecutor.submit { + try { + it.action(errorMessage, error) + } catch (ex: Exception) { + logger.error("Failed to execute error listener action", ex) + } + } + } + } + + /** + * Wait for next phrase matching condition and return its result + * + * @return + */ + @JvmOverloads + fun next(condition: (String) -> Boolean = { true }): CompletableFuture { + //No need for synchronization since ReferenceRegistry is synchronized + waiters.removeIf { it.isDone } + val res = FuturePhrase(condition) + waiters.add(res) + return res + } + + /** + * Get next phrase matching pattern + * + * @param pattern + * @return + */ + fun next(pattern: String): CompletableFuture { + return next { it -> it.matches(pattern.toRegex()) } + } + + /** + * Block until specific phrase is received + * + * @param timeout + * @param predicate + * @return + * @throws PortException + */ + @JvmOverloads + fun waitFor(timeout: Duration, predicate: (String) -> Boolean = { true }): String { + return next(predicate).get(timeout.toMillis(), TimeUnit.MILLISECONDS) + } + + /** + * Hook specific reaction to the specific phrase. Whenever it is possible, it is better to use `weakOnPhrase` to avoid memory leaks due to obsolete listeners. + * + * @param condition + * @param action + */ + fun onPhrase(condition: (String) -> Boolean, owner: Any? = null, action: (String) -> Unit) { + val listener = PhraseListener(condition, owner, action) + listeners.add(listener) + } + + /** + * Add weak phrase listener + * + * @param condition + * @param action + * @return + */ + fun weakOnPhrase(condition: (String) -> Boolean, owner: Any? = null, action: (String) -> Unit) { + val listener = PhraseListener(condition, owner, action) + listeners.add(listener, false) + } + + fun weakOnPhrase(pattern: String, owner: Any? = null, action: (String) -> Unit) { + weakOnPhrase({ it.matches(pattern.toRegex()) }, owner, action) + } + + fun weakOnPhrase(owner: Any? = null, action: (String) -> Unit) { + weakOnPhrase({ true }, owner, action) + } + + /** + * Remove a specific phrase listener + * + * @param listener + */ + fun removePhraseListener(owner: Any) { + this.listeners.removeIf { it.owner == owner } + } + + /** + * Add action to phrase matching specific pattern + * + * @param pattern + * @param action + * @return + */ + fun onPhrase(pattern: String, owner: Any? = null, action: (String) -> Unit) { + onPhrase({ it.matches(pattern.toRegex()) }, owner, action) + } + + /** + * Add reaction to any phrase + * + * @param action + * @return + */ + fun onAnyPhrase(owner: Any? = null, action: (String) -> Unit) { + onPhrase({ true }, owner, action) + } + + /** + * Add error listener + * + * @param listener + * @return + */ + fun onError(owner: Any? = null, listener: (String, Throwable?) -> Unit) { + this.exceptionListeners.add(ErrorListener(owner, listener)) + } + + /** + * Add weak error listener + * + * @param listener + * @return + */ + fun weakOnError(owner: Any? = null, listener: (String, Throwable?) -> Unit) { + this.exceptionListeners.add(ErrorListener(owner, listener), false) + } + + /** + * remove specific error listener + * + */ + fun removeErrorListener(owner: Any) { + this.exceptionListeners.removeIf { it.owner == owner } + } + + /** + * Send async message to port + * + * @param message + */ + fun send(message: ByteArray) { + try { + open() + port.send(this, message) + } catch (e: PortException) { + throw RuntimeException("Failed to send message to port $port") + } + + } + + fun send(message: String, charset: Charset = Charsets.US_ASCII) { + send(message.toByteArray(charset)) + } + + /** + * Send and return the future with the result + * + * @param message + * @param condition + */ + fun sendAndGet(message: String, condition: (String) -> Boolean): CompletableFuture { + val res = next(condition) // in case of immediate reaction + send(message) + return res + } + + /** + * Send and block thread until specific result is obtained. All listeners and reactions work as usual. + * + * @param message + * @param timeout + * @param condition + * @return + */ + fun sendAndWait(message: String, timeout: Duration, condition: (String) -> Boolean = { true }): String { + return sendAndGet(message, condition).get(timeout.toMillis(), TimeUnit.MILLISECONDS) + } + + /** + * Cancel all pending waiting actions and release the port. Does not close the port + */ + @Throws(Exception::class) + override fun close() { + close(Duration.ofMillis(1000)) + } + + /** + * Blocking close operation. Waits at most for timeout to finish all operations and then closes. + * + * @param timeout + */ + @Throws(Exception::class) + fun close(timeout: Duration) { + CompletableFuture.allOf(*waiters.toTypedArray()).get(timeout.toMillis(), TimeUnit.MILLISECONDS) + port.releaseBy(this) + } + + private inner class FuturePhrase(internal val condition: (String) -> Boolean) : CompletableFuture() { + internal fun acceptPhrase(phrase: String) { + if (condition(phrase)) { + complete(phrase) + } + } + } + + private inner class PhraseListener(private val condition: (String) -> Boolean, val owner: Any? = null, private val action: (String) -> Unit) { + + internal fun acceptPhrase(phrase: String) { + if (condition(phrase)) { + context.executors.defaultExecutor.submit { + try { + action(phrase) + } catch (ex: Exception) { + logger.error("Failed to execute hooked action", ex) + } + } + } + } + } + + private inner class ErrorListener(val owner: Any? = null, val action: (String, Throwable?) -> Unit) + + companion object { + + /** + * Use temporary controller to safely send request and receive response + * + * @param port + * @param request + * @param timeout + * @return + * @throws ControlException + */ + @Throws(ControlException::class) + fun sendAndWait(port: Port, request: String, timeout: Duration): String { + try { + GenericPortController(Global, port).use { controller -> return controller.sendAndWait(request, timeout) { true } } + } catch (e: Exception) { + throw ControlException("Failed to close the port", e) + } + + } + } +} \ No newline at end of file diff --git a/dataforge-control/src/main/kotlin/hep/dataforge/control/ports/Port.kt b/dataforge-control/src/main/kotlin/hep/dataforge/control/ports/Port.kt new file mode 100644 index 00000000..c519325a --- /dev/null +++ b/dataforge-control/src/main/kotlin/hep/dataforge/control/ports/Port.kt @@ -0,0 +1,200 @@ +/* + * Copyright 2017 Alexander Nozik. + * + * 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 hep.dataforge.control.ports + +import hep.dataforge.Named +import hep.dataforge.exceptions.PortException +import hep.dataforge.exceptions.PortLockException +import hep.dataforge.meta.MetaID +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.launch +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.time.Duration +import java.util.concurrent.Executors +import java.util.concurrent.locks.ReentrantLock + + +/** + * The controller which is currently working with this handler. One + * controller can simultaneously hold many handlers, but handler could be + * held by only one controller. + */ +interface PortController { + + fun accept(byte: Byte) + + fun accept(bytes: ByteArray){ + //TODO improve performance using byte buffers + bytes.forEach { accept(it) } + } + + fun error(errorMessage: String, error: Throwable) { + //do nothing + } +} + + +/** + * The universal asynchronous port handler + * + * @author Alexander Nozik + */ +abstract class Port : AutoCloseable, MetaID, Named, CoroutineScope { + + private val portLock = ReentrantLock(true) + + private var controller: PortController? = null + + override val coroutineContext = Executors.newSingleThreadExecutor { r -> + val res = Thread(r) + res.name = "port::$name" + res.priority = Thread.MAX_PRIORITY + res + }.asCoroutineDispatcher() + + protected val logger: Logger by lazy { LoggerFactory.getLogger("port[$name]") } + + abstract val isOpen: Boolean + + private val isLocked: Boolean + get() = this.portLock.isLocked + + @Throws(PortException::class) + abstract fun open() + + /** + * Emergency hold break. + */ + @Synchronized + fun breakHold() { + if (isLocked) { + logger.warn("Breaking hold on port $name") + launch { portLock.unlock() } + } + } + + /** + * An unique ID for this port + * + * @return + */ + override fun toString(): String { + return name + } + + /** + * Acquire lock on this instance of port handler with given controller + * object. If port is currently locked by another controller, the wait until it is released. + * Only the same controller can release the port. + * + * @param controller + * @throws hep.dataforge.exceptions.PortException + */ + @Throws(PortException::class) + fun holdBy(controller: PortController) { + if (!isOpen) { + open() + } + + launch { + try { + portLock.lockInterruptibly() + } catch (ex: InterruptedException) { + logger.error("Lock on port {} is broken", toString()) + throw RuntimeException(ex) + } + } + logger.debug("Locked by {}", controller) + this.controller = controller + } + + + /** + * Receive a single byte + */ + fun receive(byte: Byte) { + controller?.accept(byte) + } + + /** + * Receive an array of bytes + */ + fun receive(bytes: ByteArray) { + controller?.accept(bytes) + } + + /** + * send the message to the port + * + * @param message + * @throws hep.dataforge.exceptions.PortException + */ + @Throws(PortException::class) + protected abstract fun send(message: ByteArray) + + /** + * Send the message if the controller is correct + * + * @param controller + * @param message + * @throws PortException + */ + @Throws(PortException::class) + fun send(controller: PortController, message: ByteArray) { + if (controller === this.controller) { + send(message) + } else { + throw PortException("Port locked by another controller") + } + } + + /** + * Release hold of this portHandler from given controller. + * + * @param controller + * @throws PortLockException in case given holder is not the one that holds + * handler + */ + @Synchronized + @Throws(PortLockException::class) + fun releaseBy(controller: PortController) { + if (isLocked) { + if (controller == this.controller) { + this.controller = null + launch { + portLock.unlock() + logger.debug("Unlocked by {}", controller) + } + } else { + throw PortLockException("Can't unlock port with wrong controller") + } + } else { + logger.warn("Attempting to release unlocked port") + } + } + + + @Throws(Exception::class) + override fun close() { + coroutineContext.close() + } + + class PortTimeoutException(timeout: Duration) : PortException() { + override val message: String = String.format("The timeout time of '%s' is exceeded", timeout) + } +} diff --git a/dataforge-control/src/main/kotlin/hep/dataforge/control/ports/PortFactory.kt b/dataforge-control/src/main/kotlin/hep/dataforge/control/ports/PortFactory.kt new file mode 100644 index 00000000..2736080d --- /dev/null +++ b/dataforge-control/src/main/kotlin/hep/dataforge/control/ports/PortFactory.kt @@ -0,0 +1,103 @@ +/* + * Copyright 2017 Alexander Nozik. + * + * 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. + */ + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.control.ports + +import hep.dataforge.description.ValueDef +import hep.dataforge.description.ValueDefs +import hep.dataforge.exceptions.ControlException +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.set +import hep.dataforge.utils.MetaFactory +import java.util.* + +/** + * + * @author [Alexander Nozik](mailto:altavir@gmail.com) + */ +object PortFactory : MetaFactory { + + private val portMap = HashMap() + + + @ValueDefs( + ValueDef(key = "type", def = "tcp", info = "The type of the port"), + ValueDef(key = "address", required = true, info = "The specific designation of this port according to type"), + ValueDef(key = "type", def = "tcp", info = "The type of the port") + ) + override fun build(meta: Meta): Port { + val protocol = meta.getString("type", "tcp") + val port = when (protocol) { + "com" -> { + if (meta.hasValue("address")) { + ComPort(meta.getString("address"), meta) + } else { + throw IllegalArgumentException("Not enough information to create a port") + } + } + "tcp" -> { + if (meta.hasValue("ip") && meta.hasValue("port")) { + TcpPort(meta.getString("ip"), meta.getInt("port"), meta) + } else { + throw IllegalArgumentException("Not enough information to create a port") + } + } + "virtual" -> buildVirtualPort(meta) + else -> throw ControlException("Unknown protocol") + } + return portMap.getOrPut(port.toMeta()) { port } + } + + private fun buildVirtualPort(meta: Meta): Port { + val className = meta.getString("class") + val theClass = Class.forName(className) + return theClass.getDeclaredConstructor(Meta::class.java).newInstance(meta) as Port + } + + /** + * Create new port or reuse existing one if it is already created + * @param portName + * @return + * @throws ControlException + */ + fun build(portName: String): Port { + return build(nameToMeta(portName)) + } + + fun nameToMeta(portName: String): Meta { + val builder = MetaBuilder("port") + .setValue("name", portName) + + val type = portName.substringBefore("::", "com") + val address = portName.substringAfter("::") + + builder["type"] = type + builder["address"] = address + + if (type == "tcp") { + builder["ip"] = address.substringBefore(":") + builder["port"] = address.substringAfter(":").toInt() + } + + return builder.build(); + } +} diff --git a/dataforge-control/src/main/kotlin/hep/dataforge/control/ports/PortHelper.kt b/dataforge-control/src/main/kotlin/hep/dataforge/control/ports/PortHelper.kt new file mode 100644 index 00000000..44dd6aa9 --- /dev/null +++ b/dataforge-control/src/main/kotlin/hep/dataforge/control/ports/PortHelper.kt @@ -0,0 +1,147 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.control.ports + +import hep.dataforge.context.Context +import hep.dataforge.context.ContextAware +import hep.dataforge.control.devices.Device +import hep.dataforge.control.devices.dispatchEvent +import hep.dataforge.description.ValueDef +import hep.dataforge.events.EventBuilder +import hep.dataforge.meta.Meta +import hep.dataforge.nullable +import hep.dataforge.states.* +import hep.dataforge.useValue +import hep.dataforge.values.ValueType +import org.slf4j.Logger +import java.time.Duration + +@StateDef(value = ValueDef(key = "connected",type = [ValueType.BOOLEAN], def = "false"), writable = true) +class PortHelper( + val device: Device, + val builder: ((Context, Meta) -> GenericPortController) = { context, meta -> GenericPortController(context, PortFactory.build(meta)) } +) : Stateful, ContextAware { + override val logger: Logger + get() = device.logger + + override val states: StateHolder + get() = device.states + + override val context: Context + get() = device.context + + + private val Device.portMeta: Meta + get() = meta.optMeta(PORT_STATE).nullable + ?: device.meta.optValue(PORT_STATE).map { + PortFactory.nameToMeta(it.string) + }.orElse(Meta.empty()) + + var connection: GenericPortController = builder(context, device.portMeta) + private set + + val connectedState = valueState(CONNECTED_STATE, getter = { connection.port.isOpen }) { old, value -> + if (old != value) { + logger.info("State 'connect' changed to $value") + if (value.boolean) { + connection.open() + } else { + connection.close() + } + //connect(value.boolean) + } + update(value) + } + + var connected by connectedState.booleanDelegate + + var debug by valueState(DEBUG_STATE) { old, value -> + if (old != value) { + logger.info("Turning debug mode to $value") + setDebugMode(value.boolean) + } + update(value) + }.booleanDelegate + + var port by metaState(PORT_STATE, getter = { connection.port.toMeta() }) { old, value -> + if (old != value) { + setDebugMode(false) + connectedState.update(false) + connection.close() + connection = builder(context, value) + connection.open() + setDebugMode(debug) + } + update(value) + }.delegate + + private val defaultTimeout: Duration = Duration.ofMillis(device.meta.getInt("port.timeout", 400).toLong()) + + val name get() = device.name + + init { + states.update(PORT_STATE, connection.port.toMeta()) + device.meta.useValue(DEBUG_STATE) { + debug = it.boolean + } + } + + private fun setDebugMode(debugMode: Boolean) { + //Add debug listener + if (debugMode) { + connection.apply { + onAnyPhrase("$name[debug]") { phrase -> logger.debug("Device {} received phrase: \n{}", name, phrase) } + onError("$name[debug]") { message, error -> logger.error("Device {} exception: \n{}", name, message, error) } + } + } else { + connection.apply { + removePhraseListener("$name[debug]") + removeErrorListener("$name[debug]") + } + } + states.update(DEBUG_STATE, debugMode) + } + + fun shutdown() { + connectedState.set(false) + } + + fun sendAndWait(request: String, timeout: Duration = defaultTimeout): String { + return connection.sendAndWait(request, timeout) { true } + } + + fun sendAndWait(request: String, timeout: Duration = defaultTimeout, predicate: (String) -> Boolean): String { + return connection.sendAndWait(request, timeout, predicate) + } + + fun send(message: String) { + connection.send(message) + device.dispatchEvent( + EventBuilder + .make(device.name) + .setMetaValue("request", message) + .build() + ) + } + + companion object { + const val CONNECTED_STATE = "connected" + const val PORT_STATE = "port" + const val DEBUG_STATE = "debug" + } + +} \ No newline at end of file diff --git a/dataforge-control/src/main/kotlin/hep/dataforge/control/ports/TcpPort.kt b/dataforge-control/src/main/kotlin/hep/dataforge/control/ports/TcpPort.kt new file mode 100644 index 00000000..78127e86 --- /dev/null +++ b/dataforge-control/src/main/kotlin/hep/dataforge/control/ports/TcpPort.kt @@ -0,0 +1,121 @@ +/* + * Copyright 2017 Alexander Nozik. + * + * 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 hep.dataforge.control.ports + +import hep.dataforge.exceptions.PortException +import hep.dataforge.meta.Meta +import hep.dataforge.meta.buildMeta +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import java.net.InetSocketAddress +import java.nio.ByteBuffer +import java.nio.channels.SocketChannel + +/** + * @author Alexander Nozik + */ +class TcpPort(val ip: String, val port: Int, val config: Meta = Meta.empty()) : Port() { + + private var channel: SocketChannel = SocketChannel.open() + + override val isOpen: Boolean + get() = channel.isConnected + + override val name = String.format("tcp::%s:%d", ip, port) + + private var listenerJob: Job? = null + + private fun openChannel(): SocketChannel{ + return SocketChannel.open(InetSocketAddress(ip, port)).apply { + this.configureBlocking(false) + } + } + + @Throws(PortException::class) + override fun open() { + launch { + if (!channel.isConnected && !channel.isConnectionPending) { + channel = openChannel() + startListener() + } + } + } + + @Synchronized + @Throws(Exception::class) + override fun close() { + launch { + if(isOpen) { + listenerJob?.cancel() + channel.shutdownInput() + channel.shutdownOutput() + channel.close() + super.close() + } + } + } + + private fun startListener() { + listenerJob = launch { + val buffer = ByteBuffer.allocate(1024) + while (true) { + try { + //read all content + do { + val num = channel.read(buffer) + if (num > 0) { + receive(buffer.toArray(num)) + } + buffer.rewind() + } while (num > 0) + delay(50) + } catch (ex: Exception) { + logger.error("Channel read error", ex) + logger.info("Reconnecting") + channel = openChannel() + } + } + } + } + + @Throws(PortException::class) + public override fun send(message: ByteArray) { + launch { + try { + channel.write(ByteBuffer.wrap(message)) + logger.debug("SEND: ${String(message)}") + } catch (ex: Exception) { + throw RuntimeException(ex) + } + } + } + + override fun toMeta(): Meta = buildMeta { + "type" to "tcp" + "name" to this@TcpPort.name + "ip" to ip + "port" to port + } +} + +fun ByteBuffer.toArray(limit: Int = limit()): ByteArray{ + rewind() + val response = ByteArray(limit) + get(response) + rewind() + return response +} \ No newline at end of file diff --git a/dataforge-control/src/main/kotlin/hep/dataforge/control/ports/VirtualPort.kt b/dataforge-control/src/main/kotlin/hep/dataforge/control/ports/VirtualPort.kt new file mode 100644 index 00000000..02c56065 --- /dev/null +++ b/dataforge-control/src/main/kotlin/hep/dataforge/control/ports/VirtualPort.kt @@ -0,0 +1,127 @@ +/* + * Copyright 2017 Alexander Nozik. + * + * 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 hep.dataforge.control.ports + +import hep.dataforge.exceptions.PortException +import hep.dataforge.meta.Configurable +import hep.dataforge.meta.Configuration +import hep.dataforge.meta.Meta +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import java.time.Duration +import java.util.concurrent.CopyOnWriteArraySet +import java.util.function.Supplier + +/** + * @author Alexander Nozik + */ +abstract class VirtualPort protected constructor(meta: Meta) : Port(), Configurable { + + private val futures = CopyOnWriteArraySet() + override var isOpen = false + var meta = Configuration(meta) + protected open val delimeter = meta.getString("delimenter", "\n") + + @Throws(PortException::class) + override fun open() { + //scheduler = Executors.newScheduledThreadPool(meta.getInt("numThreads", 4)) + isOpen = true + } + + override fun getConfig(): Configuration { + return meta + } + + override fun configure(config: Meta): Configurable { + meta.update(config) + return this + } + + override fun toString(): String { + return meta.getString("id", javaClass.simpleName) + } + + @Throws(PortException::class) + public override fun send(message: ByteArray) { + evaluateRequest(String(message, Charsets.US_ASCII)) + } + + /** + * The device logic here + * + * @param request + */ + protected abstract fun evaluateRequest(request: String) + + @Synchronized + protected fun clearCompleted() { + futures.stream().filter { future -> future.future.isCompleted }.forEach { futures.remove(it) } + } + + @Synchronized + protected fun cancelByTag(tag: String) { + futures.stream().filter { future -> future.hasTag(tag) }.forEach { it.cancel() } + } + + /** + * Plan the response with given delay + * + * @param response + * @param delay + * @param tags + */ + @Synchronized + protected fun planResponse(response: String, delay: Duration, vararg tags: String) { + clearCompleted() + val future = launch { + kotlinx.coroutines.time.delay(delay) + receive((response + delimeter).toByteArray()) + } + this.futures.add(TaggedFuture(future, *tags)) + } + + @Synchronized + protected fun planRegularResponse(responseBuilder: Supplier, delay: Duration, period: Duration, vararg tags: String) { + clearCompleted() + val future = launch { + kotlinx.coroutines.time.delay(delay) + while (true) { + receive((responseBuilder.get() + delimeter).toByteArray()) + kotlinx.coroutines.time.delay(period) + } + } + this.futures.add(TaggedFuture(future, *tags)) + } + + @Throws(Exception::class) + override fun close() { + futures.clear() + isOpen = false + super.close() + } + + private inner class TaggedFuture(internal val future: Job, vararg tags: String) { + internal val tags = setOf(*tags) + + fun hasTag(tag: String): Boolean { + return tags.contains(tag) + } + + fun cancel() { + return future.cancel() + } + } +} diff --git a/dataforge-control/src/main/resources/META-INF/services/hep.dataforge.context.PluginFactory b/dataforge-control/src/main/resources/META-INF/services/hep.dataforge.context.PluginFactory new file mode 100644 index 00000000..ef2de958 --- /dev/null +++ b/dataforge-control/src/main/resources/META-INF/services/hep.dataforge.context.PluginFactory @@ -0,0 +1 @@ +hep.dataforge.control.DeviceManager$Factory \ No newline at end of file diff --git a/dataforge-control/src/test/kotlin/hep/dataforge/control/ports/TcpPortTest.kt b/dataforge-control/src/test/kotlin/hep/dataforge/control/ports/TcpPortTest.kt new file mode 100644 index 00000000..8b993fb1 --- /dev/null +++ b/dataforge-control/src/test/kotlin/hep/dataforge/control/ports/TcpPortTest.kt @@ -0,0 +1,73 @@ +package hep.dataforge.control.ports + +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import java.net.InetSocketAddress +import java.nio.ByteBuffer +import java.nio.channels.ServerSocketChannel +import java.nio.channels.SocketChannel + + +class TcpPortTest { + var job: Job? = null + + @Before + fun startServer() { + GlobalScope.launch { + println("Starting server") + val serverSocketChannel = ServerSocketChannel.open() + + serverSocketChannel.socket().bind(InetSocketAddress(9999)) + + while (true) { + delay(0) + println("Accepting client") + serverSocketChannel.accept().use { + val buffer = ByteBuffer.allocate(1024) + val num = it.read(buffer) + println("Received $num bytes") + buffer.flip() + it.write(buffer) + buffer.rewind() + } + } + } + } + + @After + fun stopServer() { + job?.cancel() + } + + @Test + fun testClient() { + val channel: SocketChannel = SocketChannel.open(InetSocketAddress("localhost", 9999)) + println("Sending 3 bytes") + val request = "ddd".toByteArray() + channel.write(ByteBuffer.wrap(request)) + val buffer = ByteBuffer.allocate(1024) + channel.read(buffer) + buffer.flip() + val response = buffer.toArray() + assertEquals(request.size, response.size) + assertEquals(String(request), String(response)) + } + + @Test + fun testPort() { + val port = TcpPort("localhost", 9999) + port.holdBy(object : PortController { + override fun accept(byte: Byte) { + println(byte) + } + }) + port.send("ddd".toByteArray()) + Thread.sleep(500) + } +} \ No newline at end of file diff --git a/dataforge-core/build.gradle b/dataforge-core/build.gradle new file mode 100644 index 00000000..cc6b8308 --- /dev/null +++ b/dataforge-core/build.gradle @@ -0,0 +1,8 @@ +description = 'dataforge-core' + +dependencies { + compile 'ch.qos.logback:logback-classic:1.2.3' + compile 'org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.3.2' + compile group: 'org.jetbrains.kotlin', name: 'kotlin-reflect', version: kotlin_version + compile group: 'javax.cache', name: 'cache-api', version: '1.1.0' +} diff --git a/dataforge-core/dataforge-json/build.gradle b/dataforge-core/dataforge-json/build.gradle new file mode 100644 index 00000000..0ad467d1 --- /dev/null +++ b/dataforge-core/dataforge-json/build.gradle @@ -0,0 +1,22 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + +description = 'json meta type for dataforge' + +dependencies { + compile project(":dataforge-core") + compile 'com.github.cliftonlabs:json-simple:3.0.2' +} \ No newline at end of file diff --git a/dataforge-core/dataforge-json/src/main/kotlin/hep/dataforge/io/JSONMetaReader.kt b/dataforge-core/dataforge-json/src/main/kotlin/hep/dataforge/io/JSONMetaReader.kt new file mode 100644 index 00000000..8bbb7f06 --- /dev/null +++ b/dataforge-core/dataforge-json/src/main/kotlin/hep/dataforge/io/JSONMetaReader.kt @@ -0,0 +1,103 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.io + +import com.github.cliftonlabs.json_simple.JsonArray +import com.github.cliftonlabs.json_simple.JsonObject +import com.github.cliftonlabs.json_simple.Jsoner +import hep.dataforge.meta.KMetaBuilder +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.meta.buildMeta +import hep.dataforge.values.LateParseValue +import hep.dataforge.values.Value +import java.io.ByteArrayInputStream +import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader +import java.text.ParseException + +/** + * Reader for JSON meta + * + * @author Alexander Nozik + */ +object JSONMetaReader : MetaStreamReader { + + @Throws(IOException::class, ParseException::class) + override fun read(stream: InputStream, length: Long): MetaBuilder { + return if (length == 0L) { + MetaBuilder("") + } else { + val json = if (length > 0) { + //Read into intermediate buffer + val buffer = ByteArray(length.toInt()) + stream.read(buffer) + Jsoner.deserialize(InputStreamReader(ByteArrayInputStream(buffer), Charsets.UTF_8)) as JsonObject + } else { + Jsoner.deserialize(InputStreamReader(stream, Charsets.UTF_8)) as JsonObject + } + json.toMeta() + } + } + + @Throws(ParseException::class) + private fun JsonObject.toMeta(): MetaBuilder { + return buildMeta { + this@toMeta.forEach { key, value -> appendValue(this, key as String, value) } + } + } + + private fun JsonArray.toListValue(): Value { + val list = this.map {value-> + when (value) { + is JsonArray -> value.toListValue() + is Number -> Value.of(value) + is Boolean -> Value.of(value) + is String -> LateParseValue(value) + null -> Value.NULL + is JsonObject -> throw RuntimeException("Object values inside multidimensional arrays are not allowed") + else -> throw Error("Unknown token $value in json") + } + } + return Value.of(list) + } + + private fun appendValue(builder: KMetaBuilder, key: String, value: Any?) { + when (value) { + is JsonObject -> builder.attachNode(value.toMeta().rename(key)) + is JsonArray -> { + value.forEach { + if(it is JsonArray){ + builder.putValue(key, it.toListValue()) + } else { + appendValue(builder, key, it) + } + } + } + is Number -> builder.putValue(key, value) + is Boolean -> builder.putValue(key, value) + is String -> builder.putValue(key, LateParseValue(value)) + //ignore anything else + } + } + +} diff --git a/dataforge-core/dataforge-json/src/main/kotlin/hep/dataforge/io/JSONMetaType.kt b/dataforge-core/dataforge-json/src/main/kotlin/hep/dataforge/io/JSONMetaType.kt new file mode 100644 index 00000000..9e574114 --- /dev/null +++ b/dataforge-core/dataforge-json/src/main/kotlin/hep/dataforge/io/JSONMetaType.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.io + +import hep.dataforge.io.envelopes.MetaType + +class JSONMetaType : MetaType { + override val codes: List = listOf(0x4a53, 1)//JS + + override val name: String = "JSON" + + override val reader: MetaStreamReader = JSONMetaReader + + override val writer: MetaStreamWriter = JSONMetaWriter + + override val fileNameFilter: (String) -> Boolean = { it.toLowerCase().endsWith(".json") } +} + +val jsonMetaType = JSONMetaType() diff --git a/dataforge-core/dataforge-json/src/main/kotlin/hep/dataforge/io/JSONMetaWriter.kt b/dataforge-core/dataforge-json/src/main/kotlin/hep/dataforge/io/JSONMetaWriter.kt new file mode 100644 index 00000000..d88985fd --- /dev/null +++ b/dataforge-core/dataforge-json/src/main/kotlin/hep/dataforge/io/JSONMetaWriter.kt @@ -0,0 +1,123 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.io + +import com.github.cliftonlabs.json_simple.JsonArray +import com.github.cliftonlabs.json_simple.JsonObject +import com.github.cliftonlabs.json_simple.Jsoner +import hep.dataforge.meta.Meta +import hep.dataforge.values.Value +import hep.dataforge.values.ValueType +import java.io.OutputStream + +/** + * A converter from Meta object to JSON character stream + * + * @author Alexander Nozik + */ +object JSONMetaWriter : MetaStreamWriter { + + override fun write(stream: OutputStream, meta: Meta) { + val json = meta.toJson() + val string = Jsoner.prettyPrint(Jsoner.serialize(json)) + stream.write(string.toByteArray(Charsets.UTF_8)) + stream.flush() + } + + private fun Value.toJson(): Any { + return if (list.size == 1) { + when (type) { + ValueType.NUMBER -> number + ValueType.BOOLEAN -> boolean + else -> string + } + } else { + JsonArray().apply { + list.forEach { add(it.toJson()) } + } + } + } + + private fun Meta.toJson(): JsonObject { + val builder = JsonObject() + nodeNames.forEach { + val nodes = getMetaList(it) + if (nodes.size == 1) { + builder[it] = nodes[0].toJson() + } else { + val array = JsonArray() + nodes.forEach { array.add(it.toJson()) } + builder[it] = array + } + } + + valueNames.forEach { + builder[it] = getValue(it).toJson() + } + + return builder + } +} + + +//private class JSONWriter : StringWriter() { +// +// private var indentlevel = 0 +// +// override fun write(c: Int) { +// val ch = c.toChar() +// if (ch == '[' || ch == '{') { +// super.write(c) +// super.write("\n") +// indentlevel++ +// writeIndentation() +// } else if (ch == ',') { +// super.write(c) +// super.write("\n") +// writeIndentation() +// } else if (ch == ']' || ch == '}') { +// super.write("\n") +// indentlevel-- +// writeIndentation() +// super.write(c) +// } else if (ch == ':') { +// super.write(c) +// super.write(spaceaftercolon) +// } else { +// super.write(c) +// } +// +// } +// +// private fun writeIndentation() { +// for (i in 0 until indentlevel) { +// super.write(indentstring) +// } +// } +// +// companion object { +// internal val indentstring = " " //define as you wish +// internal val spaceaftercolon = " " //use "" if you don't want space after colon +// } +//} + + diff --git a/dataforge-core/dataforge-json/src/main/resources/META-INF/services/hep.dataforge.io.envelopes.MetaType b/dataforge-core/dataforge-json/src/main/resources/META-INF/services/hep.dataforge.io.envelopes.MetaType new file mode 100644 index 00000000..eb5ac983 --- /dev/null +++ b/dataforge-core/dataforge-json/src/main/resources/META-INF/services/hep.dataforge.io.envelopes.MetaType @@ -0,0 +1 @@ +hep.dataforge.io.JSONMetaType diff --git a/dataforge-core/dataforge-json/src/test/kotlin/hep/dataforge/io/JSONMetaTypeTest.kt b/dataforge-core/dataforge-json/src/test/kotlin/hep/dataforge/io/JSONMetaTypeTest.kt new file mode 100644 index 00000000..2b22742a --- /dev/null +++ b/dataforge-core/dataforge-json/src/test/kotlin/hep/dataforge/io/JSONMetaTypeTest.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.io + +import hep.dataforge.get +import hep.dataforge.meta.buildMeta +import org.junit.Assert.assertEquals +import org.junit.Test + +class JSONMetaTypeTest { + @Test + fun testRead() { + val json = """ + { + "a" : 22, + "b" : { + "c" : [1, 2, [3.1, 3.2]] + "d" : "my string value" + } + } + """.trimMargin() + val meta = jsonMetaType.reader.readString(json) + assertEquals(22, meta["a"].int) + assertEquals(3.1, meta["b.c"][2][0].double,0.01) + } + + @Test + fun testWrite(){ + val meta = buildMeta { + "a" to 22 + "b" to { + "c" to listOf(1, 2, listOf(3.1, 3.2)) + "d" to "my string value" + } + } + val string = jsonMetaType.writer.writeString(meta) + println(string) + } +} \ No newline at end of file diff --git a/dataforge-core/docs/descriptors.md b/dataforge-core/docs/descriptors.md new file mode 100644 index 00000000..3c794217 --- /dev/null +++ b/dataforge-core/docs/descriptors.md @@ -0,0 +1,4 @@ +--- +title: Descriptors +--- + diff --git a/dataforge-core/docs/logging.md b/dataforge-core/docs/logging.md new file mode 100644 index 00000000..9b3657ac --- /dev/null +++ b/dataforge-core/docs/logging.md @@ -0,0 +1,22 @@ +--- +title: Logging and History +--- + +DataForge supports two separate logging system: + +1. Generic logging system based on `slf4j` on JVM and possibly any other native logging on remote platform. It is used +for debugging and online information about the process. Logger could be obtained in any point of the program (e.g. via +`LoggerFactrory.getLogger(...)` in JVM). Critical elements and all `ContextAware` blocks automatically have a pre-defined +logger accessible via `logger` property. + +2. Internal logging utilizes hierarchical structure called `History`. Each `Hisotory` object has a reference to `Chronocle` +which stores history entries called `Record`. Also any `History` but the root one which is `Global.history` must have a parent +`History`. Any `Record` entry appended to the history is automatically appended to its parent with appropriate trace element +which specifies where it entry comes from. One can also attach hooks to any chronicle to be triggered on entry addition. +Global history logging behavior is controlled via `Chronicler` context plugin, which is loaded by default into Global context. +History ususally could not be created on sight (only from context chronicler) and should be passed to the process explicitly. + +The key difference between logger and history is that logs are intended for debugging and information and are discarded after +being read. The history on the other hand is supposed to be the important part of the analysis and should be stored with +analysis results after it is complete. It is strongly discouraged to use `History` in a performance-sensitive code. Also +it is bad idea to output any sensitive information to log since it could be discarded. \ No newline at end of file diff --git a/dataforge-core/src/main/java/hep/dataforge/actions/ActionEvent.java b/dataforge-core/src/main/java/hep/dataforge/actions/ActionEvent.java new file mode 100644 index 00000000..a043a94f --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/actions/ActionEvent.java @@ -0,0 +1,31 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.actions; + +/** + *

ActionEvent interface.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +public interface ActionEvent { + /** + *

source.

+ * + * @return a {@link hep.dataforge.actions.Action} object. + */ + Action source(); +} diff --git a/dataforge-core/src/main/java/hep/dataforge/actions/ActionEventListener.java b/dataforge-core/src/main/java/hep/dataforge/actions/ActionEventListener.java new file mode 100644 index 00000000..2bda2f12 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/actions/ActionEventListener.java @@ -0,0 +1,31 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.actions; + +/** + *

ActionEventListener interface.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +public interface ActionEventListener { + /** + *

listenTo.

+ * + * @param event a {@link hep.dataforge.actions.ActionEvent} object. + */ + void listenTo(ActionEvent event); +} diff --git a/dataforge-core/src/main/java/hep/dataforge/actions/ActionManager.java b/dataforge-core/src/main/java/hep/dataforge/actions/ActionManager.java new file mode 100644 index 00000000..c6365656 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/actions/ActionManager.java @@ -0,0 +1,141 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.actions; + +import hep.dataforge.context.BasicPlugin; +import hep.dataforge.context.Plugin; +import hep.dataforge.context.PluginDef; +import hep.dataforge.context.PluginFactory; +import hep.dataforge.meta.Meta; +import hep.dataforge.providers.Provides; +import hep.dataforge.providers.ProvidesNames; +import hep.dataforge.tables.ReadPointSetAction; +import hep.dataforge.tables.TransformTableAction; +import hep.dataforge.utils.Optionals; +import hep.dataforge.workspace.tasks.Task; +import org.jetbrains.annotations.NotNull; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +/** + * A support manager to dynamically load actions and tasks into the context + * + * @author Alexander Nozik + */ +@PluginDef(name = "actions", group = "hep.dataforge", info = "A list of available actions and task for given context") +public class ActionManager extends BasicPlugin { + private final Map actionMap = new HashMap<>(); + private final Map taskMap = new HashMap<>(); + + public ActionManager() { + //TODO move somewhere else + putAction(TransformTableAction.class); + putAction(ReadPointSetAction.class); + } + + protected Optional getParent() { + if (getContext().getParent() == null) { + return Optional.empty(); + } else { + return getContext().getParent().provide("actions", ActionManager.class); + } + } + + @Provides(Action.ACTION_TARGET) + public Optional optAction(String name) { + return Optionals.either(actionMap.get(name)) + .or(getParent().flatMap(parent -> parent.optAction(name))) + .opt(); + } + + @Provides(Task.TASK_TARGET) + public Optional optTask(String name) { + return Optionals.either(taskMap.get(name)) + .or(getParent().flatMap(parent -> parent.optTask(name))) + .opt(); + } + + public void put(Action action) { + if (actionMap.containsKey(action.getName())) { + LoggerFactory.getLogger(getClass()).warn("Duplicate action names in ActionManager."); + } else { + actionMap.put(action.getName(), action); + } + } + + public void put(Task task) { + if (taskMap.containsKey(task.getName())) { + LoggerFactory.getLogger(getClass()).warn("Duplicate task names in ActionManager."); + } else { + taskMap.put(task.getName(), task); + } + } + + /** + * Put a task into the manager using action construction by reflections. Action must have empty constructor + * + * @param actionClass a {@link java.lang.Class} object. + */ + public final void putAction(Class actionClass) { + try { + put(actionClass.newInstance()); + } catch (IllegalAccessException ex) { + throw new RuntimeException("Action must have default empty constructor to be registered."); + } catch (InstantiationException ex) { + throw new RuntimeException("Error while constructing Action", ex); + } + } + + public final void putTask(Class taskClass) { + try { + put(taskClass.getConstructor().newInstance()); + } catch (IllegalAccessException ex) { + throw new RuntimeException("Task must have default empty constructor to be registered."); + } catch (Exception ex) { + throw new RuntimeException("Error while constructing Task", ex); + } + } + + /** + * Stream of all available actions + * + * @return + */ + @ProvidesNames(Action.ACTION_TARGET) + public Stream listActions() { + return this.actionMap.keySet().stream(); + } + + /** + * Stream of all available tasks + * + * @return + */ + @ProvidesNames(Task.TASK_TARGET) + public Stream listTasks() { + return this.taskMap.keySet().stream(); + } + + public static class Factory extends PluginFactory { + + @NotNull + @Override + public ActionManager build(@NotNull Meta meta) { + return new ActionManager(); + } + + @NotNull + @Override + public Class getType() { + return ActionManager.class; + } + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/actions/ActionResult.java b/dataforge-core/src/main/java/hep/dataforge/actions/ActionResult.java new file mode 100644 index 00000000..fc9fc6ac --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/actions/ActionResult.java @@ -0,0 +1,41 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.actions; + +import hep.dataforge.data.NamedData; +import hep.dataforge.goals.Goal; +import hep.dataforge.io.history.Chronicle; +import hep.dataforge.meta.Meta; + +/** + * The asynchronous result of the action + * + * @author Alexander Nozik + * @param + */ +public class ActionResult extends NamedData { + + private final Chronicle log; + + public ActionResult(String name, Class type, Goal goal, Meta meta, Chronicle log) { + super(name, type, goal, meta); + this.log = log; + } + + public Chronicle log() { + return log; + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/actions/ActionStateListener.java b/dataforge-core/src/main/java/hep/dataforge/actions/ActionStateListener.java new file mode 100644 index 00000000..16bb7f27 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/actions/ActionStateListener.java @@ -0,0 +1,23 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.actions; + +/** + * + * @author Alexander Nozik + */ +public interface ActionStateListener { + void notifyActionStarted(Action action, Object argument); + void notifyActionFinished(Action action, Object result); + /** + * Notify action progress + * @param action + * @param progress the value between 0 and 1; negative values are ignored + * @param message + */ + void notifyAcionProgress(Action action, double progress, String message); + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/actions/GeneratorAction.java b/dataforge-core/src/main/java/hep/dataforge/actions/GeneratorAction.java new file mode 100644 index 00000000..805b9342 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/actions/GeneratorAction.java @@ -0,0 +1,52 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.actions; + +import hep.dataforge.context.Context; +import hep.dataforge.data.DataNode; +import hep.dataforge.goals.GeneratorGoal; +import hep.dataforge.goals.Goal; +import hep.dataforge.io.history.Chronicle; +import hep.dataforge.meta.Meta; +import org.jetbrains.annotations.NotNull; + +import java.util.stream.Stream; + +/** + * An action that does not take any input data, only generates output. Each + * output token is generated separately. + * + * @author Alexander Nozik + */ +public abstract class GeneratorAction extends GenericAction { + + public GeneratorAction(@NotNull String name,@NotNull Class outputType) { + super(name, Void.class, outputType); + } + + @Override + public DataNode run(Context context, DataNode data, Meta actionMeta) { + Chronicle log = context.getHistory().getChronicle(getName()); + + Stream> results = nameStream().map(name -> { + Goal goal = new GeneratorGoal<>(getExecutorService(context, actionMeta), () -> generateData(name)); + return new ActionResult<>(name, getOutputType(), goal, generateMeta(name), log); + }); + + return wrap(resultNodeName(), actionMeta, results); + } + + protected abstract Stream nameStream(); + + protected abstract Meta generateMeta(String name); + + protected abstract R generateData(String name); + + protected String resultNodeName() { + return ""; + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/actions/GroupBuilder.java b/dataforge-core/src/main/java/hep/dataforge/actions/GroupBuilder.java new file mode 100644 index 00000000..befd8032 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/actions/GroupBuilder.java @@ -0,0 +1,84 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.actions; + +import hep.dataforge.data.DataNode; +import hep.dataforge.data.DataNodeBuilder; +import hep.dataforge.data.DataSet; +import hep.dataforge.data.NamedData; +import hep.dataforge.description.ValueDef; +import hep.dataforge.meta.Meta; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * The class to builder groups of content with annotation defined rules + * + * @author Alexander Nozik + */ + +public class GroupBuilder { + + /** + * Create grouping rule that creates groups for different values of value + * field with name {@code tag} + * + * @param tag + * @param defaultTagValue + * @return + */ + public static GroupRule byValue(final String tag, String defaultTagValue) { + return new GroupRule() { + @Override + public List> group(DataNode input) { + Map> map = new HashMap<>(); + + input.forEach((NamedData data) -> { + String tagValue = data.getMeta().getString(tag, defaultTagValue); + if (!map.containsKey(tagValue)) { + DataNodeBuilder builder = DataSet.Companion.edit(input.getType()); + builder.setName(tagValue); + //builder.setMeta(new MetaBuilder(DEFAULT_META_NAME).putValue("tagValue", tagValue)); + //PENDING share meta here? + map.put(tagValue, builder); + } + map.get(tagValue).add(data); + }); + + return map.values().stream().>map(DataNodeBuilder::build).collect(Collectors.toList()); + } + }; + } + + @ValueDef(key = "byValue", required = true, info = "The name of annotation value by which grouping should be made") + @ValueDef(key = "defaultValue", def = "default", info = "Default value which should be used for content in which the grouping value is not presented") + public static GroupRule byMeta(Meta config) { + //TODO expand grouping options + if (config.hasValue("byValue")) { + return byValue(config.getString("byValue"), config.getString("defaultValue", "default")); + } else { + return Collections::singletonList; + } + } + + public interface GroupRule { + List> group(DataNode input); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/actions/ManyToOneAction.java b/dataforge-core/src/main/java/hep/dataforge/actions/ManyToOneAction.java new file mode 100644 index 00000000..304bf674 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/actions/ManyToOneAction.java @@ -0,0 +1,170 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.actions; + +import hep.dataforge.context.Context; +import hep.dataforge.data.Data; +import hep.dataforge.data.DataNode; +import hep.dataforge.data.NamedData; +import hep.dataforge.goals.AbstractGoal; +import hep.dataforge.goals.Goal; +import hep.dataforge.meta.Laminate; +import hep.dataforge.meta.Meta; +import hep.dataforge.meta.MetaBuilder; +import hep.dataforge.names.Name; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Action with multiple input data pieces but single output + * + * @param + * @param + * @author Alexander Nozik + */ +public abstract class ManyToOneAction extends GenericAction { + + public ManyToOneAction(@NotNull String name, @NotNull Class inputType, @NotNull Class outputType) { + super(name, inputType, outputType); + } + + @Override + @NotNull + public DataNode run(Context context, DataNode set, Meta actionMeta) { + checkInput(set); + List> groups = buildGroups(context, (DataNode) set, actionMeta); + return wrap(getResultName(set.getName(), actionMeta), set.getMeta(), groups.stream().map(group->runGroup(context, group,actionMeta))); + } + + public ActionResult runGroup(Context context, DataNode data, Meta actionMeta) { + Meta outputMeta = outputMeta(data).build(); + Goal goal = new ManyToOneGoal(context, data, actionMeta, outputMeta); + String resultName = data.getName() == null ? getName() : data.getName(); + + return new ActionResult<>(resultName, getOutputType(), goal, outputMeta, context.getHistory().getChronicle(resultName)); + } + + protected List> buildGroups(Context context, DataNode input, Meta actionMeta) { + return GroupBuilder.byMeta(inputMeta(context, input.getMeta(), actionMeta)).group(input); + } + + /** + * Perform actual calculation + * + * @param nodeName + * @param input + * @param meta + * @return + */ + protected abstract R execute(Context context, String nodeName, Map input, Laminate meta); + + /** + * Build output meta for resulting object + * + * @param input + * @return + */ + protected MetaBuilder outputMeta(DataNode input) { + MetaBuilder builder = new MetaBuilder(MetaBuilder.DEFAULT_META_NAME) + .putValue("name", input.getName()) + .putValue("type", input.getType().getName()); + input.dataStream().forEach((NamedData data) -> { + MetaBuilder dataNode = new MetaBuilder("data") + .putValue("name", data.getName()); + if (!data.getType().equals(input.getType())) { + dataNode.putValue("type", data.getType().getName()); + } +// if (!data.meta().isEmpty()) { +// dataNode.putNode(DataFactory.NODE_META_KEY, data.meta()); +// } + builder.putNode(dataNode); + }); + return builder; + } + + /** + * An action to be performed before each group evaluation + * + * @param input + */ + protected void beforeGroup(Context context, DataNode input) { + + } + + /** + * An action to be performed after each group evaluation + * + * @param output + */ + protected void afterGroup(Context context, String groupName, Meta outputMeta, R output) { + + } + + /** + * Action goal {@code fainOnError()} delegate + * + * @return + */ + protected boolean failOnError() { + return true; + } + + private class ManyToOneGoal extends AbstractGoal { + + private final Context context; + private final DataNode data; + private final Meta actionMeta; + private final Meta outputMeta; + + public ManyToOneGoal(Context context, DataNode data, Meta actionMeta, Meta outputMeta) { + super(getExecutorService(context, actionMeta)); + this.context = context; + this.data = data; + this.actionMeta = actionMeta; + this.outputMeta = outputMeta; + } + + @Override + protected boolean failOnError() { + return ManyToOneAction.this.failOnError(); + } + + @Override + public Stream> dependencies() { + return data.nodeGoal().dependencies(); + } + + @Override + protected R compute() throws Exception { + Laminate meta = inputMeta(context, data.getMeta(), actionMeta); + Thread.currentThread().setName(Name.Companion.joinString(getThreadName(actionMeta), data.getName())); + beforeGroup(context, data); + // In this moment, all the data is already calculated + Map collection = data.dataStream() + .filter(Data::isValid) // filter valid data only + .collect(Collectors.toMap(NamedData::getName, Data::get)); + R res = execute(context, data.getName(), collection, meta); + afterGroup(context, data.getName(), outputMeta, res); + return res; + } + + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/actions/OneToManyAction.java b/dataforge-core/src/main/java/hep/dataforge/actions/OneToManyAction.java new file mode 100644 index 00000000..c7c3a819 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/actions/OneToManyAction.java @@ -0,0 +1,67 @@ +package hep.dataforge.actions; + +import hep.dataforge.context.Context; +import hep.dataforge.data.Data; +import hep.dataforge.data.DataNode; +import hep.dataforge.data.DataNodeBuilder; +import hep.dataforge.data.DataTree; +import hep.dataforge.goals.Goal; +import hep.dataforge.goals.PipeGoal; +import hep.dataforge.meta.Laminate; +import hep.dataforge.meta.Meta; +import hep.dataforge.names.Name; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; + +/** + * A split action that creates multiple Data from each input element (some input elements could be ignored) + * Created by darksnake on 28-Jan-17. + */ +public abstract class OneToManyAction extends GenericAction { + + public OneToManyAction(@NotNull String name, @NotNull Class inputType, @NotNull Class outputType) { + super(name, inputType, outputType); + } + + @Override + public DataNode run(Context context, DataNode data, Meta actionMeta) { + checkInput(data); + DataNodeBuilder builder = DataTree.Companion.edit(getOutputType()); + data.forEach(datum -> { + String inputName = datum.getName(); + Laminate inputMeta = new Laminate(datum.getMeta(), actionMeta); + Map metaMap = prepareMeta(context, inputName, inputMeta); + metaMap.forEach((outputName, outputMeta) -> { + Goal goal = new PipeGoal<>(datum.getGoal(), input -> execute(context, inputName, outputName, input, inputMeta)); + Data res = new Data(getOutputType(), goal, outputMeta); + builder.putData(placement(inputName, outputName), res, false); + }); + }); + return builder.build(); + } + + /** + * The placement rule for result Data. By default each input element is transformed into a node with + * + * @param inputName + * @param outputName + * @return + */ + protected String placement(String inputName, String outputName) { + return Name.Companion.joinString(inputName, outputName); + } + + //TODO add node meta + + /** + * @param context + * @param inputName + * @param meta + * @return + */ + protected abstract Map prepareMeta(Context context, String inputName, Laminate meta); + + protected abstract R execute(Context context, String inputName, String outputName, T input, Laminate meta); + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/actions/OneToOneAction.java b/dataforge-core/src/main/java/hep/dataforge/actions/OneToOneAction.java new file mode 100644 index 00000000..98db92da --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/actions/OneToOneAction.java @@ -0,0 +1,150 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.actions; + +import hep.dataforge.context.Context; +import hep.dataforge.context.Global; +import hep.dataforge.data.DataNode; +import hep.dataforge.data.NamedData; +import hep.dataforge.goals.PipeGoal; +import hep.dataforge.io.history.Chronicle; +import hep.dataforge.meta.Laminate; +import hep.dataforge.meta.Meta; +import hep.dataforge.names.Name; +import kotlin.Pair; +import org.slf4j.Logger; + +/** + * A template to builder actions that reflect strictly one to one content + * transformations + * + * @param + * @param + * @author Alexander Nozik + * @version $Id: $Id + */ +public abstract class OneToOneAction extends GenericAction { + + public OneToOneAction(String name, Class inputType, Class outputType) { + super(name,inputType,outputType); + } + + + + @Override + public DataNode run(Context context, DataNode set, Meta actionMeta) { + checkInput(set); + if (set.isEmpty()) { + throw new RuntimeException(getName() + ": Running 1 to 1 action on empty data node"); + } + + return wrap( + set.getName(), + set.getMeta(), + set.dataStream(true).map(data -> runOne(context, data, actionMeta)) + ); + } + + /** + * Build asynchronous result for single data. Data types separated from + * action generics to be able to operate maps instead of raw data + * + * @param data + * @param actionMeta + * @return + */ + protected ActionResult runOne(Context context, NamedData data, Meta actionMeta) { + if (!this.getInputType().isAssignableFrom(data.getType())) { + throw new RuntimeException(String.format("Type mismatch in action %s. %s expected, but %s recieved", + getName(), getInputType().getName(), data.getType().getName())); + } + + Pair resultParamters = outputParameters(context, data, actionMeta); + + Laminate meta = inputMeta(context, data.getMeta(), actionMeta); + String resultName = resultParamters.getFirst(); + Meta outputMeta = resultParamters.getSecond(); + + PipeGoal goal = new PipeGoal<>(getExecutorService(context, meta), data.getGoal(), + input -> { + Thread.currentThread().setName(Name.Companion.joinString(getThreadName(actionMeta), resultName)); + return transform(context, resultName, input, meta); + } + ); + return new ActionResult<>(resultName, getOutputType(), goal, outputMeta, context.getHistory().getChronicle(resultName)); + } + + protected Chronicle getLog(Context context, String dataName) { + return context.getHistory().getChronicle(Name.Companion.joinString(dataName, getName())); + } + + /** + * @param name name of the input item + * @param input input data + * @param inputMeta combined meta for this evaluation. Includes data meta, + * group meta and action meta + * @return + */ + private R transform(Context context, String name, T input, Laminate inputMeta) { + beforeAction(context, name, input, inputMeta); + R res = execute(context, name, input, inputMeta); + afterAction(context, name, res, inputMeta); + return res; + } + + /** + * Utility method to run action outside of context or execution chain + * + * @param input + * @param metaLayers + * @return + */ + public R simpleRun(T input, Meta... metaLayers) { + return transform(Global.INSTANCE, "simpleRun", input, inputMeta(Global.INSTANCE, metaLayers)); + } + + protected abstract R execute(Context context, String name, T input, Laminate meta); + + /** + * Build output meta for given data. This meta is calculated on action call + * (no lazy calculations). By default output meta is the same as input data + * meta. + * + * @param actionMeta + * @param data + * @return + */ + protected Pair outputParameters(Context context, NamedData data, Meta actionMeta) { + return new Pair<>(getResultName(data.getName(), actionMeta), data.getMeta()); + } + + protected void afterAction(Context context, String name, R res, Laminate meta) { + Logger logger = getLogger(context, meta); + if (res == null) { + logger.error("Action {} returned 'null' on data {}", getName(), name); + throw new RuntimeException("Null result of action");//TODO add emty data to handle this + } + logger.debug("Action '{}[{}]' is finished", getName(), name); + } + + protected void beforeAction(Context context, String name, T datum, Laminate meta) { + if (context.getBoolean("actions.reportStart", false)) { + report(context, name, "Starting action {} on data with name {} with following configuration: \n\t {}", getName(), name, meta.toString()); + } + getLogger(context, meta).debug("Starting action '{}[{}]'", getName(), name); + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/connections/AutoConnectible.java b/dataforge-core/src/main/java/hep/dataforge/connections/AutoConnectible.java new file mode 100644 index 00000000..1ffe1ea9 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/connections/AutoConnectible.java @@ -0,0 +1,44 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.connections; + +import java.util.Optional; +import java.util.stream.Stream; + +public interface AutoConnectible extends Connectible { + + ConnectionHelper getConnectionHelper(); + + @Override + default void connect(Connection connection, String... roles) { + getConnectionHelper().connect(connection, roles); + } + + @Override + default Stream connections(String role, Class type) { + return getConnectionHelper().connections(role, type); + } + + default Optional optConnection(String role, Class type) { + return getConnectionHelper().optConnection(role, type); + } + + @Override + default void disconnect(Connection connection) { + getConnectionHelper().disconnect(connection); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/connections/Connectible.java b/dataforge-core/src/main/java/hep/dataforge/connections/Connectible.java new file mode 100644 index 00000000..eec2e164 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/connections/Connectible.java @@ -0,0 +1,110 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.connections; + +import hep.dataforge.UtilsKt; + +import java.util.List; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.stream.Stream; + +/** + * Something that could be connected + * + * @author Alexander Nozik + */ +public interface Connectible { + + /** + * Register a new connection with given roles + * + * @param connection + * @param roles + */ + void connect(Connection connection, String... roles); + + /** + * Get a stream of all connections with given role and type. Role could be regexp + * + * @param role + * @param type + * @param + * @return + */ + Stream connections(String role, Class type); + + /** + * Disconnect given connection + * + * @param connection + */ + void disconnect(Connection connection); + + + /** + * For each connection of given class and role. Role may be empty, but type + * is mandatory + * + * @param + * @param role + * @param type + * @param action + */ + default void forEachConnection(String role, Class type, Consumer action) { + connections(role, type).forEach(action); + } + + default void forEachConnection(Class type, Consumer action) { + forEachConnection(".*", type, action); + } + + + /** + * A list of all available roles + * + * @return + */ + default List roleDefs() { + return UtilsKt.listAnnotations(this.getClass(), RoleDef.class, true); + } + + /** + * Find a role definition for given name. Null if not found. + * + * @param name + * @return + */ + default Optional optRoleDef(String name) { + return roleDefs().stream().filter((roleDef) -> roleDef.name().equals(name)).findFirst(); + } + + /** + * A quick way to find if this object accepts connection with given role + * + * @param name + * @return + */ + default boolean acceptsRole(String name) { + return roleDefs().stream().anyMatch((roleDef) -> roleDef.name().equals(name)); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/connections/Connection.java b/dataforge-core/src/main/java/hep/dataforge/connections/Connection.java new file mode 100644 index 00000000..d55b2e20 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/connections/Connection.java @@ -0,0 +1,66 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.connections; + +import hep.dataforge.context.Context; +import hep.dataforge.meta.Meta; + +import java.util.Objects; +import java.util.Optional; + +/** + * A connection which could be applied to object that could receive connection. + * Usually connection does not invoke {@code open} method itself, but delegates it to {@code Connectible} + * + * @author Alexander Nozik + */ +public interface Connection extends AutoCloseable { + + /** + * Create a connection using context connection factory provider if it is possible + * + * @param context + * @param meta + * @return + */ + static Optional buildConnection(Connectible obj, Context context, Meta meta) { + String type = meta.getString("type"); + return Optional.ofNullable(context.findService(ConnectionFactory.class, it -> Objects.equals(it.getType(), type))) + .map(it -> it.build(obj, context, meta)); + } + + String LOGGER_ROLE = "log"; + String EVENT_HANDLER_ROLE = "eventHandler"; + + default boolean isOpen() { + return true; + } + + default void open(Object object) throws Exception { + //do nothing + } + + @Override + default void close() throws Exception { + //do nothing + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/connections/ConnectionFactory.java b/dataforge-core/src/main/java/hep/dataforge/connections/ConnectionFactory.java new file mode 100644 index 00000000..bd425c14 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/connections/ConnectionFactory.java @@ -0,0 +1,37 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.connections; + +import hep.dataforge.context.Context; +import hep.dataforge.meta.Meta; + +/** + * A factory SPI class for connections + */ +public interface ConnectionFactory { + String getType(); + + /** + * + * @param obj an object for which this connections is intended + * @param context context of the connection (could be different from connectible context) + * @param meta configuration for connection + * @param type of the connectible + * @return + */ + Connection build(T obj, Context context, Meta meta); +} diff --git a/dataforge-core/src/main/java/hep/dataforge/connections/ConnectionHelper.java b/dataforge-core/src/main/java/hep/dataforge/connections/ConnectionHelper.java new file mode 100644 index 00000000..be19b54a --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/connections/ConnectionHelper.java @@ -0,0 +1,163 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.connections; + +import hep.dataforge.Named; +import hep.dataforge.context.Context; +import hep.dataforge.meta.Meta; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.stream.Stream; + +/** + * A helper class to manage Connectible objects in the same fashion + */ +public class ConnectionHelper implements Connectible { + //TODO isolate errors inside connections + private final Map> connections = new HashMap<>(); + private final Connectible caller; + private final Logger logger = LoggerFactory.getLogger(getClass()); + + public ConnectionHelper(Connectible caller) { + this.caller = caller; + } + +// public Logger getLogger() { +// return logger; +// } + + /** + * Attach connection + * + * @param connection + * @param roles + */ + @Override + public synchronized void connect(Connection connection, String... roles) { + logger.info("Attaching connection {} with roles {}", connection.toString(), String.join(", ", roles)); + //Checking if connection could serve given roles + for (String role : roles) { + if (!acceptsRole(role)) { + logger.warn("The connectible does not support role {}", role); + } else { + roleDefs().stream().filter((roleDef) -> roleDef.name().equals(role)).forEach(rd -> { + if (!rd.objectType().isInstance(connection)) { + logger.error("Connection does not meet type requirement for role {}. Must be {}.", + role, rd.objectType().getName()); + } + }); + } + } + + Set roleSet = new HashSet<>(Arrays.asList(roles)); + if (this.connections.containsKey(connection)) { + //updating roles of existing connection + connections.get(connection).addAll(roleSet); + } else { + this.connections.put(connection, roleSet); + } + + try { + logger.debug("Opening connection {}", connection.toString()); + connection.open(caller); + } catch (Exception ex) { + throw new RuntimeException("Can not open connection", ex); + } + } + + /** + * Build connection (or connections if meta has multiple "connection" entries) and connect + * + * @param context + * @param meta + */ + public void connect(Context context, Meta meta) { + if (meta.hasMeta("connection")) { + meta.getMetaList("connection").forEach(it -> connect(context, it)); + } else { + String[] roles = meta.getStringArray("role", new String[]{}); + Connection.buildConnection(caller, context, meta).ifPresent(connection -> connect(connection, roles)); + } + } + + @Override + public synchronized void disconnect(Connection connection) { + if (connections.containsKey(connection)) { + String conName = Named.Companion.nameOf(connection); + try { + logger.debug("Closing connection {}", conName); + connection.close(); + } catch (Exception ex) { + logger.error("Can not close connection", ex); + } + logger.info("Detaching connection {}", conName); + this.connections.remove(connection); + } + } + + + /** + * Get a stream of all connections for a given role. Stream could be empty + * + * @param role + * @param type + * @param + * @return + */ + @Override + public Stream connections(String role, Class type) { + return connections.entrySet().stream() + .filter(entry -> type.isInstance(entry.getKey())) + .filter(entry -> role.isEmpty() || entry.getValue().stream().anyMatch(r -> r.matches(role))) + .map(entry -> type.cast(entry.getKey())); + } + + public Stream connections(Class type) { + return connections.entrySet().stream() + .filter(entry -> type.isInstance(entry.getKey())) + .map(entry -> type.cast(entry.getKey())); + } + + /** + * Return a unique connection or first connection satisfying condition + * + * @param role + * @param type + * @param + * @return + */ + public Optional optConnection(String role, Class type) { + return connections(role, type).findFirst(); + } + + @Override + public boolean acceptsRole(String name) { + return caller.acceptsRole(name); + } + + @Override + public List roleDefs() { + return caller.roleDefs(); + } + + @Override + public Optional optRoleDef(String name) { + return caller.optRoleDef(name); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/connections/ConnectsTo.java b/dataforge-core/src/main/java/hep/dataforge/connections/ConnectsTo.java new file mode 100644 index 00000000..7662c6a6 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/connections/ConnectsTo.java @@ -0,0 +1,29 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.connections; + +import java.lang.annotation.*; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface ConnectsTo { + String type(); + String[] roles() default {}; + Class cl(); +} diff --git a/dataforge-core/src/main/java/hep/dataforge/connections/NamedValueListener.java b/dataforge-core/src/main/java/hep/dataforge/connections/NamedValueListener.java new file mode 100644 index 00000000..90db9227 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/connections/NamedValueListener.java @@ -0,0 +1,31 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.connections; + +import hep.dataforge.values.Value; +import hep.dataforge.values.ValueFactory; + +/** + * Created by darksnake on 25-May-17. + */ +public interface NamedValueListener { + void pushValue(String valueName, Value value); + + default void pushValue(String valueName, Object obj) { + pushValue(valueName, ValueFactory.of(obj)); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/connections/RoleDef.java b/dataforge-core/src/main/java/hep/dataforge/connections/RoleDef.java new file mode 100644 index 00000000..cf9ca2b3 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/connections/RoleDef.java @@ -0,0 +1,63 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.connections; + +import java.lang.annotation.*; + +/** + * The role of connection served by this device + * + * @author Alexander Nozik + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@Repeatable(RoleDefs.class) +public @interface RoleDef { + /** + * Role name + * + * @return + */ + String name(); + + /** + * The type of the object that could play this role + * + * @return + */ + Class objectType() default Object.class; + + /** + * If true then only one connection of this role is allowed per object + * @return + */ + boolean unique() default false; + + /** + * Role description + * + * @return + */ + String info() default ""; +} diff --git a/dataforge-core/src/main/java/hep/dataforge/connections/RoleDefs.java b/dataforge-core/src/main/java/hep/dataforge/connections/RoleDefs.java new file mode 100644 index 00000000..7bc26693 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/connections/RoleDefs.java @@ -0,0 +1,36 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.connections; + +import java.lang.annotation.*; + +/** + * + * @author Alexander Nozik + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface RoleDefs { + RoleDef[] value(); +} diff --git a/dataforge-core/src/main/java/hep/dataforge/connections/ValueListener.java b/dataforge-core/src/main/java/hep/dataforge/connections/ValueListener.java new file mode 100644 index 00000000..872cad1c --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/connections/ValueListener.java @@ -0,0 +1,31 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.connections; + +import hep.dataforge.values.Value; +import hep.dataforge.values.ValueFactory; + +/** + * + * @author Alexander Nozik + */ +public interface ValueListener { + void pushValue(Value value); + + default void pushValue(Object obj){ + pushValue(ValueFactory.of(obj)); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/description/NodeDef.java b/dataforge-core/src/main/java/hep/dataforge/description/NodeDef.java new file mode 100644 index 00000000..692cd0c6 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/description/NodeDef.java @@ -0,0 +1,69 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.description; + +import java.lang.annotation.*; + +/** + *

+ * NodeDef class.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@Repeatable(NodeDefs.class) +public @interface NodeDef { + + String key(); + + String info() default ""; + + boolean multiple() default false; + + boolean required() default false; + + String[] tags() default {}; + + /** + * A list of child value descriptors + */ + ValueDef[] values() default {}; + + /** + * A target class for this node to describe + * @return + */ + Class type() default Object.class; + + /** + * The DataForge path to the resource containing the description. Following targets are supported: + *
    + *
  1. resource
  2. + *
  3. file
  4. + *
  5. class
  6. + *
  7. method
  8. + *
  9. property
  10. + *
+ * + * Does not work if [type] is provided + * + * @return + */ + String descriptor() default ""; +} diff --git a/dataforge-core/src/main/java/hep/dataforge/description/NodeDefs.java b/dataforge-core/src/main/java/hep/dataforge/description/NodeDefs.java new file mode 100644 index 00000000..c6c9e2bb --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/description/NodeDefs.java @@ -0,0 +1,34 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.description; + +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + *

NodeDefs class.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface NodeDefs { + NodeDef[] value(); +} diff --git a/dataforge-core/src/main/java/hep/dataforge/description/TypedActionDef.java b/dataforge-core/src/main/java/hep/dataforge/description/TypedActionDef.java new file mode 100644 index 00000000..81cb7d49 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/description/TypedActionDef.java @@ -0,0 +1,39 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.description; + +import java.lang.annotation.*; + +/** + * The annotation defining Action name, info, input and output types. + * + * @author Alexander Nozik + * @version $Id: $Id + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface TypedActionDef { + + String name(); + + String info() default ""; + + Class inputType() default Object.class; + + Class outputType() default Object.class; +} diff --git a/dataforge-core/src/main/java/hep/dataforge/description/ValueDef.java b/dataforge-core/src/main/java/hep/dataforge/description/ValueDef.java new file mode 100644 index 00000000..2a2fbe5a --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/description/ValueDef.java @@ -0,0 +1,52 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.description; + +import hep.dataforge.values.ValueType; + +import java.lang.annotation.*; + +/** + * Ð”ÐµÐºÐ»Ð°Ñ€Ð°Ñ†Ð¸Ñ Ð¿Ð°Ñ€Ð°Ð¼ÐµÑ‚Ñ€Ð° аннотации, который иÑползуетÑÑ Ð² контенте или методе, + * параметром которого ÑвлÑетÑÑ Ð°Ð½Ð½Ð¾Ñ‚Ð°Ñ†Ð¸Ñ + * + * @author Alexander Nozik + * @version $Id: $Id + */ +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@Repeatable(ValueDefs.class) +public @interface ValueDef { + + String key(); + + ValueType[] type() default {ValueType.STRING}; + + boolean multiple() default false; + + String def() default ""; + + String info() default ""; + + boolean required() default true; + + String[] allowed() default {}; + + Class enumeration() default Object.class; + + String[] tags() default {}; +} diff --git a/dataforge-core/src/main/java/hep/dataforge/description/ValueDefs.java b/dataforge-core/src/main/java/hep/dataforge/description/ValueDefs.java new file mode 100644 index 00000000..fd954bd2 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/description/ValueDefs.java @@ -0,0 +1,34 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.description; + +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + *

ValueDefs class.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface ValueDefs { + ValueDef[] value(); +} diff --git a/dataforge-core/src/main/java/hep/dataforge/events/Event.java b/dataforge-core/src/main/java/hep/dataforge/events/Event.java new file mode 100644 index 00000000..37392e26 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/events/Event.java @@ -0,0 +1,96 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.events; + +import hep.dataforge.meta.Meta; +import hep.dataforge.meta.MetaBuilder; +import hep.dataforge.meta.SimpleMetaMorph; +import hep.dataforge.utils.DateTimeUtils; + +import java.time.Instant; + +/** + * A metamorph representing framework event + * + * @author Alexander Nozik + */ +public class Event extends SimpleMetaMorph { + public static final String EVENT_PRIORITY_KEY = "priority"; + public static final String EVENT_TYPE_KEY = "type"; + public static final String EVENT_SOURCE_KEY = "sourceTag"; + public static final String EVENT_TIME_KEY = "time"; + + /** + * Create an event with given basic parameters and additional meta data. All + * values except type could be null or empty + * + * @param type + * @param source + * @param priority + * @param time + * @param additionalMeta + */ + public static Event make(String type, String source, int priority, Instant time, Meta additionalMeta) { + MetaBuilder builder = new MetaBuilder("event"); + if (additionalMeta != null) { + builder.update(additionalMeta.getBuilder()); + } + + builder.setValue(EVENT_TYPE_KEY, type); + + if (time == null) { + time = DateTimeUtils.now(); + } + builder.setValue(EVENT_TIME_KEY, time); + if (source != null && !source.isEmpty()) { + builder.setValue(EVENT_SOURCE_KEY, source); + } + if (priority != 0) { + builder.setValue(EVENT_PRIORITY_KEY, priority); + } + return new Event(builder.build()); + } + + //TODO add source context to event? + + public Event(Meta meta) { + super(meta); + } + + public int priority() { + return getMeta().getInt(EVENT_PRIORITY_KEY, 0); + } + + public String type() { + return getMeta().getString(EVENT_TYPE_KEY); + } + + public String sourceTag() { + return getMeta().getString(EVENT_SOURCE_KEY, ""); + } + + public Instant time() { + return getMeta().getValue(EVENT_TIME_KEY).getTime(); + } + +// /** +// * get event string representation (header) to write in logs +// * +// * @return +// */ +// @Override +// String toString(); +} diff --git a/dataforge-core/src/main/java/hep/dataforge/events/EventBuilder.java b/dataforge-core/src/main/java/hep/dataforge/events/EventBuilder.java new file mode 100644 index 00000000..6c554217 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/events/EventBuilder.java @@ -0,0 +1,97 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.events; + +import hep.dataforge.meta.Meta; +import hep.dataforge.meta.MetaBuilder; +import hep.dataforge.utils.DateTimeUtils; +import hep.dataforge.utils.GenericBuilder; + +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; + +import static hep.dataforge.events.Event.*; + +/** + * Th builder class for events + * + * @author Alexander Nozik + */ +public abstract class EventBuilder implements GenericBuilder { + + protected final MetaBuilder builder = new MetaBuilder("event"); + private final Map objectMap = new HashMap<>(); + + protected EventBuilder(String type) { + this.builder.setValue(Event.EVENT_TYPE_KEY, type); + } + + public static EventBuilder make(String type) { + return new ConcreteEventBuilder(type); + } + + public EventBuilder setTime(Instant time) { + builder.setValue(EVENT_TIME_KEY, time); + return this; + } + + /** + * Set time of the event to current time + * + * @return + */ + public EventBuilder setTime() { + builder.setValue(EVENT_TIME_KEY, DateTimeUtils.now()); + return this; + } + + public EventBuilder setSource(String source) { + builder.setValue(EVENT_SOURCE_KEY, source); + return this; + } + + public EventBuilder setPriority(int priority) { + builder.setValue(EVENT_PRIORITY_KEY, priority); + return this; + } + + public EventBuilder setMetaNode(String nodeName, Meta... nodes) { + builder.setNode(nodeName, nodes); + return this; + } + + public EventBuilder setMetaValue(String valueName, Object value) { + builder.setValue(valueName, value); + return this; + } + + public Meta buildEventMeta() { + if (!builder.hasValue(EVENT_TIME_KEY)) { + setTime(); + } + return builder.build(); + } + + @Override + public Event build() { + return new Event(buildEventMeta()); + } + + private static class ConcreteEventBuilder extends EventBuilder { + + public ConcreteEventBuilder(String type) { + super(type); + } + + @Override + public EventBuilder self() { + return this; + } + + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/events/EventHandler.java b/dataforge-core/src/main/java/hep/dataforge/events/EventHandler.java new file mode 100644 index 00000000..a33f68a0 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/events/EventHandler.java @@ -0,0 +1,25 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.events; + +/** + * A handler for events + * @author Alexander Nozik + */ +@FunctionalInterface +public interface EventHandler{ + boolean pushEvent(Event event); +} diff --git a/dataforge-core/src/main/java/hep/dataforge/events/EventTransmitter.java b/dataforge-core/src/main/java/hep/dataforge/events/EventTransmitter.java new file mode 100644 index 00000000..4fc5e52f --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/events/EventTransmitter.java @@ -0,0 +1,33 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.events; + +/** + * An interface marking an object that can dispatch messages (send it to named + * responder) + * + * @author Alexander Nozik + */ +public interface EventTransmitter { + + /** + * Send message and return true if message is successfully sent + * @param address + * @param event + * @return + */ + boolean send(String address, Event event); +} diff --git a/dataforge-core/src/main/java/hep/dataforge/exceptions/AnonymousNotAlowedException.java b/dataforge-core/src/main/java/hep/dataforge/exceptions/AnonymousNotAlowedException.java new file mode 100644 index 00000000..8badd400 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/exceptions/AnonymousNotAlowedException.java @@ -0,0 +1,42 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.exceptions; + +/** + *

AnonymousNotAlowedException class.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class AnonymousNotAlowedException extends NamingException { + + /** + * Creates a new instance of AnonymousNotAlowedException + * without detail message. + */ + public AnonymousNotAlowedException() { + } + + /** + * Constructs an instance of AnonymousNotAlowedException with + * the specified detail message. + * + * @param msg the detail message. + */ + public AnonymousNotAlowedException(String msg) { + super(msg); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/exceptions/ChainPathNotSupportedException.java b/dataforge-core/src/main/java/hep/dataforge/exceptions/ChainPathNotSupportedException.java new file mode 100644 index 00000000..4fe215f3 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/exceptions/ChainPathNotSupportedException.java @@ -0,0 +1,42 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.exceptions; + +/** + *

ChainPathNotSupportedException class.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class ChainPathNotSupportedException extends NamingException { + + /** + * Creates a new instance of ChainPathNotSupportedException + * without detail message. + */ + public ChainPathNotSupportedException(){ + } + + /** + * Constructs an instance of ChainPathNotSupportedException + * with the specified detail message. + * + * @param msg the detail message. + */ + public ChainPathNotSupportedException(String msg) { + super(msg); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/exceptions/ConfigurationException.java b/dataforge-core/src/main/java/hep/dataforge/exceptions/ConfigurationException.java new file mode 100644 index 00000000..b7808aa5 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/exceptions/ConfigurationException.java @@ -0,0 +1,51 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.exceptions; + +import hep.dataforge.meta.Meta; + +/** + * Ошибка парÑинга аннотации. Ð’ общем Ñлучае не обрабатываетÑÑ + * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class ConfigurationException extends RuntimeException { + + Meta an; + + /** + * Creates a new instance of AnnotationParseException without + * detail message. + * + * @param source a {@link hep.dataforge.meta.Meta} object. + */ + public ConfigurationException(Meta source) { + this.an = source; + } + + /** + * Constructs an instance of AnnotationParseException with the + * specified detail message. + * + * @param source a {@link hep.dataforge.meta.Meta} object. + * @param msg the detail message. + */ + public ConfigurationException(Meta source, String msg) { + super(msg); + this.an = source; + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/exceptions/ContentException.java b/dataforge-core/src/main/java/hep/dataforge/exceptions/ContentException.java new file mode 100644 index 00000000..1355e906 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/exceptions/ContentException.java @@ -0,0 +1,54 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.exceptions; + +/** + *

ContentException class.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class ContentException extends RuntimeException { + + /** + * Creates a new instance of ContentException without detail + * message. + */ + public ContentException() { + } + + /** + * Constructs an instance of ContentException with the + * specified detail message. + * + * @param msg the detail message. + */ + public ContentException(String msg) { + super(msg); + } + + /** + *

Constructor for ContentException.

+ * + * @param string a {@link java.lang.String} object. + * @param thrwbl a {@link java.lang.Throwable} object. + */ + public ContentException(String string, Throwable thrwbl) { + super(string, thrwbl); + } + + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/exceptions/ContextLockException.java b/dataforge-core/src/main/java/hep/dataforge/exceptions/ContextLockException.java new file mode 100644 index 00000000..3853d906 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/exceptions/ContextLockException.java @@ -0,0 +1,32 @@ +package hep.dataforge.exceptions; + +import hep.dataforge.Named; + +public class ContextLockException extends RuntimeException { + private final Object locker; + + public ContextLockException(Object locker) { + this.locker = locker; + } + + public ContextLockException() { + this.locker = null; + } + + private String getObjectName() { + if (locker instanceof Named) { + return locker.getClass().getSimpleName() + ":" + ((Named) locker).getName(); + } else { + return locker.getClass().getSimpleName(); + } + } + + @Override + public String getMessage() { + if (locker == null) { + return "Context is locked"; + } else { + return "Context is locked by " + getObjectName(); + } + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/exceptions/DataFormatException.java b/dataforge-core/src/main/java/hep/dataforge/exceptions/DataFormatException.java new file mode 100644 index 00000000..2081a0a7 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/exceptions/DataFormatException.java @@ -0,0 +1,42 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.exceptions; + +/** + *

DataFormatException class.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class DataFormatException extends NamingException { + + /** + * Creates a new instance of DataSetFormatException without + * detail message. + */ + public DataFormatException() { + } + + /** + * Constructs an instance of DataSetFormatException with the + * specified detail message. + * + * @param msg the detail message. + */ + public DataFormatException(String msg) { + super(msg); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/exceptions/DescriptionNotFoundException.java b/dataforge-core/src/main/java/hep/dataforge/exceptions/DescriptionNotFoundException.java new file mode 100644 index 00000000..1c70a157 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/exceptions/DescriptionNotFoundException.java @@ -0,0 +1,42 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.exceptions; + +/** + *

DescriptionNotFoundException class.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class DescriptionNotFoundException extends DescriptorException { + + /** + * Creates a new instance of DescriptionNotFoundException + * without detail message. + */ + public DescriptionNotFoundException() { + } + + /** + * Constructs an instance of DescriptionNotFoundException with + * the specified detail message. + * + * @param msg the detail message. + */ + public DescriptionNotFoundException(String msg) { + super(msg); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/exceptions/DescriptorException.java b/dataforge-core/src/main/java/hep/dataforge/exceptions/DescriptorException.java new file mode 100644 index 00000000..b31ceb3a --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/exceptions/DescriptorException.java @@ -0,0 +1,42 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.exceptions; + +/** + *

DescriptorException class.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class DescriptorException extends RuntimeException { + + /** + * Creates a new instance of DescriptorException without detail + * message. + */ + public DescriptorException() { + } + + /** + * Constructs an instance of DescriptorException with the + * specified detail message. + * + * @param msg the detail message. + */ + public DescriptorException(String msg) { + super(msg); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/exceptions/DuplicateDescriptionException.java b/dataforge-core/src/main/java/hep/dataforge/exceptions/DuplicateDescriptionException.java new file mode 100644 index 00000000..adbaac2d --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/exceptions/DuplicateDescriptionException.java @@ -0,0 +1,45 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.exceptions; + +/** + *

DuplicateDescriptionException class.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class DuplicateDescriptionException extends DescriptorException { + + private final String name; + private final boolean forElement; + + public DuplicateDescriptionException(String name, boolean forElement) { + this.name = name; + this.forElement = forElement; + } + + @Override + public String getMessage() { + if(forElement){ + return String.format("Duplicate description for element %s", name); + } else { + return String.format("Duplicate description for parameter %s", name); + } + } + + + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/exceptions/EnvelopeFormatException.java b/dataforge-core/src/main/java/hep/dataforge/exceptions/EnvelopeFormatException.java new file mode 100644 index 00000000..009d475b --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/exceptions/EnvelopeFormatException.java @@ -0,0 +1,46 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.exceptions; + +/** + * + * @author darksnake + */ +public class EnvelopeFormatException extends RuntimeException { + + /** + * Creates a new instance of EnvelopeFormatException without + * detail message. + */ + public EnvelopeFormatException() { + } + + /** + * Constructs an instance of EnvelopeFormatException with the + * specified detail message. + * + * @param msg the detail message. + */ + public EnvelopeFormatException(String msg) { + super(msg); + } + + public EnvelopeFormatException(String message, Throwable cause) { + super(message, cause); + } + + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/exceptions/EnvelopeTargetNotFoundException.java b/dataforge-core/src/main/java/hep/dataforge/exceptions/EnvelopeTargetNotFoundException.java new file mode 100644 index 00000000..b6cfb9b8 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/exceptions/EnvelopeTargetNotFoundException.java @@ -0,0 +1,57 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.exceptions; + +import hep.dataforge.meta.Meta; + +/** + * An exception thrown in case message target is not found by current {@code Responder} + * @author Alexander Nozik + */ +public class EnvelopeTargetNotFoundException extends RuntimeException { + + private String targetType; + private String targetName; + private Meta targetMeta; + + public EnvelopeTargetNotFoundException(String targetName) { + this.targetName = targetName; + } + + public EnvelopeTargetNotFoundException(String targetType, String targetName) { + this.targetType = targetType; + this.targetName = targetName; + } + + public EnvelopeTargetNotFoundException(Meta targetMeta) { + this.targetMeta = targetMeta; + } + + public EnvelopeTargetNotFoundException(String targetName, Meta targetMeta) { + this.targetName = targetName; + this.targetMeta = targetMeta; + } + + public EnvelopeTargetNotFoundException(String targetType, String targetName, Meta targetMeta) { + this.targetType = targetType; + this.targetName = targetName; + this.targetMeta = targetMeta; + } + + public String getTargetType() { + return targetType; + } + + public String getTargetName() { + return targetName; + } + + public Meta getTargetMeta() { + return targetMeta; + } + + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/exceptions/NameNotFoundException.java b/dataforge-core/src/main/java/hep/dataforge/exceptions/NameNotFoundException.java new file mode 100644 index 00000000..b2a18de2 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/exceptions/NameNotFoundException.java @@ -0,0 +1,74 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.exceptions; + +/** + *

NameNotFoundException class.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class NameNotFoundException extends NamingException { + + private String name; + + /** + * Creates a new instance of + * NameNotFoundException without detail message. + */ + public NameNotFoundException() { + super(); + } + + /** + * Constructs an instance of + * NameNotFoundException with the specified detail message. + * + * @param name a {@link java.lang.String} object. + */ + public NameNotFoundException(String name) { + this.name = name; + } + + /** + *

Constructor for NameNotFoundException.

+ * + * @param name a {@link java.lang.String} object. + * @param msg a {@link java.lang.String} object. + */ + public NameNotFoundException(String name, String msg) { + super(msg); + this.name = name; + } + + @Override + public String getMessage() { + return super.getMessage() + buildMessage(); + } + + protected String buildMessage() { + return " The element with name \"" + getName() + "\" is not found."; + } + + /** + *

Getter for the field name.

+ * + * @return a {@link java.lang.String} object. + */ + public String getName() { + return this.name; + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/exceptions/NamingException.java b/dataforge-core/src/main/java/hep/dataforge/exceptions/NamingException.java new file mode 100644 index 00000000..5dfa38cd --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/exceptions/NamingException.java @@ -0,0 +1,42 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.exceptions; + +/** + *

NamingException class.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class NamingException extends RuntimeException { + + /** + * Creates a new instance of NewException without detail + * message. + */ + public NamingException() { + } + + /** + * Constructs an instance of NewException with the specified + * detail message. + * + * @param msg the detail message. + */ + public NamingException(String msg) { + super(msg); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/exceptions/NonEmptyMetaMorphException.java b/dataforge-core/src/main/java/hep/dataforge/exceptions/NonEmptyMetaMorphException.java new file mode 100644 index 00000000..752074c0 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/exceptions/NonEmptyMetaMorphException.java @@ -0,0 +1,14 @@ +package hep.dataforge.exceptions; + +/** + * Created by darksnake on 12-Nov-16. + */ +public class NonEmptyMetaMorphException extends IllegalStateException { + private Class type; + + public NonEmptyMetaMorphException(Class type) { + super(String.format("Can not update non-empty MetaMorph for class '%s'", type.getSimpleName())); + this.type = type; + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/exceptions/NotConnectedException.java b/dataforge-core/src/main/java/hep/dataforge/exceptions/NotConnectedException.java new file mode 100644 index 00000000..fe234242 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/exceptions/NotConnectedException.java @@ -0,0 +1,32 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.exceptions; + +import hep.dataforge.connections.Connection; + +/** + * An exception thrown when the request is sent to the closed connection + * + * @author Alexander Nozik + */ +public class NotConnectedException extends Exception { + + Connection connection; + + public NotConnectedException(Connection connection) { + this.connection = connection; + } + + public Connection getConnection() { + return connection; + } + + @Override + public String getMessage() { + return "The connection is not open"; + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/exceptions/NotDefinedException.java b/dataforge-core/src/main/java/hep/dataforge/exceptions/NotDefinedException.java new file mode 100644 index 00000000..7ab29a86 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/exceptions/NotDefinedException.java @@ -0,0 +1,43 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.exceptions; + +/** + * This exception is used when some parameters or functions are not defined by + * user. + * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class NotDefinedException extends IllegalStateException { + + /** + * Creates a new instance of + * NotDefinedException without detail message. + */ + public NotDefinedException() { + } + + /** + * Constructs an instance of + * NotDefinedException with the specified detail message. + * + * @param msg the detail message. + */ + public NotDefinedException(String msg) { + super(msg); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/exceptions/PathSyntaxException.java b/dataforge-core/src/main/java/hep/dataforge/exceptions/PathSyntaxException.java new file mode 100644 index 00000000..96d4db97 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/exceptions/PathSyntaxException.java @@ -0,0 +1,42 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.exceptions; + +/** + * Ошибка ÑинтакÑиÑа пути + * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class PathSyntaxException extends NamingException { + + /** + * Creates a new instance of PathSyntaxException without detail + * message. + */ + public PathSyntaxException() { + } + + /** + * Constructs an instance of PathSyntaxException with the + * specified detail message. + * + * @param msg the detail message. + */ + public PathSyntaxException(String msg) { + super(msg); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/exceptions/TargetNotProvidedException.java b/dataforge-core/src/main/java/hep/dataforge/exceptions/TargetNotProvidedException.java new file mode 100644 index 00000000..71d9bb90 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/exceptions/TargetNotProvidedException.java @@ -0,0 +1,48 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.exceptions; + +/** + *

TargetNotProvidedException class.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class TargetNotProvidedException extends NameNotFoundException { + + /** + * Creates a new instance of TargetNotProvided without detail + * message. + */ + public TargetNotProvidedException(String target) { + super(target); + } + + /** + * Constructs an instance of TargetNotProvided with the + * specified detail message. + * + * @param msg the detail message. + */ + public TargetNotProvidedException(String msg, String target) { + super(msg, target); + } + + @Override + protected String buildMessage() { + return "The target \"" + getName() + "\" is not provided."; + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/exceptions/ValueConversionException.java b/dataforge-core/src/main/java/hep/dataforge/exceptions/ValueConversionException.java new file mode 100644 index 00000000..b3d6e359 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/exceptions/ValueConversionException.java @@ -0,0 +1,53 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.exceptions; + +import hep.dataforge.values.Value; +import hep.dataforge.values.ValueType; + +/** + *

ValueConversionException class.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class ValueConversionException extends RuntimeException { + + Value value; + ValueType to; + + + /** + *

Constructor for ValueConversionException.

+ * + * @param value a {@link hep.dataforge.values.Value} object. + * @param to a {@link hep.dataforge.values.ValueType} object. + */ + public ValueConversionException(Value value, ValueType to) { + this.value = value; + this.to = to; + } + + /** + * {@inheritDoc} + */ + @Override + public String getMessage() { + return String.format("Failed to convert value '%s' of type %s to %s", value.getString(), value.getType(), to.name()); + } + + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/goals/AbstractGoal.java b/dataforge-core/src/main/java/hep/dataforge/goals/AbstractGoal.java new file mode 100644 index 00000000..ddd19b1c --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/goals/AbstractGoal.java @@ -0,0 +1,148 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.goals; + +import hep.dataforge.utils.ReferenceRegistry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.ForkJoinPool; + +/** + * @author Alexander Nozik + */ +public abstract class AbstractGoal implements Goal { + + private final ReferenceRegistry> listeners = new ReferenceRegistry<>(); + + + private final Executor executor; + protected final GoalResult result = new GoalResult(); + private CompletableFuture computation; + private Thread thread; + + public AbstractGoal(Executor executor) { + this.executor = executor; + } + + public AbstractGoal() { + this.executor = ForkJoinPool.commonPool(); + } + + protected Logger getLogger() { + return LoggerFactory.getLogger(getClass()); + } + + @Override + public synchronized void run() { + if (!isRunning()) { + //start all dependencies so they will occupy threads + computation = CompletableFuture + .allOf(dependencies() + .map(dep -> { + dep.run();//starting all dependencies + return dep.asCompletableFuture(); + }) + .toArray(CompletableFuture[]::new)) + .whenCompleteAsync((res, err) -> { + if (err != null) { + getLogger().error("One of goal dependencies failed with exception", err); + if (failOnError()) { + this.result.completeExceptionally(err); + } + } + + try { + thread = Thread.currentThread(); + //trigger start hooks + listeners.forEach(GoalListener::onGoalStart); + T r = compute(); + //triggering result hooks + listeners.forEach(listener -> listener.onGoalComplete(r)); + this.result.complete(r); + } catch (Exception ex) { + //trigger exception hooks + getLogger().error("Exception during goal execution", ex); + listeners.forEach(listener -> listener.onGoalFailed(ex)); + this.result.completeExceptionally(ex); + } finally { + thread = null; + } + }, executor); + } + } + + public Executor getExecutor() { + return executor; + } + + protected abstract T compute() throws Exception; + + /** + * If true the goal will result in error if any of dependencies throws exception. + * Otherwise it will be calculated event if some of dependencies are failed. + * + * @return + */ + protected boolean failOnError() { + return true; + } + + /** + * Abort internal goals process without canceling result. Use with + * care + */ + protected void abort() { + if (isRunning()) { + if (this.computation != null) { + this.computation.cancel(true); + } + if (thread != null) { + thread.interrupt(); + } + } + } + + public boolean isRunning() { + return this.result.isDone() || this.computation != null; + } + + /** + * Abort current goals if it is in progress and set result. Useful for + * caching purposes. + * + * @param result + */ + public final synchronized boolean complete(T result) { + abort(); + return this.result.complete(result); + } + + @Override + public void registerListener(GoalListener listener) { + listeners.add(listener,true); + } + + @Override + public CompletableFuture asCompletableFuture() { + return result; + } + + + protected class GoalResult extends CompletableFuture { + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + if (mayInterruptIfRunning) { + abort(); + } + return super.cancel(mayInterruptIfRunning); + } + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/goals/GeneratorGoal.java b/dataforge-core/src/main/java/hep/dataforge/goals/GeneratorGoal.java new file mode 100644 index 00000000..c41d9ff5 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/goals/GeneratorGoal.java @@ -0,0 +1,41 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.goals; + +import java.util.concurrent.Executor; +import java.util.function.Supplier; +import java.util.stream.Stream; + +/** + * A goal which has no dependencies but generates result in a lazy way + * + * @param + * @author Alexander Nozik + */ +public class GeneratorGoal extends AbstractGoal { + + private final Supplier sup; + + public GeneratorGoal(Executor executor, Supplier sup) { + super(executor); + this.sup = sup; + } + + public GeneratorGoal(Supplier sup) { + this.sup = sup; + } + + @Override + protected T compute() throws Exception { + return sup.get(); + } + + @Override + public Stream> dependencies() { + return Stream.empty(); + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/goals/Goal.java b/dataforge-core/src/main/java/hep/dataforge/goals/Goal.java new file mode 100644 index 00000000..4030b170 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/goals/Goal.java @@ -0,0 +1,150 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.goals; + +import hep.dataforge.context.Global; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.*; +import java.util.function.BiConsumer; +import java.util.stream.Stream; + +/** + * An elementary lazy calculation which could be linked into a chain. By default, Goal wraps a CompletableFuture which is triggered with {@code run} method. + * + * @author Alexander Nozik + */ +public interface Goal extends RunnableFuture { + + /** + * A stream of all bound direct dependencies + * + * @return + */ + Stream> dependencies(); + + /** + * Start this goal goals. Triggers start of dependent goals + */ + void run(); + + /** + * Convert this goal to CompletableFuture. + * + * @return + */ + CompletableFuture asCompletableFuture(); + + /** + * Start and get sync result + * + * @return + */ + default T get() { + try { + run(); + return asCompletableFuture().get(); + } catch (Exception ex) { + throw new RuntimeException("Failed to reach the goal", ex); + } + } + + /** + * Cancel current goal + * @param mayInterruptIfRunning + * @return + */ + @Override + default boolean cancel(boolean mayInterruptIfRunning) { + return asCompletableFuture().cancel(mayInterruptIfRunning); + } + + default boolean cancel() { + return cancel(true); + } + + @Override + default boolean isCancelled() { + return asCompletableFuture().isCancelled(); + } + + @Override + default boolean isDone() { + return asCompletableFuture().isDone(); + } + + boolean isRunning(); + + @Override + default T get(long timeout, @NotNull TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + return asCompletableFuture().get(timeout, unit); + } + + /** + * Register a listener. Result listeners are triggered before {@code result} is set so they will be evaluated before any subsequent goals are started. + * + * @param listener + */ + void registerListener(GoalListener listener); + + /** + * Add on start hook which is executed in goal thread + * + * @param r + */ + default Goal onStart(Runnable r) { + return onStart(Global.INSTANCE.getDispatcher(), r); + } + + /** + * Add on start hook which is executed using custom executor + * + * @param executor + * @param r + */ + default Goal onStart(Executor executor, Runnable r) { + registerListener(new GoalListener() { + @Override + public void onGoalStart() { + executor.execute(r); + } + + }); + return this; + } + + /** + * Handle results using global dispatch thread + * + * @param consumer + */ + default Goal onComplete(BiConsumer consumer) { + return onComplete(Global.INSTANCE.getDispatcher(), consumer); + } + + + + /** + * handle using custom executor + * + * @param exec + * @param consumer + */ + default Goal onComplete(Executor exec, BiConsumer consumer) { + registerListener(new GoalListener() { + @Override + public void onGoalComplete(T result) { + exec.execute(() -> consumer.accept(result, null)); + } + + @Override + public void onGoalFailed(Throwable ex) { + exec.execute(() -> consumer.accept(null, ex)); + } + }); + return this; + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/goals/GoalListener.java b/dataforge-core/src/main/java/hep/dataforge/goals/GoalListener.java new file mode 100644 index 00000000..bb347f7c --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/goals/GoalListener.java @@ -0,0 +1,29 @@ +package hep.dataforge.goals; + +import org.jetbrains.annotations.Nullable; + +/** + * A universal goal state listener + * Created by darksnake on 19-Mar-17. + */ +public interface GoalListener { + + /** + * Do something when the goal actually starts. Notification is done after each reset and subsequent start + */ + default void onGoalStart() { + + } + + /** + * Notify that goal is completed with result + * @param result + */ + default void onGoalComplete(T result){ + + } + + default void onGoalFailed(@Nullable Throwable ex){ + + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/goals/MultiInputGoal.java b/dataforge-core/src/main/java/hep/dataforge/goals/MultiInputGoal.java new file mode 100644 index 00000000..28107163 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/goals/MultiInputGoal.java @@ -0,0 +1,181 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.goals; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * + * @author Alexander Nozik + * @param + */ +public abstract class MultiInputGoal extends AbstractGoal { + + //TODO replace RuntimeExceptions with specific exceptions + public static String DEFAULT_SLOT = ""; + + private final Map bindings = new HashMap<>(); + + public MultiInputGoal(ExecutorService executor) { + super(executor); + } + + /** + * Bind the output slot of given goal to input slot of this goal + * + * @param dependency + * @param inputSlot + */ + protected void bindInput(Goal dependency, String inputSlot) { + if (!this.bindings.containsKey(inputSlot)) { + createBinding(inputSlot, Object.class); + } + bindings.get(inputSlot).bind(dependency); + } + + //PENDING add default bining results? + protected final void createBinding(String slot, Binding binding) { + this.bindings.put(slot, binding); + } + + protected final void createBinding(String slot, Class type) { + this.bindings.put(slot, new SimpleBinding(type)); + } + + protected final void createListBinding(String slot, Class type) { + this.bindings.put(slot, new ListBinding(type)); + } + + @Override + protected T compute() throws Exception { + return compute(gatherData()); + } + + protected Map gatherData() { + Map data = new ConcurrentHashMap<>(); + bindings.forEach((slot, binding) -> { + if (!binding.isBound()) { + throw new RuntimeException("Required slot " + slot + " not boud"); + } + data.put(slot, binding.getResult()); + }); + return data; + } + + @Override + public Stream> dependencies() { + Stream> res = Stream.empty(); + for (Binding bnd : this.bindings.values()) { + res = Stream.concat(res, bnd.dependencies()); + } + return res; + } + + protected abstract T compute(Map data); + + protected interface Binding { + + /** + * Start bound goal and return its result + * + * @return + */ + T getResult(); + + boolean isBound(); + + void bind(Goal goal); + + Stream dependencies(); + } + + protected class SimpleBinding implements Binding { + + private final Class type; + private Goal goal; + + public SimpleBinding(Class type) { + this.type = type; + } + + @Override + public T getResult() { + goal.run(); + Object res = goal.asCompletableFuture().join(); + if (type.isInstance(res)) { + return (T) res; + } else { + throw new RuntimeException("Type mismatch in goal result"); + } + } + + @Override + public boolean isBound() { + return goal != null; + } + + @Override + public synchronized void bind(Goal goal) { + if (isBound()) { + throw new RuntimeException("Goal already bound"); + } + this.goal = goal; + } + + @Override + public Stream dependencies() { + return Stream.concat(Stream.of(goal), goal.dependencies()); + } + + } + + protected class ListBinding implements Binding> { + + private final Class type; + private final Set goals = new HashSet<>(); + + public ListBinding(Class type) { + this.type = type; + } + + @Override + public Set getResult() { + return goals.stream().parallel().map(goal -> { + goal.run(); + Object res = goal.asCompletableFuture().join(); + if (type.isInstance(res)) { + return (T) res; + } else { + throw new RuntimeException("Type mismatch in goal result"); + } + }).collect(Collectors.toSet()); + } + + @Override + public boolean isBound() { + return !goals.isEmpty(); + } + + @Override + public synchronized void bind(Goal goal) { + this.goals.add(goal); + } + + @Override + public Stream dependencies() { + return goals.stream().flatMap(g -> Stream.concat(Stream.of(g), g.dependencies())); + } + + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/goals/PipeGoal.java b/dataforge-core/src/main/java/hep/dataforge/goals/PipeGoal.java new file mode 100644 index 00000000..48e021d6 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/goals/PipeGoal.java @@ -0,0 +1,56 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.goals; + +import java.util.concurrent.Executor; +import java.util.function.Function; +import java.util.stream.Stream; + +/** + * A one-to one pipeline goal + * + * @author Alexander Nozik + * @param + * @param + */ +public class PipeGoal extends AbstractGoal { + + private final Goal source; + private final Function transformation; + + public PipeGoal(Executor executor, Goal source, Function transformation) { + super(executor); + this.source = source; + this.transformation = transformation; + } + + public PipeGoal(Goal source, Function transformation) { + this.source = source; + this.transformation = transformation; + } + + @Override + protected T compute() { + return transformation.apply(source.get()); + } + + @Override + public Stream> dependencies() { + return Stream.of(source); + } + + /** + * Attach new pipeline goal to this one using same executor + * + * @param + * @param trans + * @return + */ + public PipeGoal andThen(Function trans) { + return new PipeGoal<>(getExecutor(), this, trans); + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/goals/StaticGoal.java b/dataforge-core/src/main/java/hep/dataforge/goals/StaticGoal.java new file mode 100644 index 00000000..7e27f29c --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/goals/StaticGoal.java @@ -0,0 +1,33 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.goals; + +import java.util.stream.Stream; + + +/** + * Goal with a-priori known result + * + * @param + * @author Alexander Nozik + */ +public class StaticGoal extends AbstractGoal { + private final T result; + + public StaticGoal(T result) { + this.result = result; + } + + @Override + public Stream> dependencies() { + return Stream.empty(); + } + + @Override + protected T compute() throws Exception { + return result; + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/io/ColumnedDataReader.java b/dataforge-core/src/main/java/hep/dataforge/io/ColumnedDataReader.java new file mode 100644 index 00000000..21c8c5ad --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/io/ColumnedDataReader.java @@ -0,0 +1,86 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.io; + +import hep.dataforge.tables.Table; +import hep.dataforge.tables.Tables; +import hep.dataforge.tables.ValuesParser; +import hep.dataforge.tables.ValuesReader; +import hep.dataforge.values.Values; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + *

+ * ColumnedDataReader class.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class ColumnedDataReader implements Iterable { + + private ValuesReader reader; + + public ColumnedDataReader(InputStream stream, ValuesParser parser) { + + this.reader = new ValuesReader(stream, parser); + } + + public ColumnedDataReader(InputStream stream, String... format) { + this.reader = new ValuesReader(stream, format); + } + + public ColumnedDataReader(Path path) throws IOException { + String headline = Files.lines(path) + .filter(line -> line.startsWith("#f") || (!line.isEmpty() && !line.startsWith("#"))) + .findFirst().get().substring(2); + + InputStream stream = Files.newInputStream(path); + Iterator iterator = new LineIterator(stream); + if (!iterator.hasNext()) { + throw new IllegalStateException(); + } + this.reader = new ValuesReader(iterator, headline); + } + + public void skipLines(int n) { + reader.skip(n); + } + + @NotNull + @Override + public Iterator iterator() { + return reader; + } + + public Table toTable() { + List points = new ArrayList<>(); + for (Values p : this) { + if (p != null) { + points.add(p); + } + } + return Tables.infer(points); + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/io/ColumnedDataWriter.java b/dataforge-core/src/main/java/hep/dataforge/io/ColumnedDataWriter.java new file mode 100644 index 00000000..59ccbaa8 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/io/ColumnedDataWriter.java @@ -0,0 +1,143 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.io; + +import hep.dataforge.tables.MetaTableFormat; +import hep.dataforge.tables.Table; +import hep.dataforge.tables.TableFormat; +import hep.dataforge.utils.Misc; +import hep.dataforge.values.Values; +import org.slf4j.LoggerFactory; + +import java.io.*; +import java.nio.charset.Charset; +import java.util.Collection; +import java.util.Scanner; + +/** + * Вывод форматированного набора данных в файл или любой другой поток вывода + * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class ColumnedDataWriter implements AutoCloseable { + + private final PrintWriter writer; + private final TableFormat format; + + + public ColumnedDataWriter(OutputStream stream, String... names) { + this(stream, MetaTableFormat.Companion.forNames(names)); + } + + + public ColumnedDataWriter(OutputStream stream, TableFormat format) { + this(stream, Misc.UTF, format); + } + + public ColumnedDataWriter(OutputStream stream, Charset encoding, TableFormat format) { + this.writer = new PrintWriter(new OutputStreamWriter(stream, encoding)); + this.format = format; + } + + public ColumnedDataWriter(File file, boolean append, String... names) throws FileNotFoundException { + this(file, append, Charset.defaultCharset(), MetaTableFormat.Companion.forNames(names)); + } + + public ColumnedDataWriter(File file, boolean append, Charset encoding, TableFormat format) throws FileNotFoundException { + this(new FileOutputStream(file, append), encoding, format); + } + + /** + * {@inheritDoc} + * + * @throws java.lang.Exception + */ + @Override + public void close() throws Exception { + this.writer.close(); + } + + /** + * Добавить одноÑтрочный или многоÑтрочный комментарий + * + * @param str a {@link java.lang.String} object. + */ + public void comment(String str) { + Scanner sc = new Scanner(str); + while (sc.hasNextLine()) { + if (!str.startsWith("#")) { + writer.print("#\t"); + } + writer.println(sc.nextLine()); + } + } + + public void writePoint(Values point) { + writer.println(IOUtils.formatDataPoint(format, point)); + writer.flush(); + } + + public void writePointList(Collection collection) { + collection.stream().forEach((dp) -> { + writePoint(dp); + }); + } + + public void writeFormatHeader() { + writer.println(IOUtils.formatCaption(format)); + writer.flush(); + } + + public void ln() { + writer.println(); + writer.flush(); + } + + public static void writeTable(File file, Table data, String head, boolean append, String... names) throws IOException { + try (FileOutputStream os = new FileOutputStream(file, append)) { + writeTable(os, data, head, names); + } + } + + public static void writeTable(OutputStream stream, Table data, String head, String... names) { + ColumnedDataWriter writer; + TableFormat format; + if (data.getFormat().getNames().size() == 0) { + //ЕÑли набор задан в Ñвободной форме, то конÑтруируетÑÑ Ð°Ð²Ñ‚Ð¾Ð¼Ð°Ñ‚Ð¸Ñ‡ÐµÑкий формат по первой точке + format = MetaTableFormat.Companion.forValues(data.iterator().next()); + LoggerFactory.getLogger(ColumnedDataWriter.class) + .debug("No DataSet format defined. Constucting default based on the first data point"); + } else { + format = data.getFormat(); + } + + if (names.length != 0) { + format = TableFormat.subFormat(format, names); + } + + writer = new ColumnedDataWriter(stream, format); + writer.comment(head); + writer.ln(); + + writer.writeFormatHeader(); + for (Values dp : data) { + writer.writePoint(dp); + } + writer.ln(); + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/io/IOUtils.java b/dataforge-core/src/main/java/hep/dataforge/io/IOUtils.java new file mode 100644 index 00000000..bc624b1f --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/io/IOUtils.java @@ -0,0 +1,390 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.io; + +import hep.dataforge.tables.ListTable; +import hep.dataforge.tables.NavigableValuesSource; +import hep.dataforge.tables.TableFormat; +import hep.dataforge.values.Value; +import hep.dataforge.values.ValueType; +import hep.dataforge.values.Values; + +import java.io.*; +import java.math.BigDecimal; +import java.net.URI; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Optional; +import java.util.Scanner; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import static hep.dataforge.values.Value.NULL_STRING; +import static java.util.regex.Pattern.compile; + +/** + *

+ * IOUtils class.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class IOUtils { + public static final Charset ASCII_CHARSET = Charset.forName("US-ASCII"); + public static final Charset UTF8_CHARSET = Charset.forName("UTF-8"); + + + /** + * Constant ANSI_RESET="\u001B[0m" + */ + public static final String ANSI_RESET = "\u001B[0m"; + /** + * Constant ANSI_BLACK="\u001B[30m" + */ + public static final String ANSI_BLACK = "\u001B[30m"; + /** + * Constant ANSI_RED="\u001B[31m" + */ + public static final String ANSI_RED = "\u001B[31m"; + /** + * Constant ANSI_GREEN="\u001B[32m" + */ + public static final String ANSI_GREEN = "\u001B[32m"; + /** + * Constant ANSI_YELLOW="\u001B[33m" + */ + public static final String ANSI_YELLOW = "\u001B[33m"; + /** + * Constant ANSI_BLUE="\u001B[34m" + */ + public static final String ANSI_BLUE = "\u001B[34m"; + /** + * Constant ANSI_PURPLE="\u001B[35m" + */ + public static final String ANSI_PURPLE = "\u001B[35m"; + /** + * Constant ANSI_CYAN="\u001B[36m" + */ + public static final String ANSI_CYAN = "\u001B[36m"; + /** + * Constant ANSI_WHITE="\u001B[37m" + */ + public static final String ANSI_WHITE = "\u001B[37m"; + + + public static String wrapANSI(String str, String ansiColor) { + return ansiColor + str + ANSI_RESET; + } + + /** + * Resolve a path either in URI or local file form + * + * @param path + * @return + */ + public static Path resolvePath(String path) { + if (path.matches("\\w:[\\\\/].*")) { + return new File(path).toPath(); + } else { + return Paths.get(URI.create(path)); + } + } + + public static String[] parse(String line) { + Scanner scan = new Scanner(line); + ArrayList tokens = new ArrayList<>(); + String token; + Pattern pat = compile("[\"\'].*[\"\']"); + while (scan.hasNext()) { + if (scan.hasNext("[\"\'].*")) { + token = scan.findInLine(pat); + if (token != null) { + token = token.substring(1, token.length() - 1); + } else { + throw new RuntimeException("Syntax error."); + } + } else { + token = scan.next(); + } + tokens.add(token); + } + return tokens.toArray(new String[0]); + + } + + public static NavigableValuesSource readColumnedData(String fileName, String... names) throws FileNotFoundException { + return readColumnedData(new File(fileName), names); + } + + public static NavigableValuesSource readColumnedData(File file, String... names) throws FileNotFoundException { + return readColumnedData(new FileInputStream(file)); + } + + public static NavigableValuesSource readColumnedData(InputStream stream, String... names) { + ColumnedDataReader reader; + if (names.length == 0) { + reader = new ColumnedDataReader(stream); + } else { + reader = new ColumnedDataReader(stream, names); + } + ListTable.Builder res = new ListTable.Builder(names); + for (Values dp : reader) { + res.row(dp); + } + return res.build(); + } + + public static String formatCaption(TableFormat format) { + return "#f " + format.getColumns() + .map(columnFormat -> formatWidth(columnFormat.getName(), getDefaultTextWidth(columnFormat.getPrimaryType()))) + .collect(Collectors.joining("\t")); + } + + public static String formatDataPoint(TableFormat format, Values dp) { + return format.getColumns() + .map(columnFormat -> format(dp.getValue(columnFormat.getName()), getDefaultTextWidth(columnFormat.getPrimaryType()))) + .collect(Collectors.joining("\t")); + } + + public static File[] readFileMask(File workDir, String mask) { + File dir; + String newMask; + //отрываем инфомацию о директории + if (mask.contains(File.separator)) { + int k = mask.lastIndexOf(File.separatorChar); + dir = new File(workDir, mask.substring(0, k)); + newMask = mask.substring(k + 1); + } else { + dir = workDir; + newMask = mask; + } + + String regex = newMask.toLowerCase().replace(".", "\\.").replace("?", ".?").replace("*", ".+"); + return dir.listFiles(new RegexFilter(regex)); + } + + public static File getFile(File file, String path) { + File f = new File(path); + + if (f.isAbsolute()) { + return f; + } else if (file.isDirectory()) { + return new File(file, path); + } else { + return new File(file.getParentFile(), path); + } + } + + private static class RegexFilter implements FilenameFilter { + + String regex; + + public RegexFilter(String regex) { + this.regex = regex; + } + + @Override + public boolean accept(File dir, String name) { + return name.toLowerCase().matches(regex); + } + + } + + /** + * Get pre-defined default width for given value type + * + * @param type + * @return + */ + public static int getDefaultTextWidth(ValueType type) { + switch (type) { + case NUMBER: + return 8; + case BOOLEAN: + return 6; + case STRING: + return 15; + case TIME: + return 20; + case NULL: + return 6; + default: + throw new AssertionError(type.name()); + } + } + + public static String formatWidth(String val, int width) { + if (width > 0) { + return String.format("%" + width + "s", val); + } else { + return val; + } + } + + private static DecimalFormat getExpFormat(int width) { + return new DecimalFormat(String.format("0.%sE0#;-0.%sE0#", grids(width - 6), grids(width - 7))); + } + + private static String grids(int num) { + if (num <= 0) { + return ""; + } + StringBuilder b = new StringBuilder(); + for (int i = 0; i < num; i++) { + b.append("#"); + } + return b.toString(); + } + + private static String formatNumber(Number number, int width) { + try { + BigDecimal bd = new BigDecimal(number.toString()); +// if (number instanceof BigDecimal) { +// bd = (BigDecimal) number; +// } else if (number instanceof Integer) { +// bd = BigDecimal.valueOf(number.getInt()); +// } else { +// +// bd = BigDecimal.valueOf(number.doubleValue()); +// } + + if (bd.precision() - bd.scale() > 2 - width) { + if (number instanceof Integer) { + return String.format("%d", number); + } else { + return String.format("%." + (width - 1) + "g", bd.stripTrailingZeros()); + } + //return getFlatFormat().format(bd); + } else { + return getExpFormat(width).format(bd); + } + } catch (Exception ex) { + return number.toString(); + } + } + + public static String format(Value val, int width) { + switch (val.getType()) { + case BOOLEAN: + if (width >= 5) { + return Boolean.toString(val.getBoolean()); + } else if (val.getBoolean()) { + return formatWidth("+", width); + } else { + return formatWidth("-", width); + } + case NULL: + return formatWidth(NULL_STRING, width); + case NUMBER: + return formatWidth(formatNumber(val.getNumber(), width), width); + case STRING: + return formatWidth(val.getString(), width); + case TIME: + //TODO add time shortening + return formatWidth(val.getString(), width); + default: + throw new IllegalArgumentException("Unsupported input value type"); + } + } + + /** + * Iterate over text lines in the input stream until new line satisfies the given condition. + * The operation is non-buffering so after it, the stream position is at the end of stopping string. + * The iteration stops when stream is exhausted. + * + * @param stream + * @param charset charset name for string encoding + * @param stopCondition + * @param action + * @return the stop line (fist line that satisfies the stopping condition) + */ + @Deprecated + public static String forEachLine(InputStream stream, String charset, Predicate stopCondition, Consumer action) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + while (true) { + try { + int b = stream.read(); + if (b == -1) { + return baos.toString(charset).trim(); + } + if (b == '\n') { + String line = baos.toString(charset).trim(); + baos.reset(); + if (stopCondition.test(line)) { + return line; + } else { + action.accept(line); + } + } else { + baos.write(b); + } + } catch (IOException ex) { + try { + return baos.toString(charset).trim(); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + } + } + + /** + * Return optional next line not fitting skip condition. + * + * @param stream + * @param charset + * @param skipCondition + * @return + */ + public static Optional nextLine(InputStream stream, String charset, Predicate skipCondition) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + while (true) { + try { + int b = stream.read(); + if (b == '\n') { + String line = baos.toString(charset).trim(); + baos.reset(); + if (!skipCondition.test(line)) { + return Optional.of(line); + } + } else { + baos.write(b); + } + } catch (IOException ex) { + return Optional.empty(); + } + } + } + + public static void writeString(DataOutput output, String string) throws IOException { + byte[] bytes = string.getBytes(UTF8_CHARSET); + output.writeShort(bytes.length); + output.write(bytes); + } + + public static String readString(DataInput input) throws IOException { + int size = input.readShort(); + byte[] bytes = new byte[size]; + input.readFully(bytes); + return new String(bytes, UTF8_CHARSET); + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/io/LineIterator.java b/dataforge-core/src/main/java/hep/dataforge/io/LineIterator.java new file mode 100644 index 00000000..837e4a4f --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/io/LineIterator.java @@ -0,0 +1,127 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.io; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.UnsupportedEncodingException; +import java.util.Iterator; + +/** + * Разбивает поток на Ñтроки и поÑледовательно их Ñчитывает. Строки, + * начинающиеÑÑ Ñ '*' и пуÑтые Ñтроки игнрорируютÑÑ. Пробелы в начале и конце + * Ñтроки игнорируютÑÑ + * + * @author Alexander Nozik, based on commons-io code + * @version $Id: $Id + */ +public class LineIterator implements Iterator, AutoCloseable { + + private String commentStr = "#"; + + private final BufferedReader reader; + private String cachedLine; + private boolean finished = false; + + public LineIterator(InputStream stream, String charset) throws UnsupportedEncodingException { + this(new InputStreamReader(stream, charset)); + } + + public LineIterator(InputStream stream) { + this(new InputStreamReader(stream)); + } + + public LineIterator(File file) throws FileNotFoundException { + this(new FileReader(file)); + } + + public LineIterator(Reader reader) { + this.reader = new BufferedReader(reader); + } + + public String getCommentStr() { + return commentStr; + } + + public void setCommentStr(String commentStr) { + this.commentStr = commentStr; + } + + /** + * {@inheritDoc} + * + * @return + */ + @Override + public boolean hasNext() { + if (cachedLine != null) { + return true; + } else if (finished) { + return false; + } else { + try { + while (reader.ready()) { + String line = reader.readLine(); + if (line == null) { + finished = true; + return false; + } else if (isValidLine(line.trim())) { + cachedLine = line.trim(); + return true; + } + } + } catch (IOException ex) { + return false; + } + } + return false; + } + + protected boolean isValidLine(String line) { + return !line.isEmpty() && !line.startsWith(commentStr); + } + + /** + * {@inheritDoc} + * + * @return + */ + @Override + public String next() { + if (!hasNext()) { + return null; + } + String currentLine = cachedLine; + cachedLine = null; + return currentLine; + } + + /** + * {@inheritDoc} + */ + @Override + public void close() { + finished = true; + cachedLine = null; + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/io/LogbackConfigurator.java b/dataforge-core/src/main/java/hep/dataforge/io/LogbackConfigurator.java new file mode 100644 index 00000000..9762cef6 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/io/LogbackConfigurator.java @@ -0,0 +1,45 @@ +package hep.dataforge.io; + +import ch.qos.logback.classic.BasicConfigurator; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.PatternLayout; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.ConsoleAppender; +import ch.qos.logback.core.encoder.LayoutWrappingEncoder; + +/** + * Created by darksnake on 14-Nov-16. + */ +public class LogbackConfigurator extends BasicConfigurator { + @Override + public void configure(LoggerContext lc) { + addInfo("Setting up default configuration."); + + ConsoleAppender ca = new ConsoleAppender<>(); +// OutputStreamAppender ca = new OutputStreamAppender<>(); +// ca.setOutputStream(System.out); +// ca.setOutputStream(Global.instance().io().out()); + ca.setContext(lc); + ca.setName("console"); + LayoutWrappingEncoder encoder = new LayoutWrappingEncoder<>(); + encoder.setContext(lc); + + + // same as + // PatternLayout layout = new PatternLayout(); + // layout.setPattern("%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"); + PatternLayout layout = new PatternLayout(); + layout.setPattern("%d{HH:mm:ss.SSS} %highlight(%-5level) %logger{36} - %msg%n"); + + layout.setContext(lc); + layout.start(); + encoder.setLayout(layout); + + ca.setEncoder(encoder); + ca.start(); + + Logger rootLogger = lc.getLogger(Logger.ROOT_LOGGER_NAME); + rootLogger.addAppender(ca); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/io/MetaStreamReader.java b/dataforge-core/src/main/java/hep/dataforge/io/MetaStreamReader.java new file mode 100644 index 00000000..d48a8b21 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/io/MetaStreamReader.java @@ -0,0 +1,87 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.io; + +import hep.dataforge.meta.MetaBuilder; +import kotlin.text.Charsets; +import org.jetbrains.annotations.NotNull; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.text.ParseException; + +import static java.nio.file.StandardOpenOption.READ; + +/** + * The reader of stream containing meta in some text or binary format. By + * default reader returns meta as-is without substitutions and includes so it + * does not need context to operate. + * + * @author Alexander Nozik + */ +public interface MetaStreamReader { + + /** + * read {@code length} bytes from InputStream and interpret it as + * MetaBuilder. If {@code length < 0} then parse input stream until end of + * annotation is found. + *

+ * The returned builder could be later transformed or + *

+ * + * @param stream a stream that should be read. + * @param length a number of bytes from stream that should be read. Any + * negative value . + * @return a {@link hep.dataforge.meta.Meta} object. + * @throws java.io.IOException if any. + * @throws java.text.ParseException if any. + */ + MetaBuilder read(@NotNull InputStream stream, long length) throws IOException, ParseException; + + default MetaBuilder read(InputStream stream) throws IOException, ParseException { + if(stream == null){ + throw new RuntimeException("Stream is null"); + } + return read(stream, -1); + } + + /** + * Read the Meta from file. The whole file is considered to be Meta file. + * + * @param file + * @return + */ + default MetaBuilder readFile(Path file) { + try (InputStream stream = Files.newInputStream(file, READ)) { + return read(stream, Files.size(file)); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + /** + * Read Meta from string + * + * @param string + * @return + * @throws IOException + * @throws ParseException + */ + default MetaBuilder readString(String string) throws IOException, ParseException { + byte[] bytes; + bytes = string.getBytes(Charsets.UTF_8); + ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + return read(bais, bytes.length); + } + + default MetaBuilder readBuffer(ByteBuffer buffer) throws IOException, ParseException { + return read(new ByteArrayInputStream(buffer.array()), buffer.limit()); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/io/MetaStreamWriter.java b/dataforge-core/src/main/java/hep/dataforge/io/MetaStreamWriter.java new file mode 100644 index 00000000..4cc093a2 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/io/MetaStreamWriter.java @@ -0,0 +1,44 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.io; + +import hep.dataforge.meta.Meta; +import org.jetbrains.annotations.NotNull; + +import java.io.*; +import java.nio.charset.Charset; + +/** + * The writer of meta to stream in some text or binary format + * + * @author Alexander Nozik + */ +public interface MetaStreamWriter { + + /** + * write Meta object to the giver OuputStream using given charset (if it is + * possible) + * + * @param stream + * @param meta charset is used + */ + void write(@NotNull OutputStream stream, @NotNull Meta meta) throws IOException; + + + default String writeString(Meta meta) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + write(baos, meta); + } catch (IOException e) { + throw new Error(e); + } + return new String(baos.toByteArray(), Charset.forName("UTF-8")); + } + + default void writeToFile(File file, Meta meta) throws IOException { + write(new FileOutputStream(file), meta); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/io/XMLMetaReader.java b/dataforge-core/src/main/java/hep/dataforge/io/XMLMetaReader.java new file mode 100644 index 00000000..aac7aec3 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/io/XMLMetaReader.java @@ -0,0 +1,139 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.io; + +import hep.dataforge.meta.MetaBuilder; +import hep.dataforge.values.LateParseValue; +import hep.dataforge.values.NamedValue; +import hep.dataforge.values.ValueFactory; +import kotlin.text.Charsets; +import org.jetbrains.annotations.NotNull; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; + +import static javax.xml.parsers.DocumentBuilderFactory.newInstance; + +/** + * A default reader for XML represented Meta + * + * @author Alexander Nozik + */ +public class XMLMetaReader implements MetaStreamReader { + @Override + public MetaBuilder read(@NotNull InputStream stream, long length) throws IOException, ParseException { + try { + DocumentBuilderFactory factory = newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + + InputSource source; + if (length < 0) { + source = new InputSource(new InputStreamReader(stream, Charsets.UTF_8.newDecoder())); + } else { + byte[] bytes = new byte[(int) length]; + stream.read(bytes); + source = new InputSource(new ByteArrayInputStream(bytes)); + } + + Element element = builder.parse(source).getDocumentElement(); + return buildNode(element); + } catch (SAXException | ParserConfigurationException ex) { + throw new RuntimeException(ex); + } + } + + private MetaBuilder buildNode(Element element) { + MetaBuilder res = new MetaBuilder(decodeName(element.getTagName())); + List values = getValues(element); + List elements = getElements(element); + + for (NamedValue value : values) { + res.putValue(decodeName(value.getName()), value.getAnonymous()); + } + + for (Element e : elements) { + //ОÑтавлÑем только те Ñлементы, в которых еÑÑ‚ÑŒ что-то кроме текÑта. + //Те, в которых только текÑÑ‚ уже поÑчитаны как Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¸Ñ + if (e.hasAttributes() || e.getElementsByTagName("*").getLength() > 0) { + res.putNode(buildNode(e)); + } + } + + //запиÑываем Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¸Ñ Ñ‚Ð¾Ð»ÑŒÐºÐ¾ еÑли нет наÑледников + if (!element.getTextContent().isEmpty() && (element.getElementsByTagName("*").getLength() == 0)) { + res.putValue(decodeName(element.getTagName()), element.getTextContent()); + } + //res.putContent(new AnnotatedData("xmlsource", element)); + + return res; + } + + /** + * Возвращает ÑпиÑок вÑех подÑлементов + * + * @param element + * @return + */ + private List getElements(Element element) { + List res = new ArrayList<>(); + NodeList nodes = element.getElementsByTagName("*"); + for (int i = 0; i < nodes.getLength(); i++) { + Element elNode = (Element) nodes.item(i); + if (elNode.getParentNode().equals(element)) { + res.add(elNode); + } + } + return res; + } + + private List getValues(Element element) { + List res = new ArrayList<>(); + NamedNodeMap attributes = element.getAttributes(); + for (int i = 0; i < attributes.getLength(); i++) { + Node node = attributes.item(i); + String name = node.getNodeName(); + res.add(new NamedValue(name, new LateParseValue(normalizeValue(node.getNodeValue())))); + } + + List elements = getElements(element); + for (Element elNode : elements) { + if (!(elNode.getElementsByTagName("*").getLength() > 0 || elNode.hasAttributes())) { + String name = elNode.getTagName(); + if (elNode.getTextContent().isEmpty()) { + res.add(new NamedValue(name, ValueFactory.of(Boolean.TRUE))); + } else { + res.add(new NamedValue(name, new LateParseValue(elNode.getTextContent()))); + } + + } + } + return res; + + } + + private String normalizeValue(String value) { + return value.replace("\\n", "\n"); + } + + private String decodeName(String str) { + return str.replaceFirst("^_(\\d)", "$1") + .replace("_at_", "@"); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/io/XMLMetaWriter.java b/dataforge-core/src/main/java/hep/dataforge/io/XMLMetaWriter.java new file mode 100644 index 00000000..ef377a74 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/io/XMLMetaWriter.java @@ -0,0 +1,117 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.io; + +import hep.dataforge.NamedKt; +import hep.dataforge.meta.Meta; +import hep.dataforge.meta.MetaNode; +import hep.dataforge.values.Value; +import org.jetbrains.annotations.NotNull; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.io.OutputStream; +import java.util.List; +import java.util.stream.Collectors; + +/** + * A writer for XML represented Meta + * + * @author Alexander Nozik + */ +public class XMLMetaWriter implements MetaStreamWriter { + + @Override + public void write(@NotNull OutputStream stream, @NotNull Meta meta) { + try { + + Document doc = getXMLDocument(meta); + TransformerFactory tf = TransformerFactory.newInstance(); + Transformer transformer = tf.newTransformer(); + + //PENDING add constructor customization of writer? + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); + transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); +// transformer.setOutputProperty(OutputKeys.METHOD, "text"); +// StringWriter writer = new StringWriter(); + transformer.transform(new DOMSource(doc), new StreamResult(stream)); + } catch (TransformerException ex) { + throw new RuntimeException(ex); + } + } + + private Document getXMLDocument(Meta meta) { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.newDocument(); + Element element = getXMLElement(meta, doc); + doc.appendChild(element); + return doc; + } catch (ParserConfigurationException ex) { + throw new RuntimeException(ex); + } + } + + private String encodeName(String str) { + return str.replaceFirst("^(\\d)", "_$1") + .replace("@", "_at_"); + } + + private Element getXMLElement(Meta meta, Document doc) { + String elementName; + if (NamedKt.isAnonymous(meta)) { + elementName = MetaNode.DEFAULT_META_NAME; + } else { + elementName = meta.getName(); + } + Element res = doc.createElement(encodeName(elementName)); + + + meta.getValueNames(true).forEach(valueName -> { + List valueList = meta.getValue(valueName).getList(); + if (valueList.size() == 1) { + String value = valueList.get(0).getString(); + if (value.startsWith("[")) { + value = "[" + value + "]"; + } + res.setAttribute(encodeName(valueName), value); + } else { + String val = valueList + .stream() + .map(Value::getString) + .collect(Collectors.joining(", ", "[", "]")); + res.setAttribute(encodeName(valueName), val); + } + }); + + meta.getNodeNames(true).forEach(nodeName -> { + List elementList = meta.getMetaList(nodeName); + if (elementList.size() == 1) { + Element el = getXMLElement(elementList.get(0), doc); + res.appendChild(el); + } else { + for (Meta element : elementList) { + Element el = getXMLElement(element, doc); + res.appendChild(el); + } + } + + }); + return res; + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/io/history/Chronicle.java b/dataforge-core/src/main/java/hep/dataforge/io/history/Chronicle.java new file mode 100644 index 00000000..07258240 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/io/history/Chronicle.java @@ -0,0 +1,126 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.io.history; + +import hep.dataforge.Named; +import hep.dataforge.exceptions.AnonymousNotAlowedException; +import hep.dataforge.utils.ReferenceRegistry; +import org.jetbrains.annotations.Nullable; +import org.slf4j.helpers.MessageFormatter; + +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.function.Consumer; +import java.util.stream.Stream; + +/** + * A in-memory log that can store a finite number of entries. The difference between logger events and log is that log + * is usually part the part of the analysis result an should be preserved. + * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class Chronicle implements History, Named { + public static final String CHRONICLE_TARGET = "log"; + + private final String name; + private final ReferenceRegistry> listeners = new ReferenceRegistry<>(); + private ConcurrentLinkedQueue entries = new ConcurrentLinkedQueue<>(); + private History parent; + + public Chronicle(String name, @Nullable History parent) { + if (name == null || name.isEmpty()) { + throw new AnonymousNotAlowedException(); + } + this.name = name; + this.parent = parent; + } + + protected int getMaxLogSize() { + return 1000; + } + + @Override + public void report(Record entry) { + entries.add(entry); + if (entries.size() >= getMaxLogSize()) { + entries.poll();// Ограничение на размер лога +// getLogger().warn("Log at maximum capacity!"); + } + listeners.forEach((Consumer listener) -> { + listener.accept(entry); + }); + + if (parent != null) { + Record newEntry = pushTrace(entry, getName()); + parent.report(newEntry); + } + } + + /** + * Add a weak report listener to this report + * + * @param logListener + */ + public void addListener(Consumer logListener) { + this.listeners.add(logListener, true); + } + + private Record pushTrace(Record entry, String toTrace) { + return new Record(entry, toTrace); + } + + public void clear() { + entries.clear(); + } + + public History getParent() { + return parent; + } + + public Stream getEntries(){ + return entries.stream(); + } + +// public void print(PrintWriter out) { +// out.println(); +// entries.forEach((entry) -> { +// out.println(entry.toString()); +// }); +// out.println(); +// out.flush(); +// } + + public Chronicle getChronicle() { + return this; + } + + @Override + public String getName() { + return name; + } + + @Override + public void report(String str, Object... parameters) { + Record entry = new Record(MessageFormatter.arrayFormat(str, parameters).getMessage()); + Chronicle.this.report(entry); + } + + @Override + public void reportError(String str, Object... parameters) { + Chronicle.this.report(new Record("[ERROR] " + MessageFormatter.arrayFormat(str, parameters).getMessage())); + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/io/history/History.java b/dataforge-core/src/main/java/hep/dataforge/io/history/History.java new file mode 100644 index 00000000..58c5553f --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/io/history/History.java @@ -0,0 +1,43 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.io.history; + + +/** + * An object that could handle and store its own report. A purpose of DataForge report + is different from standard logging because analysis report is part of the + result. Therfore logable objects should be used only when one needs to sore + resulting report. + * + * @author Alexander Nozik + */ +public interface History { + + Chronicle getChronicle(); + + default void report(String str, Object... parameters){ + getChronicle().report(str, parameters); + } + + default void report(Record entry){ + getChronicle().report(entry); + } + + default void reportError(String str, Object... parameters){ + getChronicle().reportError(str, parameters); + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/io/history/Record.java b/dataforge-core/src/main/java/hep/dataforge/io/history/Record.java new file mode 100644 index 00000000..cb55f192 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/io/history/Record.java @@ -0,0 +1,107 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.io.history; + +import hep.dataforge.meta.Meta; +import hep.dataforge.meta.MetaBuilder; +import hep.dataforge.meta.MetaMorph; +import hep.dataforge.utils.DateTimeUtils; +import org.jetbrains.annotations.NotNull; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static java.lang.String.format; + +/** + *

+ LogEntry class.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class Record implements MetaMorph { + + private static final DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss.SSS").withZone(ZoneId.systemDefault()); + private final List sourceTrace = new ArrayList<>(); + private final String message; + private final Instant time; + + public Record(Meta meta) { + this.time = meta.getValue("timestame").getTime(); + this.message = meta.getString("message"); + this.sourceTrace.addAll(Arrays.asList(meta.getStringArray("trace"))); + } + + public Record(Record entry, String traceAdd) { + this.sourceTrace.addAll(entry.sourceTrace); + if (traceAdd != null && !traceAdd.isEmpty()) { + this.sourceTrace.add(0, traceAdd); + } + this.message = entry.message; + this.time = entry.time; + } + + public Record(Instant time, String message) { + this.time = time; + this.message = message; + } + + public Record(String message) { + this.time = DateTimeUtils.now(); + this.message = message; + } + + public String getMessage() { + return message; + } + + public Instant getTime() { + return time; + } + + public String getTraceString(){ + return String.join(".", sourceTrace); + } + + /** + * {@inheritDoc} + * + * @return + */ + @Override + public String toString() { + String traceStr = getTraceString(); + if (traceStr.isEmpty()) { + return format("(%s) %s", dateFormat.format(time), message); + } else { + return format("(%s) %s: %s", dateFormat.format(time), traceStr, message); + } + } + + @NotNull + @Override + public Meta toMeta() { + return new MetaBuilder("record") + .setValue("timestamp", time) + .setValue("message", message) + .setValue("trace", sourceTrace); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/meta/ConfigChangeListener.java b/dataforge-core/src/main/java/hep/dataforge/meta/ConfigChangeListener.java new file mode 100644 index 00000000..ecef6df0 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/meta/ConfigChangeListener.java @@ -0,0 +1,57 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.meta; + +import hep.dataforge.names.Name; +import hep.dataforge.values.Value; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + * An observer that could be attached to annotation and listen to any change + * within it + * + * @author Alexander Nozik + */ +public interface ConfigChangeListener { + + /** + * notify that value item is changed + * + * @param name the full path of value that has been changed + * @param oldItem the item of values before change. If null, than value + * has not been existing before change + * @param newItem the item of values after change. If null, then value has + * been removed + */ + void notifyValueChanged(@NotNull Name name, @Nullable Value oldItem, @Nullable Value newItem); + + /** + * notify that element item is changed + * + * @param name the full path of element that has been changed + * @param oldItem the item of elements before change. If null, than item + * has not been existing before change + * @param newItem the item of elements after change. If null, then item has + * been removed + */ + default void notifyNodeChanged(@NotNull Name name, @NotNull List oldItem, @NotNull List newItem) { + //do nothing by default + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/meta/Configurable.java b/dataforge-core/src/main/java/hep/dataforge/meta/Configurable.java new file mode 100644 index 00000000..22b3ebba --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/meta/Configurable.java @@ -0,0 +1,46 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.meta; + +/** + * An object with mutable configuration + * + * @author Alexander Nozik + */ +public interface Configurable { + + /** + * get editable configuration + * + * @return + */ + Configuration getConfig(); + + default Configurable configure(Meta config) { + getConfig().update(config); + return this; + } + + default Configurable configureValue(String key, Object Value) { + this.getConfig().setValue(key, Value); + return this; + } + + default Configurable configureNode(String key, Meta... node) { + this.getConfig().setNode(key, node); + return this; + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/meta/ConfigurableMergeRule.java b/dataforge-core/src/main/java/hep/dataforge/meta/ConfigurableMergeRule.java new file mode 100644 index 00000000..bc00cd6a --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/meta/ConfigurableMergeRule.java @@ -0,0 +1,100 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.meta; + +import hep.dataforge.values.Value; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +/** + * ÐаÑтраиваемое правило Ð¾Ð±ÑŠÐµÐ´Ð¸Ð½ÐµÐ½Ð¸Ñ Ð°Ð½Ð½Ð¾Ñ‚Ð°Ñ†Ð¸Ð¹ + * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class ConfigurableMergeRule extends CustomMergeRule { + + /** + *

Constructor for ConfigurableMergeRule.

+ * + * @param valueToJoin a {@link java.util.Set} object. + * @param elementsToJoin a {@link java.util.Set} object. + */ + public ConfigurableMergeRule(Collection valueToJoin, Collection elementsToJoin) { + setValueMerger(valueToJoin); + setMetaMerger(elementsToJoin); + } + + /** + *

Constructor for ConfigurableMergeRule.

+ * + * @param toJoin a {@link java.util.Set} object. + */ + public ConfigurableMergeRule(Collection toJoin) { + this(toJoin, toJoin); + } + + /** + *

Constructor for ConfigurableMergeRule.

+ * + * @param toJoin a {@link java.lang.String} object. + */ + public ConfigurableMergeRule(String... toJoin) { + this(Arrays.asList(toJoin)); + } + + private void setValueMerger(Collection valueToJoin) { + this.valueMerger = (String name, List first, List second) -> { + //ОбъединÑем, еÑли Ñлемент в ÑпиÑке на объединение + if (valueToJoin.contains(name)) { + List list = new ArrayList<>(); + list.addAll(first); + list.addAll(second); + return list; + } else { + return first; + } + }; + } + + private void setMetaMerger(Collection metaToJoin) { + this.elementMerger = (String name, List first, List second) -> { + //ОбъединÑем, еÑли Ñлемент в ÑпиÑке на объединение + if (metaToJoin.contains(name)) { + List list = new ArrayList<>(); + list.addAll(first); + list.addAll(second); + return list; + } else { + // еÑли не в ÑпиÑке, то заменÑем + return first; + } + }; + } + + /** + * {@inheritDoc} + */ + @Override + protected String mergeName(String mainName, String secondName) { + return mainName; + } + + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/meta/Configuration.java b/dataforge-core/src/main/java/hep/dataforge/meta/Configuration.java new file mode 100644 index 00000000..6ef92de1 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/meta/Configuration.java @@ -0,0 +1,205 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.meta; + +import hep.dataforge.names.Name; +import hep.dataforge.utils.ReferenceRegistry; +import hep.dataforge.values.Value; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * A mutable annotation that exposes MuttableAnnotationNode edit methods and + * adds automatically inherited observers. + * + * @author Alexander Nozik + */ +public class Configuration extends MutableMetaNode { + + /** + * The meta node or value descriptor tag to mark an element non + * configurable. It could be set only once. + */ + public static final String FINAL_TAG = "final"; + + protected final ReferenceRegistry observers = new ReferenceRegistry<>(); + + /** + * Create empty root configuration + * + * @param name + */ + public Configuration(String name) { + super(name); + } + + public Configuration() { + super(); + } + + /** + * Create a root configuration populated by given meta + * + * @param meta + */ + public Configuration(Meta meta) { + super(meta.getName()); + meta.getValueNames(true).forEach(valueName -> { + setValueItem(valueName, meta.getValue(valueName)); + }); + + meta.getNodeNames(true).forEach(nodeName -> { + List item = meta.getMetaList(nodeName).stream() + .map(Configuration::new) + .collect(Collectors.toList()); + setNodeItem(nodeName, new ArrayList<>(item)); + + }); + } + + /** + * Notify all observers that element is changed + * + * @param name + * @param oldItem + * @param newItem + */ + @Override + protected void notifyNodeChanged(Name name, @NotNull List oldItem, @NotNull List newItem) { + observers.forEach((ConfigChangeListener obs) -> obs.notifyNodeChanged(name, oldItem, newItem)); + super.notifyNodeChanged(name, oldItem, newItem); + } + + /** + * Notify all observers that value is changed + * + * @param name + * @param oldItem + * @param newItem + */ + @Override + protected void notifyValueChanged(Name name, Value oldItem, Value newItem) { + observers.forEach((ConfigChangeListener obs) -> obs.notifyValueChanged(name, oldItem, newItem)); + super.notifyValueChanged(name, oldItem, newItem); + } + + /** + * Add new observer for this configuration + * + * @param strongReference if true, then configuration prevents observer from + * being recycled by GC + * @param observer + */ + public void addListener(boolean strongReference, ConfigChangeListener observer) { + this.observers.add(observer, strongReference); + } + + /** + * addObserver(observer, true) + * + * @param observer + */ + public void addListener(ConfigChangeListener observer) { + addListener(true, observer); + } + + //PENDING add value observers inheriting value class by wrapper + + /** + * Remove an observer from this configuration + * + * @param observer + */ + public void removeListener(ConfigChangeListener observer) { + this.observers.remove(observer); + } + + /** + * update this configuration replacing all old values and nodes + * + * @param meta + */ + public void update(Meta meta, boolean notify) { + if (meta != null) { + meta.getValueNames(true).forEach((valueName) -> { + setValue(valueName, meta.getValue(valueName), notify); + }); + + meta.getNodeNames(true).forEach((elementName) -> { + setNode(elementName, + meta.getMetaList(elementName).stream() + .map(Configuration::new) + .collect(Collectors.toList()), + notify + ); + }); + } + } + + public void update(Meta meta) { + update(meta, true); + } + + @Override + public Configuration self() { + return this; + } + + @Override + public Configuration putNode(Meta an) { + super.putNode(new Configuration(an)); + return self(); + } + + /** + * Return existing node if it exists, otherwise build and attach empty child + * node + * + * @param name + * @return + */ + public Configuration requestNode(String name) { + return optMeta(name).map(it -> (Configuration) it).orElseGet(() -> { + Configuration child = createChildNode(name); + super.attachNode(child); + return child; + }); + } + + @Override + protected Configuration createChildNode(String name) { + return new Configuration(name); + } + + @Override + protected Configuration cloneNode(Meta node) { + return new Configuration(node); + } + + @Override + @Nullable + public Configuration getParent() { + return super.getParent(); + } + + public Configuration rename(String newName) { + return super.setName(newName); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/meta/CustomMergeRule.java b/dataforge-core/src/main/java/hep/dataforge/meta/CustomMergeRule.java new file mode 100644 index 00000000..97cacb48 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/meta/CustomMergeRule.java @@ -0,0 +1,66 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.meta; + +import hep.dataforge.names.Name; +import hep.dataforge.values.Value; +import hep.dataforge.values.ValueFactory; + +import java.util.List; + +/** + *

CustomMergeRule class.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class CustomMergeRule extends MergeRule { + + protected ListMergeRule valueMerger; + protected ListMergeRule elementMerger; + + /** + *

Constructor for CustomMergeRule.

+ * + * @param itemMerger a {@link hep.dataforge.meta.ListMergeRule} object. + * @param elementMerger a {@link hep.dataforge.meta.ListMergeRule} object. + */ + public CustomMergeRule(ListMergeRule itemMerger, ListMergeRule elementMerger) { + this.valueMerger = itemMerger; + this.elementMerger = elementMerger; + } + + protected CustomMergeRule() { + } + + /** + * {@inheritDoc} + */ + @Override + protected String mergeName(String mainName, String secondName) { + return mainName; + } + + @Override + protected Value mergeValues(Name valueName, Value first, Value second) { + return ValueFactory.of(valueMerger.merge(valueName.toString(), first.getList(), second.getList())); + } + + @Override + protected List mergeNodes(Name nodeName, List mainNodes, List secondaryNodes) { + return elementMerger.merge(nodeName.toString(), mainNodes, secondaryNodes); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/meta/DelegateConfigChangeListener.java b/dataforge-core/src/main/java/hep/dataforge/meta/DelegateConfigChangeListener.java new file mode 100644 index 00000000..532a05de --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/meta/DelegateConfigChangeListener.java @@ -0,0 +1,72 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.meta; + +import hep.dataforge.names.Name; +import hep.dataforge.values.Value; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Objects; + +/** + * That class that delegates methods to other observer giving fixed name prefix. + * Is used to make child elements of annotation observable. + * + * @author Alexander Nozik + */ +public class DelegateConfigChangeListener implements ConfigChangeListener { + + private final ConfigChangeListener observer; + private final Name prefix; + + public DelegateConfigChangeListener(ConfigChangeListener observer, Name prefix) { + this.observer = observer; + this.prefix = prefix; + } + + @Override + public void notifyValueChanged(@NotNull Name name, Value oldValues, Value newItems) { + observer.notifyValueChanged(prefix.plus(name), oldValues, newItems); + } + + @Override + public void notifyNodeChanged(@NotNull Name name, @NotNull List oldValues, @NotNull List newItems) { + observer.notifyNodeChanged(prefix.plus(name), oldValues, newItems); + } + + @Override + public int hashCode() { + int hash = 7; + hash = 47 * hash + Objects.hashCode(this.observer); + hash = 47 * hash + Objects.hashCode(this.prefix); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final DelegateConfigChangeListener other = (DelegateConfigChangeListener) obj; + return Objects.equals(this.observer, other.observer) && Objects.equals(this.prefix, other.prefix); + } + + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/meta/JoinRule.java b/dataforge-core/src/main/java/hep/dataforge/meta/JoinRule.java new file mode 100644 index 00000000..2e0b9822 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/meta/JoinRule.java @@ -0,0 +1,57 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.meta; + +import hep.dataforge.names.Name; +import hep.dataforge.values.Value; +import hep.dataforge.values.ValueFactory; + +import java.util.ArrayList; +import java.util.List; + +/** + *

JoinRule class.

+ * + * @author darksnake + * @version $Id: $Id + */ +public class JoinRule extends MergeRule { + + /** + * {@inheritDoc} + */ + @Override + protected String mergeName(String mainName, String secondName) { + return mainName; + } + + @Override + protected Value mergeValues(Name valueName, Value first, Value second) { + List list = new ArrayList<>(); + list.addAll(first.getList()); + list.addAll(second.getList()); + return ValueFactory.of(list); + } + + @Override + protected List mergeNodes(Name nodeName, List mainNodes, List secondaryNodes) { + List list = new ArrayList<>(); + list.addAll(mainNodes); + list.addAll(secondaryNodes); + return list; + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/meta/ListMergeRule.java b/dataforge-core/src/main/java/hep/dataforge/meta/ListMergeRule.java new file mode 100644 index 00000000..94d883f6 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/meta/ListMergeRule.java @@ -0,0 +1,30 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.meta; + +import java.util.List; + +/** + * The rule to join two lists of something. Mostly used to join meta nodes. + * + * @author Alexander Nozik + * @param + * @version $Id: $Id + */ +public interface ListMergeRule { + + List merge(String name, List first, List second); +} diff --git a/dataforge-core/src/main/java/hep/dataforge/meta/MergeRule.java b/dataforge-core/src/main/java/hep/dataforge/meta/MergeRule.java new file mode 100644 index 00000000..8f17a1c8 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/meta/MergeRule.java @@ -0,0 +1,193 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.meta; + +import hep.dataforge.names.Name; +import hep.dataforge.values.Value; +import hep.dataforge.values.ValueFactory; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collector; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Правило Ð¾Ð±ÑŠÐµÐ´Ð¸Ð½ÐµÐ½Ð¸Ñ Ð´Ð²ÑƒÑ… аннотаций + * + * @author Alexander Nozik + * @version $Id: $Id + */ +public abstract class MergeRule implements Collector { + + /** + * Правило Ð¾Ð±ÑŠÐµÐ´Ð¸Ð½ÐµÐ½Ð¸Ñ Ð¿Ð¾-умолчанию. ПодразумеваетÑÑ Ð¿Ñ€Ð¾ÑÑ‚Ð°Ñ Ð·Ð°Ð¼ÐµÐ½Ð° вÑех + * Ñовподающих Ñлементов. + * + * @return + */ + public static MergeRule replace() { + return new ReplaceRule(); + } + + public static MergeRule join() { + return new JoinRule(); + } + + /** + * Возвращает правило Ð¾Ð±ÑŠÐµÐ´Ð¸Ð½ÐµÐ½Ð¸Ñ Ð² котором Ñлементы, входÑщие в ÑпиÑок + * объединÑÑŽÑ‚ÑÑ, а оÑтальные заменÑÑŽÑ‚ÑÑ + * + * @param toJoin + * @return + */ + public static MergeRule custom(String... toJoin) { + return new ConfigurableMergeRule(toJoin); + } + + /** + * ВыполнÑет объединение Ñ Ð·Ð°Ð¼ÐµÐ½Ð¾Ð¹ вÑех Ñовподающих Ñлементов + * + * @param main + * @param second + * @return + */ + public static MetaBuilder replace(Meta main, Meta second) { + return replace().merge(main, second); + } + + /** + * ВыполнÑет объединение Ñ Ð¾Ð±ÑŠÐµÐ´Ð¸Ð½ÐµÐ½Ð¸ÐµÐ¼ вÑех ÑпиÑков + * + * @param main a {@link hep.dataforge.meta.Meta} object. + * @param second a {@link hep.dataforge.meta.Meta} object. + * @return a {@link hep.dataforge.meta.Meta} object. + */ + public static Meta join(Meta main, Meta second) { + return new JoinRule().merge(main, second); + } + + /** + * Метод, объединÑющий две аннотации. ПорÑдок имеет значение. ÐŸÐµÑ€Ð²Ð°Ñ + * Ð°Ð½Ð½Ð¾Ñ‚Ð°Ñ†Ð¸Ñ ÑвлÑетÑÑ Ð¾Ñновной, Ð²Ñ‚Ð¾Ñ€Ð°Ñ Ð·Ð°Ð¿Ð°Ñной + * + * @param main + * @param second + * @return + */ + public MetaBuilder merge(Meta main, Meta second) { + return mergeInPlace(main, new MetaBuilder(second)); + } + + /** + * Apply changes from main Meta to meta builder in place + * + * @param main + * @param builder + * @return + */ + public MetaBuilder mergeInPlace(Meta main, final MetaBuilder builder) { + //MetaBuilder builder = new MetaBuilder(mergeName(main.getName(), second.getName())); + builder.rename(mergeName(main.getName(), builder.getName())); + + // Overriding values + Stream.concat(main.getValueNames(), builder.getValueNames()).collect(Collectors.toSet()) + .forEach(valueName -> { + writeValue( + builder, + valueName, + mergeValues( + Name.Companion.join(builder.getFullName(), Name.Companion.of(valueName)), + main.optValue(valueName).orElse(ValueFactory.NULL), + builder.optValue(valueName).orElse(ValueFactory.NULL) + ) + ); + }); + + // Overriding nodes + Stream.concat(main.getNodeNames(), builder.getNodeNames()).collect(Collectors.toSet()) + .forEach(nodeName -> { + List mainNodes = main.getMetaList(nodeName); + List secondNodes = builder.getMetaList(nodeName); + if (mainNodes.size() == 1 && secondNodes.size() == 1) { + writeNode(builder, nodeName, Collections.singletonList(merge(mainNodes.get(0), secondNodes.get(0)))); + } else { + List item = mergeNodes(Name.Companion.join(builder.getFullName(), + Name.Companion.of(nodeName)), mainNodes, secondNodes); + writeNode(builder, nodeName, item); + } + }); + + return builder; + } + + protected abstract String mergeName(String mainName, String secondName); + + /** + * @param valueName full name of the value relative to root + * @param first + * @param second + * @return + */ + protected abstract Value mergeValues(Name valueName, Value first, Value second); + + /** + * @param nodeName full name of the node relative to root + * @param mainNodes + * @param secondaryNodes + * @return + */ + protected abstract List mergeNodes(Name nodeName, List mainNodes, List secondaryNodes); + + protected void writeValue(MetaBuilder builder, String name, Value item) { + builder.setValue(name, item); + } + + protected void writeNode(MetaBuilder builder, String name, List item) { + builder.setNode(name, item); + } + + @Override + public Supplier supplier() { + return () -> new MetaBuilder(""); + } + + //TODO test it via Laminate collector + @Override + public BiConsumer accumulator() { + return ((builder, meta) -> mergeInPlace(meta, builder)); + } + + @Override + public BinaryOperator combiner() { + return this::merge; + } + + @Override + public Function finisher() { + return MetaBuilder::build; + } + + @Override + public Set characteristics() { + return Collections.emptySet(); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/meta/Meta.java b/dataforge-core/src/main/java/hep/dataforge/meta/Meta.java new file mode 100644 index 00000000..9d67d5fb --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/meta/Meta.java @@ -0,0 +1,250 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.meta; + +import hep.dataforge.Named; +import hep.dataforge.NamedKt; +import hep.dataforge.exceptions.AnonymousNotAlowedException; +import hep.dataforge.io.XMLMetaWriter; +import hep.dataforge.providers.Provider; +import hep.dataforge.providers.Provides; +import hep.dataforge.providers.ProvidesNames; +import hep.dataforge.values.Value; +import hep.dataforge.values.ValueProvider; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * The main building block of the DataForge. + *

+ * TODO documentation here! + *

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +@MorphTarget(target = SealedNode.class) +public abstract class Meta implements Provider, Named, ValueProvider, MetaMorph, MetaProvider { + + private static final Meta EMPTY = new EmptyMeta(); + + /** + * Build an empty annotation with given name FIXME make a separate simple + * class for empty annotation for performance + * + * @param name a {@link java.lang.String} object. + * @return a {@link hep.dataforge.meta.Meta} object. + */ + public static Meta buildEmpty(String name) { + return new MetaBuilder(name).build(); + } + + /** + * Empty anonymous meta + * + * @return + */ + public static Meta empty() { + return EMPTY; + } + + /** + * Return modifiable {@link MetaBuilder} witch copies data from this meta. Initial meta not changed. + * + * @return a {@link hep.dataforge.meta.MetaBuilder} object. + */ + public MetaBuilder getBuilder() { + return new MetaBuilder(this); + } + + /** + * Get guaranteed unmodifiable copy of this meta + * @return + */ + public SealedNode getSealed(){ + return new SealedNode(this); + } + + /** + * Return the meta node with given name + * + * @param path + * @return + */ + public abstract List getMetaList(String path); + + /** + * Return index of given meta node inside appropriate meta list if it present. + * Otherwise return -1. + * + * @param node + * @return + */ + public int indexOf(Meta node) { + if (NamedKt.isAnonymous(node)) { + throw new AnonymousNotAlowedException("Anonimous nodes are not allowed in 'indexOf'"); + } + List list = getMetaList(node.getName()); + return list.indexOf(node); + } + + @Provides(META_TARGET) + public Optional optMeta(String path) { + return getMetaList(path).stream().findFirst().map(it -> it); + } + + public abstract boolean isEmpty(); + + /** + * List value names of direct descendants. Excludes hidden values + * + * @return a {@link java.util.Collection} object. + */ + @ProvidesNames(VALUE_TARGET) + public final Stream getValueNames() { + return getValueNames(false); + } + + public abstract Stream getValueNames(boolean includeHidden); + + /** + * List node names of direct descendants. Excludes hidden nodes + * + * @return a {@link java.util.Collection} object. + */ + @ProvidesNames(META_TARGET) + public final Stream getNodeNames() { + return getNodeNames(false); + } + + public abstract Stream getNodeNames(boolean includeHidden); + + + /** + * Return a child node with given name or empty node if child node not found + * + * @param path + * @return + */ + public final Meta getMetaOrEmpty(String path) { + return getMeta(path, Meta.empty()); + } + + @Override + public boolean equals(Object obj) { + if (super.equals(obj)) { + return true; + } else if (obj instanceof Meta) { + Meta other = (Meta) obj; + return Objects.equals(getName(), other.getName()) && equalsIgnoreName(other); + } else { + return false; + } + } + + /** + * Check if two annotations are equal ignoring their names. Names of child + * elements are not ignored + * + * @param other + * @return + */ + public boolean equalsIgnoreName(Meta other) { + boolean valuesEqual = getValueNames(true) + .allMatch(valueName -> other.hasValue(valueName) && getValue(valueName).equals(other.getValue(valueName))); + + boolean nodesEqual = getNodeNames(true) + .allMatch(nodeName -> other.hasMeta(nodeName) && getMetaList(nodeName).equals(other.getMetaList(nodeName))); + + return valuesEqual && nodesEqual; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 59 * hash + Objects.hashCode(getName()); + for (String valueName : getValueNames(true).collect(Collectors.toList())) { + hash = 59 * hash + Objects.hashCode(getValue(valueName)); + } + for (String elementName : getNodeNames(true).collect(Collectors.toList())) { + hash = 59 * hash + Objects.hashCode(getMetaList(elementName)); + } + return hash; + } + + @Override + public String toString() { + return new XMLMetaWriter().writeString(this); + } + + @Override + public String getDefaultTarget() { + return META_TARGET; + } + + @Override + public String getDefaultChainTarget() { + return VALUE_TARGET; + } + + @NotNull + @Override + public Meta toMeta() { + return this; + } + + + private static class EmptyMeta extends Meta { + + @Override + public List getMetaList(String path) { + return Collections.emptyList(); + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public Stream getValueNames(boolean includeHidden) { + return Stream.empty(); + } + + @Override + public Stream getNodeNames(boolean includeHidden) { + return Stream.empty(); + } + + @NotNull + @Override + public String getName() { + return ""; + } + + @Override + public Optional optValue(@NotNull String path) { + return Optional.empty(); + } + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/meta/MetaBuilder.java b/dataforge-core/src/main/java/hep/dataforge/meta/MetaBuilder.java new file mode 100644 index 00000000..7d85af31 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/meta/MetaBuilder.java @@ -0,0 +1,258 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.meta; + +import hep.dataforge.names.Name; +import hep.dataforge.utils.GenericBuilder; +import hep.dataforge.values.Value; +import hep.dataforge.values.ValueProvider; + +import java.util.*; +import java.util.function.BiFunction; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; + +/** + * A convenient builder to construct immutable or mutable annotations. or + * configurations. All passed annotations are recreated as AnnotationBuilders, + * "forgetting" previous parents and listeners if any. + * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class MetaBuilder extends MutableMetaNode implements GenericBuilder { + + public MetaBuilder() { + super(); + } + + // private ValueProvider valueContext; + public MetaBuilder(String name) { + super(name); + } + + + /** + * A deep copy constructor + * + * @param meta + */ + public MetaBuilder(Meta meta) { + super(meta.getName()); + meta.getValueNames(true).forEach((valueName) -> { + setValueItem(valueName, meta.getValue(valueName)); + }); + + meta.getNodeNames(true).forEach((elementName) -> { + List item = meta.getMetaList(elementName).stream() + .map(MetaBuilder::new) + .collect(Collectors.toList()); + setNodeItem(elementName, new ArrayList<>(item)); + }); + } + + /** + * return an immutable meta based on this builder + * + * @return a {@link hep.dataforge.meta.Meta} object. + */ + @Override + public Meta build() { + return new SealedNode(this); + } + + public MetaBuilder rename(String newName) { + if (Objects.equals(this.getName(), newName)) { + return this; + } else { + return super.setName(newName); + } + } + + @Override + public MetaBuilder getParent() { + return super.getParent(); + } + + public MetaBuilder setNode(String name, Collection elements) { + if (elements == null || elements.isEmpty()) { + super.removeNode(name); + } else { + super.setNode(name); + for (Meta element : elements) { + MetaBuilder newElement = new MetaBuilder(element); + if (!name.equals(newElement.getName())) { + newElement.rename(name); + } + super.putNode(newElement); + } + } + return self(); + } + + public MetaBuilder updateNode(String name, Meta element) { + return updateNode(name, MergeRule.replace(), element); + } + + /** + * Update an element or element item using given merge rule + * + * @param name + * @param rule + * @param elements + * @return + */ + public MetaBuilder updateNode(String name, MergeRule rule, Meta... elements) { + if (!hasMeta(name)) { + MetaBuilder.this.setNode(name, elements); + } else { + Name n = Name.Companion.of(name); + if (n.getLength() == 1) { + optChildNodeItem(name).ifPresent(list -> { + if (list.size() != elements.length) { + throw new RuntimeException("Can't update element item with an item of different size"); + } else { + MetaBuilder[] newList = new MetaBuilder[list.size()]; + for (int i = 0; i < list.size(); i++) { + newList[i] = rule.merge(elements[i], list.get(i)).rename(name); + } + super.setNode(name, newList); + } + }); + } else { + getMeta(n.cutLast().toString()).updateNode(n.getLast().toString(), rule, elements); + } + } + + + return this; + } + + /** + * Update this annotation with new Annotation + * + * @param annotation a {@link hep.dataforge.meta.MetaBuilder} object. + * @param valueMerger a {@link hep.dataforge.meta.ListMergeRule} object. + * @param elementMerger a {@link hep.dataforge.meta.ListMergeRule} object. + * @return a {@link hep.dataforge.meta.MetaBuilder} object. + */ + public MetaBuilder update(Meta annotation, + ListMergeRule valueMerger, + ListMergeRule elementMerger) { + return new CustomMergeRule(valueMerger, elementMerger).mergeInPlace(annotation, this); + } + + public MetaBuilder update(Meta meta) { + MergeRule rule; + switch (meta.getString("@mergeRule", "replace")) { + case "join": + rule = new JoinRule(); + break; + default: + rule = new ReplaceRule(); + } + return rule.mergeInPlace(meta, this); + } + + /** + * Update values (replacing existing ones) from map + * + * @param values + * @return + */ + public MetaBuilder update(Map values) { + values.forEach(this::setValue); + return self(); + } + + @Override + public MetaBuilder self() { + return this; + } + + /** + * Create an empty child node + * + * @param name + * @return + */ + @Override + protected MetaBuilder createChildNode(String name) { + return new MetaBuilder(name); + } + + @Override + protected MetaBuilder cloneNode(Meta node) { + return new MetaBuilder(node); + } + + /** + * Attach node without cloning it first to this one and change its parent. + * This is much faster then set or put operation but all external + * changes to the attached node will reflect on this one. + * + * @param node + */ + @Override + public void attachNode(MetaBuilder node) { + super.attachNode(node); + } + + /** + * Attach a list of nodes, changing each node's parent to this node + * + * @param name + * @param nodes + */ + @Override + public void attachNodeItem(String name, List nodes) { + super.attachNodeItem(name, nodes); + } + + /** + * Recursively apply node and value transformation to node. If node + * transformation creates new node, then new node is returned. + *

+ * The order of transformation is the following: + *

+ *
    + *
  • Parent node transformation
  • + *
  • Parent node values transformation (using only values after node transformation is applied)
  • + *
  • Children nodes transformation (using only nodes after parent node transformation is applied)
  • + *
+ * + * @param nodeTransform + * @return + */ + public MetaBuilder transform(final UnaryOperator nodeTransform, final BiFunction valueTransform) { + MetaBuilder res = nodeTransform.apply(this); + res.values.replaceAll(valueTransform); + res.nodes.values().forEach((item) -> { + item.replaceAll((MetaBuilder t) -> t.transform(nodeTransform, valueTransform)); + }); + return res; + } + + /** + * Make a transformation substituting values in place using substitution pattern and given valueProviders + * + * @return + */ + public MetaBuilder substituteValues(ValueProvider... providers) { + return transform(UnaryOperator.identity(), (String key, Value val) -> MetaUtils.transformValue(val, providers)); + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/meta/MetaNode.java b/dataforge-core/src/main/java/hep/dataforge/meta/MetaNode.java new file mode 100644 index 00000000..a3ed612d --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/meta/MetaNode.java @@ -0,0 +1,251 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.meta; + +import hep.dataforge.exceptions.NameNotFoundException; +import hep.dataforge.names.Name; +import hep.dataforge.providers.Provides; +import hep.dataforge.values.Value; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * An immutable representation of annotation node. Descendants could be mutable + *

+ * TODO Documentation! + *

+ * + * @author Alexander Nozik + */ +@MorphTarget(target = SealedNode.class) +public abstract class MetaNode extends Meta implements MetaMorph { + public static final String DEFAULT_META_NAME = "meta"; + + private static final long serialVersionUID = 1L; + + protected String name; + protected final Map> nodes = new LinkedHashMap<>(); + protected final Map values = new LinkedHashMap<>(); + + + protected MetaNode() { + this(DEFAULT_META_NAME); + } + + protected MetaNode(String name) { + if(name == null){ + this.name = ""; + } else { + this.name = name; + } + } + + protected MetaNode(Meta meta){ + this.name = meta.getName(); + + meta.getValueNames(true).forEach(valueName -> { + this.values.put(valueName, meta.getValue(valueName)); + }); + + meta.getNodeNames(true).forEach(nodeName -> { + this.nodes.put(nodeName, meta.getMetaList(nodeName).stream().map(this::cloneNode).collect(Collectors.toList())); + }); + } + + /** + * get the node corresponding to the first token of given path + * + * @param path + * @return + */ + protected T getHead(Name path) { + return optHead(path) + .orElseThrow(() -> new NameNotFoundException(path.toString())); + } + + protected Optional optHead(Name path) { + Name head = path.getFirst(); + return optChildNodeItem(head.entry()) + .map(child -> MetaUtils.query(child, head.getQuery()).get(0)); + } + + @Provides(META_TARGET) + @Override + public Optional optMeta(String path) { + if(path.isEmpty()){ + return Optional.of(this); + } else { + return getMetaList(path).stream().findFirst().map(it -> it); + } + } + + /** + * Return a node list using path notation and null if node does not exist + * + * @param path + * @return + */ + @SuppressWarnings("unchecked") + protected List getMetaList(Name path) { + if (path.getLength() == 0) { + throw new RuntimeException("Empty path not allowed"); + } + List res; + if (path.getLength() == 1) { + res = optChildNodeItem(path.ignoreQuery().toString()).orElse(Collections.emptyList()); + } else { + res = optHead(path).map(it -> it.getMetaList(path.cutFirst())).orElse(Collections.emptyList()); + } + + if (!res.isEmpty() && path.hasQuery()) { + //Filtering nodes using query + return MetaUtils.query(res, path.getQuery()); + } else { + return res; + } + } + + /** + * Return a value using path notation and null if it does not exist + * + * @param path + * @return + */ + @SuppressWarnings("unchecked") + public Optional optValue(Name path) { + if (path.getLength() == 0) { + throw new RuntimeException("Empty path not allowed"); + } + if (path.getLength() == 1) { + return Optional.ofNullable(values.get(path.toString())); + } else { + return optHead(path).flatMap( it -> it.optValue(path.cutFirst())); + } + } + + /** + * Return a list of all nodes for given name filtered by query if it exists. + * If node not found or there are no results for the query, the exception is + * thrown. + * + * @param name + * @return + */ + @Override + public List getMetaList(String name) { + return getMetaList(Name.Companion.of(name)); + } + + @Override + public T getMeta(String path) { + return getMetaList(path).stream().findFirst().orElseThrow(() -> new NameNotFoundException(path)); + } + + @Override + public Optional optValue(@NotNull String name) { + return optValue(Name.Companion.of(name)); + } + + /** + * {@inheritDoc} + * + * @return + */ + @Override + public Stream getNodeNames(boolean includeHidden) { + Stream res = this.nodes.keySet().stream(); + if (includeHidden) { + return res; + } else { + return res.filter(it -> !it.startsWith("@")); + } + } + + /** + * {@inheritDoc} + * + * @return + */ + @Override + public String getName() { + return name; + } + + /** + * {@inheritDoc} + * + * @return + */ + @Override + public Stream getValueNames(boolean includeHidden) { + Stream res = this.values.keySet().stream(); + if (includeHidden) { + return res; + } else { + return res.filter(it -> !it.startsWith("@")); + } + } + + /** + * Return a direct descendant node with given name. Return null if it is not found. + * + * @param name + * @return + */ + protected Optional> optChildNodeItem(String name) { + return Optional.ofNullable(nodes.get(name)); + } + + /** + * Return value of this node with given name. Return null if it is not found. + * + * @param name + * @return + */ + protected Value getChildValue(String name) { + return values.get(name); + } + + protected boolean isValidElementName(String name) { + return !(name.contains("[") || name.contains("]") || name.contains("$")); + } + + + /** + * Create a deep copy of the node but do not set parent or name. Deep copy + * does not clone listeners + * + * @param node + * @return + */ + protected abstract T cloneNode(Meta node); + + /** + * Return type checked variant of this node. The operation does not create new node, just exposes generic-free variant of the node. + * + * @return + */ + public abstract T self(); + + + @Override + public boolean isEmpty() { + return this.nodes.isEmpty() && this.values.isEmpty(); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/meta/MetaParser.java b/dataforge-core/src/main/java/hep/dataforge/meta/MetaParser.java new file mode 100644 index 00000000..de30fc31 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/meta/MetaParser.java @@ -0,0 +1,63 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.meta; + +import hep.dataforge.context.ContextAware; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.text.ParseException; + +/** + * An interface which allows conversion from Meta to string and vise versa + * + * @author Alexander Nozik + * @version $Id: $Id + */ +public interface MetaParser extends ContextAware { + + /** + * Convert Annotation to String + * + * @param source a T object. + * @return a {@link java.lang.String} object. + */ + String toString(Meta source); + + /** + * Convert String representation to Annotation object + * + * @param string a {@link java.lang.String} object. + * @return a T object. + * @throws java.text.ParseException if any. + */ + Meta fromString(String string) throws ParseException; + + /** + * read {@code length} bytes from InputStream and interpret it as + * Annotation. If {@code length < 0} then parse input stream until end of + * annotation is found. + * + * @param stream a {@link java.io.InputStream} object. + * @param length a long. + * @param encoding a {@link java.nio.charset.Charset} object. + * @throws java.io.IOException if any. + * @throws java.text.ParseException if any. + * @return a {@link hep.dataforge.meta.Meta} object. + */ + Meta fromStream(InputStream stream, long length, Charset encoding) throws IOException, ParseException; +} diff --git a/dataforge-core/src/main/java/hep/dataforge/meta/MetaProvider.java b/dataforge-core/src/main/java/hep/dataforge/meta/MetaProvider.java new file mode 100644 index 00000000..b86708e0 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/meta/MetaProvider.java @@ -0,0 +1,82 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.meta; + +import hep.dataforge.exceptions.NameNotFoundException; +import hep.dataforge.providers.Path; +import hep.dataforge.providers.Provider; +import hep.dataforge.providers.Provides; + +import java.util.Optional; +import java.util.function.Supplier; + +import static hep.dataforge.meta.MetaNode.DEFAULT_META_NAME; + +/** + * @author Alexander Nozik + */ +public interface MetaProvider { + + String META_TARGET = DEFAULT_META_NAME; + + /** + * Build a meta provider from given general provider + * + * @param provider + * @return + */ + static MetaProvider buildFrom(Provider provider) { + if (provider instanceof MetaProvider) { + return (MetaProvider) provider; + } + return path -> provider.provide(Path.of(path, META_TARGET)).map(Meta.class::cast); + } + + +// default boolean hasMeta(String path) { +// return optMeta(path).isPresent(); +// } + + @Provides(META_TARGET) + Optional optMeta(String path); + + default Meta getMeta(String path) { + return optMeta(path).orElseThrow(() -> new NameNotFoundException(path)); + } + + /** + * Return a child node with given name or default if child node not found + * + * @param path + * @param def + * @return + */ + default Meta getMeta(String path, Meta def) { + return optMeta(path).orElse(def); + } + + default Meta getMeta(String path, Supplier def) { + return optMeta(path).orElseGet(def); + } + + default boolean hasMeta(String path) { + return optMeta(path).isPresent(); + } + +// +// @Override +// public default Value getValue(String path) { +// Name pathName = Name.of(path); +// String metaPath = pathName.cutLast().toString(); +// if(hasMeta(metaPath)){ +// return getMeta(metaPath).getValue(pathName.getLast().toString()); +// } else { +// return null; +// } +// } +// + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/meta/MetaUtils.java b/dataforge-core/src/main/java/hep/dataforge/meta/MetaUtils.java new file mode 100644 index 00000000..cb5771b6 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/meta/MetaUtils.java @@ -0,0 +1,345 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.meta; + +import hep.dataforge.exceptions.NamingException; +import hep.dataforge.io.IOUtils; +import hep.dataforge.names.Name; +import hep.dataforge.values.*; +import kotlin.Pair; + +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +/** + * Utilities to work with meta + * + * @author Alexander Nozik + */ +public class MetaUtils { + + /** + * Find all nodes with given path that satisfy given condition. Return empty + * list if no such nodes are found. + * + * @param root + * @param path + * @param condition + * @return + */ + public static List findNodes(Meta root, String path, Predicate condition) { + if (!root.hasMeta(path)) { + return Collections.emptyList(); + } else { + return root.getMetaList(path).stream() + .filter(condition) + .collect(Collectors.toList()); + } + } + + /** + * Return the first node with given path that satisfies condition. Null if + * no such nodes are found. + * + * @param root + * @param path + * @param condition + * @return + */ + public static Optional findNode(Meta root, String path, Predicate condition) { + List list = findNodes(root, path, condition); + if (list.isEmpty()) { + return Optional.empty(); + } else { + return Optional.of(list.get(0)); + } + } + + /** + * Find node by given key-value pair + * + * @param root + * @param path + * @param key + * @param value + * @return + */ + public static Optional findNodeByValue(Meta root, String path, String key, Object value) { + return findNode(root, path, (m) -> m.hasValue(key) && m.getValue(key).equals(ValueFactory.of(value))); + } + + /** + * The transformation which should be performed on each value before it is + * returned to user. Basically is used to ensure automatic substitutions. If + * the reference not found in the current annotation scope than the value is + * returned as-is. + *

+ * the notation for template is the following: {@code ${path|def}} where + * {@code path} is the path for value in the context and {@code def} is the + * default value. + *

+ * + * @param val + * @param contexts a list of contexts to draw value from + * @return + */ + public static Value transformValue(Value val, ValueProvider... contexts) { + if (contexts.length == 0) { + return val; + } + if (val.getType().equals(ValueType.STRING) && val.getString().contains("$")) { + String valStr = val.getString(); +// Matcher matcher = Pattern.compile("\\$\\{(?.*)\\}").matcher(valStr); + Matcher matcher = Pattern.compile("\\$\\{(?[^|]*)(?:\\|(?.*))?}").matcher(valStr); + while (matcher.find()) { + String group = matcher.group(); + String sub = matcher.group("sub"); + String replacement = matcher.group("def"); + for (ValueProvider context : contexts) { + if (context != null && context.hasValue(sub)) { + replacement = context.getString(sub); + break; + } + } + if (replacement != null) { + valStr = valStr.replace(group, replacement); + } + } + return ValueFactory.parse(valStr, false); + } else { + return val; + } + } + + /** + * Apply query for given list ob objects using extracted meta as reference + * + * @param objects + * @param query + * @param metaExtractor + * @param + * @return + */ + public static List query(List objects, String query, Function metaExtractor) { + if (query.isEmpty()) { + return objects; + } + try { + int num = Integer.parseInt(query); + if (num < 0 || num >= objects.size()) { + throw new NamingException("No list element with given index"); + } + return Collections.singletonList(objects.get(num)); + } catch (NumberFormatException ex) { + List> predicates = new ArrayList<>(); + String[] tokens = query.split(","); + for (String token : tokens) { + predicates.add(buildQueryPredicate(token)); + } + Predicate predicate = meta -> { + AtomicBoolean res = new AtomicBoolean(true); + predicates.forEach(p -> { + if (!p.test(meta)) { + res.set(false); + } + }); + return res.get(); + }; + return objects.stream().filter(obj -> predicate.test(metaExtractor.apply(obj))).collect(Collectors.toList()); + } + + + } + + /** + * Build a meta predicate for a given single token + * + * @param token + * @return + */ + private static Predicate buildQueryPredicate(String token) { + String[] split = token.split("="); + if (split.length == 2) { + String key = split[0].trim(); + String value = split[1].trim(); + //TODO implement compare operators + return meta -> meta.getValue(key, ValueFactory.NULL).equals(ValueFactory.of(value)); + } else { + throw new NamingException("'" + token + "' is not a valid query"); + } + } + + /** + * Apply query to node list + * + * @param + * @param nodeList + * @param query + * @return + */ + public static List query(List nodeList, String query) { + return query(nodeList, query, it -> it); + } + + /** + * A stream containing pairs + * + * @param prefix + * @return + */ + private static Stream> nodeStream(Name prefix, Meta node, boolean includeRoot) { + Stream> subNodeStream = node.getNodeNames().flatMap(nodeName -> { + List metaList = node.getMetaList(nodeName); + Name nodePrefix; + if (prefix == null || prefix.isEmpty()) { + nodePrefix = Name.Companion.of(nodeName); + } else { + nodePrefix = prefix.plus(nodeName); + } + if (metaList.size() == 1) { + return nodeStream(nodePrefix, metaList.get(0), true); + } else { + return IntStream.range(0, metaList.size()).boxed() + .flatMap(i -> { + String subPrefix = String.format("%s[%d]", nodePrefix, i); + Meta subNode = metaList.get(i); + return nodeStream(Name.Companion.ofSingle(subPrefix), subNode, true); + }); + } + }); + if (includeRoot) { + return Stream.concat(Stream.of(new Pair<>(prefix, node)), subNodeStream); + } else { + return subNodeStream; + } + } + + public static Stream> nodeStream(Meta node) { + return nodeStream(Name.Companion.empty(), node, false); + } + + public static Stream> valueStream(Meta node) { + return nodeStream(Name.Companion.empty(), node, true).flatMap((Pair entry) -> { + Name key = entry.getFirst(); + Meta childMeta = entry.getSecond(); + return childMeta.getValueNames().map((String valueName) -> new Pair<>(key.plus(valueName), childMeta.getValue(valueName))); + }); + } + + /** + * Write Meta node to binary output stream. + * + * @param out + * @param meta node to serialize + * @param includeName include node name in serialization + */ + public static void writeMeta(ObjectOutput out, Meta meta, boolean includeName) { + try { + // write name if it is required + if (includeName) { + IOUtils.writeString(out, meta.getName()); + } + out.writeShort((int) meta.getValueNames(true).count()); + //writing values in format [name length, name, value] + meta.getValueNames(true).forEach(valName -> { + try { + IOUtils.writeString(out, valName); + ValueUtilsKt.writeValue(out, meta.getValue(valName)); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + + out.writeShort((int) meta.getNodeNames(true).count()); + meta.getNodeNames(true).forEach(nodeName -> { + try { + IOUtils.writeString(out, nodeName); + List metas = meta.getMetaList(nodeName); + out.writeShort(metas.size()); + for (Meta m : metas) { + //ignoring names for children + writeMeta(out, m, false); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + out.flush(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static void writeMeta(ObjectOutput out, Meta meta) { + writeMeta(out, meta, true); + } + + /** + * Read Meta node from serial stream as MetaBuilder + * + * @param in + * @param name the name of the node. If null, then the name is being read from stream + * @return + * @throws IOException + */ + public static MetaBuilder readMeta(ObjectInput in, String name) throws IOException { + MetaBuilder res = new MetaBuilder(name); + if (name == null) { + res.setName(IOUtils.readString(in)); + } + short valSize = in.readShort(); + for (int i = 0; i < valSize; i++) { + String valName = IOUtils.readString(in); + Value val = ValueUtilsKt.readValue(in); + res.setValue(valName, val); + } + short nodeSize = in.readShort(); + for (int i = 0; i < nodeSize; i++) { + String nodeName = IOUtils.readString(in); + short listSize = in.readShort(); + List nodeList = new ArrayList<>(); + for (int j = 0; j < listSize; j++) { + nodeList.add(readMeta(in, nodeName)); + } + res.setNodeItem(nodeName, nodeList); + } + + return res; + } + + public static MetaBuilder readMeta(ObjectInput in) throws IOException { + return readMeta(in, null); + } + + /** + * Check each of given paths in the given node. Return first subnode that do actually exist + * + * @param meta + * @param paths + * @return + */ + public static Optional optEither(Meta meta, String... paths) { + return Stream.of(paths).map(meta::optMeta).filter(Optional::isPresent).findFirst().map(Optional::get); + } + + public static Optional optEitherValue(Meta meta, String... paths) { + return Stream.of(paths).map(meta::optValue).filter(Optional::isPresent).findFirst().map(Optional::get); + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/meta/MutableMetaNode.java b/dataforge-core/src/main/java/hep/dataforge/meta/MutableMetaNode.java new file mode 100644 index 00000000..6b074408 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/meta/MutableMetaNode.java @@ -0,0 +1,582 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.meta; + +import hep.dataforge.NamedKt; +import hep.dataforge.exceptions.AnonymousNotAlowedException; +import hep.dataforge.exceptions.NamingException; +import hep.dataforge.names.Name; +import hep.dataforge.values.Value; +import hep.dataforge.values.ValueFactory; +import org.jetbrains.annotations.Nullable; + +import java.io.Serializable; +import java.util.*; + +/** + * A mutable annotation node equipped with observers. + * + * @author Alexander Nozik + */ +@MorphTarget(target = SealedNode.class) +public abstract class MutableMetaNode extends MetaNode + implements Serializable { + + protected T parent; + + protected MutableMetaNode() { + super(); + } + + protected MutableMetaNode(String name) { + super(name); + this.parent = null; + } + + protected MutableMetaNode(Meta meta) { + this.name = meta.getName(); + meta.getValueNames().forEach(valName -> setValue(valName, meta.getValue(valName), false)); + meta.getNodeNames().forEach(nodeName -> setNode(nodeName, meta.getMetaList(nodeName), false)); + } + + /** + * Parent aware name of this node including query string + * + * @return + */ + public Name getQualifiedName() { + if (parent == null) { + return Name.Companion.ofSingle(getName()); + } else { + int index = parent.indexOf(this); + if (index >= 0) { + return Name.Companion.ofSingle(String.format("%s[%d]", getName(), index)); + } else { + return Name.Companion.ofSingle(getName()); + } + } + } + + /** + * Full name including all ncestors + * + * @return + */ + public Name getFullName() { + if (parent == null) { + return Name.Companion.of(getName()); + } else { + return Name.Companion.join(parent.getFullName(), getQualifiedName()); + } + } + + /** + * Notify all observers that element is changed + * + * @param name + * @param oldItem + * @param newItem + */ + protected void notifyNodeChanged(Name name, List oldItem, List newItem) { + if (parent != null) { + parent.notifyNodeChanged(getQualifiedName().plus(name), oldItem, newItem); + } + } + + /** + * Notify all observers that value is changed + * + * @param name + * @param oldItem + * @param newItem + */ + protected void notifyValueChanged(Name name, Value oldItem, Value newItem) { + if (parent != null) { + parent.notifyValueChanged(getQualifiedName().plus(name), oldItem, newItem); + } + } + + @Nullable + protected T getParent() { + return parent; + } + + /** + * Add a copy of given meta to the node list with given name. Create a new + * one if it does not exist + * + * @param node + * @param notify notify listeners + */ + public T putNode(String name, Meta node, boolean notify) { + if (!isValidElementName(name)) { + throw new NamingException(String.format("\"%s\" is not a valid element name in the annotation", name)); + } + + //do not put empty nodes + if (node.isEmpty()) { + return self(); + } + + T newNode = transformNode(name, node); + List list = super.nodes.get(name); + List oldList = list != null ? new ArrayList<>(list) : Collections.emptyList(); + if (list == null) { + List newList = new ArrayList<>(); + newList.add(newNode); + this.setNodeItem(name, newList); + list = newList; + } else { + //Adding items to existing list. No need to update parents and listeners + list.add(newNode); + } + if (notify) { + notifyNodeChanged(Name.Companion.of(node.getName()), oldList, new ArrayList<>(list)); + } + return self(); + } + + /** + * putNode(element,true) + * + * @param element + * @return + */ + public T putNode(Meta element) { + if (element.isEmpty()) { + return self(); + } + if (NamedKt.isAnonymous(element) && !element.hasValue("@name")) { + throw new AnonymousNotAlowedException(); + } + return putNode(element.getName(), element, true); + } + + /** + * Same as {@code putNode(Meta)}, but also renames new node + * + * @param name + * @param element + * @return + */ + public T putNode(String name, Meta element) { + return putNode(name, element, true); + } + + /** + * Add new value to the value item with the given name. Create new one if it + * does not exist. null arguments are ignored (Value.NULL is still could be + * used) + * + * @param name + * @param value + * @param notify notify listeners + */ + public T putValue(Name name, Value value, boolean notify) { + if (value != null) { + Optional oldValue = optValue(name); + if (oldValue.isPresent()) { + List list = new ArrayList<>(oldValue.get().getList()); + list.add(value); + + Value newValue = Value.Companion.of(list); + + setValueItem(name, newValue); + + if (notify) { + notifyValueChanged(name, oldValue.get(), newValue); + } + } else { + setValueItem(name, value); + } + } + return self(); + } + + public T putValue(String name, Value value, boolean notify) { + if (!isValidElementName(name)) { + throw new NamingException(String.format("'%s' is not a valid element name in the meta", name)); + } + return putValue(Name.Companion.of(name), value, notify); + } + + /** + * setValue(name, value, true) + * + * @param name + * @param value + * @return + */ + public T putValue(String name, Value value) { + return putValue(name, value, true); + } + + /** + *

+ * setNode.

+ * + * @param element + */ + public T setNode(Meta element) { + if (NamedKt.isAnonymous(element)) { + throw new AnonymousNotAlowedException(); + } + + String nodeName = element.getName(); + if (!isValidElementName(nodeName)) { + throw new NamingException(String.format("\"%s\" is not a valid element name in the meta", nodeName)); + } + this.setNode(nodeName, element); + return self(); + } + + /** + * Set or replace current node or node list with this name + * + * @param name + * @param elements + * @param notify + * @return + */ + public T setNode(String name, List elements, boolean notify) { + if (elements != null && !elements.isEmpty()) { + List oldNodeItem; + if (hasMeta(name)) { + oldNodeItem = new ArrayList<>(getMetaList(name)); + } else { + oldNodeItem = Collections.emptyList(); + } + setNodeItem(name, elements); + if (notify) { + notifyNodeChanged(Name.Companion.of(name), oldNodeItem, getMetaList(name)); + } + } else { + removeNode(name); + } + return self(); + } + + /** + * setNode(name,elements,true) + * + * @param name + * @param elements + * @return + */ + public T setNode(String name, List elements) { + return setNode(name, elements, true); + } + + /** + * ДобавлÑет новый Ñлемент, ÑÑ‚Ð¸Ñ€Ð°Ñ Ñтарый + * + * @param name + * @param elements + */ + public T setNode(String name, Meta... elements) { + return setNode(name, Arrays.asList(elements)); + } + + /** + * Replace a value item with given name. If value is null, remove corresponding value from node. + * + * @param name + * @param value + */ + public T setValue(Name name, Value value, boolean notify) { + if (value == null ) { + removeValue(name); + } else { + Optional oldValueItem = optValue(name); + + setValueItem(name, value); + if (notify) { + notifyValueChanged(name, oldValueItem.orElse(null), value); + } + } + return self(); + } + + public T setValue(String name, Value value, boolean notify) { + return setValue(Name.Companion.of(name), value, notify); + } + + /** + * setValue(name, value, true) + * + * @param name + * @param value + * @return + */ + public T setValue(String name, Value value) { + return setValue(name, value, true); + } + + + public T setValue(String name, Object object) { + if(object ==null){ + removeValue(name); + return self(); + } else { + return setValue(name, ValueFactory.of(object)); + } + } + + /** + * Adds new value to the list of values with given name. Ignores null value. + * Does not replace old Value! + * + * @param name + * @param value + * @return + */ + public T putValue(String name, Object value) { + if (value != null) { + putValue(name, Value.Companion.of(value)); + } + return self(); + } + + public T putValues(String name, Object[] values) { + if (values != null && values.length > 0) { + for (Object obj : values) { + putValue(name, obj); + } + } + return self(); + } + + public T putValues(String name, String... values) { + if (values != null && values.length > 0) { + for (Object obj : values) { + putValue(name, obj); + } + } + return self(); + } + + /** + * Rename this node + * + * @param name + */ + protected T setName(String name) { + if (parent != null) { + throw new RuntimeException("Can't rename attached node"); + } + this.name = name; + return self(); + } + + /** + * Remove node list at given path (including descending tree) and notify + * listener + * + * @param path + */ + public T removeNode(String path) { + if (hasMeta(path)) { + List oldNode = getMetaList(path); + + if (nodes.containsKey(path)) { + nodes.remove(path); + } else { + Name namePath = Name.Companion.of(path); + if (namePath.getLength() > 1) { + //FIXME many path to string and string to path conversions + getHead(namePath).removeNode(namePath.cutFirst().toString()); + } + } + + notifyNodeChanged(Name.Companion.of(path), oldNode, Collections.emptyList()); + } + return self(); + } + + /** + * Replace or remove given direct descendant child node if it is present and + * notify listeners. + * + * @param child + */ + public void replaceChildNode(T child, Meta node) { + String nodeName = child.getName(); + if (hasMeta(nodeName)) { + List oldNode = getMetaList(nodeName); + int index = nodes.get(nodeName).indexOf(child); + if (node == null) { + nodes.get(nodeName).remove(index); + } else { + nodes.get(nodeName).set(index, transformNode(child.getName(), node)); + } + notifyNodeChanged(Name.Companion.ofSingle(nodeName), oldNode, getMetaList(nodeName)); + } + } + + /** + * Remove value with given path and notify listener + * + * @param path + */ + public void removeValue(Name path) { + Optional oldValue = optValue(path); + if (oldValue.isPresent()) { + if (path.getLength() > 1) { + getHead(path).removeValue(path.cutFirst().toString()); + } else { + this.values.remove(path.getUnescaped()); + } + notifyValueChanged(path, oldValue.get(), null); + } + } + + public void removeValue(String path) { + removeValue(Name.Companion.of(path)); + } + + /** + * Replaces node with given path with given item or creates new one + * + * @param path + * @param elements + */ + protected void setNodeItem(String path, List elements) { + if (!nodes.containsKey(path)) { + Name namePath = Name.Companion.of(path); + if (namePath.getLength() > 1) { + String headName = namePath.getFirst().entry(); + T headNode; + if (nodes.containsKey(headName)) { + headNode = getHead(namePath); + } else { + headNode = createChildNode(headName); + attachNode(headNode); + } + + headNode.setNodeItem(namePath.cutFirst().toString(), elements); + } else { + //single token path + this.nodes.put(path, transformNodeItem(path, elements)); + } + } else { + // else reset contents of the node + this.nodes.put(path, transformNodeItem(path, elements)); + } + } + + protected void setValueItem(Name namePath, Value value) { + if (namePath.getLength() > 1) { + String headName = namePath.getFirst().entry(); + T headNode; + if (nodes.containsKey(headName)) { + headNode = getHead(namePath); + } else { + headNode = createChildNode(headName); + attachNode(headNode); + } + headNode.setValueItem(namePath.cutFirst(), value); + } else { + //single token path + this.values.put(namePath.getUnescaped(), value); + } + } + + protected void setValueItem(String path, Value value) { + setValueItem(Name.Companion.of(path), value); + } + + /** + * Transform list of nodes changing their name and parent + * + * @param name + * @param item + * @return + */ + private List transformNodeItem(String name, List item) { + List res = new ArrayList<>(); + item.stream().map((an) -> transformNode(name, an)).peek((el) -> el.parent = this).forEach(res::add); + return res; + } + + private T transformNode(String name, Meta node) { + T el = cloneNode(node); + el.setName(name); + el.parent = this; + return el; + } + + /** + * Create but do not attach new child node + * + * @param name + * @return + */ + protected abstract T createChildNode(String name); + +// /** +// * Create a deep copy of the node but do not set parent or name. Deep copy +// * does not clone listeners +// * +// * @param node +// * @return +// */ +// protected abstract T cloneNode(Meta node); + + /** + * Attach node item without transformation. Each node's parent is changed to + * this + * + * @param name + * @param nodes + */ + public void attachNodeItem(String name, List nodes) { + nodes.forEach((T node) -> { + node.parent = this; + node.name = name; + }); + List oldList = this.nodes.get(name); + this.nodes.put(name, nodes); + notifyNodeChanged(Name.Companion.ofSingle(name), oldList, nodes); + } + + /** + * Add new node to the current list of nodes with the given name. Replace + * its parent with this. + * + * @param node + */ + public void attachNode(String nodeName, T node) { + if (node == null) { + throw new IllegalArgumentException("Can't attach null node"); + } + node.parent = this; + node.name = nodeName; + List list; + if (nodes.containsKey(nodeName)) { + list = nodes.get(nodeName); + } else { + list = new ArrayList<>(); + nodes.put(nodeName, list); + } + List oldList = new ArrayList<>(list); + list.add(node); + notifyNodeChanged(Name.Companion.ofSingle(nodeName), oldList, list); + } + + public void attachNode(T node) { + attachNode(node.getName(), node); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/meta/ReplaceRule.java b/dataforge-core/src/main/java/hep/dataforge/meta/ReplaceRule.java new file mode 100644 index 00000000..461ac912 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/meta/ReplaceRule.java @@ -0,0 +1,53 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.meta; + +import hep.dataforge.names.Name; +import hep.dataforge.values.Value; + +import java.util.List; + +/** + * Always use the element from main meta + * + * @author Alexander Nozik + */ +class ReplaceRule extends MergeRule { + + + /** + * {@inheritDoc} + */ + @Override + protected String mergeName(String mainName, String secondName) { + return mainName; + } + + + @Override + protected Value mergeValues(Name valueName, Value first, Value second) { + if (first.isNull()) { + return second; + } else return first; + } + + @Override + protected List mergeNodes(Name nodeName, List mainNodes, List secondaryNodes) { + if (mainNodes.isEmpty()) { + return secondaryNodes; + } else return mainNodes; + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/meta/SealedNode.java b/dataforge-core/src/main/java/hep/dataforge/meta/SealedNode.java new file mode 100644 index 00000000..48a75182 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/meta/SealedNode.java @@ -0,0 +1,40 @@ +package hep.dataforge.meta; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Unmodifiable meta node + */ +public final class SealedNode extends MetaNode { + + public SealedNode(Meta meta) { + super(meta.getName()); + meta.getValueNames(true).forEach((valueName) -> { + super.values.put(valueName,meta.getValue(valueName)); + }); + + meta.getNodeNames(true).forEach((nodeName) -> { + List item = meta.getMetaList(nodeName).stream() + .map(SealedNode::new) + .collect(Collectors.toList()); + super.nodes.put(nodeName, new ArrayList<>(item)); + }); + } + + @Override + public SealedNode getSealed() { + return this; + } + + @Override + protected SealedNode cloneNode(Meta node) { + return new SealedNode(node); + } + + @Override + public SealedNode self() { + return this; + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/meta/SimpleConfigurable.java b/dataforge-core/src/main/java/hep/dataforge/meta/SimpleConfigurable.java new file mode 100644 index 00000000..7c972942 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/meta/SimpleConfigurable.java @@ -0,0 +1,151 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.meta; + +import hep.dataforge.names.Name; +import hep.dataforge.values.Value; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * A simple implementation of configurable that applies observer to + * configuration on creation + * + * @author Alexander Nozik + */ +public class SimpleConfigurable implements Configurable { + + private final Configuration configuration; + + + /** + * Create a pre-configured instance + * + * @param configuration + */ + public SimpleConfigurable(Configuration configuration) { + this.configuration = configuration; + configuration.addListener(new ConfigChangeListener() { + + @Override + public void notifyValueChanged(@NotNull Name name, Value oldItem, Value newItem) { + applyValueChange(name.getUnescaped(), oldItem, newItem); + } + + @Override + public void notifyNodeChanged(@NotNull Name name, @NotNull List oldItem, @NotNull List newItem) { + applyNodeChange(name.getUnescaped(), oldItem, newItem); + } + }); + } + + public SimpleConfigurable(Meta meta) { + this(new Configuration(meta)); + } + + + public SimpleConfigurable() { + this(new Configuration()); + } + + /** + * {@inheritDoc } + * + * @return + */ + @Override + public final Configuration getConfig() { + return configuration; + } + + + /** + * Apply the whole new configuration. It does not change configuration, + * merely applies changes + * + * @param config + */ + protected void applyConfig(Meta config) { + //does nothing by default + } + + /** + * Apply specific value change. By default applies the whole configuration. + * + * @param name + * @param oldValue + * @param newValue + */ + protected void applyValueChange(@NotNull String name, Value oldValue, Value newValue) { + applyConfig(getConfig()); + } + + /** + * Apply specific element change. By default applies the whole + * configuration. + * + * @param name + * @param oldItem + * @param newItem + */ + protected void applyNodeChange(String name, List oldItem, List newItem) { + applyConfig(getConfig()); + } + + /** + * Add additional getConfig observer to configuration + * + * @param observer + */ + public void addConfigObserver(ConfigChangeListener observer) { + this.getConfig().addListener(observer); + } + + /** + * remove additional getConfig observer from configuration + * + * @param observer + */ + public void removeConfigObserver(ConfigChangeListener observer) { + this.getConfig().addListener(observer); + } + + /** + * validate incoming configuration changes and return correct version (all + * invalid values are excluded). By default just returns unchanged + * configuration. + * + * @param config + * @return + */ + protected Meta validate(Meta config) { + return config; + } + + /** + * Applies changes from given config to this one + * + * @param config + */ + @Override + public Configurable configure(Meta config) { + //Check and correct input configuration + getConfig().update(config, false); + applyConfig(getConfig()); + return this; + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/meta/Template.java b/dataforge-core/src/main/java/hep/dataforge/meta/Template.java new file mode 100644 index 00000000..eaaac16b --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/meta/Template.java @@ -0,0 +1,106 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.meta; + +import hep.dataforge.providers.Provider; +import hep.dataforge.values.*; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.function.UnaryOperator; + +import static hep.dataforge.meta.MetaUtils.transformValue; + +/** + * @author Alexander Nozik + */ +@Deprecated +public class Template implements Metoid, UnaryOperator { + + /** + * Template itself + */ + private final Meta template; + + /** + * Default values + */ + private final Meta def; + + public Template(Meta template) { + this.template = template; + this.def = Meta.empty(); + } + + public Template(Meta template, Meta def) { + this.template = template; + this.def = def; + } + + /** + * Build a Meta using given template. + * + * @param template + * @return + */ + public static MetaBuilder compileTemplate(Meta template, Meta data) { + return new Template(template).compile(data); + } + + public static MetaBuilder compileTemplate(Meta template, Map data) { + return new Template(template).compile(new MapValueProvider(data), null); + } + + @Override + public Meta getMeta() { + return template; + } + + /** + * Compile template using given meta and value providers. + * + * @param valueProvider + * @param metaProvider + * @return + */ + public MetaBuilder compile(ValueProvider valueProvider, MetaProvider metaProvider) { + MetaBuilder res = new MetaBuilder(getMeta()); + MetaUtils.nodeStream(res).forEach(pair -> { + MetaBuilder node = (MetaBuilder) pair.getSecond(); + if (node.hasValue("@include")) { + String includePath = pair.getSecond().getString("@include"); + if (metaProvider != null && metaProvider.hasMeta(includePath)) { + MetaBuilder parent = node.getParent(); + parent.replaceChildNode(node, metaProvider.getMeta(includePath)); + } else if (def.hasMeta(includePath)) { + MetaBuilder parent = node.getParent(); + parent.replaceChildNode(node, def.getMeta(includePath)); + } else { + LoggerFactory.getLogger(MetaUtils.class) + .warn("Can't compile template meta node with name {} not provided", includePath); + } + } + }); + + MetaUtils.valueStream(res).forEach(pair -> { + Value val = pair.getSecond(); + if (val.getType().equals(ValueType.STRING) && val.getString().contains("$")) { + res.setValue(pair.getFirst().toString(), transformValue(val, valueProvider, def)); + } + }); + return res; + } + + public MetaBuilder compile(Provider provider) { + return compile(ValueUtils.asValueProvider(provider), MetaProvider.buildFrom(provider)); + } + + @Override + public MetaBuilder apply(Meta data) { + return compile(data, data); + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/names/AbstractNamedSet.java b/dataforge-core/src/main/java/hep/dataforge/names/AbstractNamedSet.java new file mode 100644 index 00000000..a666c90a --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/names/AbstractNamedSet.java @@ -0,0 +1,61 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.names; + +/** + *

AbstractNamedSet class.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class AbstractNamedSet implements NameSetContainer { + + private final NameList names; + + /** + *

Constructor for AbstractNamedSet.

+ * + * @param names a {@link hep.dataforge.names.NameList} object. + */ + public AbstractNamedSet(NameList names) { + this.names = names; + } + + /** + *

Constructor for AbstractNamedSet.

+ * + * @param list an array of {@link java.lang.String} objects. + */ + public AbstractNamedSet(String[] list) { + this.names = new NameList(list); + } + + /** + *

Constructor for AbstractNamedSet.

+ * + * @param set a {@link hep.dataforge.names.NameSetContainer} object. + */ + public AbstractNamedSet(NameSetContainer set) { + this.names = set.getNames(); + } + + /** {@inheritDoc} */ + @Override + public NameList getNames() { + return names; + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/names/AlphanumComparator.java b/dataforge-core/src/main/java/hep/dataforge/names/AlphanumComparator.java new file mode 100644 index 00000000..d8b20fdb --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/names/AlphanumComparator.java @@ -0,0 +1,126 @@ +/* + * The Alphanum Algorithm is an improved sorting algorithm for strings + * containing numbers. Instead of sorting numbers in ASCII order like + * a standard sort, this algorithm sorts numbers in numeric order. + * + * The Alphanum Algorithm is discussed at http://www.DaveKoelle.com + * + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +package hep.dataforge.names; + +import java.util.Comparator; + +/** + * This is an updated version with enhancements made by Daniel Migowski, + * Andre Bogus, and David Koelle + * To use this class: + * Use the static "sort" method from the java.util.Collections class: + * Collections.sort(your list, new AlphanumComparator()); + */ +public class AlphanumComparator implements Comparator { + public static final AlphanumComparator INSTANCE = new AlphanumComparator(); + + private Comparator comparator = new NaturalComparator(); + + public AlphanumComparator(Comparator comparator) { + this.comparator = comparator; + } + + public AlphanumComparator() { + + } + + private final boolean isDigit(char ch) { + return ch >= 48 && ch <= 57; + } + + /** + * Length of string is passed in for improved efficiency (only need to calculate it once) + **/ + private final String getChunk(String s, int slength, int marker) { + StringBuilder chunk = new StringBuilder(); + char c = s.charAt(marker); + chunk.append(c); + marker++; + if (isDigit(c)) { + while (marker < slength) { + c = s.charAt(marker); + if (!isDigit(c)) + break; + chunk.append(c); + marker++; + } + } else { + while (marker < slength) { + c = s.charAt(marker); + if (isDigit(c)) + break; + chunk.append(c); + marker++; + } + } + return chunk.toString(); + } + + public int compare(String s1, String s2) { + + int thisMarker = 0; + int thatMarker = 0; + int s1Length = s1.length(); + int s2Length = s2.length(); + + while (thisMarker < s1Length && thatMarker < s2Length) { + String thisChunk = getChunk(s1, s1Length, thisMarker); + thisMarker += thisChunk.length(); + + String thatChunk = getChunk(s2, s2Length, thatMarker); + thatMarker += thatChunk.length(); + + // If both chunks contain numeric characters, sort them numerically + int result = 0; + if (isDigit(thisChunk.charAt(0)) && isDigit(thatChunk.charAt(0))) { + // Simple chunk comparison by length. + int thisChunkLength = thisChunk.length(); + result = thisChunkLength - thatChunk.length(); + // If equal, the first different number counts + if (result == 0) { + for (int i = 0; i < thisChunkLength; i++) { + result = thisChunk.charAt(i) - thatChunk.charAt(i); + if (result != 0) { + return result; + } + } + } + } else { + result = comparator.compare(thisChunk, thatChunk); + } + + if (result != 0) + return result; + } + + return s1Length - s2Length; + } + + private static class NaturalComparator implements Comparator { + public int compare(String o1, String o2) { + return o1.compareTo(o2); + } + } +} \ No newline at end of file diff --git a/dataforge-core/src/main/java/hep/dataforge/names/AnonymousNotAlowed.java b/dataforge-core/src/main/java/hep/dataforge/names/AnonymousNotAlowed.java new file mode 100644 index 00000000..1f5907d9 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/names/AnonymousNotAlowed.java @@ -0,0 +1,32 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.names; + +import java.lang.annotation.*; + +/** + * An annotation for class implementing {@code Named} interface that states that empty + * name is not allowed for this class. + * + * @author Alexander Nozik + */ +@Target(value = ElementType.TYPE) +@Retention(value = RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface AnonymousNotAlowed { + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/names/NameSetContainer.java b/dataforge-core/src/main/java/hep/dataforge/names/NameSetContainer.java new file mode 100644 index 00000000..ff8572c6 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/names/NameSetContainer.java @@ -0,0 +1,32 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.names; + +/** + * A container class for a set of names + * @author Alexander Nozik + */ +public interface NameSetContainer { + /** + * Get the names helper + * @return + */ + NameList getNames(); + + default String[] namesAsArray(){ + return getNames().asArray(); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/names/NamedMetaHolder.java b/dataforge-core/src/main/java/hep/dataforge/names/NamedMetaHolder.java new file mode 100644 index 00000000..321922a4 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/names/NamedMetaHolder.java @@ -0,0 +1,65 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.names; + +import hep.dataforge.Named; +import hep.dataforge.exceptions.AnonymousNotAlowedException; +import hep.dataforge.meta.Meta; +import hep.dataforge.meta.MetaHolder; + + +public class NamedMetaHolder extends MetaHolder implements Named { + + private String name; + + public NamedMetaHolder(String name, Meta meta) { + super(meta); + if ((name == null || name.isEmpty()) && getClass().isAnnotationPresent(AnonymousNotAlowed.class)) { + throw new AnonymousNotAlowedException(); + } + this.name = name; + } + + /** + * Create anonymous instance if it is allowed + * + * @param meta + */ + public NamedMetaHolder(Meta meta) { + this(null, meta); + } + + /** + * An instance with blank meta + * + * @param name + */ + public NamedMetaHolder(String name) { + this(name, null); + } + + /** + * An instance with blank meta + */ + public NamedMetaHolder() { + this(null, null); + } + + @Override + public String getName() { + return this.name; + } + + /** + * Protected method to set name later. Use it with caution + * + * @param name + */ + protected final void setName(String name) { + this.name = name; + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/names/NamesUtils.java b/dataforge-core/src/main/java/hep/dataforge/names/NamesUtils.java new file mode 100644 index 00000000..db569513 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/names/NamesUtils.java @@ -0,0 +1,249 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.names; + +import hep.dataforge.exceptions.NameNotFoundException; +import hep.dataforge.values.Values; + +import java.util.ArrayList; +import java.util.List; + +import static java.lang.System.arraycopy; +import static java.util.Arrays.asList; + +/** + *

+ * NamesUtils class.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class NamesUtils { + + /** + * проверка того, что два набора имен полноÑтью Ñовпадают Ñ Ñ‚Ð¾Ñ‡Ð½Ð¾Ñтью до + * порÑдка + * + * @param names1 an array of {@link java.lang.String} objects. + * @param names2 an array of {@link java.lang.String} objects. + * @return a boolean. + */ + public static boolean areEqual(String[] names1, String[] names2) { + if (names1.length != names2.length) { + return false; + } + for (int i = 0; i < names2.length; i++) { + if (!names1[i].equals(names2[i])) { + return false; + } + + } + return true; + } + + /** + * Проверка того, что два Names Ñодержат одинаковый набор имен, без учета + * порÑдка. + * + * @param named1 a {@link hep.dataforge.names.NameList} object. + * @param named2 a {@link hep.dataforge.names.NameList} object. + * @return a boolean. + */ + public static boolean areEqual(NameList named1, NameList named2) { + return (named1.contains(named2)) && (named2.contains(named1)); + } + + /** + *

+ * combineNames.

+ * + * @param names1 an array of {@link java.lang.String} objects. + * @param names2 a {@link java.lang.String} object. + * @return an array of {@link java.lang.String} objects. + */ + public static String[] combineNames(String[] names1, String... names2) { + + /* + * Ðе обÑÐ·Ð°Ñ‚ÐµÐ»ÑŒÐ½Ð°Ñ Ð´Ð¾Ñ€Ð¾Ð³Ð°Ñ Ð¿Ñ€Ð¾Ð²ÐµÑ€ÐºÐ°, чтобы иÑключить дублирование имен + */ + if (!notIncludesEquals(names1, names2)) { + throw new IllegalArgumentException("Names must be different."); + } + String[] res = new String[names1.length + names2.length]; + arraycopy(names1, 0, res, 0, names1.length); + arraycopy(names2, 0, res, names1.length, names2.length); + return res; + } + + /** + * Собирает из двух маÑÑивов имен один, при Ñтом убирает дублирующиеÑÑ Ð¸Ð¼ÐµÐ½Ð° + * + * @param names1 an array of {@link java.lang.String} objects. + * @param names2 a {@link java.lang.String} object. + * @return an array of {@link java.lang.String} objects. + */ + public static String[] combineNamesWithEquals(String[] names1, String... names2) { + ArrayList strings = new ArrayList<>(); + strings.addAll(asList(names1)); + for (String name : names2) { + if (!strings.contains(name)) { + strings.add(name); + } + } + String[] res = new String[strings.size()]; + return strings.toArray(res); + } + + /** + *

+ * contains.

+ * + * @param nameList an array of {@link java.lang.String} objects. + * @param name a {@link java.lang.String} object. + * @return a boolean. + */ + public static boolean contains(String[] nameList, String name) { + for (String nameList1 : nameList) { + if (nameList1.equals(name)) { + return true; + } + } + return false; + } + + /** + *

+ * exclude.

+ * + * @param named a {@link hep.dataforge.names.NameList} object. + * @param excludeName a {@link java.lang.String} object. + * @return an array of {@link java.lang.String} objects. + */ + public static String[] exclude(NameList named, String excludeName) { + List names = named.asList(); + names.remove(excludeName); + return names.toArray(new String[names.size()]); + } + + /** + *

+ * exclude.

+ * + * @param names an array of {@link java.lang.String} objects. + * @param excludeName a {@link java.lang.String} object. + * @return an array of {@link java.lang.String} objects. + */ + public static String[] exclude(String[] names, String excludeName) { + ArrayList list = new ArrayList<>(); + for (String name : names) { + if (!name.equals(excludeName)) { + list.add(name); + } + } + return list.toArray(new String[list.size()]); + } + + /** + * TODO replace by List + * + * @param set + * @return an array of {@link java.lang.Number} objects. + * @throws hep.dataforge.exceptions.NameNotFoundException if any. + */ + public static Number[] getAllNamedSetValues(Values set) throws NameNotFoundException { + Number[] res = new Number[set.getNames().size()]; + List names = set.getNames().asList(); + for (int i = 0; i < set.getNames().size(); i++) { + res[i] = set.getValue(names.get(i)).getDouble(); + } + return res; + } + + /** + *

+ * getNamedSubSetValues.

+ * + * @param set + * @param names a {@link java.lang.String} object. + * @return an array of double. + * @throws hep.dataforge.exceptions.NameNotFoundException if any. + */ + public static double[] getNamedSubSetValues(Values set, String... names) throws NameNotFoundException { + double[] res = new double[names.length]; + for (int i = 0; i < names.length; i++) { + res[i] = set.getValue(names[i]).getDouble(); + + } + return res; + } + + /** + * Проверка того, что два набора имен не переÑекаютÑÑ + * + * @param names1 an array of {@link java.lang.String} objects. + * @param names2 an array of {@link java.lang.String} objects. + * @return a boolean. + */ + public static boolean notIncludesEquals(String[] names1, String[] names2) { + for (String names11 : names1) { + for (String names21 : names2) { + if (names11.equalsIgnoreCase(names21)) { + return false; + } + } + } + return true; + } + + /** + * Generate a default axis name set for given number of dimensions + * + * @param dim + * @return + */ + public static NameList generateNames(int dim) { + switch (dim) { + case 0: + return new NameList(); + case 1: + return new NameList("x"); + case 2: + return new NameList("x", "y"); + case 3: + return new NameList("x", "y", "z"); + default: + List names = new ArrayList<>(); + for (int i = 0; i < dim; i++) { + names.add("axis_" + (i + 1)); + } + return new NameList(names); + } + } + + /** + * Check if given string is a valid size=1 name + * @param token + * @return + */ + public static boolean isValidToken(String token){ + try { + return Name.Companion.of(token).getLength() == 1; + }catch (Exception ex){ + return false; + } + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/providers/Path.java b/dataforge-core/src/main/java/hep/dataforge/providers/Path.java new file mode 100644 index 00000000..4603e3c5 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/providers/Path.java @@ -0,0 +1,102 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.providers; + +import hep.dataforge.names.Name; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; + +/** + *

+ * Path interface.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +public interface Path { + + String EMPTY_TARGET = ""; + String PATH_SEGMENT_SEPARATOR = "/"; + String TARGET_SEPARATOR = "::"; + + static Path of(String path) { + SegmentedPath p = SegmentedPath.of(path); + + if (p.optTail().isPresent()) { + return p; + } else { + return p.head(); + } + } + + /** + * Create a path with given target override (even if it is provided by the path itself) + * + * @param target + * @param path + * @return + */ + static Path of(String target, String path) { + return of(path).withTarget(target); + } + + @NotNull + static Path of(String target, Name name) { + return new PathSegment(target, name); + } + + /** + * The Name of first segment + * + * @return a {@link hep.dataforge.names.Name} object. + */ + Name getName(); + + /** + * Returns non-empty optional containing the chain without first segment in case of chain path. + * + * @return + */ + Optional optTail(); + + /** + * The target of first segment + * + * @return a {@link java.lang.String} object. + */ + String getTarget(); + + /** + * Return new path with different target + * + * @return + */ + Path withTarget(String target); + + /** + * Create or append chain path + * + * @param segment + * @return + */ + Path append(Path segment); + + default Path append(String target, String name) { + return append(Path.of(target, name)); + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/providers/PathSegment.java b/dataforge-core/src/main/java/hep/dataforge/providers/PathSegment.java new file mode 100644 index 00000000..734eabde --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/providers/PathSegment.java @@ -0,0 +1,94 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.providers; + +import hep.dataforge.exceptions.NamingException; +import hep.dataforge.names.Name; + +import java.util.Arrays; +import java.util.Optional; + +/** + * Сегмент пути. ПредÑтавлÑет Ñобой пару цель::имÑ. ЕÑли цель не указана или + * пуÑтаÑ, иÑпользуетÑÑ Ñ†ÐµÐ»ÑŒ по-умолчанию Ð´Ð»Ñ Ð´Ð°Ð½Ð½Ð¾Ð³Ð¾ провайдера + * + * @author Alexander Nozik + * @version $Id: $Id + */ +class PathSegment implements Path { + + private Name name; + private String target; + + + public PathSegment(String target, Name name) { + this.name = name; + this.target = target; + } + + public PathSegment(String path) { + if (path == null || path.isEmpty()) { + throw new NamingException("Empty path"); + } + if (path.contains(TARGET_SEPARATOR)) { + String[] split = path.split(TARGET_SEPARATOR, 2); + this.target = split[0]; + this.name = Name.Companion.of(split[1]); + } else { + this.target = EMPTY_TARGET; + this.name = Name.Companion.of(path); + } + } + + @Override + public Name getName() { + return name; + } + + + @Override + public Optional optTail() { + return Optional.empty(); + } + + @Override + public String getTarget() { + if (target == null) { + return EMPTY_TARGET; + } else { + return target; + } + } + + @Override + public Path withTarget(String target) { + return new PathSegment(target, name); + } + + @Override + public Path append(Path segment) { + return new SegmentedPath(getTarget(), Arrays.asList(this, new PathSegment(segment.getTarget(), segment.getName()))); + } + + @Override + public String toString() { + if(target.isEmpty()){ + return getName().getUnescaped(); + } else { + return target + ":" + getName().getUnescaped(); + } + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/providers/Provider.java b/dataforge-core/src/main/java/hep/dataforge/providers/Provider.java new file mode 100644 index 00000000..f431e383 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/providers/Provider.java @@ -0,0 +1,99 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.providers; + +import java.util.Optional; +import java.util.stream.Stream; + +/** + * A marker utility interface for providers. + * + * @author Alexander Nozik + */ +public interface Provider { + + + default Optional provide(Path path) { + return Providers.provide(this, path); + } + + /** + * Stream of available names with given target. Only top level names are listed, no chain path. + * + * @param target + * @return + */ + default Stream listContent(String target) { + if (target.isEmpty()) { + target = getDefaultTarget(); + } + return Providers.listContent(this, target); + } + + /** + * Default target for this provider + * + * @return + */ + default String getDefaultTarget() { + return ""; + } + + /** + * Default target for next chain segment + * + * @return + */ + default String getDefaultChainTarget() { + return ""; + } + + + //utils + + + /** + * Type checked provide + * + * @param path + * @param type + * @param + * @return + */ + default Optional provide(String path, Class type) { + return provide(Path.of(path)).map(type::cast); + } + + default Optional provide(String target, String name, Class type) { + return provide(Path.of(target, name)).map(type::cast); + } + + default Optional provide(Path path, Class type) { + return provide(path).map(type::cast); + } + + /** + * Stream of all elements with given target + * + * @param target + * @param type + * @param + * @return + */ + default Stream provideAll(String target, Class type) { + return listContent(target).map(it -> provide(target, it, type).orElseThrow(() -> new IllegalStateException("The element " + it + " is declared but not provided"))); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/providers/Providers.java b/dataforge-core/src/main/java/hep/dataforge/providers/Providers.java new file mode 100644 index 00000000..4efd5355 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/providers/Providers.java @@ -0,0 +1,119 @@ +package hep.dataforge.providers; + +import hep.dataforge.exceptions.ChainPathNotSupportedException; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Utility methods for providers + * Created by darksnake on 25-Apr-17. + */ +public class Providers { + /** + * Provide using custom resolver. + * + * @param path + * @param resolver + * @return + */ + public static Optional provide(Path path, Function> resolver) { + Optional opt = resolver.apply(path.getName().toString()); + Optional tailOpt = path.optTail(); + if (tailOpt.isPresent()) { + return opt.flatMap(res -> { + if (res instanceof Provider) { + Provider p = (Provider) res; + //using default chain target if needed + Path tail = tailOpt.get(); + if (tail.getTarget().isEmpty()) { + tail = tail.withTarget(p.getDefaultChainTarget()); + } + return p.provide(tail); + } else { + throw new ChainPathNotSupportedException(); + } + }); + } else { + return opt; + } + } + + public static Optional provide(Object provider, Path path) { + return provide(path, str -> provideDirect(provider, path.getTarget(), str)); + } + + + public static Collection listTargets(Object provider) { + return findProviders(provider.getClass()).keySet(); + } + + @SuppressWarnings("unchecked") + public static Stream listContent(Object provider, String target) { + return Stream.of(provider.getClass().getMethods()) + .filter(method -> method.isAnnotationPresent(ProvidesNames.class)) + .filter(method -> Objects.equals(method.getAnnotation(ProvidesNames.class).value(), target)) + .findFirst() + .map(method -> { + try { + Object list = method.invoke(provider); + if (list instanceof Stream) { + return (Stream) list; + } else if (list instanceof Collection) { + return ((Collection) list).stream(); + } else { + throw new Error("Wrong method annotated with ProvidesNames"); + } + } catch (Exception e) { + throw new RuntimeException("Failed to provide names by reflections", e); + } + }).orElse(Stream.empty()); + } + + /** + * Provide direct descendant without using chain path + * + * @param provider + * @param target + * @param name + * @return + */ + private static Optional provideDirect(Object provider, String target, String name) { + Map providers = findProviders(provider.getClass()); + + // using default target if needed + if (target.isEmpty() && provider instanceof Provider) { + target = ((Provider) provider).getDefaultTarget(); + } + + if (!providers.containsKey(target)) { + return Optional.empty(); + } else { + Method method = providers.get(target); + try { + Object result = method.invoke(provider, name); + if (result instanceof Optional) { + return (Optional) result; + } else { + return Optional.ofNullable(result); + } + } catch (IllegalAccessException | InvocationTargetException | ClassCastException e) { + throw new RuntimeException("Failed to provide by reflections. The method " + method.getName() + " is not a provider method", e); + } + } + } + + private static Map findProviders(Class cl) { + return Stream.of(cl.getMethods()) + .filter(method -> method.isAnnotationPresent(Provides.class)) + .collect(Collectors.toMap(method -> method.getAnnotation(Provides.class).value(), method -> method)); + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/providers/Provides.java b/dataforge-core/src/main/java/hep/dataforge/providers/Provides.java new file mode 100644 index 00000000..c0bfe27b --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/providers/Provides.java @@ -0,0 +1,20 @@ +package hep.dataforge.providers; + +import java.lang.annotation.*; + +/** + * An annotation to mark provider methods. Provider method must take single string as argument and return {@link java.util.Optional} + * Created by darksnake on 25-Apr-17. + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface Provides { + /** + * The name of the target this method provides + * + * @return + */ + String value(); +} diff --git a/dataforge-core/src/main/java/hep/dataforge/providers/ProvidesNames.java b/dataforge-core/src/main/java/hep/dataforge/providers/ProvidesNames.java new file mode 100644 index 00000000..4e34db27 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/providers/ProvidesNames.java @@ -0,0 +1,23 @@ +package hep.dataforge.providers; + +import java.lang.annotation.*; + +/** + * Annotates the method that returns either collection of String or stream of String + * Created by darksnake on 26-Apr-17. + */ + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface ProvidesNames { + /** + * The name of the target this method provides + * + * @return + */ + String value(); + + +} \ No newline at end of file diff --git a/dataforge-core/src/main/java/hep/dataforge/providers/SegmentedPath.java b/dataforge-core/src/main/java/hep/dataforge/providers/SegmentedPath.java new file mode 100644 index 00000000..d710ced0 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/providers/SegmentedPath.java @@ -0,0 +1,153 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.providers; + +import hep.dataforge.names.Name; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Путь в формате target1::path1/target2::path2. Блоки между / называютÑÑ + * Ñегментами. + * + * @author Alexander Nozik + * @version $Id: $Id + */ +class SegmentedPath implements Path { + + @NotNull + static SegmentedPath of(@NotNull String pathStr) { + if (pathStr.isEmpty()) { + throw new IllegalArgumentException("Empty argument in the path constructor"); + } + String[] split = normalize(pathStr).split(PATH_SEGMENT_SEPARATOR); + LinkedList segments = new LinkedList<>(); + + for (String segmentStr : split) { + segments.add(new PathSegment(segmentStr)); + } + + String target = segments.get(0).getTarget(); + return new SegmentedPath(target, segments); + } + + private final LinkedList segments; + + /** + * for target inheritance + */ + private final String defaultTarget; + + SegmentedPath(String defaultTarget, Collection segments) { + if (segments.isEmpty()) { + throw new IllegalArgumentException("Zero length paths are not allowed"); + } + this.defaultTarget = defaultTarget; + this.segments = new LinkedList<>(segments); + } + + /** + * remove leading and trailing separators + * + * @param path + * @return + */ + private static String normalize(String path) { + String res = path.trim(); + // remove leading separators + while (res.startsWith(PATH_SEGMENT_SEPARATOR)) { + res = res.substring(1); + } + while (res.endsWith(PATH_SEGMENT_SEPARATOR)) { + res = res.substring(0, res.length() - 1); + } + return res; + + } + + /** + * {@inheritDoc} + */ + @Override + public String getTarget() { + String target = segments.getFirst().getTarget(); + if(target.isEmpty()){ + return defaultTarget; + } else { + return target; + } + } + + /** + * {@inheritDoc} + */ + @Override + public Name getName() { + return segments.getFirst().getName(); + } + + + public PathSegment head() { + return this.segments.peekFirst(); + } + + + public int size() { + return this.segments.size(); + } + + @Override + public Optional optTail() { + if (segments.size() <= 1) { + return Optional.empty(); + } else { + List newSegments = segments.subList(1, segments.size()); + return Optional.of(new SegmentedPath(defaultTarget, newSegments)); + } + } + + +// public String getFinalTarget() { +// // Идем по Ñегментам в обратном порÑдке и ищем первый раз, когда поÑвлÑетÑÑ Ð¾Ð±ÑŠÑÐ²Ð»ÐµÐ½Ð½Ð°Ñ Ñ†ÐµÐ»ÑŒ +// for (Iterator it = segments.descendingIterator(); it.hasNext(); ) { +// Path segment = it.next(); +// if (!segment.target().equals(EMPTY_TARGET)) { +// return segment.target(); +// } +// } +// //ЕÑли цель не объÑвлена ни в одном из Ñегментов, возвращаем пуÑтую цель +// return EMPTY_TARGET; +// } + + @Override + public Path withTarget(String target) { + return new SegmentedPath(target, segments); + } + + @Override + public Path append(Path segment) { + List list = new ArrayList<>(this.segments); + list.add(new PathSegment(segment.getTarget(), segment.getName())); + return new SegmentedPath(defaultTarget, list); + } + + @Override + public String toString() { + return String.join("/",segments.stream().map(PathSegment::toString).collect(Collectors.toList())); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/states/MetaStateDef.java b/dataforge-core/src/main/java/hep/dataforge/states/MetaStateDef.java new file mode 100644 index 00000000..afb91020 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/states/MetaStateDef.java @@ -0,0 +1,59 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.states; + +import hep.dataforge.description.NodeDef; + +import java.lang.annotation.*; + +/** + * The definition of state for a stateful object. + * + * @author Alexander Nozik + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@Repeatable(MetaStateDefs.class) +public @interface MetaStateDef { + + /** + * The definition for metastate content + * @return + */ + NodeDef value(); + + /** + * This state could be read + * + * @return + */ + boolean readable() default true; + + /** + * This state could be written + * + * @return + */ + boolean writable() default false; +} diff --git a/dataforge-core/src/main/java/hep/dataforge/states/MetaStateDefs.java b/dataforge-core/src/main/java/hep/dataforge/states/MetaStateDefs.java new file mode 100644 index 00000000..ce9a67f0 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/states/MetaStateDefs.java @@ -0,0 +1,36 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.states; + +import java.lang.annotation.*; + +/** + * + * @author Alexander Nozik + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface MetaStateDefs { + MetaStateDef[] value(); +} diff --git a/dataforge-core/src/main/java/hep/dataforge/states/StateDef.java b/dataforge-core/src/main/java/hep/dataforge/states/StateDef.java new file mode 100644 index 00000000..16229481 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/states/StateDef.java @@ -0,0 +1,59 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.states; + +import hep.dataforge.description.ValueDef; + +import java.lang.annotation.*; + +/** + * The definition of state for a stateful object. + * + * @author Alexander Nozik + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@Repeatable(StateDefs.class) +public @interface StateDef { + + /** + * The definition for state value + * @return + */ + ValueDef value(); + + /** + * This state could be read + * + * @return + */ + boolean readable() default true; + + /** + * This state could be written + * + * @return + */ + boolean writable() default false; +} diff --git a/dataforge-core/src/main/java/hep/dataforge/states/StateDefs.java b/dataforge-core/src/main/java/hep/dataforge/states/StateDefs.java new file mode 100644 index 00000000..f012bef3 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/states/StateDefs.java @@ -0,0 +1,36 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.states; + +import java.lang.annotation.*; + +/** + * + * @author Alexander Nozik + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface StateDefs { + StateDef[] value(); +} diff --git a/dataforge-core/src/main/java/hep/dataforge/tables/Adapters.java b/dataforge-core/src/main/java/hep/dataforge/tables/Adapters.java new file mode 100644 index 00000000..a4c784e4 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/tables/Adapters.java @@ -0,0 +1,180 @@ +package hep.dataforge.tables; + +import hep.dataforge.exceptions.NameNotFoundException; +import hep.dataforge.meta.Meta; +import hep.dataforge.meta.MetaBuilder; +import hep.dataforge.names.Name; +import hep.dataforge.values.Value; +import hep.dataforge.values.ValueMap; +import hep.dataforge.values.Values; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; +import java.util.stream.Stream; + +/** + * Utility methods to work with adapters + */ +public class Adapters { + /** + * Build a basic adapter or a custom adapter depending on @class meta value + * + * @param meta + * @return + */ + @NotNull + @SuppressWarnings("unchecked") + public static ValuesAdapter buildAdapter(Meta meta) { + if (meta.hasValue("@class")) { + try { + Class type = (Class) Class.forName(meta.getString("@class")); + return type.getConstructor(Meta.class).newInstance(meta); + } catch (Exception e) { + throw new RuntimeException("Can't create an instance of custom Adapter class", e); + } + } else { + return new BasicAdapter(meta); + } + } + + public static final String X_AXIS = "x"; + public static final String Y_AXIS = "y"; + public static final String Z_AXIS = "z"; + + + public static final String VALUE_KEY = "value"; + public static final String ERROR_KEY = "err"; + public static final String LO_KEY = "lo"; + public static final String UP_KEY = "up"; + public static final String TITILE_KEY = "@title"; + + + public static final String X_VALUE_KEY = X_AXIS;//Name.joinString(X_AXIS, VALUE_KEY); + public static final String X_ERROR_KEY = Name.Companion.joinString(X_AXIS, ERROR_KEY); + public static final String Y_VALUE_KEY = Y_AXIS;//Name.joinString(Y_AXIS, VALUE_KEY); + public static final String Y_ERROR_KEY = Y_AXIS + "." + ERROR_KEY; + + public static Value getValue(ValuesAdapter adapter, String axis, Values point) { + return adapter.getComponent(point, axis); + } + + public static Optional optError(ValuesAdapter adapter, String axis, Values point) { + return adapter.optComponent(point, Name.Companion.joinString(axis, ERROR_KEY)); + } + + public static Value getError(ValuesAdapter adapter, String axis, Values point) { + return optError(adapter, axis, point).orElseThrow(() -> new NameNotFoundException("Value not found in the value set")); + } + + public static Double getUpperBound(ValuesAdapter adapter, String axis, Values point) { + return adapter.optComponent(point, Name.Companion.joinString(axis, UP_KEY)).map(Value::getDouble) + .orElseGet(() -> + getValue(adapter, axis, point).getDouble() + + optError(adapter, axis, point).map(Value::getDouble).orElse(0d) + ); + + } + + public static Double getLowerBound(ValuesAdapter adapter, String axis, Values point) { + return adapter.optComponent(point, Name.Companion.joinString(axis, LO_KEY)).map(Value::getDouble) + .orElseGet(() -> + getValue(adapter, axis, point).getDouble() - + optError(adapter, axis, point).map(Value::getDouble).orElse(0d) + ); + } + + /** + * Get a title for the axis from the adapter + * + * @param adapter + * @param axis + * @return + */ + public static String getTitle(ValuesAdapter adapter, String axis) { + return adapter.getMeta().getString(Name.Companion.joinString(axis, TITILE_KEY), axis); + } + + public static Value getXValue(ValuesAdapter adapter, Values point) { + return adapter.getComponent(point, X_VALUE_KEY); + } + + public static Optional optXError(ValuesAdapter adapter, Values point) { + return adapter.optComponent(point, X_ERROR_KEY).map(Value::getDouble); + } + + public static Value getYValue(ValuesAdapter adapter, Values point) { + return adapter.getComponent(point, Y_VALUE_KEY); + } + + public static Optional optYError(ValuesAdapter adapter, Values point) { + return adapter.optComponent(point, Y_ERROR_KEY).map(Value::getDouble); + } + + public static Values buildXYDataPoint(ValuesAdapter adapter, double x, double y, double yErr) { + return ValueMap.Companion.of(new String[]{ + adapter.getComponentName(X_VALUE_KEY), + adapter.getComponentName(Y_VALUE_KEY), + adapter.getComponentName(Y_ERROR_KEY) + }, x, y, yErr); + } + + public static Values buildXYDataPoint(ValuesAdapter adapter, double x, double y) { + return ValueMap.Companion.of(new String[]{ + adapter.getComponentName(X_VALUE_KEY), + adapter.getComponentName(Y_VALUE_KEY) + }, x, y); + } + + public static Values buildXYDataPoint(double x, double y, double yErr) { + return buildXYDataPoint(DEFAULT_XYERR_ADAPTER, x, y, yErr); + } + + public static Values buildXYDataPoint(double x, double y) { + return buildXYDataPoint(DEFAULT_XY_ADAPTER, x, y); + } + + @NotNull + public static ValuesAdapter buildXYAdapter(String xName, String yName) { + return new BasicAdapter(new MetaBuilder().setValue(X_VALUE_KEY, xName).setValue(Y_VALUE_KEY, yName)); + } + + @NotNull + public static ValuesAdapter buildXYAdapter(String xName, String yName, String yErrName) { + return new BasicAdapter(new MetaBuilder() + .setValue(X_VALUE_KEY, xName) + .setValue(Y_VALUE_KEY, yName) + .setValue(Y_ERROR_KEY, yErrName) + ); + } + + @NotNull + public static ValuesAdapter buildXYAdapter(String xName, String xErrName, String yName, String yErrName) { + return new BasicAdapter(new MetaBuilder() + .setValue(X_VALUE_KEY, xName) + .setValue(X_ERROR_KEY, xErrName) + .setValue(Y_VALUE_KEY, yName) + .setValue(Y_ERROR_KEY, yErrName) + ); + } + + + public static ValuesAdapter DEFAULT_XY_ADAPTER = buildXYAdapter(X_VALUE_KEY, Y_VALUE_KEY); + + public static ValuesAdapter DEFAULT_XYERR_ADAPTER = buildXYAdapter(X_VALUE_KEY, Y_VALUE_KEY, Y_ERROR_KEY); + + /** + * Return a default TableFormat corresponding to adapter. Fills all of components explicitly presented in adapter as well as given components. + * + * @return + */ + public static TableFormat getFormat(ValuesAdapter adapter, String... components) { + TableFormatBuilder builder = new TableFormatBuilder(); + + Stream.concat(adapter.listComponents(), Stream.of(components)).distinct().forEach(component -> + builder.addNumber(adapter.getComponentName(component), component) + ); + + return builder.build(); + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/tables/BasicAdapter.java b/dataforge-core/src/main/java/hep/dataforge/tables/BasicAdapter.java new file mode 100644 index 00000000..d1fdaea3 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/tables/BasicAdapter.java @@ -0,0 +1,65 @@ +package hep.dataforge.tables; + +import hep.dataforge.meta.Meta; +import hep.dataforge.meta.MetaHolder; +import hep.dataforge.meta.MetaUtils; +import hep.dataforge.values.Value; +import hep.dataforge.values.Values; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +/** + * Simple hash map based adapter + */ +public class BasicAdapter extends MetaHolder implements ValuesAdapter { + + private final Map mappings = new HashMap<>(6); + + public BasicAdapter(Meta meta) { + super(meta); + updateMapping(); + } + + private void updateMapping() { + MetaUtils.valueStream(getMeta()).forEach(pair -> { + mappings.put(pair.getFirst().toString(), pair.getSecond().getString()); +// +// if(pair.getKey().endsWith(".value")){ +// mappings.put(pair.getKey().replace(".value",""),pair.getValue().getString()); +// } else { +// mappings.put(pair.getKey(), pair.getValue().getString()); +// } + }); + } + + @Override + public Optional optComponent(Values values, String component) { + return values.optValue(getComponentName(component)); + } + + @Override + public String getComponentName(String component) { + return mappings.getOrDefault(component, component); + } + + @Override + public Stream listComponents() { + return mappings.keySet().stream(); + } + + @NotNull + @Override + public Meta toMeta() { + if (getClass() == BasicAdapter.class) { + return getMeta(); + } else { + //for custom adapters + return getMeta().getBuilder().putValue("@class", getClass().getName()); + } + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/tables/Filtering.java b/dataforge-core/src/main/java/hep/dataforge/tables/Filtering.java new file mode 100644 index 00000000..de7b9353 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/tables/Filtering.java @@ -0,0 +1,168 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.tables; + +import hep.dataforge.description.NodeDef; +import hep.dataforge.exceptions.NameNotFoundException; +import hep.dataforge.meta.Meta; +import hep.dataforge.values.Value; +import hep.dataforge.values.ValueFactory; +import hep.dataforge.values.ValueRange; +import hep.dataforge.values.Values; + +import java.util.List; +import java.util.function.Predicate; + +/** + *

+ * Filtering class.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class Filtering { + + /** + * A simple condition that DataPoint has all presented tags + * + * @param tags a {@link java.lang.String} object. + * @return a {@link java.util.function.Predicate} object. + */ + public static Predicate getTagCondition(final String... tags) { + return (Values dp) -> { + boolean pass = true; + for (String tag : tags) { + pass = pass & dp.hasTag(tag); + } + return pass; + }; + } + + /** + * Simple condition that field with name {@code valueName} is from a to b. + * Both could be infinite. + * + * @param valueName a {@link java.lang.String} object. + * @param a a {@link hep.dataforge.values.Value} object. + * @param b a {@link hep.dataforge.values.Value} object. + * @return a {@link java.util.function.Predicate} object. + */ + public static Predicate getValueCondition(final String valueName, final Value a, final Value b) { + if (a.compareTo(b) >= 0) { + throw new IllegalArgumentException(); + } + return (Values dp) -> { + if (!dp.getNames().contains(valueName)) { + return false; + } else { + try { + return new ValueRange(a, b).contains((dp.getValue(valueName))); + } catch (NameNotFoundException ex) { + //Считаем, что еÑли такого имени нет, то теÑÑ‚ автоматичеÑки провален + return false; + } + } + }; + } + + public static Predicate getValueEqualityCondition(final String valueName, final Value equals) { + return (Values dp) -> { + if (!dp.getNames().contains(valueName)) { + return false; + } else { + try { + return (dp.getValue(valueName).equals(equals)); + } catch (NameNotFoundException ex) { + //Считаем, что еÑли такого имени нет, то теÑÑ‚ автоматичеÑки провален + return false; + } + } + }; + } + + /** + *

+ * buildConditionSet.

+ * + * @param an a {@link hep.dataforge.meta.Meta} object. + * @return a {@link java.util.function.Predicate} object. + */ + @NodeDef(key = "is", multiple = true, info = "The filtering condition that must be satisfied", descriptor = "method::hep.dataforge.tables.Filtering.buildCondition") + @NodeDef(key = "not", multiple = true, info = "The filtering condition that must NOT be satisfied", descriptor = "method::hep.dataforge.tables.Filtering.buildCondition") + public static Predicate buildConditionSet(Meta an) { + Predicate res = null; + if (an.hasMeta("is")) { + for (Meta condition : an.getMetaList("is")) { + Predicate predicate = buildCondition(condition); + if (res == null) { + res = predicate; + } else { + res = res.or(predicate); + } + } + } + + if (an.hasMeta("not")) { + for (Meta condition : an.getMetaList("not")) { + Predicate predicate = buildCondition(condition).negate(); + if (res == null) { + res = predicate; + } else { + res = res.or(predicate); + } + } + } + return res; + } + + /** + *

+ * buildCondition.

+ * + * @param an a {@link hep.dataforge.meta.Meta} object. + * @return a {@link java.util.function.Predicate} object. + */ + public static Predicate buildCondition(Meta an) { + Predicate res = null; + if (an.hasValue("tag")) { + List tagList = an.getValue("tag").getList(); + String[] tags = new String[tagList.size()]; + for (int i = 0; i < tagList.size(); i++) { + tags[i] = tagList.get(i).getString(); + } + res = getTagCondition(tags); + } + if (an.hasValue("value")) { + String valueName = an.getValue("value").getString(); + Predicate valueCondition; + if (an.hasValue("equals")) { + Value equals = an.getValue("equals"); + valueCondition = getValueEqualityCondition(valueName, equals); + } else { + Value from = an.getValue("from", ValueFactory.of(Double.NEGATIVE_INFINITY)); + Value to = an.getValue("to", ValueFactory.of(Double.POSITIVE_INFINITY)); + valueCondition = getValueCondition(valueName, from, to); + } + + if (res == null) { + res = valueCondition; + } else { + res = res.or(valueCondition); + } + } + return res; + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/tables/MaskPoint.java b/dataforge-core/src/main/java/hep/dataforge/tables/MaskPoint.java new file mode 100644 index 00000000..f9b2acbd --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/tables/MaskPoint.java @@ -0,0 +1,70 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.tables; + +import hep.dataforge.exceptions.NameNotFoundException; +import hep.dataforge.names.NameList; +import hep.dataforge.values.Value; +import hep.dataforge.values.Values; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; +import java.util.Optional; + +/** + *

+ * MaskPoint class.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class MaskPoint implements Values { + + private final Map nameMap; + private final Values source; + private final NameList names; + + public MaskPoint(Values source, Map nameMap) { + this.source = source; + this.nameMap = nameMap; + names = new NameList(nameMap.keySet()); + } + + @Override + public boolean hasValue(@NotNull String path) { + return nameMap.containsKey(path); + } + + /** + * {@inheritDoc} + * + * @return + */ + @Override + public NameList getNames() { + return names; + } + + /** + * {@inheritDoc} + */ + @NotNull + @Override + public Optional optValue(@NotNull String name) throws NameNotFoundException { + return source.optValue(nameMap.get(name)); + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/tables/NavigableValuesSource.java b/dataforge-core/src/main/java/hep/dataforge/tables/NavigableValuesSource.java new file mode 100644 index 00000000..9aa03c78 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/tables/NavigableValuesSource.java @@ -0,0 +1,35 @@ +package hep.dataforge.tables; + +import hep.dataforge.exceptions.NameNotFoundException; +import hep.dataforge.values.Value; +import hep.dataforge.values.Values; + +/** + * Created by darksnake on 14-Apr-17. + */ +public interface NavigableValuesSource extends ValuesSource { + Values getRow(int i); + + /** + * + * Get a specific value + * @param name + * @param index + * @return + * @throws NameNotFoundException + */ + default Value get(String name, int index) throws NameNotFoundException { + return getRow(index).getValue(name); + } + + default double getDouble(String name, int index){ + return get(name, index).getDouble(); + } + + /** + * Number of rows in the table + * + * @return + */ + int size(); +} diff --git a/dataforge-core/src/main/java/hep/dataforge/tables/ReadPointSetAction.java b/dataforge-core/src/main/java/hep/dataforge/tables/ReadPointSetAction.java new file mode 100644 index 00000000..383259ba --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/tables/ReadPointSetAction.java @@ -0,0 +1,77 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.tables; + +import hep.dataforge.actions.OneToOneAction; +import hep.dataforge.context.Context; +import hep.dataforge.description.TypedActionDef; +import hep.dataforge.io.LineIterator; +import hep.dataforge.meta.Laminate; + +import java.io.IOException; +import java.io.InputStream; + +@TypedActionDef(name = "readdataset", inputType = InputStream.class, outputType = Table.class, info = "Read DataSet from text file") +public class ReadPointSetAction extends OneToOneAction { + + public ReadPointSetAction() { + super("readdataset", InputStream.class, Table.class); + } + + public static final String READ_DATA_SET_ACTION_NAME = "readdataset"; + + /** + * {@inheritDoc} + * + * @param source + * @return + */ + @Override + protected Table execute(Context context, String name, InputStream source, Laminate meta) { + ListTable.Builder fileData; + + String encoding = meta.getString("encoding", "UTF-8"); + try { + LineIterator iterator = new LineIterator(source, encoding); + + String dataSetName = meta.getString("dataSetName", name); + + ValuesReader dpReader; + if (meta.hasValue("columnNames")) { + String[] names = meta.getStringArray("columnNames"); + dpReader = new ValuesReader(iterator, names); + fileData = new ListTable.Builder(names); + } else { + dpReader = new ValuesReader(iterator, iterator.next()); + fileData = new ListTable.Builder(dataSetName); + } + + int headerLines = meta.getInt("headerLength", 0); + if (headerLines > 0) { + dpReader.skip(headerLines); + } + + while (dpReader.hasNext()) { + fileData.row(dpReader.next()); + } + + } catch (IOException ex) { + throw new RuntimeException("Can't open data source"); + } + return fileData.build(); + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/tables/SimpleValuesSource.java b/dataforge-core/src/main/java/hep/dataforge/tables/SimpleValuesSource.java new file mode 100644 index 00000000..2769f206 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/tables/SimpleValuesSource.java @@ -0,0 +1,51 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.tables; + +import hep.dataforge.values.Values; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * + * @author Alexander Nozik + */ +public class SimpleValuesSource implements ValuesSource { + + private final TableFormat format; + private final List points; + + public SimpleValuesSource(TableFormat format, List points) { + this.format = format; + this.points = new ArrayList<>(points); + } + + public SimpleValuesSource(TableFormat format) { + this.format = format; + this.points = new ArrayList<>(); + } + + public SimpleValuesSource(String... names) { + this.format = MetaTableFormat.Companion.forNames(names); + this.points = new ArrayList<>(); + } + + public TableFormat getFormat() { + return format; + } + + @Override + public Iterator iterator() { + return points.iterator(); + } + + public void addRow(Values p) { + this.points.add(p); + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/tables/TableFormat.java b/dataforge-core/src/main/java/hep/dataforge/tables/TableFormat.java new file mode 100644 index 00000000..3ef10a87 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/tables/TableFormat.java @@ -0,0 +1,74 @@ +package hep.dataforge.tables; + +import hep.dataforge.exceptions.NameNotFoundException; +import hep.dataforge.meta.Meta; +import hep.dataforge.meta.MetaBuilder; +import hep.dataforge.meta.MetaMorph; +import hep.dataforge.names.NameList; +import hep.dataforge.names.NameSetContainer; +import org.jetbrains.annotations.NotNull; + +import java.util.Iterator; +import java.util.stream.Stream; + +/** + * A description of table columns + * Created by darksnake on 12.07.2017. + */ +public interface TableFormat extends NameSetContainer, Iterable, MetaMorph { + + @NotNull + static TableFormat subFormat(TableFormat format, String... names) { + NameList theNames = new NameList(names); + return () -> format.getColumns().filter(it -> theNames.contains(it.getName())); + } + + /** + * Convert this table format to its meta representation + * + * @return + */ + @NotNull + @Override + default Meta toMeta() { + MetaBuilder builder = new MetaBuilder("format"); + getColumns().forEach(column -> builder.putNode(column.toMeta())); + return builder; + } + + /** + * Names of the columns + * + * @return + */ + @Override + default NameList getNames() { + return new NameList(getColumns().map(ColumnFormat::getName)); + } + + /** + * Column format for given name + * + * @param column + * @return + */ + default ColumnFormat getColumn(String column) { + return getColumns() + .filter(it -> it.getName().equals(column)) + .findFirst() + .orElseThrow(() -> new NameNotFoundException(column)); + } + + /** + * Stream of column formats + * + * @return + */ + Stream getColumns(); + + @NotNull + @Override + default Iterator iterator() { + return getColumns().iterator(); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/tables/TableFormatBuilder.java b/dataforge-core/src/main/java/hep/dataforge/tables/TableFormatBuilder.java new file mode 100644 index 00000000..c3c4c341 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/tables/TableFormatBuilder.java @@ -0,0 +1,205 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.tables; + +import hep.dataforge.exceptions.NamingException; +import hep.dataforge.meta.Meta; +import hep.dataforge.meta.MetaBuilder; +import hep.dataforge.values.ValueType; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static hep.dataforge.meta.MetaNode.DEFAULT_META_NAME; +import static hep.dataforge.tables.ColumnFormat.TAG_KEY; + +public class TableFormatBuilder implements TableFormat { + + /** + * Build a format containing given columns. If some of columns do not exist in initial format, + * they are replaced by default column format. + * + * @param format initial format + * @param names + * @return + */ + public static TableFormat subSet(TableFormat format, String... names) { + MetaBuilder newFormat = new MetaBuilder(format.toMeta()); + newFormat.setNode("column", Stream.of(names) + .map(name -> format.getColumn(name).toMeta()) + .collect(Collectors.toList()) + ); + return new MetaTableFormat(newFormat); + } + + private MetaBuilder builder = new MetaBuilder("format"); + private Map columns; +// private MetaBuilder defaultColumn; + + public TableFormatBuilder() { + columns = new LinkedHashMap<>(); + } + + public TableFormatBuilder(String... names) { + this(); + for (String name : names) { + add(name); + } + } + + public TableFormatBuilder(Iterable names) { + this(); + for (String name : names) { + add(name); + } + } + + private MetaBuilder add(String name, String... tags) { + if (!columns.containsKey(name)) { + MetaBuilder columnBuilder = new MetaBuilder("column").putValue("name", name).putValues(TAG_KEY, tags); + columns.put(name, columnBuilder); + return columnBuilder; + } else { + throw new NamingException("Duplicate name"); + } + } + + public TableFormatBuilder addColumn(String name, String... roles) { + add(name, roles); + return this; + } + + public TableFormatBuilder addColumn(String name, String title, ValueType type, String... tags) { + add(name, tags).setValue("title", title).setValue("type", type.toString()); + return this; + } + + public TableFormatBuilder addColumn(String name, String title, int width, ValueType type, String... tags) { + add(name, tags).setValue("title", title) + .setValue("type", type.toString()) + .setValue("width", width); + return this; + } + + public TableFormatBuilder addColumn(String name, ValueType type, String... tags) { + add(name, tags).setValue("type", type.toString()); + return this; + } + + public TableFormatBuilder addColumn(String name, int width, ValueType type, String... tags) { + add(name, tags).setValue("type", type.toString()).setValue("width", width); + return this; + } + + public TableFormatBuilder addString(String name, String... tags) { + return addColumn(name, ValueType.STRING, tags); + } + + public TableFormatBuilder addNumber(String name, String... tags) { + return addColumn(name, ValueType.NUMBER, tags); + } + + public TableFormatBuilder addTime(String name, String... tags) { + return addColumn(name, ValueType.TIME, tags); + } + + /** + * Add default timestamp column named "timestamp" + * + * @return + */ + public TableFormatBuilder addTime() { + return addColumn("timestamp", ValueType.TIME, "timestamp"); + } + + public TableFormatBuilder setType(String name, ValueType... type) { + if (!columns.containsKey(name)) { + add(name); + } + for (ValueType t : type) { + columns.get(name).putValue("type", t.toString()); + } + return this; + } + + /** + * Add custom meta to the table + * + * @param meta + * @return + */ + public TableFormatBuilder setMeta(Meta meta) { + builder.setNode(DEFAULT_META_NAME, meta); + return this; + } + + /** + * Apply transformation to custom meta section + * + * @param transform + * @return + */ + public TableFormatBuilder updateMeta(Consumer transform) { + MetaBuilder meta = new MetaBuilder(builder.getMeta(DEFAULT_META_NAME, Meta.empty())); + transform.accept(meta); + setMeta(meta); + return this; + } + +// public TableFormatBuilder setRole(String name, String... role) { +// if (!columns.containsKey(name)) { +// add(name); +// } +// for (String r : role) { +// columns.get(name).setValue("role", r); +// } +// return this; +// } + + public TableFormatBuilder setTitle(String name, String title) { + if (!columns.containsKey(name)) { + add(name); + } + columns.get(name).putValue("title", title); + return this; + } + + public TableFormatBuilder setWidth(String name, int width) { + if (!columns.containsKey(name)) { + add(name); + } + columns.get(name).putValue("width", width); + return this; + } + + public TableFormat build() { + for (Meta m : columns.values()) { + builder.putNode(m); + } +// if (defaultColumn != null) { +// builder.setNode("defaultColumn", defaultColumn); +// } + return new MetaTableFormat(builder.build()); + } + + @Override + public Stream getColumns() { + return columns.values().stream().map(ColumnFormat::new); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/tables/TransformTableAction.java b/dataforge-core/src/main/java/hep/dataforge/tables/TransformTableAction.java new file mode 100644 index 00000000..e735c38f --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/tables/TransformTableAction.java @@ -0,0 +1,81 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.tables; + +import hep.dataforge.actions.OneToOneAction; +import hep.dataforge.context.Context; +import hep.dataforge.description.NodeDef; +import hep.dataforge.description.TypedActionDef; +import hep.dataforge.meta.Laminate; +import hep.dataforge.meta.Meta; +import hep.dataforge.values.Values; + +import java.util.function.Predicate; + +import static hep.dataforge.tables.Filtering.buildConditionSet; + +/** + * Table transformation action + * + * @author Alexander Nozik + * @version $Id: $Id + */ +@TypedActionDef(name = "transformTable", inputType = Table.class, outputType = Table.class, info = "Filter dataset with given filtering rules") +@NodeDef(key = "filters", required = true, info = "The filtering condition.", descriptor = "method::hep.dataforge.tables.Filtering.buildConditionSet") +public class TransformTableAction extends OneToOneAction { + + public TransformTableAction() { + super("transformTable", Table.class, Table.class); + } + + /** + * {@inheritDoc} + * + * @return + */ + @Override + protected Table execute(Context context, String name, Table input, Laminate meta) { + Predicate filterSet = buildFilter(meta); + + Table res; + if (filterSet != null) { + res = Tables.filter(input, filterSet); + } else { + res = input; + } + if (res.size() == 0) { + throw new RuntimeException("The resulting DataSet is empty"); + } + return res; + } + + private Predicate buildFilter(Meta meta) { + Predicate res = null; + if (meta.hasMeta("filter")) { + for (Meta filter : meta.getMetaList("filter")) { + Predicate predicate = buildConditionSet(filter); + if (res == null) { + res = predicate; + } else { + res = res.and(predicate); + } + } + return res; + } else { + return null; + } + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/tables/ValuesAdapter.java b/dataforge-core/src/main/java/hep/dataforge/tables/ValuesAdapter.java new file mode 100644 index 00000000..9765bc50 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/tables/ValuesAdapter.java @@ -0,0 +1,77 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.tables; + +import hep.dataforge.exceptions.NameNotFoundException; +import hep.dataforge.meta.MetaMorph; +import hep.dataforge.meta.MetaUtils; +import hep.dataforge.meta.Metoid; +import hep.dataforge.values.Value; +import hep.dataforge.values.Values; + +import java.util.Optional; +import java.util.stream.Stream; + +/** + * An adapter to read specific components from Values + * + * @author Alexander Nozik + */ +public interface ValuesAdapter extends Metoid, MetaMorph { + + String ADAPTER_KEY = "@adapter"; + + /** + * Get a value with specific designation from given DataPoint + * + * @param point + * @param component + * @return + */ + default Value getComponent(Values point, String component) { + return optComponent(point, component).orElseThrow(() -> new NameNotFoundException("Component with name " + component + " not found", component)); + } + + default public String getComponentName(String component) { + return getMeta().getString(component); + } + + /** + * Opt a specific component + * + * @param values + * @param component + * @return + */ + default Optional optComponent(Values values, String component) { + return values.optValue(getComponentName(component)); + } + + default Optional optDouble(Values values, String component) { + return optComponent(values, component).map(Value::getDouble); + } + + /** + * List all components declared in this adapter. + * + * @return + */ + default Stream listComponents() { + return MetaUtils.valueStream(getMeta()).map(it -> it.getFirst().toString()); + } + + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/tables/ValuesListener.java b/dataforge-core/src/main/java/hep/dataforge/tables/ValuesListener.java new file mode 100644 index 00000000..05247226 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/tables/ValuesListener.java @@ -0,0 +1,30 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.tables; + +import hep.dataforge.values.Values; + +import java.util.function.Consumer; + +/** + * A functional interface representing DataPoint listener + * + * @author Alexander Nozik + * @version $Id: $Id + */ +public interface ValuesListener extends Consumer { + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/tables/VirtualDataPoint.java b/dataforge-core/src/main/java/hep/dataforge/tables/VirtualDataPoint.java new file mode 100644 index 00000000..00378174 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/tables/VirtualDataPoint.java @@ -0,0 +1,50 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.tables; + +import hep.dataforge.names.NameList; +import hep.dataforge.values.Value; +import hep.dataforge.values.Values; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; +import java.util.function.BiFunction; + +/** + * A DataPoint that uses another data point or another object as a source but does not copy data + * itself + * + * @author Alexander Nozik + */ +public class VirtualDataPoint implements Values { + + private final S source; + private final BiFunction transformation; + private final NameList names; + + public VirtualDataPoint(S source, BiFunction transformation, String... names) { + this.source = source; + this.transformation = transformation; + this.names = new NameList(names); + } + + @NotNull + @Override + public Optional optValue(@NotNull String name) { + if (hasValue(name)) { + return Optional.ofNullable(transformation.apply(name, source)); + } else { + return Optional.empty(); + } + } + + @Override + public NameList getNames() { + return names; + } + + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/tables/XYPoissonAdapter.java b/dataforge-core/src/main/java/hep/dataforge/tables/XYPoissonAdapter.java new file mode 100644 index 00000000..c9cb9081 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/tables/XYPoissonAdapter.java @@ -0,0 +1,60 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.tables; + +import hep.dataforge.meta.Meta; +import hep.dataforge.utils.Optionals; +import hep.dataforge.values.Value; +import hep.dataforge.values.ValueFactory; +import hep.dataforge.values.Values; + +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Stream; + +/** + * Specialized adapter for poissonian distributed values + * + * @author darksnake + */ +public class XYPoissonAdapter extends BasicAdapter { + + public XYPoissonAdapter(Meta meta) { + super(meta); + } + + @Override + public Optional optComponent(Values values, String component) { + if (Objects.equals(component, Adapters.Y_ERROR_KEY)) { + return Optionals.either(super.optComponent(values, Adapters.Y_ERROR_KEY)).or(() -> { + double y = Adapters.getYValue(this, values).getDouble(); + if (y > 0) { + return Optional.of(ValueFactory.of(Math.sqrt(y))); + } else { + return Optional.empty(); + } + }).opt(); + } else { + return super.optComponent(values, component); + } + } + + @Override + public Stream listComponents() { + return Stream.concat(super.listComponents(), Stream.of(Adapters.Y_ERROR_KEY)).distinct(); + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/utils/ArgumentChecker.java b/dataforge-core/src/main/java/hep/dataforge/utils/ArgumentChecker.java new file mode 100644 index 00000000..7ab6e1b9 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/utils/ArgumentChecker.java @@ -0,0 +1,59 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.utils; + +/** + * An utility class providing easy access to Commons Math argument check + * exceptions + * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class ArgumentChecker { + + /** + *

checkEqualDimensions.

+ * + * @param dimensions a int. + */ + public static void checkEqualDimensions(int... dimensions) { + if (dimensions.length > 1) { + for (int i = 1; i < dimensions.length; i++) { + if (dimensions[i] != dimensions[0]) { + throw new IllegalArgumentException(); + } + + } + } + } + + /** + *

checkNotNull.

+ * + * @param obj a {@link java.lang.Object} object. + */ + public static void checkNotNull(Object... obj) { + if (obj == null) { + throw new IllegalArgumentException(); + } + for (Object obj1 : obj) { + if (obj1 == null) { + throw new IllegalArgumentException(); + } + } + } + +} diff --git a/dataforge-core/src/main/java/hep/dataforge/utils/ContextMetaFactory.java b/dataforge-core/src/main/java/hep/dataforge/utils/ContextMetaFactory.java new file mode 100644 index 00000000..8c5abbc6 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/utils/ContextMetaFactory.java @@ -0,0 +1,36 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.utils; + +import hep.dataforge.context.Context; +import hep.dataforge.context.Global; +import hep.dataforge.meta.Meta; +import hep.dataforge.meta.MetaBuilder; + +/** + * A generic parameterized factory interface + * + * @author Alexander Nozik + * @param + */ +@FunctionalInterface +public interface ContextMetaFactory { + T build(Context context, Meta meta); + + default T build(){ + return build(Global.INSTANCE, MetaBuilder.buildEmpty(null)); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/utils/DateTimeUtils.java b/dataforge-core/src/main/java/hep/dataforge/utils/DateTimeUtils.java new file mode 100644 index 00000000..ee89ba8e --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/utils/DateTimeUtils.java @@ -0,0 +1,33 @@ +package hep.dataforge.utils; + +import hep.dataforge.values.TimeValue; +import hep.dataforge.values.Value; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; + +/** + * Created by darksnake on 14-Oct-16. + */ +public class DateTimeUtils { + private static DateTimeFormatter SUFFIX_FORMATTER = DateTimeFormatter.ofPattern("dd_MM_yyyy_A"); + + public static Instant now() { + return LocalDateTime.now().toInstant(ZoneOffset.UTC); + } + + /** + * Build a unique file suffix based on current date-time + * + * @return + */ + public static String fileSuffix() { + return SUFFIX_FORMATTER.format(LocalDateTime.now()); + } + + public static Value nowValue() { + return new TimeValue(now()); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/utils/GenericBuilder.java b/dataforge-core/src/main/java/hep/dataforge/utils/GenericBuilder.java new file mode 100644 index 00000000..1c27b35d --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/utils/GenericBuilder.java @@ -0,0 +1,28 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.utils; + +/** + * A universal GenericBuilder pattern. + * @param The type of builder result + * @param the type of builder itself + * @author Alexander Nozik + */ +public interface GenericBuilder { + /** + * current state of the builder + * @return + */ + B self(); + + /** + * Build resulting object + * @return + */ + T build(); + + //TODO seam builder after builder +} diff --git a/dataforge-core/src/main/java/hep/dataforge/utils/InvocationTarget.java b/dataforge-core/src/main/java/hep/dataforge/utils/InvocationTarget.java new file mode 100644 index 00000000..e4c1b624 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/utils/InvocationTarget.java @@ -0,0 +1,37 @@ +package hep.dataforge.utils; + +import java.lang.reflect.InvocationTargetException; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; + +/** + * An object that could receive a custom command. By default uses reflections to invoke a public method + * Created by darksnake on 06-May-17. + */ +public interface InvocationTarget { + /** + * @param command + * @param arguments + * @return + */ + default Object invoke(String command, Object... arguments) { + try { + return getClass().getMethod(command, Stream.of(arguments).map(Object::getClass).toArray(i -> new Class[i])) + .invoke(this, arguments); + } catch (IllegalAccessException | NoSuchMethodException e) { + throw new RuntimeException("Can't resolve command", e); + } catch (InvocationTargetException e) { + throw new RuntimeException("Execution of command failed", e); + } + } + + /** + * Execute a command asynchronously + * @param command + * @param arguments + * @return + */ + default CompletableFuture invokeInFuture(String command, Object... arguments) { + return CompletableFuture.supplyAsync(() -> invoke(command, arguments)); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/utils/MetaFactory.java b/dataforge-core/src/main/java/hep/dataforge/utils/MetaFactory.java new file mode 100644 index 00000000..95eab92e --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/utils/MetaFactory.java @@ -0,0 +1,17 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.utils; + +import hep.dataforge.meta.Meta; + +/** + * + * @author Alexander Nozik + */ +@FunctionalInterface +public interface MetaFactory { + T build(Meta meta); +} diff --git a/dataforge-core/src/main/java/hep/dataforge/utils/Misc.java b/dataforge-core/src/main/java/hep/dataforge/utils/Misc.java new file mode 100644 index 00000000..df1166c1 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/utils/Misc.java @@ -0,0 +1,45 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.utils; + +import java.nio.charset.Charset; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.CancellationException; + +/** + * @author Alexander Nozik + */ +public class Misc { + public static final Charset UTF = Charset.forName("UTF-8"); + + /** + * A synchronized lru cache + * + * @param + * @param + * @param maxItems + * @return + */ + public static Map getLRUCache(int maxItems) { + return Collections.synchronizedMap(new LinkedHashMap() { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return super.size() > maxItems; + } + }); + } + + /** + * Check if current thread is interrupted and throw exception if it is + */ + public static void checkThread() { + if (Thread.currentThread().isInterrupted()) { + throw new CancellationException(); + } + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/utils/MultiCounter.java b/dataforge-core/src/main/java/hep/dataforge/utils/MultiCounter.java new file mode 100644 index 00000000..78bdb1ff --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/utils/MultiCounter.java @@ -0,0 +1,98 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.utils; + +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.Map; + +import static java.lang.Integer.valueOf; + +/** + * TODO еÑÑ‚ÑŒ объект MultiDimensionalCounter, иÑползовать его? + * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class MultiCounter { + + private HashMap counts = new HashMap<>(); + String name; + + /** + *

Constructor for MultiCounter.

+ * + * @param name a {@link java.lang.String} object. + */ + public MultiCounter(String name) { + this.name = name; + } + + /** + *

getCount.

+ * + * @param name a {@link java.lang.String} object. + * @return a int. + */ + public int getCount(String name) { + return counts.getOrDefault(name, -1); + } + + /** + *

increase.

+ * + * @param name a {@link java.lang.String} object. + */ + synchronized public void increase(String name) { + if (counts.containsKey(name)) { + Integer count = counts.get(name); + counts.remove(name); + counts.put(name, count + 1); + } else { + counts.put(name, valueOf(1)); + } + } + + /** + *

print.

+ * + * @param out a {@link java.io.PrintWriter} object. + */ + public void print(PrintWriter out) { + out.printf("%nValues for counter %s%n%n", this.name); + for (Map.Entry entry : counts.entrySet()) { + + String keyName = entry.getKey(); + Integer value = entry.getValue(); + out.printf("%s : %d%n", keyName, value); + } + } + + /** + *

reset.

+ * + * @param name a {@link java.lang.String} object. + */ + public void reset(String name) { + counts.remove(name); + } + /** + *

resetAll.

+ */ + public void resetAll() { + this.counts = new HashMap<>(); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/utils/NamingUtils.java b/dataforge-core/src/main/java/hep/dataforge/utils/NamingUtils.java new file mode 100644 index 00000000..fcc52ecf --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/utils/NamingUtils.java @@ -0,0 +1,120 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.utils; + +import hep.dataforge.exceptions.NamingException; +import java.util.function.Predicate; + +/** + * TODO Ñменить название + * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class NamingUtils { + + /** + *

+ * getMainName.

+ * + * @param name a {@link java.lang.String} object. + * @return a {@link java.lang.String} object. + */ + public static String getMainName(String name) { + String[] parse = parseName(name); + return parse[0]; + } + + /** + *

+ * getSubName.

+ * + * @param name a {@link java.lang.String} object. + * @return a {@link java.lang.String} object. + */ + public static String getSubName(String name) { + int index = name.indexOf("."); + if (index < 0) { + return null; + } else { + return name.substring(index + 1); + } + } + + /** + *

+ * parseArray.

+ * + * @param array a {@link java.lang.String} object. + * @return an array of {@link java.lang.String} objects. + */ + public static String[] parseArray(String array) { + String str = array.trim(); + String[] res; + if (str.startsWith("[")) { + if (str.endsWith("]")) { + str = str.substring(1, str.length() - 1); + } else { + throw new NamingException("Wrong syntax in array of names"); + } + } + res = str.split(","); + for (int i = 0; i < res.length; i++) { + res[i] = res[i].trim(); + } + + return res; + } + + /** + *

+ * buildArray.

+ * + * @param array an array of {@link java.lang.String} objects. + * @return a {@link java.lang.String} object. + */ + public static String buildArray(String[] array) { + StringBuilder res = new StringBuilder(); + res.append("["); + for (int i = 0; i < array.length - 1; i++) { + res.append(array[i]); + res.append(","); + } + res.append(array[array.length - 1]); + res.append("]"); + return res.toString(); + } + + /** + *

+ * parseName.

+ * + * @param name a {@link java.lang.String} object. + * @return an array of {@link java.lang.String} objects. + */ + public static String[] parseName(String name) { + return name.trim().split("."); + } + + public static boolean wildcardMatch(String mask, String str) { + return str.matches(mask.replace("?", ".?").replace("*", ".*?")); + } + + public static Predicate wildcardMatchCondition(String mask) { + String pattern = mask.replace("?", ".?").replace("*", ".*?"); + return str -> str.matches(pattern); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/utils/Optionals.java b/dataforge-core/src/main/java/hep/dataforge/utils/Optionals.java new file mode 100644 index 00000000..106857e7 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/utils/Optionals.java @@ -0,0 +1,63 @@ +package hep.dataforge.utils; + +import java.util.*; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Temporary Either implementation. Waiting for Java 9 to replace + * Created by darksnake on 19-Apr-17. + */ +public class Optionals { + + public static Optionals either(Supplier>... sups) { + return new Optionals(Arrays.asList(sups)); + } + + public static Optionals either(Stream>> stream) { + return new Optionals(stream.collect(Collectors.toList())); + } + + public static Optionals either(Optional opt) { + return new Optionals(() -> opt); + } + + public static Optionals either(T opt) { + return new Optionals(() -> Optional.ofNullable(opt)); + } + + private Optionals(Collection>> set) { + this.sups.addAll(set); + } + + private Optionals(Supplier> sup) { + this.sups.add(sup); + } + + private final List>> sups = new ArrayList<>(); + + public Optionals or(Supplier> opt) { + List>> newSups = new ArrayList<>(sups); + newSups.add(opt); + return new Optionals<>(newSups); + } + + public Optionals or(Optional opt) { + return or(() -> opt); + } + + public Optionals or(V val) { + return or(() -> Optional.ofNullable(val)); + } + + public Optional opt() { + for (Supplier> sup : sups) { + Optional opt = sup.get(); + if (opt.isPresent()) { + return opt; + } + } + return Optional.empty(); + } +} diff --git a/dataforge-core/src/main/java/hep/dataforge/utils/ReferenceRegistry.java b/dataforge-core/src/main/java/hep/dataforge/utils/ReferenceRegistry.java new file mode 100644 index 00000000..7b4f2a62 --- /dev/null +++ b/dataforge-core/src/main/java/hep/dataforge/utils/ReferenceRegistry.java @@ -0,0 +1,104 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.utils; + +import org.jetbrains.annotations.NotNull; + +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.util.*; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * A registry of listener references. References could be weak to allow GC to + * finalize referenced objects. + * + * @author Alexander Nozik + */ +public class ReferenceRegistry extends AbstractCollection { + + private final Set> weakRegistry = new CopyOnWriteArraySet<>(); + /** + * Used only to store strongreferences + */ + private final Set strongRegistry = new CopyOnWriteArraySet<>(); + + /** + * Listeners could be added either as strong references or weak references. Thread safe + * + * @param obj + */ + public boolean add(T obj, boolean isStrong) { + if (isStrong) { + strongRegistry.add(obj); + } + return weakRegistry.add(new WeakReference<>(obj)); + } + + /** + * Add a strong reference to registry + * + * @param obj + */ + @Override + public boolean add(T obj) { + return add(obj, true); + } + + @Override + public boolean remove(Object obj) { + strongRegistry.remove(obj); + Reference reference = weakRegistry.stream().filter(it -> obj.equals(it.get())).findFirst().orElse(null); + return reference != null && weakRegistry.remove(reference); + } + + @Override + public boolean removeIf(Predicate filter) { + return strongRegistry.removeIf(filter) && weakRegistry.removeIf(it -> filter.test(it.get())); + } + + @Override + public void clear() { + strongRegistry.clear(); + weakRegistry.clear(); + } + + /** + * Clean up all null entries from weak registry + */ + private void cleanUp() { + weakRegistry.removeIf(ref -> ref.get() == null); + } + + @NotNull + @Override + public Iterator iterator() { +// cleanUp(); + return weakRegistry.stream().map(Reference::get).filter(Objects::nonNull).iterator(); + } + + + @Override + public int size() { + return weakRegistry.size(); + } + + public Optional findFirst(Predicate predicate) { + return this.weakRegistry.stream() + .map(Reference::get) + .filter((t) -> t != null && predicate.test(t)) + .findFirst(); + } + + public List findAll(Predicate predicate) { + return this.weakRegistry.stream() + .map(Reference::get) + .filter((t) -> t != null && predicate.test(t)) + .collect(Collectors.toList()); + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/CoreExtensions.kt b/dataforge-core/src/main/kotlin/hep/dataforge/CoreExtensions.kt new file mode 100644 index 00000000..9c037749 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/CoreExtensions.kt @@ -0,0 +1,179 @@ +package hep.dataforge + +import hep.dataforge.context.Context +import hep.dataforge.context.ContextBuilder +import hep.dataforge.context.Global +import hep.dataforge.context.Plugin +import hep.dataforge.data.Data +import hep.dataforge.data.NamedData +import hep.dataforge.goals.Coal +import hep.dataforge.goals.Goal +import hep.dataforge.goals.StaticGoal +import hep.dataforge.goals.pipe +import hep.dataforge.meta.* +import hep.dataforge.values.NamedValue +import hep.dataforge.values.Value +import hep.dataforge.values.ValueProvider +import hep.dataforge.values.ValueType +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.future.await +import java.time.Instant +import java.util.stream.Collectors +import kotlin.streams.asSequence + +/** + * Core DataForge classes extensions + * Created by darksnake on 26-Apr-17. + */ + +// Context extensions + +/** + * Build a child plugin using given name, plugins list and custom build script + */ +fun Context.buildContext(name: String, vararg plugins: Class, init: ContextBuilder.() -> Unit = {}): Context { + val builder = ContextBuilder(name, this) + plugins.forEach { + builder.plugin(it) + } + builder.apply(init) + return builder.build() +} + +fun buildContext(name: String, vararg plugins: Class, init: ContextBuilder.() -> Unit = {}): Context { + return Global.buildContext(name = name, plugins = *plugins, init = init) +} + +//Value operations + +operator fun Value.plus(other: Value): Value = + when (this.type) { + ValueType.NUMBER -> Value.of(this.number + other.number); + ValueType.TIME -> Value.of(Instant.ofEpochMilli(this.time.toEpochMilli() + other.time.toEpochMilli())) + ValueType.NULL -> other; + else -> throw RuntimeException("Operation plus not allowed for ${this.type}"); + } + +operator fun Value.minus(other: Value): Value = + when (this.type) { + ValueType.NUMBER -> Value.of(this.number - other.number); + ValueType.TIME -> Value.of(Instant.ofEpochMilli(this.time.toEpochMilli() - other.time.toEpochMilli())) + else -> throw RuntimeException("Operation minus not allowed for ${this.type}"); + } + +operator fun Value.times(other: Value): Value = + when (this.type) { + ValueType.NUMBER -> Value.of(this.number * other.number); + else -> throw RuntimeException("Operation minus not allowed for ${this.type}"); + } + +operator fun Value.plus(other: Any): Value = this + Value.of(other) + +operator fun Value.minus(other: Any): Value = this - Value.of(other) + +operator fun Value.times(other: Any): Value = this * Value.of(other) + +//Value comparison + +fun Value?.isNull(): Boolean = this == null || this.isNull + + +//Meta operations + +operator fun Meta.get(path: String): Value = this.getValue(path) + +operator fun Value.get(index: Int): Value = this.list[index] + +operator fun > MutableMetaNode.set(path: String, value: Any): T = this.setValue(path, value) + +operator fun > T.plusAssign(value: NamedValue) { + this.setValue(value.name, value.anonymous); +} + +operator fun > T.plusAssign(meta: Meta) { + this.putNode(meta); +} + +/** + * Create a new meta with added node + */ +operator fun Meta.plus(meta: Meta): Meta = this.builder.putNode(meta) + +/** + * create a new meta with added value + */ +operator fun Meta.plus(value: NamedValue): Meta = this.builder.putValue(value.name, value.anonymous) + +/** + * Get a value if it is present and apply action to it + */ +fun ValueProvider.useValue(valueName: String, action: (Value) -> Unit) { + optValue(valueName).ifPresent(action) +} + +/** + * Get a meta node if it is present and apply action to it + */ +fun MetaProvider.useMeta(metaName: String, action: (Meta) -> Unit) { + optMeta(metaName).ifPresent(action) +} + +/** + * Perform some action on each meta element of the list if it is present + */ +fun Meta.useEachMeta(metaName: String, action: (Meta) -> Unit) { + if (hasMeta(metaName)) { + getMetaList(metaName).forEach(action) + } +} + +/** + * Get all meta nodes with the given name and apply action to them + */ +fun Meta.useMetaList(metaName: String, action: (List) -> Unit) { + if (hasMeta(metaName)) { + action(getMetaList(metaName)) + } +} + +fun Meta.asMap(transform: (Value) -> T): Map { + return MetaUtils.valueStream(this).collect(Collectors.toMap({ it.first.toString() }, { transform(it.second) })) +} + +val > MetaNode.childNodes: List + get() = this.nodeNames.flatMap { this.getMetaList(it).stream() }.toList() + +val Meta.childNodes: List + get() = this.nodeNames.flatMap { this.getMetaList(it).stream() }.toList() + +val Meta.values: Map + get() = this.valueNames.asSequence().associate { it to this.getValue(it) } + +/** + * Configure a configurable using in-place build meta + */ +fun T.configure(transform: KMetaBuilder.() -> Unit): T { + this.configure(buildMeta(this.config.name, transform)); + return this; +} + + + +//suspending functions + +/** + * Use goal as a suspending function + */ +suspend fun Goal.await(): R { + return when { + this is Coal -> this.await()//A special case for Coal + this is StaticGoal -> this.get()//optimization for static goals + else -> this.asCompletableFuture().await() + } +} + +inline fun Data.pipe(scope: CoroutineScope, noinline transform: suspend (T) -> R): Data = + Data(R::class.java, this.goal.pipe(scope, transform), this.meta) + +inline fun NamedData.pipe(scope: CoroutineScope, noinline transform: suspend (T) -> R): NamedData = + NamedData(this.name, R::class.java, this.goal.pipe(scope, transform), this.meta) \ No newline at end of file diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/Misc.kt b/dataforge-core/src/main/kotlin/hep/dataforge/Misc.kt new file mode 100644 index 00000000..33d6a81e --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/Misc.kt @@ -0,0 +1,19 @@ +package hep.dataforge + +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaNode +import hep.dataforge.names.Name +import java.util.stream.Collectors +import java.util.stream.Stream + +fun Stream.toList(): List { + return collect(Collectors.toList()) +} + +fun String?.asName(): Name { + return Name.of(this) +} + +fun > MetaNode.findNode(path: String, predicate: Meta.() -> Boolean): MetaNode? { + return this.getMetaList(path).firstOrNull(predicate) +} \ No newline at end of file diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/Named.kt b/dataforge-core/src/main/kotlin/hep/dataforge/Named.kt new file mode 100644 index 00000000..d3f58944 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/Named.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge + +/** + * Any object that have name + * + * @author Alexander Nozik + */ +interface Named { + + /** + * The name of this object instance + * + * @return + */ + val name: String + + companion object { + const val ANONYMOUS = "" + + /** + * Get the name of given object. If object is Named its name is used, + * otherwise, use Object.toString + * + * @param obj + * @return + */ + fun nameOf(obj: Any): String { + return if (obj is Named) { + obj.name + } else { + obj.toString() + } + } + } +} + +/** + * Check if this object has an empty name and therefore is anonymous. + * @return + */ +val Named.isAnonymous: Boolean + get() = this.name == Named.ANONYMOUS diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/NumberExtensions.kt b/dataforge-core/src/main/kotlin/hep/dataforge/NumberExtensions.kt new file mode 100644 index 00000000..0ec22ad7 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/NumberExtensions.kt @@ -0,0 +1,56 @@ +package hep.dataforge + +import java.math.BigDecimal +import java.math.MathContext + +/** + * Extension of basic java classes + * Created by darksnake on 29-Apr-17. + */ + +//Number + +/** + * Convert a number to BigDecimal + */ +fun Number.toBigDecimal(mathContext: MathContext = MathContext.UNLIMITED): BigDecimal { + return when(this){ + is BigDecimal -> this + is Int, is Long, is Double, is Float -> this.toBigDecimal(mathContext) + else -> this.toDouble().toBigDecimal(mathContext) + } +} + +operator fun Number.plus(other: Number): Number { + return this.toBigDecimal().add(other.toBigDecimal()); +} + +operator fun Number.minus(other: Number): Number { + return this.toBigDecimal().subtract(other.toBigDecimal()); +} + +operator fun Number.div(other: Number): Number { + return this.toBigDecimal().divide(other.toBigDecimal()); +} + +operator fun Number.times(other: Number): Number { + return this.toBigDecimal().multiply(other.toBigDecimal()); +} + +operator fun Number.compareTo(other: Number): Int { + return this.toBigDecimal().compareTo(other.toBigDecimal()); +} + +/** + * Generate iterable sequence from range + */ +infix fun ClosedFloatingPointRange.step(step: Double): Sequence { + return generateSequence(this.start) { + val res = it + step; + if (res > this.endInclusive) { + null + } else { + res + } + } +} \ No newline at end of file diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/Types.kt b/dataforge-core/src/main/kotlin/hep/dataforge/Types.kt new file mode 100644 index 00000000..7053b906 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/Types.kt @@ -0,0 +1,29 @@ +package hep.dataforge + +import java.lang.annotation.Inherited +import kotlin.reflect.KClass +import kotlin.reflect.full.findAnnotation +import kotlin.reflect.jvm.jvmName + +/** + * A text label for internal DataForge type classification. Alternative for mime container type. + * + * The DataForge type notation presumes that type `A.B.C` is the subtype of `A.B` + */ +@MustBeDocumented +@Inherited +annotation class Type(val id: String) + +/** + * Utils to get type of classes and objects + */ +object Types { + operator fun get(cl: KClass<*>): String { + return cl.findAnnotation()?.id ?: cl.jvmName + } + + operator fun get(obj: Any): String{ + return get(obj::class) + } +} + diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/Utils.kt b/dataforge-core/src/main/kotlin/hep/dataforge/Utils.kt new file mode 100644 index 00000000..a7fe3de9 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/Utils.kt @@ -0,0 +1,81 @@ +package hep.dataforge + +import java.lang.reflect.AnnotatedElement +import java.util.* +import kotlin.reflect.KAnnotatedElement +import kotlin.reflect.KClass +import kotlin.reflect.KFunction +import kotlin.reflect.jvm.javaMethod + +inline val Optional?.nullable: T? + get() = this?.orElse(null) + +inline val T?.optional: Optional + get() = Optional.ofNullable(this) + +/** + * To use instead of ?: for block operations + */ +inline fun T?.orElse(sup: () -> T): T { + return this?: sup.invoke() +} + + +//Annotations + +fun AnnotatedElement.listAnnotations(type: Class, searchSuper: Boolean = true): List { + if (this is Class<*>) { + val res = ArrayList() + val array = getDeclaredAnnotationsByType(type) + res.addAll(Arrays.asList(*array)) + if (searchSuper) { + val superClass = this.superclass + if (superClass != null) { + res.addAll(superClass.listAnnotations(type, true)) + } + for (cl in this.interfaces) { + res.addAll(cl.listAnnotations(type, true)) + } + } + return res; + } else { + val array = getAnnotationsByType(type) + return Arrays.asList(*array) + } +} + +fun KAnnotatedElement.listAnnotations(type: KClass, searchSuper: Boolean = true): List { + return when { + this is KClass<*> -> return this.java.listAnnotations(type.java, searchSuper) + this is KFunction<*> -> return this.javaMethod?.listAnnotations(type.java, searchSuper) ?: emptyList() + else -> { + //TODO does not work for containers + this.annotations.filterIsInstance(type.java) + } + + } +} + +inline fun KAnnotatedElement.listAnnotations(searchSuper: Boolean = true): List { + return listAnnotations(T::class, searchSuper) +} + + +//object IO { +// /** +// * Create an output stream that copies its output into each of given streams +// */ +// fun mirrorOutput(vararg outs: OutputStream): OutputStream { +// return object : OutputStream() { +// override fun write(b: Int) = outs.forEach { it.write(b) } +// +// override fun write(b: ByteArray?) = outs.forEach { it.write(b) } +// +// override fun write(b: ByteArray?, off: Int, len: Int) = outs.forEach { it.write(b, off, len) } +// +// override fun flush() = outs.forEach { it.flush() } +// +// override fun close() = outs.forEach { it.close() } +// } +// } +//} \ No newline at end of file diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/actions/Action.kt b/dataforge-core/src/main/kotlin/hep/dataforge/actions/Action.kt new file mode 100644 index 00000000..a59da0a3 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/actions/Action.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.actions + +import hep.dataforge.Named +import hep.dataforge.context.Context +import hep.dataforge.data.DataNode +import hep.dataforge.description.Described +import hep.dataforge.meta.Meta + +/** + * The action is an independent process that could be performed on one + * dependency or set of uniform dependencies. The number and naming of results + * not necessarily is the same as in input. + * + * + * @author Alexander Nozik + * @param - the main type of input data + * @param - the main type of resulting object +*/ +interface Action : Named, Described { + + fun run(context: Context, data: DataNode, actionMeta: Meta): DataNode + + companion object { + const val ACTION_TARGET = "action" + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/actions/GenericAction.kt b/dataforge-core/src/main/kotlin/hep/dataforge/actions/GenericAction.kt new file mode 100644 index 00000000..435856a7 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/actions/GenericAction.kt @@ -0,0 +1,162 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.actions + +import hep.dataforge.context.Context +import hep.dataforge.data.DataNode +import hep.dataforge.data.DataSet +import hep.dataforge.data.NamedData +import hep.dataforge.description.ActionDescriptor +import hep.dataforge.description.TypedActionDef +import hep.dataforge.io.output.Output +import hep.dataforge.io.output.Output.Companion.TEXT_TYPE +import hep.dataforge.io.output.SelfRendered +import hep.dataforge.meta.Laminate +import hep.dataforge.meta.Meta +import hep.dataforge.names.Name +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.util.concurrent.ExecutorService +import java.util.stream.Stream + +/** + * A basic implementation of Action interface + * + * @param + * @param + * @author Alexander Nozik + */ +abstract class GenericAction( + override val name: String, + val inputType: Class, + val outputType: Class +) : Action, SelfRendered { + + private val definition: TypedActionDef? + get() = if (javaClass.isAnnotationPresent(TypedActionDef::class.java)) { + javaClass.getAnnotation(TypedActionDef::class.java) + } else { + null + } + + protected val isEmptyInputAllowed: Boolean + get() = false + + + /** + * {@inheritDoc} + * + * @return + */ + override val descriptor: ActionDescriptor + get() = ActionDescriptor.build(this) + + protected fun isParallelExecutionAllowed(meta: Meta): Boolean { + return meta.getBoolean("@allowParallel", true) + } + + /** + * Generate the name of the resulting data based on name of input data and action meta + * + * @param inputName + * @param actionMeta + * @return + */ + protected fun getResultName(inputName: String, actionMeta: Meta): String { + var res = inputName + if (actionMeta.hasValue(RESULT_GROUP_KEY)) { + res = Name.joinString(actionMeta.getString(RESULT_GROUP_KEY, ""), res) + } + return res + } + + /** + * Wrap result of single or separate executions into DataNode + * + * @return + */ + protected fun wrap(name: String, meta: Meta, result: Stream>): DataNode { + val builder = DataSet.edit(outputType) + result.forEach { builder.add(it) } + builder.name = name + builder.meta = meta + return builder.build() + } + + protected fun checkInput(input: DataNode<*>) { + if (!inputType.isAssignableFrom(input.type)) { + //FIXME add specific exception + throw RuntimeException(String.format("Type mismatch on action %s start. Expected %s but found %s.", + name, inputType.simpleName, input.type.name)) + } + } + + /** + * Get common singleThreadExecutor for this action + * + * @return + */ + protected fun getExecutorService(context: Context, meta: Meta): ExecutorService { + return if (isParallelExecutionAllowed(meta)) { + context.executors.defaultExecutor + } else { + context.dispatcher + } + + } + + /** + * Return the root process name for this action + * + * @return + */ + protected fun getThreadName(actionMeta: Meta): String { + return actionMeta.getString("@action.thread", "action::$name") + } + + protected fun getLogger(context: Context, actionMeta: Meta): Logger { + return LoggerFactory.getLogger(context.name + "." + actionMeta.getString("@action.logger", getThreadName(actionMeta))) + } + + override fun render(output: Output, meta: Meta) { + output.render(descriptor, meta) + } + + protected fun inputMeta(context: Context, vararg meta: Meta): Laminate { + return Laminate(*meta).withDescriptor(descriptor) + } + + /** + * Push given object to output + * + * @param context + * @param dataName + * @param obj + * @param meta + */ + @JvmOverloads + protected fun render(context: Context, dataName: String, obj: Any, meta: Meta = Meta.empty()) { + context.output[dataName, name, TEXT_TYPE].render(obj, meta) + } + + protected fun report(context: Context, reportName: String, entry: String, vararg params: Any) { + context.history.getChronicle(reportName).report(entry, *params) + } + + companion object { + const val RESULT_GROUP_KEY = "@action.resultGroup" + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/actions/KAction.kt b/dataforge-core/src/main/kotlin/hep/dataforge/actions/KAction.kt new file mode 100644 index 00000000..7da190d3 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/actions/KAction.kt @@ -0,0 +1,284 @@ +package hep.dataforge.actions + +import hep.dataforge.context.Context +import hep.dataforge.data.DataFilter +import hep.dataforge.data.DataNode +import hep.dataforge.data.DataSet +import hep.dataforge.data.NamedData +import hep.dataforge.exceptions.AnonymousNotAlowedException +import hep.dataforge.goals.Goal +import hep.dataforge.goals.join +import hep.dataforge.goals.pipe +import hep.dataforge.io.history.Chronicle +import hep.dataforge.meta.Laminate +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.names.Name +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.plus +import kotlinx.coroutines.runBlocking +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.util.stream.Collectors +import java.util.stream.Stream + + +class ActionEnv(val context: Context, val name: String, val meta: Meta, val log: Chronicle) + + +/** + * Action environment + */ +class PipeBuilder(val context: Context, val actionName: String, var name: String, var meta: MetaBuilder) { + lateinit var result: suspend ActionEnv.(T) -> R; + + var logger: Logger = LoggerFactory.getLogger("${context.name}[$actionName:$name]") + + /** + * Calculate the result of goal + */ + fun result(f: suspend ActionEnv.(T) -> R) { + result = f; + } +} + +/** + * Coroutine based pipe action. + * KPipe supports custom CoroutineContext which allows to override specific way coroutines are created. + * KPipe is executed inside {@link PipeBuilder} object, which holds name of given data, execution context, meta and log. + * Notice that name and meta could be changed. Output object receives modified name and meta. + */ +class KPipe( + actionName: String, + inputType: Class, + outputType: Class, + private val action: PipeBuilder.() -> Unit) : GenericAction(actionName, inputType, outputType) { + + + override fun run(context: Context, data: DataNode, actionMeta: Meta): DataNode { + if (!this.inputType.isAssignableFrom(data.type)) { + throw RuntimeException("Type mismatch in action $name. $inputType expected, but ${data.type} received") + } + val builder = DataSet.edit(outputType) + data.dataStream(true).forEach { item -> + val laminate = Laminate(item.meta, actionMeta) + + val prefix = actionMeta.getString("@namePrefix", "") + val suffix = actionMeta.getString("@nameSuffix", "") + + val pipe = PipeBuilder( + context, + name, + prefix + item.name + suffix, + laminate.builder + ).apply(action) + + val env = ActionEnv( + context, + pipe.name, + pipe.meta, + context.history.getChronicle(Name.joinString(pipe.name, name)) + ) + + val dispatcher = context + getExecutorService(context, laminate).asCoroutineDispatcher() + + val goal = item.goal.pipe(dispatcher) { goalData -> + pipe.logger.debug("Starting action ${this.name} on ${pipe.name}") + pipe.result.invoke(env, goalData).also { + pipe.logger.debug("Finished action ${this.name} on ${pipe.name}") + } + } + val res = NamedData(env.name, outputType, goal, env.meta) + builder.add(res) + } + + return builder.build(); + } +} + + +class JoinGroup(val context: Context, name: String? = null, internal val node: DataNode) { + var name: String = name ?: node.name; + var meta: MetaBuilder = node.meta.builder + + lateinit var result: suspend ActionEnv.(Map) -> R + + fun result(f: suspend ActionEnv.(Map) -> R) { + this.result = f; + } + +} + + +class JoinGroupBuilder(val context: Context, val meta: Meta) { + + + private val groupRules: MutableList<(Context, DataNode) -> List>> = ArrayList(); + + /** + * introduce grouping by value name + */ + fun byValue(tag: String, defaultTag: String = "@default", action: JoinGroup.() -> Unit) { + groupRules += { context, node -> + GroupBuilder.byValue(tag, defaultTag).group(node).map { + JoinGroup(context, null, node).apply(action) + } + } + } + + /** + * Add a single fixed group to grouping rules + */ + fun group(groupName: String, filter: DataFilter, action: JoinGroup.() -> Unit) { + groupRules += { context, node -> + listOf( + JoinGroup(context, groupName, filter.filter(node)).apply(action) + ) + } + } + + /** + * Apply transformation to the whole node + */ + fun result(resultName: String, f: suspend ActionEnv.(Map) -> R) { + groupRules += { context, node -> + listOf(JoinGroup(context, resultName, node).apply { + //TODO Meta mutator could be inserted here + result(f) + }) + } + } + + internal fun buildGroups(context: Context, input: DataNode): Stream> { + return groupRules.stream().flatMap { it.invoke(context, input).stream() } + } + +} + + +/** + * The same rules as for KPipe + */ +class KJoin( + actionName: String, + inputType: Class, + outputType: Class, + private val action: JoinGroupBuilder.() -> Unit) : GenericAction(actionName, inputType, outputType) { + + override fun run(context: Context, data: DataNode, actionMeta: Meta): DataNode { + if (!this.inputType.isAssignableFrom(data.type)) { + throw RuntimeException("Type mismatch in action $name. $inputType expected, but ${data.type} received") + } + + val builder = DataSet.edit(outputType) + + JoinGroupBuilder(context, actionMeta).apply(action).buildGroups(context, data).forEach { group -> + + val laminate = Laminate(group.meta, actionMeta) + + val goalMap: Map> = group.node + .dataStream() + .filter { it.isValid } + .collect(Collectors.toMap({ it.name }, { it.goal })) + + val groupName: String = group.name; + + if (groupName.isEmpty()) { + throw AnonymousNotAlowedException("Anonymous groups are not allowed"); + } + + val env = ActionEnv( + context, + groupName, + laminate.builder, + context.history.getChronicle(Name.joinString(groupName, name)) + ) + + val dispatcher = context + getExecutorService(context, group.meta).asCoroutineDispatcher() + + val goal = goalMap.join(dispatcher) { group.result.invoke(env, it) } + val res = NamedData(env.name, outputType, goal, env.meta) + builder.add(res) + } + + return builder.build(); + } + +} + + +class FragmentEnv(val context: Context, val name: String, var meta: MetaBuilder, val log: Chronicle) { + lateinit var result: suspend (T) -> R + + fun result(f: suspend (T) -> R) { + result = f; + } +} + + +class SplitBuilder(val context: Context, val name: String, val meta: Meta) { + internal val fragments: MutableMap.() -> Unit> = HashMap() + + /** + * Add new fragment building rule. If the framgent not defined, result won't be available even if it is present in the map + * @param name the name of a fragment + * @param rule the rule to transform fragment name and meta using + */ + fun fragment(name: String, rule: FragmentEnv.() -> Unit) { + fragments[name] = rule + } +} + +class KSplit( + actionName: String, + inputType: Class, + outputType: Class, + private val action: SplitBuilder.() -> Unit) : GenericAction(actionName, inputType, outputType) { + + override fun run(context: Context, data: DataNode, actionMeta: Meta): DataNode { + if (!this.inputType.isAssignableFrom(data.type)) { + throw RuntimeException("Type mismatch in action $name. $inputType expected, but ${data.type} received") + } + + val builder = DataSet.edit(outputType) + + + runBlocking { + data.dataStream(true).forEach { + + val laminate = Laminate(it.meta, actionMeta) + + val split = SplitBuilder(context, it.name, actionMeta).apply(action) + + + val dispatcher = context + getExecutorService(context, laminate).asCoroutineDispatcher() + + // Create a map of results in a single goal + //val commonGoal = it.goal.pipe(dispatcher) { split.result.invoke(env, it) } + + // apply individual fragment rules to result + split.fragments.forEach { name, rule -> + val env = FragmentEnv( + context, + name, + laminate.builder, + context.history.getChronicle(Name.joinString(it.name, name)) + ) + + rule.invoke(env) + + val goal = it.goal.pipe(dispatcher, env.result) + + val res = NamedData(env.name, outputType, goal, env.meta) + builder.add(res) + } + } + } + + return builder.build(); + } +} + +inline fun DataNode.pipe(context: Context, meta: Meta, name: String = "pipe", noinline action: PipeBuilder.() -> Unit): DataNode { + return KPipe(name, T::class.java, R::class.java, action).run(context, this, meta); +} \ No newline at end of file diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/cache/CachePlugin.kt b/dataforge-core/src/main/kotlin/hep/dataforge/cache/CachePlugin.kt new file mode 100644 index 00000000..afd7d1a5 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/cache/CachePlugin.kt @@ -0,0 +1,208 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.cache + +import hep.dataforge.context.BasicPlugin +import hep.dataforge.context.Plugin +import hep.dataforge.context.PluginDef +import hep.dataforge.context.PluginFactory +import hep.dataforge.data.Data +import hep.dataforge.data.DataNode +import hep.dataforge.data.DataTree +import hep.dataforge.data.NamedData +import hep.dataforge.goals.Goal +import hep.dataforge.goals.GoalListener +import hep.dataforge.meta.Meta +import hep.dataforge.names.Name +import java.io.Serializable +import java.util.concurrent.CompletableFuture +import java.util.stream.Stream +import javax.cache.Cache +import javax.cache.CacheException +import javax.cache.CacheManager +import javax.cache.Caching + +/** + * @author Alexander Nozik + */ +@PluginDef(name = "cache", group = "hep.dataforge", info = "Data caching plugin") +class CachePlugin(meta: Meta) : BasicPlugin(meta) { + + /** + * Set cache bypass condition for data + * + * @param bypass + */ + var bypass: (Data<*>) -> Boolean = { _ -> false } + + private val manager: CacheManager by lazy { + try { + Caching.getCachingProvider(context.classLoader).cacheManager.also { + context.logger.info("Loaded cache manager $it") + } + } catch (ex: CacheException) { + context.logger.warn("Cache provider not found. Will use default cache implementation.") + DefaultCacheManager(context, meta) + } + } + + override fun detach() { + super.detach() + manager.close() + } + + + fun cache(cacheName: String, data: Data, id: Meta): Data { + if (bypass(data) || !Serializable::class.java.isAssignableFrom(data.type)) { + return data + } else { + val cache = getCache(cacheName, data.type) + val cachedGoal = object : Goal { + private val result = CompletableFuture() + + override fun dependencies(): Stream> { + return if (cache.containsKey(id)) { + Stream.empty() + } else { + Stream.of(data.goal) + } + } + + override fun run() { + //TODO add executor + synchronized(cache) { + when { + data.goal.isDone -> data.future.thenAccept { result.complete(it) } + cache.containsKey(id) -> { + logger.info("Cached result found. Restoring data from cache for id {}", id.hashCode()) + CompletableFuture.supplyAsync { cache.get(id) }.whenComplete { res, err -> + if (res != null) { + result.complete(res) + } else { + evalData() + } + + if (err != null) { + logger.error("Failed to load data from cache", err) + } + } + } + else -> evalData() + } + } + } + + private fun evalData() { + data.goal.run() + data.goal.onComplete { res, err -> + if (err != null) { + result.completeExceptionally(err) + } else { + result.complete(res) + try { + cache.put(id, res) + } catch (ex: Exception) { + context.logger.error("Failed to put result into the cache", ex) + } + + } + } + } + + override fun asCompletableFuture(): CompletableFuture { + return result + } + + override fun isRunning(): Boolean { + return !result.isDone + } + + override fun registerListener(listener: GoalListener) { + //do nothing + } + } + return Data(data.type, cachedGoal, data.meta) + } + } + + fun cacheNode(cacheName: String, node: DataNode, nodeId: Meta): DataNode { + val builder = DataTree.edit(node.type).also { + it.name = node.name + it.meta = node.meta + //recursively caching nodes + node.nodeStream(false).forEach { child -> + it.add(cacheNode(Name.joinString(cacheName, child.name), child, nodeId)) + } + //caching direct data children + node.dataStream(false).forEach { datum -> + it.putData(datum.name, cache(cacheName, datum, nodeId.builder.setValue("dataName", datum.name))) + } + } + + return builder.build() + } + + fun cacheNode(cacheName: String, node: DataNode, idFactory: (NamedData<*>) -> Meta): DataNode { + val builder = DataTree.edit(node.type).also {cached-> + cached.name = node.name + cached.meta = node.meta + //recursively caching everything + node.dataStream(true).forEach { datum -> + cached.putData(datum.name, cache(cacheName, datum, idFactory.invoke(datum))) + } + } + + return builder.build() + } + + private fun getCache(name: String, type: Class): Cache { + return manager.getCache(name, Meta::class.java, type) + ?: manager.createCache(name, MetaCacheConfiguration(meta, type)) + } + + // @Override + // protected synchronized void applyConfig(Meta config) { + // //reset the manager + // if (manager != null) { + // manager.close(); + // } + // manager = null; + // super.applyConfig(config); + // } + + fun invalidate(cacheName: String) { + manager.destroyCache(cacheName) + } + + fun invalidate() { + manager.cacheNames?.forEach { this.invalidate(it) } + } + + class Factory : PluginFactory() { + override val type: Class + get() = CachePlugin::class.java + + override fun build(meta: Meta): Plugin { + return CachePlugin(meta) + } + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/cache/DataCacheException.kt b/dataforge-core/src/main/kotlin/hep/dataforge/cache/DataCacheException.kt new file mode 100644 index 00000000..52a57fcd --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/cache/DataCacheException.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.cache + +/** + * + * @author Alexander Nozik + */ +class DataCacheException : Exception { + + /** + * Creates a new instance of `DataCacheException` without detail + * message. + */ + constructor() {} + + /** + * Constructs an instance of `DataCacheException` with the + * specified detail message. + * + * @param msg the detail message. + */ + constructor(msg: String) : super(msg) {} + + constructor(message: String, cause: Throwable) : super(message, cause) {} + + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/cache/DefaultCache.kt b/dataforge-core/src/main/kotlin/hep/dataforge/cache/DefaultCache.kt new file mode 100644 index 00000000..4d24d958 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/cache/DefaultCache.kt @@ -0,0 +1,300 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.cache + +import hep.dataforge.Named +import hep.dataforge.context.Context +import hep.dataforge.context.ContextAware +import hep.dataforge.io.envelopes.* +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaHolder +import hep.dataforge.meta.MetaMorph +import hep.dataforge.nullable +import hep.dataforge.utils.Misc +import java.io.* +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardOpenOption.CREATE +import java.nio.file.StandardOpenOption.WRITE +import java.util.* +import javax.cache.Cache +import javax.cache.configuration.CacheEntryListenerConfiguration +import javax.cache.configuration.Configuration +import javax.cache.integration.CompletionListener +import javax.cache.processor.EntryProcessor +import javax.cache.processor.EntryProcessorException +import javax.cache.processor.EntryProcessorResult + +/** + * Default implementation for jCache caching + * Created by darksnake on 10-Feb-17. + */ +class DefaultCache( + private val name: String, + private val manager: DefaultCacheManager, + private val keyType: Class, + private val valueType: Class) : MetaHolder(manager.meta), Cache, ContextAware { + + private val softCache: MutableMap by lazy { + Misc.getLRUCache(meta.getInt("softCache.size", 500)) + } + + private val hardCache = HashMap() + private val cacheDir: Path = manager.rootCacheDir.resolve(name) + get() { + Files.createDirectories(field) + return field + } + + init { + scanDirectory() + } + + // private Envelope read(Path file) { + // return reader.read(file); + // } + + @Synchronized + private fun scanDirectory() { + if (hardCacheEnabled()) { + hardCache.clear() + try { + Files.list(cacheDir).filter { it -> it.endsWith("df") }.forEach { file -> + try { + val envelope = reader.read(file) + hardCache[envelope.meta] = file + } catch (e: Exception) { + logger.error("Failed to read cache file {}. Deleting corrupted file.", file.toString()) + file.toFile().delete() + } + } + } catch (e: IOException) { + throw RuntimeException("Can't list contents of" + cacheDir.toString()) + } + + } + } + + private fun getID(key: K): Meta { + return when (key) { + is Meta -> key + is MetaMorph -> key.toMeta() + else -> throw RuntimeException("Can't convert the cache key to meta") + } + } + + override fun get(key: K): V? { + val id: Meta = getID(key) + + return softCache[key] ?: getFromHardCache(id).map { cacheFile -> + try { + ObjectInputStream(reader.read(cacheFile).data.stream).use { ois -> + (valueType.cast(ois.readObject())).also { + softCache[key] = it + } + } + } catch (ex: Exception) { + logger.error("Failed to read cached object with id '{}' from file with message: {}. Deleting corrupted file.", id.toString(), ex.message) + hardCache.remove(id) + cacheFile.toFile().delete() + null + } + }.nullable + } + + + override fun getAll(keys: Set): Map? { + return null + } + + override fun containsKey(key: K): Boolean { + val id: Meta = getID(key) + return softCache.containsKey(key) || getFromHardCache(id).isPresent + } + + private fun getFromHardCache(id: Meta): Optional { + //work around for meta numeric hashcode inequality + return hardCache.entries.stream().filter { entry -> entry.key == id }.findFirst().map { it.value } + } + + override fun loadAll(keys: Set, replaceExistingValues: Boolean, completionListener: CompletionListener) { + + } + + private fun hardCacheEnabled(): Boolean { + return meta.getBoolean("fileCache.enabled", true) + } + + @Synchronized + override fun put(key: K, data: V) { + val id: Meta = getID(key) + softCache[key] = data + if (hardCacheEnabled() && data is Serializable) { + var fileName = data.javaClass.simpleName + if (data is Named) { + fileName += "[" + (data as Named).name + "]" + } + fileName += Integer.toUnsignedLong(id.hashCode()).toString() + ".df" + + val file = cacheDir.resolve(fileName) + + try { + Files.newOutputStream(file, WRITE, CREATE).use { fos -> + val baos = ByteArrayOutputStream() + val oos = ObjectOutputStream(baos) + oos.writeObject(data) + val builder = EnvelopeBuilder().meta(id).data(baos.toByteArray()) + baos.close() + writer.write(fos, builder.build()) + hardCache.put(id, file) + } + } catch (ex: IOException) { + logger.error("Failed to write data with id hashcode '{}' to file with message: {}", id.hashCode(), ex.message) + } + + } + } + + override fun getAndPut(key: K, value: V): V { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + + override fun putAll(map: Map) { + map.forEach { id, data -> this.put(id, data) } + } + + override fun putIfAbsent(key: K, value: V): Boolean { + throw UnsupportedOperationException() + } + + override fun remove(key: K): Boolean { + throw UnsupportedOperationException() + } + + override fun remove(key: K, oldValue: V): Boolean { + throw UnsupportedOperationException() + } + + override fun getAndRemove(key: K): V { + throw UnsupportedOperationException() + } + + override fun replace(key: K, oldValue: V, newValue: V): Boolean { + throw UnsupportedOperationException() + } + + override fun replace(key: K, value: V): Boolean { + throw UnsupportedOperationException() + } + + override fun getAndReplace(key: K, value: V): V { + throw UnsupportedOperationException() + } + + override fun removeAll(keys: Set) { + throw UnsupportedOperationException() + } + + override fun removeAll() { + clear() + } + + override fun clear() { + //TODO add uninitialized check + softCache.clear() + try { + if (hardCacheEnabled() && Files.exists(cacheDir)) { + cacheDir.toFile().deleteRecursively() + } + } catch (e: IOException) { + logger.error("Failed to delete cache directory {}", cacheDir, e) + } + + } + + @Throws(EntryProcessorException::class) + override fun invoke(key: K, entryProcessor: EntryProcessor, vararg arguments: Any): T { + throw UnsupportedOperationException() + } + + override fun invokeAll(keys: Set, entryProcessor: EntryProcessor, vararg arguments: Any): Map> { + throw UnsupportedOperationException() + } + + override fun getName(): String { + return name + } + + override fun getCacheManager(): DefaultCacheManager { + return manager + } + + override fun close() { + + } + + override fun isClosed(): Boolean { + return false + } + + override fun unwrap(clazz: Class): T { + return clazz.cast(this) + } + + override fun registerCacheEntryListener(cacheEntryListenerConfiguration: CacheEntryListenerConfiguration) { + throw UnsupportedOperationException() + } + + override fun deregisterCacheEntryListener(cacheEntryListenerConfiguration: CacheEntryListenerConfiguration) { + throw UnsupportedOperationException() + } + + override fun iterator(): MutableIterator> { + return softCache.entries.stream() + .map { entry -> DefaultEntry(entry.key) { entry.value } } + .iterator() + } + + + override fun > getConfiguration(clazz: Class): C { + return clazz.cast(MetaCacheConfiguration(meta, valueType)) + } + + override val context: Context = cacheManager.context + + private inner class DefaultEntry(private val key: K, private val supplier: () -> V) : Cache.Entry { + + override fun getKey(): K { + return key + } + + override fun getValue(): V { + return supplier() + } + + override fun unwrap(clazz: Class): T { + return clazz.cast(this) + } + } + + companion object { + + private val reader = DefaultEnvelopeReader() + private val writer = DefaultEnvelopeWriter(DefaultEnvelopeType.INSTANCE, xmlMetaType) + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/cache/DefaultCacheManager.kt b/dataforge-core/src/main/kotlin/hep/dataforge/cache/DefaultCacheManager.kt new file mode 100644 index 00000000..c4798fef --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/cache/DefaultCacheManager.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.cache + +import hep.dataforge.context.Context +import hep.dataforge.context.ContextAware +import hep.dataforge.context.Global +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaHolder +import java.net.URI +import java.nio.file.Path +import java.util.* +import java.util.concurrent.ConcurrentHashMap +import javax.cache.CacheManager +import javax.cache.Caching +import javax.cache.configuration.Configuration +import javax.cache.spi.CachingProvider + +/** + * Created by darksnake on 08-Feb-17. + */ +class DefaultCacheManager(override val context: Context, cfg: Meta) : MetaHolder(cfg), CacheManager, ContextAware { + private var map: MutableMap> = ConcurrentHashMap(); + + val rootCacheDir: Path + get() = context.tmpDir.resolve("cache") + + override fun getCachingProvider(): CachingProvider { + return DefaultCachingProvider(context) + } + + override fun getURI(): URI { + return rootCacheDir.toUri() + } + + override fun getClassLoader(): ClassLoader { + return Caching.getDefaultClassLoader() + } + + override fun getProperties(): Properties { + return Properties() + } + + + @Throws(IllegalArgumentException::class) + override fun > createCache(cacheName: String, configuration: C): DefaultCache { + return DefaultCache(cacheName, this, configuration.keyType, configuration.valueType) + } + + @Suppress("UNCHECKED_CAST") + override fun getCache(cacheName: String, keyType: Class, valueType: Class): DefaultCache { + return map.getOrPut(cacheName) { DefaultCache(cacheName, this, keyType, valueType) } as DefaultCache + } + + @Suppress("UNCHECKED_CAST") + override fun getCache(cacheName: String): DefaultCache { + return map.getOrPut(cacheName) { DefaultCache(cacheName, this, Any::class.java, Any::class.java) } as DefaultCache + } + + override fun getCacheNames(): Iterable { + return map.keys + } + + override fun destroyCache(cacheName: String) { + val cache = map[cacheName] + if (cache != null) { + cache.clear() + cache.close() + map.remove(cacheName) + } + } + + override fun enableManagement(cacheName: String, enabled: Boolean) { + //do nothing + } + + override fun enableStatistics(cacheName: String, enabled: Boolean) { + //do nothing + } + + override fun close() { + map.values.forEach { it.close() } + map.clear() + } + + override fun isClosed(): Boolean { + return map.isEmpty() + } + + override fun unwrap(clazz: Class): T { + return if (clazz == DefaultCacheManager::class.java) { + @Suppress("UNCHECKED_CAST") + DefaultCacheManager(Global, Meta.empty()) as T + } else { + throw IllegalArgumentException("Wrong wrapped class") + } + } + + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/cache/DefaultCachingProvider.kt b/dataforge-core/src/main/kotlin/hep/dataforge/cache/DefaultCachingProvider.kt new file mode 100644 index 00000000..e0fb8b2e --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/cache/DefaultCachingProvider.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.cache + +import hep.dataforge.context.Context +import hep.dataforge.context.ContextAware +import java.net.URI +import java.util.* +import javax.cache.CacheManager +import javax.cache.configuration.OptionalFeature +import javax.cache.spi.CachingProvider + +/** + * Created by darksnake on 08-Feb-17. + */ +class DefaultCachingProvider(override val context: Context) : CachingProvider, ContextAware { + + override fun getCacheManager(uri: URI, classLoader: ClassLoader, properties: Properties): CacheManager? { + return null + } + + override fun getDefaultClassLoader(): ClassLoader { + return context.classLoader + } + + override fun getDefaultURI(): URI? { + return null + } + + override fun getDefaultProperties(): Properties? { + return null + } + + override fun getCacheManager(uri: URI, classLoader: ClassLoader): CacheManager? { + return null + } + + override fun getCacheManager(): CacheManager? { + return null + } + + override fun close() { + + } + + override fun close(classLoader: ClassLoader) { + + } + + override fun close(uri: URI, classLoader: ClassLoader) { + + } + + override fun isSupported(optionalFeature: OptionalFeature): Boolean { + return false + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/cache/MetaCacheConfiguration.kt b/dataforge-core/src/main/kotlin/hep/dataforge/cache/MetaCacheConfiguration.kt new file mode 100644 index 00000000..bf24aa0c --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/cache/MetaCacheConfiguration.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.cache + +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaHolder + +import javax.cache.configuration.Configuration + +/** + * Meta implementation of JCache configuration + * Created by darksnake on 10-Feb-17. + */ +class MetaCacheConfiguration(config: Meta, private val valueType: Class) : MetaHolder(config), Configuration { + + override fun getKeyType(): Class { + return Meta::class.java + } + + override fun getValueType(): Class { + return valueType + } + + override fun isStoreByValue(): Boolean { + return true + } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/context/BasicPlugin.kt b/dataforge-core/src/main/kotlin/hep/dataforge/context/BasicPlugin.kt new file mode 100644 index 00000000..5059f340 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/context/BasicPlugin.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.context + +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaHolder + +/** + * A base for plugin implementation + * + * @author Alexander Nozik + */ +abstract class BasicPlugin(meta: Meta = Meta.empty(), tag: PluginTag? = null) : MetaHolder(meta.sealed), Plugin { + + private var _context: Context? = null + override val context: Context + get() = _context ?: throw RuntimeException("Plugin not attached") + + override fun dependsOn(): Array { + return if (tag.hasValue("dependsOn")) { + tag.getStringArray("dependsOn").map { PluginTag.fromString(it) }.toTypedArray() + } else { + emptyArray() + } + } + + /** + * If tag is not defined, then the name of class is used + * + * @return + */ + override val tag: PluginTag by lazy { tag ?: PluginTag.resolve(javaClass) } + + /** + * Load this plugin to the Global without annotation + */ + fun startGlobal() { + if (_context != null && Global != context) { + Global.logger.warn("Loading plugin as global from non-global context") + } + Global.plugins.load(this) + } + + override fun attach(context: Context) { + this._context = context + } + + override fun detach() { + this._context = null + } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/context/Chronicler.kt b/dataforge-core/src/main/kotlin/hep/dataforge/context/Chronicler.kt new file mode 100644 index 00000000..a72fdaa9 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/context/Chronicler.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.context + +import hep.dataforge.description.ValueDef +import hep.dataforge.io.history.Chronicle +import hep.dataforge.io.history.History +import hep.dataforge.io.history.Record +import hep.dataforge.meta.Meta +import hep.dataforge.names.Name +import hep.dataforge.providers.Provides +import hep.dataforge.values.ValueType +import java.util.* + +@ValueDef( + key = "printHistory", + type = [ValueType.BOOLEAN], + def = "false", + info = "If true, print all incoming records in default context output" +) +@PluginDef( + name = "chronicler", + group = "hep.dataforge", + support = true, + info = "The general support for history logging" +) +class Chronicler(meta: Meta) : BasicPlugin(meta), History { + + private val recordPusher: (Record) -> Unit = { Global.console.render(it) } + + private val root: Chronicle by lazy { + Chronicle( + context.name, + if (context == Global) { + null + } else { + Global.history + } + ).also { + if (meta.getBoolean("printHistory", false)) { + it.addListener(recordPusher) + } + } + } + + override fun getChronicle(): Chronicle = root + + + private val historyCache = HashMap() + + @Provides(Chronicle.CHRONICLE_TARGET) + fun optChronicle(logName: String): Optional { + return Optional.ofNullable(historyCache[logName]) + } + + /** + * get or builder current log creating the whole log hierarchy + * + * @param reportName + * @return + */ + fun getChronicle(reportName: String): Chronicle { + return historyCache[reportName] ?: run { + val name = Name.of(reportName) + val parent: History? = when { + name.length > 1 -> getChronicle(name.cutLast().toString()) + else -> root + } + Chronicle(name.last.toString(), parent).also { + synchronized(this){ + historyCache[reportName] = it + } + } + } + } +} \ No newline at end of file diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/context/Context.kt b/dataforge-core/src/main/kotlin/hep/dataforge/context/Context.kt new file mode 100644 index 00000000..b9eadc42 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/context/Context.kt @@ -0,0 +1,438 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.context + +import hep.dataforge.Named +import hep.dataforge.context.Plugin.Companion.PLUGIN_TARGET +import hep.dataforge.data.binary.Binary +import hep.dataforge.data.binary.StreamBinary +import hep.dataforge.io.IOUtils +import hep.dataforge.io.OutputManager +import hep.dataforge.meta.KMetaBuilder +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaID +import hep.dataforge.meta.buildMeta +import hep.dataforge.nullable +import hep.dataforge.optional +import hep.dataforge.providers.Provider +import hep.dataforge.providers.Provides +import hep.dataforge.providers.ProvidesNames +import hep.dataforge.useMeta +import hep.dataforge.values.BooleanValue +import hep.dataforge.values.Value +import hep.dataforge.values.ValueProvider +import hep.dataforge.values.ValueProvider.Companion.VALUE_TARGET +import kotlinx.coroutines.CoroutineScope +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.File +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.util.* +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.stream.Stream +import kotlin.collections.HashMap +import kotlin.collections.set +import kotlin.coroutines.CoroutineContext +import kotlin.streams.asSequence +import kotlin.streams.asStream + +/** + * + * + * The local environment for anything being done in DataForge framework. Contexts are organized into tree structure with [Global] at the top. + * Each context has a set of named [Value] properties which are taken from parent context in case they are not found in local context. + * Context implements [ValueProvider] interface and therefore could be uses as a value source for substitutions etc. + * Context contains [PluginManager] which could be used any number of configurable named plugins. + * Also Context has its own logger and [OutputManager] to govern all the input and output being made inside the context. + * @author Alexander Nozik + */ +open class Context( + final override val name: String, + val parent: Context? = Global, + classLoader: ClassLoader? = null, + private val properties: MutableMap = ConcurrentHashMap() +) : Provider, ValueProvider, Named, AutoCloseable, MetaID, CoroutineScope { + + /** + * A class loader for this context. Parent class loader is used by default + */ + open val classLoader: ClassLoader = classLoader ?: parent?.classLoader ?: Global.classLoader + + /** + * Plugin manager for this Context + * + * @return + */ + val plugins: PluginManager by lazy { PluginManager(this) } + var logger: Logger = LoggerFactory.getLogger(name) + private val lock by lazy { ContextLock(this) } + + + /** + * Decorator for property with name "inheritOutput" + * + * If true, then [output] would produce between parent output (if not default) and output of this context + */ + private var inheritOutput: Boolean + get() = properties.getOrDefault("inheritOutput", BooleanValue.TRUE).boolean + set(value) = properties.set("inheritOutput", BooleanValue.ofBoolean(value)) + + /** + * Return IO manager of this context. By default parent IOManager is + * returned. + * + * If [inheritOutput] is true and output is present for this context, then produce split between parent output an this + * + * @return the io + */ + var output: OutputManager + get() { + val thisOutput = plugins[OutputManager::class, false] + return when { + parent == null -> thisOutput ?: Global.consoleOutputManager // default console for Global + thisOutput == null -> parent.output// return parent output manager if not defined for this one + !inheritOutput -> thisOutput // do not inherit parent output + else -> { + val prentOutput = parent.output + if (prentOutput === Global.consoleOutputManager) { + thisOutput + } else { + OutputManager.split(thisOutput, parent.output) + } + + } + } + } + set(newOutput) { + //remove old output + lock.operate { + plugins.get(false)?.let { plugins.remove(it) } + plugins.load(newOutput) + } + } + + + /** + * A property showing that dispatch thread is started in the context + */ + private var started = false + + /** + * A dispatch thread executor for current context + * + * @return + */ + val dispatcher: ExecutorService by lazy { + logger.info("Initializing dispatch thread executor in {}", name) + Executors.newSingleThreadExecutor { r -> + Thread(r).apply { + priority = 8 // slightly higher priority + isDaemon = true + name = this@Context.name + "_dispatch" + }.also { started = true } + } + } + + /** + * Find out if context is locked + * + * @return + */ + val isLocked: Boolean + get() = lock.isLocked + + open val history: Chronicler + get() = plugins[Chronicler::class] ?: parent?.history ?: Global.history + + /** + * {@inheritDoc} namespace does not work + */ + override fun optValue(path: String): Optional { + return (properties[path] ?: parent?.optValue(path).nullable).optional + } + + /** + * Add property to context + * + * @param name + * @param value + */ + fun setValue(name: String, value: Any) { + lock.operate { properties[name] = Value.of(value) } + } + + override fun getDefaultTarget(): String { + return Plugin.PLUGIN_TARGET + } + + @Provides(Plugin.PLUGIN_TARGET) + fun getPlugin(pluginName: String): Plugin? { + return plugins.get(PluginTag.fromString(pluginName)) + } + + @ProvidesNames(Plugin.PLUGIN_TARGET) + fun listPlugins(): Collection { + return plugins.map { it.name } + } + + @ProvidesNames(ValueProvider.VALUE_TARGET) + fun listValues(): Collection { + return properties.keys + } + + + fun getProperties(): Map { + return Collections.unmodifiableMap(properties) + } + + + inline fun get(): T? { + return get(T::class.java) + } + + @JvmOverloads + fun load(type: Class, meta: Meta = Meta.empty()): T { + return plugins.load(type, meta) + } + + inline fun load(noinline metaBuilder: KMetaBuilder.() -> Unit = {}): T { + return plugins.load(metaBuilder) + } + + + /** + * Opt a plugin extending given class + * + * @param type + * @param + * @return + */ + operator fun get(type: Class): T? { + return plugins + .stream(true) + .asSequence().filterIsInstance(type) + .firstOrNull() + } + + /** + * Get existing plugin or load it with default meta + */ + fun getOrLoad(type: Class): T { + return get(type) ?: load(type) + } + + private val serviceCache: MutableMap, ServiceLoader<*>> = HashMap() + + /** + * Get stream of services of given class provided by Java SPI or any other service loading API. + * + * @param serviceClass + * @param + * @return + */ + fun serviceStream(serviceClass: Class): Stream { + synchronized(serviceCache) { + val loader: ServiceLoader<*> = serviceCache.getOrPut(serviceClass) { ServiceLoader.load(serviceClass, classLoader) } + return loader.asSequence().filterIsInstance(serviceClass).asStream() + } + } + + /** + * Find specific service provided by java SPI + */ + fun findService(serviceClass: Class, condition: (T) -> Boolean): T? { + return serviceStream(serviceClass).filter(condition).findFirst().nullable + } + + /** + * Get identity for this context + * + * @return + */ + override fun toMeta(): Meta { + return buildMeta("context") { + update(properties) + plugins.stream(true).forEach { plugin -> + if (plugin.javaClass.isAnnotationPresent(PluginDef::class.java)) { + if (!plugin.javaClass.getAnnotation(PluginDef::class.java).support) { + putNode(plugin.toMeta()) + } + + } + } + } + } + + /** + * Lock this context by given object + * + * @param obj + */ + fun lock(obj: Any) { + this.lock.lock(obj) + parent?.lock(obj) + } + + /** + * Unlock the context by given object + * + * @param obj + */ + fun unlock(obj: Any) { + this.lock.unlock(obj) + parent?.unlock(obj) + } + + + /** + * Free up resources associated with this context + * + * @throws Exception + */ + @Throws(Exception::class) + override fun close() { + //detach all plugins + plugins.close() + + if (started) { + dispatcher.shutdown() + } + } + + + /** + * Return the root directory for this IOManager. By convention, Context + * should not have access outside root directory to prevent System damage. + * + * @return a [java.io.File] object. + */ + val rootDir: Path by lazy { + properties[ROOT_DIRECTORY_CONTEXT_KEY] + ?.let { value -> Paths.get(value.string).also { Files.createDirectories(it) } } + ?: parent?.rootDir + ?: File(System.getProperty("user.home")).toPath() + } + + /** + * The working directory for output and temporary files. Is always inside root directory + * + * @return + */ + val workDir: Path by lazy { + val workDirProperty = properties[WORK_DIRECTORY_CONTEXT_KEY] + when { + workDirProperty != null -> rootDir.resolve(workDirProperty.string).also { Files.createDirectories(it) } + else -> rootDir.resolve(".dataforge").also { Files.createDirectories(it) } + } + } + + /** + * Get the default directory for file data. By default uses context root directory + * @return + */ + val dataDir: Path by lazy { + properties[DATA_DIRECTORY_CONTEXT_KEY]?.let { IOUtils.resolvePath(it.string) } ?: rootDir + } + + /** + * The directory for temporary files. This directory could be cleaned up any + * moment. Is always inside root directory. + * + * @return + */ + val tmpDir: Path by lazy { + properties[TEMP_DIRECTORY_CONTEXT_KEY] + ?.let { value -> rootDir.resolve(value.string).also { Files.createDirectories(it) } } + ?: rootDir.resolve(".dataforge/.temp").also { Files.createDirectories(it) } + } + + + fun getDataFile(path: String): FileReference { + return FileReference.openDataFile(this, path) + } + + /** + * Get a file where `path` is relative to root directory or absolute. + * @param path a [java.lang.String] object. + * @return a [java.io.File] object. + */ + fun getFile(path: String): FileReference { + return FileReference.openFile(this, path) + } + + /** + * Get the context based classpath resource + */ + fun getResource(name: String): Binary? { + val resource = classLoader.getResource(name) + return resource?.let { StreamBinary { it.openStream() } } + } + + /** + * For anything but values and plugins, list all elements with given target, provided by plugins + */ + override fun provideAll(target: String, type: Class): Stream { + return if (target == PLUGIN_TARGET || target == VALUE_TARGET) { + super.provideAll(target, type) + } else { + plugins.stream(true).flatMap { it.provideAll(target, type) } + } + } + + + open val executors: ExecutorPlugin + get() = plugins[ExecutorPlugin::class] ?: parent?.executors ?: Global.executors + + override val coroutineContext: CoroutineContext + get() = this.executors.coroutineContext + + companion object { + + const val ROOT_DIRECTORY_CONTEXT_KEY = "rootDir" + const val WORK_DIRECTORY_CONTEXT_KEY = "workDir" + const val DATA_DIRECTORY_CONTEXT_KEY = "dataDir" + const val TEMP_DIRECTORY_CONTEXT_KEY = "tempDir" + + /** + * Build a new context based on given meta + * + * @param name + * @param parent + * @param meta + * @return + */ + @JvmOverloads + @JvmStatic + fun build(name: String, parent: Context = Global, meta: Meta = Meta.empty()): Context { + val builder = ContextBuilder(name, parent) + + meta.useMeta("properties") { builder.properties(it) } + + meta.optString("rootDir").ifPresent { builder.setRootDir(it) } + + meta.optValue("classpath").ifPresent { value -> value.list.stream().map { it.string }.forEach { builder.classPath(it) } } + + meta.getMetaList("plugin").forEach { builder.plugin(it) } + + return builder.build() + } + } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/context/ContextAware.kt b/dataforge-core/src/main/kotlin/hep/dataforge/context/ContextAware.kt new file mode 100644 index 00000000..0f0bcfed --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/context/ContextAware.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.context + +import hep.dataforge.Named +import kotlinx.coroutines.* +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +/** + * The interface for something that encapsulated in context + * + * @author Alexander Nozik + * @version $Id: $Id + */ +interface ContextAware { + /** + * Get context for this object + * + * @return + */ + val context: Context + + @JvmDefault + val logger: Logger + get() = if (this is Named) { + LoggerFactory.getLogger(context.name + "." + (this as Named).name) + } else { + context.logger + } +} + +fun ContextAware.launch( + context: CoroutineContext = EmptyCoroutineContext, + start: CoroutineStart = CoroutineStart.DEFAULT, + block: suspend CoroutineScope.() -> Unit): Job = this.context.launch(context, start, block) + +fun ContextAware.async( + context: CoroutineContext = EmptyCoroutineContext, + start: CoroutineStart = CoroutineStart.DEFAULT, + block: suspend CoroutineScope.() -> R): Deferred = this.context.async(context, start, block) + diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/context/ContextBuilder.kt b/dataforge-core/src/main/kotlin/hep/dataforge/context/ContextBuilder.kt new file mode 100644 index 00000000..2dc8aae5 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/context/ContextBuilder.kt @@ -0,0 +1,217 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.context + +import hep.dataforge.context.Context.Companion.DATA_DIRECTORY_CONTEXT_KEY +import hep.dataforge.context.Context.Companion.ROOT_DIRECTORY_CONTEXT_KEY +import hep.dataforge.io.OutputManager +import hep.dataforge.meta.* +import hep.dataforge.values.Value +import hep.dataforge.values.asValue +import java.io.IOException +import java.net.MalformedURLException +import java.net.URI +import java.net.URL +import java.net.URLClassLoader +import java.nio.file.Files +import java.nio.file.Paths +import java.util.* +import java.util.function.BiPredicate +import kotlin.collections.ArrayList +import kotlin.collections.HashMap + +/** + * A builder for context + */ +class ContextBuilder(var name: String, val parent: Context = Global) { + + val properties = HashMap() + + private val classPath = ArrayList() + + private val plugins = HashSet() + + var output: OutputManager? = null + set(value) { + plugins.removeIf{it is OutputManager} + if (value != null) { + plugins.add(value) + } + } + + var rootDir: String + get() = properties[ROOT_DIRECTORY_CONTEXT_KEY]?.toString() ?: parent.rootDir.toString() + set(value) { + val path = parent.rootDir.resolve(value) + //Add libraries to classpath + val libPath = path.resolve("lib") + if (Files.isDirectory(libPath)) { + classPath(libPath.toUri()) + } + properties[ROOT_DIRECTORY_CONTEXT_KEY] = path.toString().asValue() + } + + var dataDir: String + get() = properties[DATA_DIRECTORY_CONTEXT_KEY]?.toString() + ?: parent.getString(DATA_DIRECTORY_CONTEXT_KEY, parent.rootDir.toString()) + set(value) { + properties[DATA_DIRECTORY_CONTEXT_KEY] = value.asValue() + } + + fun properties(config: Meta): ContextBuilder { + if (config.hasMeta("property")) { + config.getMetaList("property").forEach { propertyNode -> + properties[propertyNode.getString("key")] = propertyNode.getValue("value") + } + } else if (config.name == "properties") { + MetaUtils.valueStream(config).forEach { pair -> properties[pair.first.toString()] = pair.second } + } + return this + } + + fun properties(action: KMetaBuilder.() -> Unit): ContextBuilder = properties(buildMeta("properties", action)) + + fun plugin(plugin: Plugin): ContextBuilder { + this.plugins.add(plugin) + return this + } + + /** + * Load and configure a plugin. Use parent PluginLoader for resolution + * + * @param type + * @param meta + * @return + */ + @JvmOverloads + fun plugin(type: Class, meta: Meta = Meta.empty()): ContextBuilder { + val tag = PluginTag.resolve(type) + return plugin(parent.plugins.pluginLoader[tag, meta]) + } + + inline fun plugin(noinline metaBuilder: KMetaBuilder.() -> Unit = {}): ContextBuilder { + return plugin(T::class.java, buildMeta("plugin", metaBuilder)) + } + + fun plugin(tag: String, meta: Meta): ContextBuilder { + val pluginTag = PluginTag.fromString(tag) + return plugin(parent.plugins.pluginLoader[pluginTag, meta]) + } + + + @Suppress("UNCHECKED_CAST") + fun plugin(meta: Meta): ContextBuilder { + val plMeta = meta.getMetaOrEmpty(MetaBuilder.DEFAULT_META_NAME) + return when { + meta.hasValue("name") -> plugin(meta.getString("name"), plMeta) + meta.hasValue("class") -> { + val type: Class = Class.forName(meta.getString("class")) as? Class + ?: throw RuntimeException("Failed to initialize plugin from meta") + plugin(type, plMeta) + } + else -> throw IllegalArgumentException("Malformed plugin definition") + } + } + + fun classPath(vararg path: URL): ContextBuilder { + classPath.addAll(Arrays.asList(*path)) + return this + } + + fun classPath(path: URI): ContextBuilder { + try { + classPath.add(path.toURL()) + } catch (e: MalformedURLException) { + throw RuntimeException("Malformed classpath") + } + + return this + } + + /** + * Create additional classpath from a list of strings + * + * @param pathStr + * @return + */ + fun classPath(pathStr: String): ContextBuilder { + val path = Paths.get(pathStr) + return when { + Files.isDirectory(path) -> try { + Files.find(path, -1, BiPredicate { subPath, _ -> subPath.toString().endsWith(".jar") }) + .map { it.toUri() }.forEach { this.classPath(it) } + this + } catch (e: IOException) { + throw RuntimeException("Failed to load library", e) + } + Files.exists(path) -> classPath(path.toUri()) + else -> this + } + } + + fun classPath(paths: Collection): ContextBuilder { + classPath.addAll(paths) + return this + } + + /** + * Create new IO manager for this context if needed (using default input and output of parent) and set its root + * + * @param rootDir + * @return + */ + fun setRootDir(rootDir: String): ContextBuilder { + this.rootDir = rootDir + return this + } + + fun setDataDir(dataDir: String): ContextBuilder { + this.rootDir = dataDir + return this + } + + fun build(): Context { + // automatically add lib directory + val classLoader = if (classPath.isEmpty()) { + null + } else { + URLClassLoader(classPath.toTypedArray(), parent.classLoader) + } + + return Context(name, parent, classLoader, properties).apply { + // this@ContextBuilder.output?.let { +// plugins.load(it) +// } + this@ContextBuilder.plugins.forEach { + plugins.load(it) + } + +// //If custom paths are defined, use new plugin to direct to them +// if (properties.containsKey(ROOT_DIRECTORY_CONTEXT_KEY) || properties.containsKey(DATA_DIRECTORY_CONTEXT_KEY)) { +// if (pluginManager.get(false) == null) { +// pluginManager.load(SimpleOutputManager(Global.console)) +// } +// } + + } + + } +} + +fun ContextBuilder.plugin(key: String, metaBuilder: KMetaBuilder.() -> Unit = {}){ + parent.plugins.load(key, buildMeta(transform = metaBuilder)) +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/context/ContextLock.kt b/dataforge-core/src/main/kotlin/hep/dataforge/context/ContextLock.kt new file mode 100644 index 00000000..c0c47bc5 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/context/ContextLock.kt @@ -0,0 +1,66 @@ +package hep.dataforge.context + +import hep.dataforge.exceptions.ContextLockException +import java.util.* +import java.util.concurrent.ExecutionException + +/** + * Lock class for context + */ +class ContextLock(override val context: Context) : ContextAware { + /** + * A set of objects that lock this context + */ + private val lockers = HashSet() + + val isLocked: Boolean + get() = !lockers.isEmpty() + + @Synchronized + fun lock(`object`: Any) { + this.lockers.add(`object`) + } + + @Synchronized + fun unlock(`object`: Any) { + this.lockers.remove(`object`) + } + + /** + * Throws [ContextLockException] if context is locked + */ + private fun tryOperate() { + lockers.stream().findFirst().ifPresent { lock -> throw ContextLockException(lock) } + } + + /** + * Apply thread safe lockable object modification + * + * @param mod + */ + @Synchronized + fun operate(mod: () -> T): T { + tryOperate() + try { + return context.dispatcher.submit(mod).get() + } catch (e: InterruptedException) { + throw RuntimeException(e) + } catch (e: ExecutionException) { + throw RuntimeException(e) + } + + } + + @Synchronized + fun operate(mod: () -> Unit) { + tryOperate() + try { + context.dispatcher.submit(mod).get() + } catch (e: InterruptedException) { + throw RuntimeException(e) + } catch (e: ExecutionException) { + throw RuntimeException(e) + } + + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/context/ExecutorPlugin.kt b/dataforge-core/src/main/kotlin/hep/dataforge/context/ExecutorPlugin.kt new file mode 100644 index 00000000..eb9896ce --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/context/ExecutorPlugin.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.context + +import hep.dataforge.meta.Meta +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.cancel +import java.util.concurrent.ExecutorService +import java.util.concurrent.ForkJoinPool +import kotlin.coroutines.CoroutineContext + +/** + * Plugin managing execution + */ +interface ExecutorPlugin : Plugin, CoroutineScope { + /** + * Default executor for this plugin + */ + val defaultExecutor: ExecutorService + + /** + * Create or load custom executor + */ + fun getExecutor(meta: Meta): ExecutorService +} + +@PluginDef(group = "hep.dataforge", name = "executor", support = true, info = "Executor plugin") +class DefaultExecutorPlugin(meta: Meta = Meta.empty()) : BasicPlugin(meta), ExecutorPlugin { + private val executors = HashMap(); + + /** + * Create a default executor that uses plugin meta + */ + override val defaultExecutor: ExecutorService by lazy { + logger.info("Initializing default executor in {}", context.name) + getExecutor(meta) + } + + override fun getExecutor(meta: Meta): ExecutorService { + synchronized(context) { + return executors.getOrPut(meta) { + val workerName = meta.getString("workerName", "worker"); + val threads = meta.getInt("threads", Runtime.getRuntime().availableProcessors()) + val factory = { pool: ForkJoinPool -> + ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool).apply { + name = "${context.name}_$workerName-$poolIndex" + } + } + ForkJoinPool( + threads, + factory, null, false) + } + } + } + + override val coroutineContext: CoroutineContext by lazy { defaultExecutor.asCoroutineDispatcher() } + + + override fun detach() { + executors.values.forEach { it.shutdown() } + coroutineContext.cancel() + super.detach() + } +} + diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/context/FileReference.kt b/dataforge-core/src/main/kotlin/hep/dataforge/context/FileReference.kt new file mode 100644 index 00000000..3b0985e7 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/context/FileReference.kt @@ -0,0 +1,195 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.context + +import hep.dataforge.context.FileReference.FileReferenceScope.* +import hep.dataforge.data.binary.Binary +import hep.dataforge.data.binary.FileBinary +import hep.dataforge.names.Name +import java.io.File +import java.io.OutputStream +import java.net.URI +import java.nio.channels.SeekableByteChannel +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.StandardOpenOption + + +/** + * A context aware reference to file with content not managed by DataForge + */ +class FileReference private constructor(override val context: Context, val path: Path, val scope: FileReferenceScope = WORK) : ContextAware { + + /** + * Absolute path for this reference + */ + val absolutePath: Path = when (scope) { + SYS -> path + DATA -> context.dataDir.resolve(path) + WORK -> context.workDir.resolve(path) + TMP -> context.tmpDir.resolve(path) + }.toAbsolutePath() + + /** + * The name of the file excluding path + */ + val name: String = path.fileName.toString() + + /** + * Get binary references by this file reference + */ + val binary: Binary + get() { + return if (exists) { + FileBinary(absolutePath) + } else { + Binary.EMPTY + } + } + + /** + * A flag showing that internal modification of reference content is allowed + */ + val mutable: Boolean = scope == WORK || scope == TMP + + val exists: Boolean = Files.exists(absolutePath) + + + private fun prepareWrite() { + if (!mutable) { + throw RuntimeException("Trying to write to immutable file reference") + } + absolutePath.parent.apply { + if (!Files.exists(this)) { + Files.createDirectories(this) + } + } + } + + /** + * Write and replace content of the file + */ + fun write(content: ByteArray) { + prepareWrite() + Files.write(absolutePath, content, StandardOpenOption.WRITE, StandardOpenOption.CREATE) + } + + fun append(content: ByteArray) { + prepareWrite() + Files.write(absolutePath, content, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.APPEND) + } + + + /** + * Output stream for this file reference + * + * TODO cache stream? + */ + val outputStream: OutputStream + get() { + prepareWrite() + return Files.newOutputStream(absolutePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING) + } + + val channel: SeekableByteChannel + get() { + prepareWrite() + return Files.newByteChannel(absolutePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING) + } + + /** + * Delete refenrenced file on exit + */ + fun delete() { + Files.deleteIfExists(absolutePath) + } + +// /** +// * Checksum of the file +// */ +// val md5 = MessageDigest.getInstance("MD5").digest(Files.readAllBytes(absolutePath)) + + + enum class FileReferenceScope { + SYS, // absolute system path, content immutable + DATA, // a reference in data directory, content immutable + WORK, // a reference in work directory, mutable + TMP // A temporary file reference, mutable + } + + companion object { + + private fun resolvePath(parent: Path, name: String): Path { + return if (name.contains("://")) { + Paths.get(URI(name)) + } else { + parent.resolve(name) + } + } + + /** + * Provide a reference to a new file in tmp directory with unique ID. + */ + fun newTmpFile(context: Context, prefix: String, suffix: String = "tmp"): FileReference { + val path = Files.createTempFile(context.tmpDir, prefix, suffix) + return FileReference(context, path, TMP) + } + + /** + * Create a reference for a file in a work directory. File itself is not created + */ + fun newWorkFile(context: Context, prefix: String, suffix: String, path: Name = Name.EMPTY): FileReference { + val dir = if (path.isEmpty()) { + context.workDir + } else { + val relativeDir = path.tokens.joinToString(File.separator) { it.toString() } + resolvePath(context.workDir,relativeDir) + } + + val file = dir.resolve("$prefix.$suffix") + return FileReference(context, file, WORK) + } + + /** + * Create a reference using data scope file using path + */ + fun openDataFile(context: Context, path: Path): FileReference { + return FileReference(context, path, DATA) + } + + fun openDataFile(context: Context, name: String): FileReference { + val path = resolvePath(context.dataDir,name) + return FileReference(context, path, DATA) + } + + /** + * Create a reference to the system scope file using path + */ + fun openFile(context: Context, path: Path): FileReference { + return FileReference(context, path, SYS) + } + + /** + * Create a reference to the system scope file using string + */ + fun openFile(context: Context, path: String): FileReference { + return FileReference(context, resolvePath(context.rootDir, path), SYS) + } + + } +} \ No newline at end of file diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/context/Global.kt b/dataforge-core/src/main/kotlin/hep/dataforge/context/Global.kt new file mode 100644 index 00000000..2212fe33 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/context/Global.kt @@ -0,0 +1,160 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.context + +import hep.dataforge.io.SimpleOutputManager +import hep.dataforge.io.output.Output +import hep.dataforge.io.output.StreamOutput +import hep.dataforge.meta.buildMeta +import hep.dataforge.orElse +import hep.dataforge.utils.ReferenceRegistry +import hep.dataforge.values.Value +import java.io.File +import java.util.* +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +/** + * A singleton global context. Automatic root for the whole context hierarchy. Also stores the registry for active contexts. + * + * @author Alexander Nozik + */ +object Global : Context("GLOBAL", null, Thread.currentThread().contextClassLoader) { + + init { + Locale.setDefault(Locale.US) + } + + /** + * System console output + */ + val console: Output = StreamOutput(this, System.out) + + /** + * The default output manager based on console output. Attached to global but not registered in plugin manager + */ + val consoleOutputManager = SimpleOutputManager(console).apply { attach(this@Global) } + + /** + * The global context independent temporary user directory. This directory + * is used to store user configuration files. Never use it to store data. + * + * @return + */ + val userDirectory: File + get() { + val userDir = File(System.getProperty("user.home")) + val dfUserDir = File(userDir, ".dataforge") + if (!dfUserDir.exists()) { + dfUserDir.mkdir() + } + return dfUserDir + } + + override val history: Chronicler by lazy { + Chronicler(buildMeta("chronicler", "printHistory" to true)).apply { startGlobal() } + } + + override val executors: ExecutorPlugin + get() = plugins[ExecutorPlugin::class].orElse { + logger.debug("No executor plugin found. Using default executor.") + plugins.load(DefaultExecutorPlugin()) + } + + /** + * {@inheritDoc} + * + * @param path + * @return + */ + override fun optValue(path: String): Optional { + return Optional.ofNullable(getProperties()[path]) + } + + /** + * {@inheritDoc} + * + * @param path + * @return + */ + override fun hasValue(path: String): Boolean { + return getProperties().containsKey(path) + } + + /** + * Closing all contexts + * + * @throws Exception + */ + @Throws(Exception::class) + override fun close() { + logger.info("Shutting down GLOBAL") + for (ctx in contextRegistry) { + ctx.close() + } + dispatchThreadExecutor.shutdown() + super.close() + } + + + private val contextRegistry = ReferenceRegistry() + private val dispatchThreadExecutor = Executors.newSingleThreadExecutor { r -> + val res = Thread(r, "DF_DISPATCH") + res.priority = Thread.MAX_PRIORITY + res + } + + /** + * A single thread executor for DataForge messages dispatch. No heavy calculations should be done on this thread + * + * @return + */ + fun dispatchThreadExecutor(): ExecutorService { + return dispatchThreadExecutor + } + + /** + * Get previously builder context o builder a new one + * + * @param name + * @return + */ + @Synchronized + fun getContext(name: String): Context { + return contextRegistry + .findFirst { ctx -> ctx.name == name } + .orElseGet { + val ctx = Context(name) + contextRegistry.add(ctx) + ctx + } + } + + /** + * Close all contexts and terminate framework + */ + @JvmStatic + fun terminate() { + try { + close() + } catch (e: Exception) { + logger.error("Exception while terminating DataForge framework", e) + } + + } + + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/context/Plugin.kt b/dataforge-core/src/main/kotlin/hep/dataforge/context/Plugin.kt new file mode 100644 index 00000000..bc7fec25 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/context/Plugin.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.context + +import hep.dataforge.Named +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.meta.MetaID +import hep.dataforge.meta.Metoid +import hep.dataforge.providers.Provider + +/** + * The interface to define a Context plugin. A plugin stores all runtime features of a context. + * The plugin is by default configurable and a Provider (both features could be ignored). + * The plugin must in most cases have an empty constructor in order to be able to load it from library. + * + * + * The plugin lifecycle is the following: + * + * + * create - configure - attach - detach - destroy + * + * + * Configuration of attached plugin is possible for a context which is not in a runtime mode, but it is not recommended. + * + * @author Alexander Nozik + */ +interface Plugin : Named, Metoid, ContextAware, Provider, MetaID { + + /** + * Get tag for this plugin + * + * @return + */ + val tag: PluginTag + + /** + * The name of this plugin ignoring version and group + * + * @return + */ + override val name: String + get() = tag.name + + /** + * Plugin dependencies which are required to attach this plugin. Plugin + * dependencies must be initialized and enabled in the Context before this + * plugin is enabled. + * + * @return + */ + fun dependsOn(): Array + + /** + * Start this plugin and attach registration info to the context. This method + * should be called only via PluginManager to avoid dependency issues. + * + * @param context + */ + fun attach(context: Context) + + /** + * Stop this plugin and remove registration info from context and other + * plugins. This method should be called only via PluginManager to avoid + * dependency issues. + */ + fun detach() + + + override fun toMeta(): Meta { + return MetaBuilder("plugin") + .putValue("context", context.name) + .putValue("type", this.javaClass.name) + .putNode("tag", tag.toMeta()) + .putNode("meta", meta) + .build() + } + + companion object { + + const val PLUGIN_TARGET = "plugin" + } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/context/PluginDef.kt b/dataforge-core/src/main/kotlin/hep/dataforge/context/PluginDef.kt new file mode 100644 index 00000000..6344aa44 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/context/PluginDef.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.context + +import java.lang.annotation.Inherited + +/** + * @param support Designate a support plugin that does not affect computation results + * @author Alexander Nozik + */ +@Target(AnnotationTarget.CLASS, AnnotationTarget.FILE) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +@Inherited +annotation class PluginDef(val group: String = "", val name: String, val version: String = "", val info: String, val dependsOn: Array = [], val support: Boolean = true) diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/context/PluginLoader.kt b/dataforge-core/src/main/kotlin/hep/dataforge/context/PluginLoader.kt new file mode 100644 index 00000000..006f5bff --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/context/PluginLoader.kt @@ -0,0 +1,152 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.context + +import hep.dataforge.meta.Meta +import hep.dataforge.nullable +import hep.dataforge.toList +import hep.dataforge.utils.MetaFactory +import java.util.* +import java.util.stream.Stream +import java.util.stream.StreamSupport +import kotlin.reflect.KClass +import kotlin.reflect.full.isSubclassOf + + +/** + * Created by darksnake on 08-Sep-16. + */ +abstract class PluginFactory() : MetaFactory { + + abstract val type: Class + + val tag: PluginTag + get() = PluginTag.resolve(type) + + override fun build(meta: Meta): Plugin { + try { + val constructor = type.getConstructor(Meta::class.java) + return constructor.newInstance(meta) as Plugin + } catch (e: Exception) { + throw RuntimeException("Failed to build plugin $tag using default constructor") + } + + } + + companion object { + fun fromClass(type: Class): PluginFactory { + return object : PluginFactory() { + override val type: Class = type + } + } + } +} + +/** + * A resolution strategy for plugins + * + * @author Alexander Nozik + */ +interface PluginLoader { + + /** + * Load the most suitable plugin of all provided by its tag + * + * @param tag + * @return + */ + fun opt(tag: PluginTag, meta: Meta): Plugin? + + fun opt(type: KClass, meta: Meta): T? + + + operator fun get(tag: PluginTag, meta: Meta): Plugin { + return opt(tag, meta) ?: throw RuntimeException("No plugin matching $tag found") + } + + operator fun get(type: KClass, meta: Meta): T { + return opt(type, meta) ?: throw RuntimeException("No plugin of type $type found") + } + + /** + * List tags provided by this repository + * + * @return + */ + fun listTags(): List + + +} + + +/** + * Created by darksnake on 10-Apr-17. + */ +abstract class AbstractPluginLoader : PluginLoader { + override fun opt(tag: PluginTag, meta: Meta): Plugin? { + return factories() + .filter { factory -> tag.matches(factory.tag) } + .sorted { p1, p2 -> this.compare(p1, p2) } + .findFirst().map { it -> it.build(meta) }.nullable + } + + @Suppress("UNCHECKED_CAST") + override fun opt(type: KClass, meta: Meta): T { + val factory = factories() + .filter { factory -> factory.type.kotlin.isSubclassOf(type) } + .sorted { p1, p2 -> this.compare(p1, p2) } + .findFirst().orElseGet { PluginFactory.fromClass(type.java) } + return factory.build(meta) as T + //.map { it -> it.build(meta) }.nullable as T? + } + + + protected fun compare(p1: PluginFactory, p2: PluginFactory): Int { + return Integer.compare(p1.tag.getInt("priority", 0), p2.tag.getInt("priority", 0)) + } + + override fun listTags(): List { + return factories().map { it.tag }.toList() + } + + protected abstract fun factories(): Stream +} + +/** + * The plugin resolver that searches classpath for Plugin services and loads the + * best one + * + * @author Alexander Nozik + */ +class ClassPathPluginLoader(context: Context) : AbstractPluginLoader() { + + private val loader: ServiceLoader + + init { + val cl = context.classLoader + loader = ServiceLoader.load(PluginFactory::class.java, cl) + } + + override fun factories(): Stream { + return StreamSupport.stream(loader.spliterator(), false) + } +} \ No newline at end of file diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/context/PluginManager.kt b/dataforge-core/src/main/kotlin/hep/dataforge/context/PluginManager.kt new file mode 100644 index 00000000..83d0615a --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/context/PluginManager.kt @@ -0,0 +1,192 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.context + +import hep.dataforge.exceptions.ContextLockException +import hep.dataforge.meta.KMetaBuilder +import hep.dataforge.meta.Meta +import hep.dataforge.meta.buildMeta +import java.util.* +import java.util.stream.Stream +import kotlin.reflect.KClass + +/** + * The manager for plugin system. Should monitor plugin dependencies and locks. + * + * @property context A context for this plugin manager + * @author Alexander Nozik + */ +class PluginManager(override val context: Context) : ContextAware, AutoCloseable, Iterable { + + /** + * A set of loaded plugins + */ + private val plugins = HashSet() + + /** + * A class path resolver + */ + var pluginLoader: PluginLoader = ClassPathPluginLoader(context) + + private val parent: PluginManager? = context.parent?.plugins + + + fun stream(recursive: Boolean): Stream { + return if (recursive && parent != null) { + Stream.concat(plugins.stream(), parent.stream(true)) + } else { + plugins.stream() + } + } + + /** + * Get for existing plugin + */ + fun get(recursive: Boolean = true, predicate: (Plugin) -> Boolean): Plugin? { + return plugins.find(predicate) ?: if (recursive && parent != null) { + parent.get(true, predicate) + } else { + null + } + } + + /** + * Find a loaded plugin via its tag + * + * @param tag + * @return + */ + operator fun get(tag: PluginTag, recursive: Boolean = true): Plugin? { + return get(recursive) { tag.matches(it.tag) } + } + + /** + * Find a loaded plugin via its class + * + * @param tag + * @param type + * @param + * @return + */ + @Suppress("UNCHECKED_CAST") + operator fun get(type: KClass, recursive: Boolean = true): T? { + return get(recursive) { type.isInstance(it) } as T? + } + + inline fun get(recursive: Boolean = true): T? { + return get(T::class, recursive) + } + + /** + * Load given plugin into this manager and return loaded instance. + * Throw error if plugin of the same class already exists in manager + * + * @param plugin + * @return + */ + fun load(plugin: T): T { + if (context.isLocked) { + throw ContextLockException() + } + val existing = get(plugin::class, false) + if ( existing == null) { + loadDependencies(plugin) + + logger.info("Loading plugin {} into {}", plugin.name, context.name) + plugin.attach(context) + plugins.add(plugin) + return plugin + } else if(existing.meta == plugin.meta){ + return existing + } else{ + throw RuntimeException("Plugin of type ${plugin::class} already exists in ${context.name}") + } + } + + fun loadDependencies(plugin: Plugin){ + for (tag in plugin.dependsOn()) { + load(tag) + } + } + + fun remove(plugin: Plugin){ + if (context.isLocked) { + throw ContextLockException() + } + if (plugins.contains(plugin)){ + logger.info("Removing plugin {} from {}", plugin.name, context.name) + plugin.detach() + plugins.remove(plugin) + } + } + + /** + * Get plugin instance via plugin reolver and load it. + * + * @param tag + * @return + */ + @JvmOverloads + fun load(tag: PluginTag, meta: Meta = Meta.empty()): Plugin { + val loaded = get(tag, false) + return when { + loaded == null -> load(pluginLoader[tag, meta]) + loaded.meta == meta -> loaded // if meta is the same, return existing plugin + else -> throw RuntimeException("Can't load plugin with tag $tag. Plugin with this tag and different configuration already exists in context.") + } + } + + /** + * Load plugin by its class and meta. Ignore if plugin with this meta is already loaded. + */ + fun load(type: KClass, meta: Meta = Meta.empty()): T { + val loaded = get(type, false) + return when { + loaded == null -> load(pluginLoader[type, meta]) + loaded.meta.equalsIgnoreName(meta) -> loaded // if meta is the same, return existing plugin + else -> throw RuntimeException("Can't load plugin with type $type. Plugin with this type and different configuration already exists in context.") + } + } + + inline fun load(noinline metaBuilder: KMetaBuilder.() -> Unit = {}): T { + return load(T::class, buildMeta("plugin", metaBuilder)) + } + + @JvmOverloads + fun load(type: Class, meta: Meta = Meta.empty()): T { + return load(type.kotlin, meta) + } + + @JvmOverloads + fun load(name: String, meta: Meta = Meta.empty()): Plugin { + return load(PluginTag.fromString(name), meta) + } + + /** + * Get existing plugin or load it with default meta + */ + fun getOrLoad(type: KClass): T { + return get(type) ?: load(type) + } + + @Throws(Exception::class) + override fun close() { + this.plugins.forEach { it.detach() } + } + + override fun iterator(): Iterator = plugins.iterator() + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/context/PluginTag.kt b/dataforge-core/src/main/kotlin/hep/dataforge/context/PluginTag.kt new file mode 100644 index 00000000..080e8db6 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/context/PluginTag.kt @@ -0,0 +1,128 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.context + +import hep.dataforge.description.ValueDef +import hep.dataforge.description.ValueDefs +import hep.dataforge.meta.* + +/** + * The tag which contains information about name, group and version of some + * object. It also could contain any complex rule to define version ranges + * + * @author Alexander Nozik + */ +//@ValueDef(name = "role", multiple = true,info = "The list of roles this plugin implements") +//@ValueDef(name = "priority", type = "NUMBER", info = "Plugin load priority. Used for plugins with the same role") +@ValueDefs( + ValueDef(key = "group", def = ""), + ValueDef(key = "name", required = true) +) +class PluginTag(meta: Meta) : SimpleMetaMorph(meta) { + + val name by meta.stringValue() + + val group by meta.stringValue(def = "") + + constructor(name: String, group: String = "hep.dataforge", description: String? = null, version: String? = null, vararg dependsOn: String) : this( + buildMeta("plugin") { + "name" to name + "group" to group + description?.let { "description" to it } + version?.let { "version" to it } + if (dependsOn.isNotEmpty()) { + dependsOn.let { "dependsOn" to it } + } + } + ) + + /** + * Check if given tag is compatible (in range) of this tag + * + * @param otherTag + * @return + */ + fun matches(otherTag: PluginTag): Boolean { + return matchesName(otherTag) && matchesGroup(otherTag) + } + + private fun matchesGroup(otherTag: PluginTag): Boolean { + return this.group.isEmpty() || this.group == otherTag.group + } + + private fun matchesName(otherTag: PluginTag): Boolean { + return this.name == otherTag.name + } + + + /** + * Build standard string representation of plugin tag + * `group.name[version]`. Both group and version could be empty. + * + * @return + */ + override fun toString(): String { + var theGroup = group + if (!theGroup.isEmpty()) { + theGroup += ":" + } + + return theGroup + name + } + + companion object { + + /** + * Build new PluginTag from standard string representation + * + * @param tag + * @return + */ + fun fromString(tag: String): PluginTag { + val sepIndex = tag.indexOf(":") + return if (sepIndex >= 0) { + PluginTag(group = tag.substring(0, sepIndex), name = tag.substring(sepIndex + 1)) + } else { + PluginTag(tag) + } + } + + /** + * Resolve plugin tag either from [PluginDef] annotation or Plugin instance. + * + * @param type + * @return + */ + fun resolve(type: Class): PluginTag { + //if definition is present + return if (type.isAnnotationPresent(PluginDef::class.java)) { + val builder = MetaBuilder("tag") + val def = type.getAnnotation(PluginDef::class.java) + builder.putValue("group", def.group) + builder.putValue("name", def.name) + builder.putValue("description", def.info) + builder.putValue("version", def.version) + for (dep in def.dependsOn) { + builder.putValue("dependsOn", dep) + } + PluginTag(builder) + } else { //getting plugin instance to find tag + PluginTag.fromString(type.simpleName ?: "undefined") + } + } + + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/data/CheckedDataNode.kt b/dataforge-core/src/main/kotlin/hep/dataforge/data/CheckedDataNode.kt new file mode 100644 index 00000000..a374f011 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/data/CheckedDataNode.kt @@ -0,0 +1,50 @@ +package hep.dataforge.data + +import hep.dataforge.meta.Meta +import org.slf4j.LoggerFactory +import java.util.stream.Stream + +/** + * A wrapper for DataNode that allowes to access speciffically typed content. + * Created by darksnake on 07-Sep-16. + */ +class CheckedDataNode(private val node: DataNode<*>, override val type: Class) : DataNode { + + override val meta: Meta + get() = node.meta + + override val isEmpty: Boolean + get() = dataStream(true).count() == 0L + + override val name: String = node.name + + init { + //TODO add warning for incompatible types + if (isEmpty) { + LoggerFactory.getLogger(javaClass).warn("The checked node is empty") + } + } + + override fun optData(key: String): Data? { + return node.optData(key)?.let { d -> + if (type.isAssignableFrom(d.type)) { + d.cast(type) + } else { + null + } + } + } + + override fun optNode(nodeName: String): DataNode? { + return node.optNode(nodeName)?.checked(type) + } + + override fun dataStream(recursive: Boolean): Stream> { + return node.dataStream(recursive).filter { d -> type.isAssignableFrom(d.type) }.map { d -> d.cast(type) } + } + + override fun nodeStream(recursive: Boolean): Stream> { + return node.nodeStream(recursive).filter { n -> type.isAssignableFrom(n.type) }.map { n -> n.checked(type) } + } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/data/CustomDataFilter.kt b/dataforge-core/src/main/kotlin/hep/dataforge/data/CustomDataFilter.kt new file mode 100644 index 00000000..0246607e --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/data/CustomDataFilter.kt @@ -0,0 +1,156 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.data + +import hep.dataforge.description.NodeDef +import hep.dataforge.description.NodeDefs +import hep.dataforge.description.ValueDef +import hep.dataforge.description.ValueDefs +import hep.dataforge.meta.Meta +import hep.dataforge.meta.SimpleMetaMorph +import hep.dataforge.values.ValueType + +import java.util.function.BiPredicate + +/** + * A meta-based data filter + * + * @author Alexander Nozik + */ +@NodeDefs( + NodeDef(key = "include", info = "Define inclusion rule for data and/or dataNode. If not inclusion rule is present, everything is included by default.", descriptor = "method::hep.dataforge.data.CustomDataFilter.applyMeta"), + NodeDef(key = "exclude", info = "Define exclusion rule for data and/or dataNode. Exclusion rules are allied only to included items.", descriptor = "method::hep.dataforge.data.CustomDataFilter.applyMeta") +) +class CustomDataFilter(meta: Meta) : SimpleMetaMorph(meta), DataFilter { + + private var nodeCondition: BiPredicate>? = null + private var dataCondition: BiPredicate>? = null + + private fun applyMask(pattern: String): String { + return pattern.replace(".", "\\.").replace("?", ".").replace("*", ".*?") + } + + init { + applyMeta(meta) + } + + fun acceptNode(nodeName: String, node: DataNode<*>): Boolean { + return this.nodeCondition == null || this.nodeCondition!!.test(nodeName, node) + } + + fun acceptData(dataName: String, data: Data<*>): Boolean { + return this.dataCondition == null || this.dataCondition!!.test(dataName, data) + } + + override fun filter(node: DataNode): DataNode { + return DataSet.edit(node.type).apply { + node.dataStream(true).forEach { d -> + if (acceptData(d.name, d)) { + add(d) + } + } + }.build() + + } + + private fun includeData(dataCondition: BiPredicate>) { + if (this.dataCondition == null) { + this.dataCondition = dataCondition + } else { + this.dataCondition = this.dataCondition!!.or(dataCondition) + } + } + + private fun includeData(namePattern: String, type: Class<*>?) { + val limitingType: Class<*> = type ?: Any::class.java + val predicate = BiPredicate> { name, data -> name.matches(namePattern.toRegex()) && limitingType.isAssignableFrom(data.type) } + includeData(predicate) + } + + private fun excludeData(dataCondition: BiPredicate>) { + if (this.dataCondition != null) { + this.dataCondition = this.dataCondition!!.and(dataCondition.negate()) + } + } + + private fun excludeData(namePattern: String) { + excludeData(BiPredicate { name, _ -> name.matches(namePattern.toRegex()) }) + } + + private fun includeNode(namePattern: String, type: Class<*>?) { + val limitingType: Class<*> = type ?: Any::class.java + val predicate = BiPredicate> { name, data -> name.matches(namePattern.toRegex()) && limitingType.isAssignableFrom(data.type) } + includeNode(predicate) + } + + private fun includeNode(nodeCondition: BiPredicate>) { + if (this.nodeCondition == null) { + this.nodeCondition = nodeCondition + } else { + this.nodeCondition = this.nodeCondition!!.or(nodeCondition) + } + } + + private fun excludeNode(nodeCondition: BiPredicate>) { + if (this.nodeCondition != null) { + this.nodeCondition = this.nodeCondition!!.and(nodeCondition.negate()) + } + } + + private fun excludeNode(namePattern: String) { + excludeNode(BiPredicate { name, _ -> name.matches(namePattern.toRegex()) }) + } + + private fun getPattern(node: Meta): String { + return when { + node.hasValue("mask") -> applyMask(node.getString("mask")) + node.hasValue("pattern") -> node.getString("pattern") + else -> ".*" + } + } + + @ValueDefs( + ValueDef(key = "mask", info = "Add rule using glob mask"), + ValueDef(key = "pattern", info = "Add rule rule using regex pattern"), + ValueDef(key = "forData", type = arrayOf(ValueType.BOOLEAN), def = "true", info = "Apply this rule to individual data"), + ValueDef(key = "forNodes", type = arrayOf(ValueType.BOOLEAN), def = "true", info = "Apply this rule to data nodes") + ) + private fun applyMeta(meta: Meta) { + if (meta.hasMeta("include")) { + meta.getMetaList("include").forEach { include -> + val namePattern = getPattern(include) + var type: Class<*> = Any::class.java + if (include.hasValue("type")) { + try { + type = Class.forName(include.getString("type")) + } catch (ex: ClassNotFoundException) { + throw RuntimeException("type not found", ex) + } + + } + if (include.getBoolean("forData", true)) { + includeData(namePattern, type) + } + if (include.getBoolean("forNodes", true)) { + includeNode(namePattern, type) + } + } + } + + if (meta.hasMeta("exclude")) { + meta.getMetaList("exclude").forEach { exclude -> + val namePattern = getPattern(exclude) + + if (exclude.getBoolean("forData", true)) { + excludeData(namePattern) + } + if (exclude.getBoolean("forNodes", true)) { + excludeNode(namePattern) + } + } + } + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/data/Data.kt b/dataforge-core/src/main/kotlin/hep/dataforge/data/Data.kt new file mode 100644 index 00000000..8b4188e4 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/data/Data.kt @@ -0,0 +1,135 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.data + +import hep.dataforge.data.binary.Binary +import hep.dataforge.goals.AbstractGoal +import hep.dataforge.goals.GeneratorGoal +import hep.dataforge.goals.Goal +import hep.dataforge.goals.StaticGoal +import hep.dataforge.io.envelopes.Envelope +import hep.dataforge.meta.Meta +import hep.dataforge.meta.Metoid +import java.util.concurrent.CompletableFuture +import java.util.concurrent.Executor +import java.util.function.Supplier +import java.util.stream.Stream + +/** + * A piece of data which is basically calculated asynchronously + * + * @param + * @author Alexander Nozik + * @version $Id: $Id + */ +open class Data(val type: Class, + val goal: Goal, + override val meta: Meta = Meta.empty()) : Metoid { + + /** + * Asynchronous data handler. Computation could be canceled if needed + * + * @return + */ + val future: CompletableFuture + get() = goal.asCompletableFuture() + + /** + * @return false if goal is canceled or completed exceptionally + */ + val isValid: Boolean + get() = !future.isCancelled && !future.isCompletedExceptionally + + /** + * Compute underlying goal and return sync result. + * + * @return + */ + fun get(): T { + return goal.get() + } + + /** + * Upcast the data type + * + * @param type + * @param + * @return + */ + @Suppress("UNCHECKED_CAST") + open fun cast(type: Class): Data { + if (type.isAssignableFrom(this.type)) { + return this as Data + } else { + throw IllegalArgumentException("Invalid type to upcast data") + } + } + + companion object { + + @JvmStatic + fun buildStatic(content: T, meta: Meta = Meta.empty()): Data { + //val nonNull = content as? Any ?: throw RuntimeException("Can't create data from null object") + return Data(content.javaClass, StaticGoal(content), meta) + } + + @JvmStatic + fun buildStatic(content: T): Data { + var meta = Meta.empty() + if (content is Metoid) { + meta = (content as Metoid).meta + } + return buildStatic(content, meta) + } + + fun empty(type: Class, meta: Meta): Data { + val emptyGoal = StaticGoal(null) + return Data(type, emptyGoal, meta) + } + + /** + * Build data from envelope using given lazy binary transformation + * + * @param envelope + * @param type + * @param transform + * @param + * @return + */ + fun fromEnvelope(envelope: Envelope, type: Class, transform: (Binary) -> T): Data { + val goal = object : AbstractGoal() { + @Throws(Exception::class) + override fun compute(): T { + return transform(envelope.data) + } + + override fun dependencies(): Stream> { + return Stream.empty() + } + } + return Data(type, goal, envelope.meta) + } + + fun generate(type: Class, meta: Meta, executor: Executor, sup: () -> T): Data { + return Data(type, GeneratorGoal(executor, Supplier(sup)), meta) + } + + fun generate(type: Class, meta: Meta, sup: () -> T): Data { + return Data(type, GeneratorGoal(sup), meta) + } + } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/data/DataFactory.kt b/dataforge-core/src/main/kotlin/hep/dataforge/data/DataFactory.kt new file mode 100644 index 00000000..27dae286 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/data/DataFactory.kt @@ -0,0 +1,133 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.data + +import hep.dataforge.context.Context +import hep.dataforge.data.DataFactory.Companion.FILTER_KEY +import hep.dataforge.data.DataFactory.Companion.ITEM_KEY +import hep.dataforge.data.DataFactory.Companion.NODE_KEY +import hep.dataforge.data.DataFactory.Companion.NODE_META_KEY +import hep.dataforge.description.NodeDef +import hep.dataforge.description.NodeDefs +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaNode.DEFAULT_META_NAME + +/** + * A factory for data tree + * + * @author Alexander Nozik + */ +@NodeDefs( + NodeDef(key = NODE_META_KEY, info = "Node meta-data"), + NodeDef(key = NODE_KEY, info = "Recursively add node to the builder"), + NodeDef(key = FILTER_KEY, descriptor = "hep.dataforge.data.CustomDataFilter", info = "Filter definition to be applied after node construction is finished"), + NodeDef(key = ITEM_KEY, descriptor = "method::hep.dataforge.data.DataFactory.buildData", info = "A fixed context-based node with or without actual static data") +) +abstract class DataFactory(private val baseType: Class) : DataLoader { + + override fun build(context: Context, meta: Meta): DataNode { + //Creating filter + val filter = CustomDataFilter(meta.getMetaOrEmpty(FILTER_KEY)) + + val tree = builder(context, meta).build() + //Applying filter if needed + return if (filter.meta.isEmpty) { + tree + } else { + filter.filter(tree) + } + } + + /** + * Return DataTree.Builder after node fill but before filtering. Any custom logic should be applied after it. + * + * @param context + * @param dataConfig + * @return + */ + protected fun builder(context: Context, dataConfig: Meta): DataTree.Builder { + val builder = DataTree.edit(baseType) + + // Apply node name + if (dataConfig.hasValue(NODE_NAME_KEY)) { + builder.name = dataConfig.getString(NODE_NAME_KEY) + } + + // Apply node meta + if (dataConfig.hasMeta(NODE_META_KEY)) { + builder.meta = dataConfig.getMeta(NODE_META_KEY) + } + + // Apply non-specific child nodes + if (dataConfig.hasMeta(NODE_KEY)) { + dataConfig.getMetaList(NODE_KEY).forEach { nodeMeta: Meta -> + //FIXME check types for child nodes + val node = build(context, nodeMeta) + builder.add(node) + } + } + + //Add custom items + if (dataConfig.hasMeta(ITEM_KEY)) { + dataConfig.getMetaList(ITEM_KEY).forEach { itemMeta -> builder.add(buildData(context, itemMeta)) } + } + + // Apply child nodes specific to this factory + fill(builder, context, dataConfig) + return builder + } + + @NodeDef(key = NODE_META_KEY, info = "Meta for this item") + private fun buildData(context: Context, itemMeta: Meta): NamedData { + val name = itemMeta.getString(NODE_NAME_KEY) + + val obj = itemMeta.optValue("path") + .flatMap { path -> context.provide(path.string, baseType) } + .orElse(null) + + val meta = itemMeta.optMeta(NODE_META_KEY).orElse(Meta.empty()) + + return NamedData.buildStatic(name, obj, meta) + } + + /** + * Apply children nodes and data elements to the builder. Inheriting classes + * can add their own children builders. + * + * @param builder + * @param meta + */ + protected abstract fun fill(builder: DataNodeBuilder, context: Context, meta: Meta) + + override val name: String = "default" + + companion object { + + const val NODE_META_KEY = DEFAULT_META_NAME + const val NODE_TYPE_KEY = "type" + const val NODE_KEY = "node" + const val ITEM_KEY = "item" + const val NODE_NAME_KEY = "name" + const val FILTER_KEY = "filter" + } +} + +class DummyDataFactory(baseType: Class): DataFactory(baseType){ + override fun fill(builder: DataNodeBuilder, context: Context, meta: Meta) { + // do nothing + } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/data/DataFilter.kt b/dataforge-core/src/main/kotlin/hep/dataforge/data/DataFilter.kt new file mode 100644 index 00000000..3b809cf5 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/data/DataFilter.kt @@ -0,0 +1,65 @@ +package hep.dataforge.data + +import hep.dataforge.meta.Meta +import hep.dataforge.nullable +import hep.dataforge.values.Value + +/** + * A filter that could select a subset from a DataNode without changing its type. + */ +interface DataFilter { + + /** + * Perform a selection. Resulting node contains references to the data in the initial node. + * Node structure and meta is maintained if possible. + * + * @param node + * @param + * @return + */ + fun filter(node: DataNode): DataNode + + companion object { + val IDENTITY: DataFilter = object : DataFilter { + override fun filter(node: DataNode): DataNode { + return node + } + } + + fun byPattern(pattern: String): DataFilter { + return object : DataFilter { + override fun filter(node: DataNode): DataNode { + return DataSet.edit(node.type).apply { + name = node.name + node.dataStream(true) + .filter { d -> d.name.matches(pattern.toRegex()) } + .forEach { add(it) } + + }.build() + } + } + } + + + fun byMeta(condition: (Meta) -> Boolean): DataFilter { + return object : DataFilter { + override fun filter(node: DataNode): DataNode { + return DataSet.edit(node.type).apply { + name = node.name + node.dataStream(true) + .filter { d -> + condition(d.meta) + } + .forEach { add(it) } + + }.build() + } + } + } + + fun byMetaValue(valueName: String, condition: (Value?) -> Boolean): DataFilter = + byMeta { condition(it.optValue(valueName).nullable) } + } +} + +fun DataNode.filter(filter: DataFilter): DataNode = filter.filter(this) diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/data/DataLoader.kt b/dataforge-core/src/main/kotlin/hep/dataforge/data/DataLoader.kt new file mode 100644 index 00000000..7803d8e4 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/data/DataLoader.kt @@ -0,0 +1,14 @@ +package hep.dataforge.data + +import hep.dataforge.Named +import hep.dataforge.utils.ContextMetaFactory + +/** + * A common interface for data providers + * Created by darksnake on 02-Feb-17. + */ +interface DataLoader : ContextMetaFactory>, Named { + companion object { + val SMART: DataLoader = SmartDataLoader() + } +} \ No newline at end of file diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/data/DataNode.kt b/dataforge-core/src/main/kotlin/hep/dataforge/data/DataNode.kt new file mode 100644 index 00000000..dc240a9c --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/data/DataNode.kt @@ -0,0 +1,351 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.data + +import hep.dataforge.Named +import hep.dataforge.context.Context +import hep.dataforge.exceptions.NameNotFoundException +import hep.dataforge.goals.GoalGroup +import hep.dataforge.meta.KMetaBuilder +import hep.dataforge.meta.Meta +import hep.dataforge.meta.Metoid +import hep.dataforge.meta.buildMeta +import hep.dataforge.nullable +import hep.dataforge.providers.Provider +import hep.dataforge.providers.Provides +import hep.dataforge.toList +import java.util.concurrent.Executor +import java.util.function.BiConsumer +import java.util.function.Consumer +import java.util.stream.Stream +import kotlin.streams.asSequence + +/** + * A universal data container + * + * @author Alexander Nozik + */ +interface DataNode : Iterable>, Named, Metoid, Provider { + + /** + * Get default data fragment. Access first data element in this node if it + * is not present. Useful for single data nodes. + * + * @return + */ + val data: Data? + get() = optData(DEFAULT_DATA_FRAGMENT_NAME) ?: dataStream().findFirst().nullable + + /** + * Shows if there is no data in this node + * + * @return + */ + val isEmpty: Boolean + + val size: Long + get() = count(true) + + /** + * Get Data with given Name or null if name not present + * + * @param key + * @return + */ + @Provides(DATA_TARGET) + fun optData(key: String): Data? + + fun getCheckedData(dataName: String, type: Class): Data { + val data = getData(dataName) + return if (type.isAssignableFrom(data.type)) { + data.cast(type) + } else { + throw RuntimeException(String.format("Type check failed: expected %s but found %s", + type.name, + data.type.name)) + } + } + + /** + * Compute specific Data. Blocking operation + * + * @param key + * @return + */ + operator fun get(key: String): T { + return getData(key).get() + } + + fun getData(key: String): Data { + return optData(key) ?: throw NameNotFoundException(key) + } + + /** + * Get descendant node in case of tree structure. In case of flat structure + * returns node composed of all Data elements with names that begin with + * `.`. Child node inherits meta from parent node. In case + * both nodes have meta, it is merged. + * + * @param nodeName + * @return + */ + @Provides(NODE_TARGET) + fun optNode(nodeName: String): DataNode? + + fun getNode(nodeName: String): DataNode { + return optNode(nodeName) ?: throw NameNotFoundException(nodeName) + } + + /** + * Get the node assuming it have specific type with type check + * + * @param nodeName + * @param type + * @param + * @return + */ + fun getCheckedNode(nodeName: String, type: Class): DataNode { + val node: DataNode = if (nodeName.isEmpty()) { + this + } else { + getNode(nodeName) + } + + return node.checked(type) + } + + /** + * Named dataStream of data elements including subnodes if they are present. + * Meta of each data is supposed to be laminate containing node meta. + * + * @return + */ + fun dataStream(recursive: Boolean): Stream> + + fun dataStream(): Stream> { + return dataStream(true) + } + + /** + * Iterate other all data pieces with given type with type check + * + * @param type + * @param consumer + */ + fun visit(type: Class, consumer: (NamedData) -> Unit) { + dataStream().asSequence().filter { d -> type.isAssignableFrom(d.type) } + .forEach { d -> consumer(d.cast(type)) } + } + + /** + * A stream of subnodes. Each node has composite name and Laminate meta including all higher nodes information + * + * @param recursive if true then recursive node stream is returned, otherwise only upper level children are used + * @return + */ + fun nodeStream(recursive: Boolean = true): Stream> + + /** + * Get border type for this DataNode + * + * @return + */ + val type: Class + + /** + * The current number of data pieces in this node including subnodes + * + * @return + */ + fun count(recursive: Boolean): Long { + return dataStream(recursive).count() + } + + /** + * Force start data goals for all data and wait for completion + */ + fun computeAll() { + nodeGoal().get() + } + + /** + * Computation control for data + * + * @return + */ + fun nodeGoal(): GoalGroup { + return GoalGroup(this.dataStream().map { it.goal }.toList()) + } + + /** + * Handle result when the node is evaluated. Does not trigger node evaluation. Ignores exceptional completion + * + * @param consumer + */ + fun handle(consumer: Consumer>) { + nodeGoal().onComplete { _, _ -> consumer.accept(this@DataNode) } + } + + /** + * Same as above but with custom executor + * + * @param executor + * @param consumer + */ + fun handle(executor: Executor, consumer: (DataNode) -> Unit) { + nodeGoal().onComplete(executor, BiConsumer { _, _ -> consumer(this@DataNode) }) + } + + /** + * Return a type checked node containing this one + * + * @param checkType + * @param + * @return + */ + @Suppress("UNCHECKED_CAST") + fun checked(checkType: Class): DataNode { + return if (checkType.isAssignableFrom(this.type)) { + this as DataNode + } else { + CheckedDataNode(this, checkType) + } + } + + fun filter(predicate: (String, Data) -> Boolean): DataNode { + return FilteredDataNode(this, predicate) + } + + override operator fun iterator(): Iterator> { + return dataStream().map { it -> it.cast(type) }.iterator() + } + + /** + * Create a deep copy of this node and edit it + */ + fun edit(): DataNodeBuilder { + return DataTree.edit(this.type).also { + it.name = this.name + it.meta = this.meta + this.dataStream().forEach { d -> it.add(d) } + } + } + + companion object { + + const val DATA_TARGET = "data" + const val NODE_TARGET = "node" + const val DEFAULT_DATA_FRAGMENT_NAME = "@default" + + fun empty(name: String, type: Class): DataNode { + return EmptyDataNode(name, type) + } + + fun empty(): DataNode { + return EmptyDataNode("", Any::class.java) + } + + /** + * A data node wrapping single data + * + * @param + * @param dataName + * @param data + * @param nodeMeta + * @return + */ + fun of(dataName: String, data: Data, nodeMeta: Meta): DataNode { + return DataSet.edit(data.type).apply { + name = dataName + meta = nodeMeta + putData(dataName, data) + }.build() + } + + inline fun build(noinline transform: DataNodeBuilder.() -> Unit): DataNode { + return DataTree.edit(T::class).apply(transform).build() + } + } + +} + +abstract class DataNodeBuilder(val type: Class) { + + abstract var name: String + + abstract var meta: Meta + + abstract val isEmpty: Boolean + + abstract fun putData(key: String, data: Data, replace: Boolean = false) + + operator fun set(key: String, data: Data) { + putData(key, data, false) + } + + fun putData(key: String, data: T, meta: Meta) { + return putData(key, Data.buildStatic(data, meta)) + } + + operator fun set(key: String, node: DataNode) { + putNode(key, node) + } + + abstract fun putNode(key: String, node: DataNode) + + abstract fun removeNode(nodeName: String) + + abstract fun removeData(dataName: String) + + fun add(node: DataNode) { + set(node.name, node) + } + + operator fun DataNode.unaryPlus() { + set(this.name, this) + } + + fun add(data: NamedData) { + putData(data.name, data.anonymize()) + } + + operator fun NamedData.unaryPlus() { + putData(this.name, this.anonymize()) + } + + /** + * Update this node mirroring the argument + */ + fun update(node: DataNode) { + node.dataStream(true).forEach { this.add(it) } + } + + fun putAll(dataCollection: Collection>) { + dataCollection.forEach { +it } + } + + fun putAll(map: Map>) { + map.forEach { key, data -> this[key] = data } + } + + @JvmOverloads + fun putStatic(key: String, staticData: T, meta: Meta = Meta.empty()) { + if (!type.isInstance(staticData)) { + throw IllegalArgumentException("The data mast be instance of " + type.name) + } + +NamedData.buildStatic(key, staticData, meta) + } + + abstract fun build(): DataNode +} + +fun DataNodeBuilder.load(context: Context, meta: Meta) { + val newNode = SmartDataLoader().build(context, meta) + update(newNode) +} + +fun DataNodeBuilder.load(context: Context, metaBuilder: KMetaBuilder.() -> Unit) = + load(context, buildMeta(transform = metaBuilder)) \ No newline at end of file diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/data/DataSet.kt b/dataforge-core/src/main/kotlin/hep/dataforge/data/DataSet.kt new file mode 100644 index 00000000..ab62fdc5 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/data/DataSet.kt @@ -0,0 +1,170 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.data + +import hep.dataforge.exceptions.AnonymousNotAlowedException +import hep.dataforge.meta.Meta +import hep.dataforge.names.Name +import hep.dataforge.nullable +import org.slf4j.LoggerFactory +import java.util.* +import java.util.stream.Collectors +import java.util.stream.Stream +import kotlin.reflect.KClass + +/** + * A simple static representation of DataNode + * + * @param + * @author Alexander Nozik + */ +class DataSet internal constructor( + override val name: String, + override val type: Class, + private val dataMap: Map>, + override val meta: Meta = Meta.empty() +) : DataNode { + + override val isEmpty: Boolean + get() = dataMap.isEmpty() + + override fun dataStream(recursive: Boolean): Stream> { + return dataMap.entries.stream() + .filter { it -> recursive || !it.key.contains(".") } + .map { entry -> NamedData.wrap(entry.key, entry.value, meta) } + } + + /** + * Not very effective for flat data set + * @return + */ + override fun nodeStream(recursive: Boolean): Stream> { + if (recursive) { + throw Error("Not implemented") + } + return dataStream() + .map { data -> Name.of(data.name) } // converting strings to Names + .filter { name -> name.length > 1 } //selecting only composite names + .map { name -> name.first.toString() } + .distinct() + .map { DataSet(it, type, subMap("$it."), meta) } + } + + private fun subMap(prefix: String): Map> { + return dataMap.entries.stream() + .filter { entry -> entry.key.startsWith(prefix) } + .collect(Collectors.toMap({ it.key.substring(prefix.length) }, { it.value })) + } + + override fun optData(key: String): Data? { + return Optional.ofNullable(dataMap[key]) + .map { it -> NamedData.wrap(key, it, meta) } + .nullable + } + + + override fun optNode(nodeName: String): DataNode? { + val builder = DataSetBuilder(type).apply { + name = nodeName + this.meta = this@DataSet.meta + + val prefix = "$nodeName." + + dataStream() + .filter { data -> data.name.startsWith(prefix) } + .forEach { data -> + val dataName = Name.of(data.name).cutFirst().toString() + set(dataName, data.anonymize()) + } + + } + return if (!builder.isEmpty) { + builder.build() + } else { + null + } + } + + companion object { + + /** + * The builder bound by type of data + * + * @param + * @param type + * @return + */ + @JvmStatic + fun edit(type: Class): DataSetBuilder { + return DataSetBuilder(type) + } + + fun edit(type: KClass): DataSetBuilder { + return DataSetBuilder(type.java) + } + + + /** + * Unbound builder + * + * @return + */ + @JvmStatic + fun edit(): DataSetBuilder { + return DataSetBuilder(Any::class.java) + } + } + +} + +class DataSetBuilder internal constructor(type: Class) : DataNodeBuilder(type) { + private val dataMap = LinkedHashMap>() + override var name = "" + + override val isEmpty: Boolean + get() = dataMap.isEmpty() + + override var meta: Meta = Meta.empty() + + override fun putData(key: String, data: Data, replace: Boolean) { + if (key.isEmpty()) { + throw AnonymousNotAlowedException() + } + if (type.isAssignableFrom(data.type)) { + if (replace || !dataMap.containsKey(key)) { + dataMap[key] = data + } else { + throw RuntimeException("The data with key $key already exists") + } + } else { + throw RuntimeException("Data does not satisfy class boundary") + } + } + + override fun putNode(key: String, node: DataNode) { + if (!node.meta.isEmpty) { + LoggerFactory.getLogger(javaClass).warn("Trying to add node with meta to flat DataNode. " + "Node meta could be lost. Consider using DataTree instead.") + } + //PENDING rewrap data including meta? + node.dataStream().forEach { data -> set("$key.${data.name}", data.anonymize()) } + } + + + + + override fun removeNode(nodeName: String) { + this.dataMap.entries.removeIf { entry -> entry.key.startsWith(nodeName) } + } + + override fun removeData(dataName: String) { + this.dataMap.remove(dataName) + } + + override fun build(): DataSet { + return DataSet(name, type, dataMap, meta) + } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/data/DataState.kt b/dataforge-core/src/main/kotlin/hep/dataforge/data/DataState.kt new file mode 100644 index 00000000..4d4395cb --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/data/DataState.kt @@ -0,0 +1,65 @@ +package hep.dataforge.data + +import hep.dataforge.names.Name + + +typealias DataStateListener = (DataNode<*>, List) -> Unit + +/** + * A mutable data storage that could be changed during computation via transactions. + * + * DataState does not inherit DataNode because DataNode contracts says it should be immutable + */ +interface DataState { + /** + * Add state change listener + * @param if true, the listener will be persistent and associated object will be in memory until removed. Otherwise the reference will be weak + */ + fun addListener(listener: DataStateListener, strong: Boolean = false) + + /** + * Remove the listener if it is present + */ + fun removeListener(listener: DataStateListener) + + /** + * Current snapshot of data in this state. The produced data node is immutable + */ + val node: DataNode + + /** + * Perform a transactional push into this state. For each data item there are 3 cases: + * 1. Data already present - it is replaced + * 2. Data is not present - it is added + * 3. Data is null - it is removed if present + * + */ + fun update(data: Map?>) + +} + +private sealed class Entry + +private class Item(val value: Data) : Entry() + +private class Node(val values: Map>) : Entry(){ + +} + +class TreeDataState: DataState{ + override fun addListener(listener: DataStateListener, strong: Boolean) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun removeListener(listener: DataStateListener) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override val node: DataNode + get() = TODO("not implemented") //To change initializer of created properties use File | Settings | File Templates. + + override fun update(data: Map?>) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + +} \ No newline at end of file diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/data/DataTree.kt b/dataforge-core/src/main/kotlin/hep/dataforge/data/DataTree.kt new file mode 100644 index 00000000..65c55e40 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/data/DataTree.kt @@ -0,0 +1,294 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.data + +import hep.dataforge.meta.Laminate +import hep.dataforge.meta.Meta +import hep.dataforge.names.Name +import hep.dataforge.nullable +import java.util.stream.Stream +import kotlin.reflect.KClass + +/** + * A tree data structure + * + * @author Alexander Nozik + */ +class DataTree constructor( + name: String, + override val type: Class, + meta: Meta, + parent: DataTree? +) : DataNode { + + override var name: String = name + private set + + private var selfMeta: Meta = meta + var parent: DataTree? = parent + private set + + private val nodeMap: MutableMap> = HashMap() + private val dataMap: MutableMap> = HashMap() + + + override val meta: Meta + get() = parent?.let { + Laminate(selfMeta, it.meta) + } ?: selfMeta + + override val isEmpty: Boolean + get() = dataMap.isEmpty() && nodeMap.isEmpty() + + + /** + * deep copy + */ + fun copy(name: String? = null): DataTree { + return DataTree(name ?: this.name, this.type, this.meta, this.parent).also { + this.nodeMap.forEach { key: String, tree: DataTree -> it.nodeMap[key] = tree.copy() } + it.dataMap.putAll(this.dataMap) + } + } + + + fun nodeNames(): Collection { + return nodeMap.keys + } + + override fun optData(key: String): Data? { + return dataStream(true) + .filter { it -> it.name == key } + .findFirst() + .nullable + } + + override fun nodeStream(recursive: Boolean): Stream> { + return if (recursive) { + nodeStream(Name.EMPTY, Laminate(meta)) + } else { + nodeMap.values.stream().map { it -> it } + } + } + + private fun nodeStream(parentName: Name, parentMeta: Laminate): Stream> { + return nodeMap.entries.stream().flatMap { nodeEntry -> + val nodeItself = Stream.of>( + NodeWrapper(nodeEntry.value, parentName.toString(), parentMeta) + ) + + val childName = parentName.plus(nodeEntry.key) + val childMeta = parentMeta.withFirstLayer(nodeEntry.value.meta) + val childStream = nodeEntry.value.nodeStream(childName, childMeta) + + Stream.concat>(nodeItself, childStream) + } + } + + + override fun dataStream(recursive: Boolean): Stream> { + return dataStream(null, Laminate(selfMeta), recursive) + } + + private fun dataStream(nodeName: Name?, nodeMeta: Laminate, recursive: Boolean): Stream> { + val dataStream = dataMap.entries.stream() + .map> { entry -> + val dataName = if (nodeName == null) Name.of(entry.key) else nodeName.plus(entry.key) + NamedData.wrap(dataName, entry.value, nodeMeta) + } + + if (recursive) { + // iterating over nodes including node name into dataStream + val subStream = nodeMap.entries.stream() + .flatMap { nodeEntry -> + val subNodeName = if (nodeName == null) Name.of(nodeEntry.key) else nodeName.plus(nodeEntry.key) + nodeEntry.value + .dataStream(subNodeName, nodeMeta.withFirstLayer(nodeEntry.value.meta), true) + } + return Stream.concat(dataStream, subStream) + } else { + return dataStream + } + } + + override fun optNode(nodeName: String): DataNode? { + return getNode(Name.of(nodeName)) + } + + private fun getNode(nodeName: Name): DataTree? { + val child = nodeName.first.toString() + return if (nodeName.length == 1) { + nodeMap[nodeName.toString()] + } else { + this.nodeMap[child]?.getNode(nodeName.cutFirst()) + } + } + + /** + * Private editor object + */ + private val editor = Builder() + + /** + * Create a deep copy of this tree and edit it + */ + override fun edit(): Builder { + return copy().editor + } + + inner class Builder : DataNodeBuilder(type) { + override var name: String + get() = this@DataTree.name + set(value) { + this@DataTree.name = value + } + override var meta: Meta + get() = this@DataTree.selfMeta + set(value) { + this@DataTree.selfMeta = value + } + + + override val isEmpty: Boolean + get() = nodeMap.isEmpty() && dataMap.isEmpty() + + @Suppress("UNCHECKED_CAST") + private fun checkedPutNode(key: String, node: DataNode<*>) { + if (type.isAssignableFrom(node.type)) { + if (!nodeMap.containsKey(key)) { + nodeMap[key] = node.edit().also { it.name = key }.build() as DataTree + } else { + throw RuntimeException("The node with key $key already exists") + } + } else { + throw RuntimeException("Node does not satisfy class boundary") + } + } + + /** + * Type checked put data method. Throws exception if types are not + * compatible + * + * @param key + * @param data + */ + @Suppress("UNCHECKED_CAST") + private fun checkedPutData(key: String, data: Data<*>, allowReplace: Boolean) { + if (type.isAssignableFrom(data.type)) { + if (!dataMap.containsKey(key) || allowReplace) { + dataMap[key] = data as Data + } else { + throw RuntimeException("The data with key $key already exists") + } + } else { + throw RuntimeException("Data does not satisfy class boundary") + } + } + + /** + * Private method to add data to the node + * + * @param keyName + * @param data + */ + private fun putData(keyName: Name, data: Data, replace: Boolean = false) { + when { + keyName.length == 0 -> throw IllegalArgumentException("Name must not be empty") + keyName.length == 1 -> { + val key = keyName.toString() + checkedPutData(key, data, replace) + } + else -> { + val head = keyName.first.toString() + (nodeMap[head] ?: DataTree(head, type, Meta.empty(), this@DataTree) + .also { nodeMap[head] = it }) + .editor.putData(keyName.cutFirst(), data, replace) + } + } + } + + override fun putData(key: String, data: Data, replace: Boolean) { + putData(Name.of(key), data, replace) + } + + private fun putNode(keyName: Name, node: DataNode) { + when { + keyName.length == 0 -> throw IllegalArgumentException("Can't put node with empty name") + keyName.length == 1 -> { + val key = keyName.toString() + checkedPutNode(key, node) + } + else -> { + val head = keyName.first.toString() + (nodeMap[head] ?: DataTree(head, type, Meta.empty(), this@DataTree) + .also { nodeMap[head] = it }) + .editor.putNode(keyName.cutFirst(), node) + } + } + } + + override fun putNode(key: String, node: DataNode) { + putNode(Name.of(key), node) + } + + override fun removeNode(nodeName: String) { + val theName = Name.of(nodeName) + val parentTree: DataTree<*>? = if (theName.length == 1) { + this@DataTree + } else { + getNode(theName.cutLast()) + } + parentTree?.nodeMap?.remove(theName.last.toString()) + } + + override fun removeData(dataName: String) { + val theName = Name.of(dataName) + val parentTree: DataTree<*>? = if (theName.length == 1) { + this@DataTree + } else { + getNode(theName.cutLast()) + } + parentTree?.dataMap?.remove(theName.last.toString()) + + } + + + override fun build(): DataTree { + return this@DataTree + } + + } + + + companion object { + + @JvmStatic + fun edit(type: Class): DataTree.Builder { + return DataTree("", type, Meta.empty(), null).editor + } + + fun edit(type: KClass): DataTree.Builder { + return edit(type.java) + } + + /** + * A general non-restricting tree builder + * + * @return + */ + fun edit(): DataTree.Builder { + return edit(Any::class.java) + } + + } +} + +inline fun DataTree(block: DataNodeBuilder.() -> Unit): DataTree { + return DataTree("", T::class.java, Meta.empty(), null).edit().apply(block).build() +} + + + diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/data/DataUtils.kt b/dataforge-core/src/main/kotlin/hep/dataforge/data/DataUtils.kt new file mode 100644 index 00000000..22fe561e --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/data/DataUtils.kt @@ -0,0 +1,222 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.data + +import hep.dataforge.await +import hep.dataforge.context.Context +import hep.dataforge.context.FileReference +import hep.dataforge.data.binary.Binary +import hep.dataforge.goals.AbstractGoal +import hep.dataforge.goals.Coal +import hep.dataforge.goals.Goal +import hep.dataforge.goals.PipeGoal +import hep.dataforge.io.MetaFileReader +import hep.dataforge.io.envelopes.EnvelopeReader +import hep.dataforge.meta.Laminate +import hep.dataforge.meta.Meta +import hep.dataforge.toList +import java.io.IOException +import java.nio.file.Files +import java.nio.file.Path +import java.util.concurrent.Executor +import java.util.function.BiFunction +import java.util.function.Function +import java.util.stream.Stream + +/** + * Created by darksnake on 06-Sep-16. + */ +object DataUtils { + + const val META_DIRECTORY = "@meta" + + /** + * Combine two data elements of different type into single data + */ + fun combine(context: Context, + data1: Data, data2: Data, + type: Class, + meta: Meta, + transform: (S1, S2) -> R): Data { +// val combineGoal = object : AbstractGoal() { +// @Throws(Exception::class) +// override fun compute(): R { +// return transform(data1.get(), data2.get()) +// } +// +// override fun dependencies(): Stream> { +// return Stream.of(data1.goal, data2.goal) +// } +// } + val goal = Coal(context, listOf(data1.goal, data2.goal)) { + val res1 = data1.goal.await() + val res2 = data2.goal.await() + transform(res1,res2) + } + return Data(type, goal, meta) + } + + + /** + * Join a uniform list of elements into a single datum + */ + fun join(data: Collection>, + type: Class, + meta: Meta, + transform: Function, R>): Data { + val combineGoal = object : AbstractGoal() { + @Throws(Exception::class) + override fun compute(): R { + return transform.apply(data.stream().map { it.get() }.toList()) + } + + override fun dependencies(): Stream> { + return data.stream().map { it.goal } + } + } + return Data(type, combineGoal, meta) + } + + fun join(dataNode: DataNode, type: Class, transform: Function, R>): Data { + val combineGoal = object : AbstractGoal() { + @Throws(Exception::class) + override fun compute(): R { + return transform.apply(dataNode.dataStream() + .filter { it.isValid } + .map { it.get() } + .toList() + ) + } + + override fun dependencies(): Stream> { + return dataNode.dataStream().map { it.goal } + } + } + return Data(type, combineGoal, dataNode.meta) + } + + /** + * Apply lazy transformation of the data using default executor. The meta of the result is the same as meta of input + * + * @param target + * @param transformation + * @param + * @return + */ + fun transform(data: Data, target: Class, transformation: (T) -> R): Data { + val goal = PipeGoal(data.goal, transformation) + return Data(target, goal, data.meta) + } + + fun transform(data: Data, target: Class, executor: Executor, transformation: (T) -> R): Data { + val goal = PipeGoal(executor, data.goal, Function(transformation)) + return Data(target, goal, data.meta) + } + + fun transform(data: NamedData, target: Class, transformation: (T) -> R): NamedData { + val goal = PipeGoal(data.goal, transformation) + return NamedData(data.name, target, goal, data.meta) + } + + /** + * A node containing single data fragment + * + * @param nodeName + * @param data + * @param + * @return + */ + fun singletonNode(nodeName: String, data: Data): DataNode { + return DataSet.edit(data.type) + .apply { putData(DataNode.DEFAULT_DATA_FRAGMENT_NAME, data) } + .build() + } + + fun singletonNode(nodeName: String, `object`: T): DataNode { + return singletonNode(nodeName, Data.buildStatic(`object`)) + } + + /** + * Reslove external meta for file if it is present + */ + fun readExternalMeta(file: FileReference): Meta? { + val metaFileDirectory = file.absolutePath.resolveSibling(META_DIRECTORY) + return MetaFileReader.resolve(metaFileDirectory, file.absolutePath.fileName.toString()).orElse(null) + } + + /** + * Read an object from a file using given transformation. Capture a file meta from default directory. Override meta is placed above file meta. + * + * @param file + * @param override + * @param type + * @param reader + * @param + * @return + */ + fun readFile(file: FileReference, override: Meta, type: Class, reader: (Binary) -> T): Data { + val filePath = file.absolutePath + if (!Files.isRegularFile(filePath)) { + throw IllegalArgumentException(filePath.toString() + " is not existing file") + } + val binary = file.binary + val fileMeta = readExternalMeta(file) + val meta = Laminate(fileMeta, override) + return Data.generate(type, meta) { reader(binary) } + } + + /** + * Read file as Binary Data. + * + * @param file + * @param override + * @return + */ + fun readFile(file: FileReference, override: Meta): Data { + return readFile(file, override, Binary::class.java) { it } + } + + + /** + * Transform envelope file into data using given transformation. The meta of the data consists of 3 layers: + * + * 1. override - dynamic meta from method argument) + * 1. captured - captured from @meta directory + * 1. own - envelope owm meta + * + * + * @param filePath + * @param override + * @param type + * @param reader a bifunction taking the binary itself and combined meta as arguments and returning + * @param + * @return + */ + fun readEnvelope(filePath: Path, override: Meta, type: Class, reader: BiFunction): Data { + try { + val envelope = EnvelopeReader.readFile(filePath) + val binary = envelope.data + val metaFileDirectory = filePath.resolveSibling(META_DIRECTORY) + val fileMeta = MetaFileReader.resolve(metaFileDirectory, filePath.fileName.toString()).orElse(Meta.empty()) + val meta = Laminate(fileMeta, override, envelope.meta) + return Data.generate(type, meta) { reader.apply(binary, meta) } + } catch (e: IOException) { + throw RuntimeException("Failed to read " + filePath.toString() + " as an envelope", e) + } + + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/data/EmptyDataNode.kt b/dataforge-core/src/main/kotlin/hep/dataforge/data/EmptyDataNode.kt new file mode 100644 index 00000000..9153b1c0 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/data/EmptyDataNode.kt @@ -0,0 +1,26 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.data + +import hep.dataforge.meta.Meta +import java.util.stream.Stream + + +class EmptyDataNode(override val name: String, override val type: Class) : DataNode { + + override val isEmpty = true + + override val meta: Meta = Meta.empty() + + override fun optData(key: String): Data? = null + + override fun dataStream(recursive: Boolean): Stream> = Stream.empty() + + override fun nodeStream(recursive: Boolean): Stream> = Stream.empty() + + override fun optNode(nodeName: String): DataNode? = null + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/data/FileDataFactory.kt b/dataforge-core/src/main/kotlin/hep/dataforge/data/FileDataFactory.kt new file mode 100644 index 00000000..14ae1b1a --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/data/FileDataFactory.kt @@ -0,0 +1,178 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.data + +import hep.dataforge.context.Context +import hep.dataforge.context.Context.Companion.DATA_DIRECTORY_CONTEXT_KEY +import hep.dataforge.context.FileReference +import hep.dataforge.data.FileDataFactory.Companion.DIRECTORY_NODE +import hep.dataforge.data.FileDataFactory.Companion.FILE_NODE +import hep.dataforge.data.binary.Binary +import hep.dataforge.description.NodeDef +import hep.dataforge.description.NodeDefs +import hep.dataforge.meta.Laminate +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.toList +import hep.dataforge.utils.NamingUtils.wildcardMatch +import java.io.IOException +import java.nio.file.Files +import java.nio.file.Path + +@NodeDefs( + NodeDef(key = FILE_NODE, info = "File data element or list of files with the same meta defined by mask."), + NodeDef(key = DIRECTORY_NODE, info = "Directory data node.") +) +open class FileDataFactory : DataFactory(Binary::class.java) { + + override val name: String = "file" + + override fun fill(builder: DataNodeBuilder, context: Context, meta: Meta) { + val parentFile: Path = when { + meta.hasMeta(DATA_DIRECTORY_CONTEXT_KEY) -> context.rootDir.resolve(meta.getString(DATA_DIRECTORY_CONTEXT_KEY)) + else -> context.dataDir + } + + /** + * Add items matching specific file name. Not necessary one. + */ + if (meta.hasMeta(FILE_NODE)) { + meta.getMetaList(FILE_NODE).forEach { node -> addFile(context, builder, parentFile, node) } + } + + /** + * Add content of the directory + */ + if (meta.hasMeta(DIRECTORY_NODE)) { + meta.getMetaList(DIRECTORY_NODE).forEach { node -> addDir(context, builder, parentFile, node) } + } + + if (meta.hasValue(FILE_NODE)) { + val fileValue = meta.getValue(FILE_NODE) + fileValue.list.forEach { fileName -> + addFile(context, builder, parentFile, MetaBuilder(FILE_NODE) + .putValue("path", fileName)) + } + } + } + + /** + * Create a data from given file. Could be overridden for additional functionality + */ + protected open fun buildFileData(file: FileReference, override: Meta): Data { + val mb = override.builder.apply { + putValue(FILE_PATH_KEY, file.absolutePath.toString()) + putValue(FILE_NAME_KEY, file.name) + }.sealed + + val externalMeta = DataUtils.readExternalMeta(file) + + val fileMeta = if (externalMeta == null) { + mb + } else { + Laminate(mb, externalMeta) + } + + return Data.buildStatic(file.binary, fileMeta) + } + + /** + * Add file or files providede via given meta to the tree + * @param context + * @param builder + * @param parentFile + * @param fileNode + */ + private fun addFile(context: Context, builder: DataNodeBuilder, parentFile: Path, fileNode: Meta) { + val files = listFiles(context, parentFile, fileNode) + when { + files.isEmpty() -> context.logger.warn("No files matching the filter: " + fileNode.toString()) + files.size == 1 -> { + val file = FileReference.openFile(context, files[0]) + val fileMeta = fileNode.getMetaOrEmpty(DataFactory.NODE_META_KEY) + builder.putData(file.name, buildFileData(file, fileMeta)) + } + else -> files.forEach { path -> + val file = FileReference.openFile(context, path) + val fileMeta = fileNode.getMetaOrEmpty(DataFactory.NODE_META_KEY) + builder.putData(file.name, buildFileData(file, fileMeta)) + } + } + } + + /** + * List files in given path + */ + protected open fun listFiles(context: Context, path: Path, fileNode: Meta): List { + val mask = fileNode.getString("path") + val parent = context.rootDir.resolve(path) + try { + return Files.list(parent).filter { wildcardMatch(mask, it.toString()) }.toList() + } catch (e: IOException) { + throw RuntimeException(e) + } + + } + + private fun addDir(context: Context, builder: DataNodeBuilder, parentFile: Path, dirNode: Meta) { + val dirBuilder = DataTree.edit(Binary::class.java) + val dir = parentFile.resolve(dirNode.getString("path")) + if (!Files.isDirectory(dir)) { + throw RuntimeException("The directory $dir does not exist") + } + dirBuilder.name = dirNode.getString(DataFactory.NODE_NAME_KEY, dirNode.name) + if (dirNode.hasMeta(DataFactory.NODE_META_KEY)) { + dirBuilder.meta = dirNode.getMeta(DataFactory.NODE_META_KEY) + } + + val recurse = dirNode.getBoolean("recursive", true) + + try { + Files.list(dir).forEach { path -> + if (Files.isRegularFile(path)) { + val file = FileReference.openFile(context, path) + dirBuilder.putData(file.name, buildFileData(file, Meta.empty())) + } else if (recurse && dir.fileName.toString() != META_DIRECTORY) { + addDir(context, dirBuilder, dir, Meta.empty()) + } + } + } catch (e: IOException) { + throw RuntimeException(e) + } + + builder.add(dirBuilder.build()) + + } + + companion object { + + const val FILE_NODE = "file" + const val FILE_MASK_NODE = "files" + const val DIRECTORY_NODE = "dir" + + const val FILE_NAME_KEY = "fileName" + const val FILE_PATH_KEY = "filePath" + + const val META_DIRECTORY = "@meta" + } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/data/FilteredDataNode.kt b/dataforge-core/src/main/kotlin/hep/dataforge/data/FilteredDataNode.kt new file mode 100644 index 00000000..e57f6765 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/data/FilteredDataNode.kt @@ -0,0 +1,46 @@ +package hep.dataforge.data + +import hep.dataforge.meta.Meta +import java.util.stream.Stream + +/** + * Filtered node does not change structure of underlying node, just filters output + * + * @param + */ +class FilteredDataNode(private val node: DataNode, private val predicate: (String, Data) -> Boolean) : DataNode { + override val name: String = node.name + + override val meta: Meta + get() = node.meta + + override val isEmpty: Boolean + get() = dataStream(true).count() == 0L + + + override fun optData(key: String): Data? { + return node.optData(key)?.let { d -> + if (predicate(key, d)) { + d + } else { + null + } + } + } + + override fun optNode(nodeName: String): DataNode? { + return node.optNode(nodeName)?.let { it -> FilteredDataNode(it, predicate) } + } + + override fun dataStream(recursive: Boolean): Stream> { + return node.dataStream(recursive).filter { d -> predicate(d.name, d.cast(type)) } + } + + override fun nodeStream(recursive: Boolean): Stream> { + return node.nodeStream(recursive).map { n -> + n.filter { name, data -> predicate(name, data) } + } + } + + override val type: Class = node.type +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/data/NamedData.kt b/dataforge-core/src/main/kotlin/hep/dataforge/data/NamedData.kt new file mode 100644 index 00000000..52da89dc --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/data/NamedData.kt @@ -0,0 +1,78 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.data + +import hep.dataforge.Named +import hep.dataforge.exceptions.AnonymousNotAlowedException +import hep.dataforge.goals.Goal +import hep.dataforge.goals.StaticGoal +import hep.dataforge.meta.Laminate +import hep.dataforge.meta.Meta +import hep.dataforge.names.AnonymousNotAlowed +import hep.dataforge.names.Name + +/** + * A data with name + * + * @author Alexander Nozik + */ +@AnonymousNotAlowed +open class NamedData(final override val name: String, type: Class, goal: Goal, meta: Meta) : Data(type, goal, meta), Named { + + init { + if (name.isEmpty()) { + throw AnonymousNotAlowedException() + } + } + + + operator fun component1() = name + operator fun component2(): T = goal.get() + + /** + * Return unnamed data corresponding to this named one + * + * @return + */ + fun anonymize(): Data { + return Data(this.type, this.goal, this.meta) + } + + override fun cast(type: Class): NamedData { + return if (type.isAssignableFrom(type)) { + @Suppress("UNCHECKED_CAST") + NamedData(name, type, goal as Goal, meta) + } else { + throw IllegalArgumentException("Invalid type to upcast data") + } + } + + companion object { + + fun buildStatic(name: String, content: T, meta: Meta): NamedData { + return NamedData(name, content.javaClass, StaticGoal(content), meta) + } + + /** + * Wrap existing data using name and layers of external meta if it is available + * + * @param name + * @param data + * @param externalMeta + * @param + * @return + */ + fun wrap(name: String, data: Data, vararg externalMeta: Meta): NamedData { + val newMeta = Laminate(data.meta).withLayer(*externalMeta) + return NamedData(name, data.type, data.goal, newMeta) + } + + fun wrap(name: Name, data: Data, externalMeta: Laminate): NamedData { + val newMeta = externalMeta.withFirstLayer(data.meta) + return NamedData(name.toString(), data.type, data.goal, newMeta) + } + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/data/NodeWrapper.kt b/dataforge-core/src/main/kotlin/hep/dataforge/data/NodeWrapper.kt new file mode 100644 index 00000000..031a8c7f --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/data/NodeWrapper.kt @@ -0,0 +1,49 @@ +package hep.dataforge.data + +import hep.dataforge.meta.Laminate +import hep.dataforge.meta.Meta +import hep.dataforge.names.Name +import java.util.stream.Stream + +/** + * Data node wrapper to add parent name and meta to existing node + * Created by darksnake on 14-Aug-16. + */ +class NodeWrapper(private val node: DataNode, parentName: String, parentMeta: Meta) : DataNode { + override val meta: Laminate + override val name: String + + override val isEmpty: Boolean + get() = node.isEmpty + + init { + if (parentMeta is Laminate) { + this.meta = parentMeta.withFirstLayer(node.meta) + } else { + this.meta = Laminate(node.meta, parentMeta) + } + this.name = if (parentName.isEmpty()) node.name else Name.joinString(parentName, node.name) + } + + override fun optData(key: String): Data? { + return node.optData(key) + } + + override fun optNode(nodeName: String): DataNode? { + return node.optNode(nodeName) + } + + override fun dataStream(recursive: Boolean): Stream> { + return node.dataStream(recursive) + } + + override fun nodeStream(recursive: Boolean): Stream> { + return node.nodeStream(recursive) + } + + override val type: Class + get() { + return node.type + } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/data/SmartDataLoader.kt b/dataforge-core/src/main/kotlin/hep/dataforge/data/SmartDataLoader.kt new file mode 100644 index 00000000..5a0387cf --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/data/SmartDataLoader.kt @@ -0,0 +1,39 @@ +package hep.dataforge.data + +import hep.dataforge.context.Context +import hep.dataforge.meta.Meta + + +/** + * A data loader that delegates loading to a specific loader + */ +class SmartDataLoader : DataLoader { + + override val name: String = "smart" + + override fun build(context: Context, meta: Meta): DataNode { + return getFactory(context, meta).build(context, meta) + } + + companion object { + const val FACTORY_TYPE_KEY = "loader" + + @Suppress("UNCHECKED_CAST") + fun getFactory(context: Context, meta: Meta): DataLoader { + return if (meta.hasValue("dataLoaderClass")) { + try { + Class.forName(meta.getString("dataLoaderClass")).getConstructor().newInstance() as DataLoader + } catch (e: Exception) { + throw RuntimeException(e) + } + + } else { + meta.optString(FACTORY_TYPE_KEY).flatMap { loader -> + context.serviceStream(DataLoader::class.java) + .filter { it -> it.name == loader } + .findFirst() ?: error("DataLoader with type $loader not found") + }.orElse(DummyDataFactory(Any::class.java)) as DataLoader + } + } + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/data/binary/Binary.kt b/dataforge-core/src/main/kotlin/hep/dataforge/data/binary/Binary.kt new file mode 100644 index 00000000..85b2b771 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/data/binary/Binary.kt @@ -0,0 +1,95 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.data.binary + +import java.io.IOException +import java.io.InputStream +import java.io.Serializable +import java.nio.ByteBuffer +import java.nio.channels.ReadableByteChannel + +/** + * An interface to represent something that one can read binary data from in a + * blocking or non-blocking way. This interface is intended for read access + * only. + * + * @author Alexander Nozik + */ +interface Binary : Serializable { + + + /** + * Get blocking input stream for this binary + * + * @return + */ + val stream: InputStream + + /** + * Get non-blocking byte channel + * + * @return + */ + val channel: ReadableByteChannel + + /** + * Read the content of this binary to a single byte buffer. + * + * @return + * @throws IOException + */ + val buffer: ByteBuffer + get() { + if (size >= 0) { + val buffer = ByteBuffer.allocate(size.toInt()) + channel.read(buffer) + return buffer + } else { + throw IOException("Can not convert binary of undefined size to buffer") + } + } + + /** + * The size of this binary. Negative value corresponds to undefined size. + * + * @return + * @throws IOException + */ + val size: Long + + @JvmDefault + fun stream(offset: Long): InputStream = stream.also { it.skip(offset) } + + /** + * Read a buffer with given dataOffset in respect to data block start and given size. + * + * @param offset + * @param size + * @return + * @throws IOException + */ + @JvmDefault + fun read(offset: Int, size: Int): ByteBuffer { + return buffer.run { + position(offset) + val array = ByteArray(size) + get(array) + ByteBuffer.wrap(array) + } + } + + /** + * + */ + @JvmDefault + fun read(start: Int): ByteBuffer { + return read(start, (size - start).toInt()) + } + + companion object { + val EMPTY: Binary = BufferedBinary(ByteArray(0)) + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/data/binary/BufferedBinary.kt b/dataforge-core/src/main/kotlin/hep/dataforge/data/binary/BufferedBinary.kt new file mode 100644 index 00000000..68d8cacf --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/data/binary/BufferedBinary.kt @@ -0,0 +1,30 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.data.binary + +import java.io.ByteArrayInputStream +import java.io.InputStream +import java.nio.ByteBuffer +import java.nio.channels.Channels +import java.nio.channels.ReadableByteChannel + + +class BufferedBinary(override val buffer: ByteBuffer) : Binary { + + override val stream: InputStream + get() = ByteArrayInputStream(buffer.array()) + + override val channel: ReadableByteChannel + get() = Channels.newChannel(stream) + + constructor(buffer: ByteArray): this(ByteBuffer.wrap(buffer)) + + override val size: Long + get() { + return buffer.limit().toLong() + } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/data/binary/FileBinary.kt b/dataforge-core/src/main/kotlin/hep/dataforge/data/binary/FileBinary.kt new file mode 100644 index 00000000..10753aeb --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/data/binary/FileBinary.kt @@ -0,0 +1,65 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.data.binary + +import java.io.IOException +import java.io.InputStream +import java.io.ObjectStreamException +import java.io.WriteAbortedException +import java.nio.ByteBuffer +import java.nio.channels.ByteChannel +import java.nio.channels.FileChannel +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardOpenOption +import java.nio.file.StandardOpenOption.READ + +/** + * + * @param file File to create binary from + * @param dataOffset dataOffset form beginning of file + */ +class FileBinary( + private val file: Path, + private val dataOffset: Long = 0, + private val _size: Long = -1 +) : Binary { + + override val stream: InputStream + get() = Files.newInputStream(file, READ).also { it.skip(dataOffset) } + + override val channel: ByteChannel + get() = FileChannel.open(file, READ).position(dataOffset) + + override val buffer: ByteBuffer + get() = read(0, size.toInt()) + + /** + * Read a buffer with given dataOffset in respect to data block start and given size. If data size w + * + * @param offset + * @param size + * @return + * @throws IOException + */ + override fun read(offset: Int, size: Int): ByteBuffer { + return FileChannel.open(file, StandardOpenOption.READ).use { it.map(FileChannel.MapMode.READ_ONLY, dataOffset + offset, size.toLong())} + } + + override val size: Long + get() = if (_size >= 0) _size else Files.size(file) - dataOffset + + @Throws(ObjectStreamException::class) + private fun writeReplace(): Any { + try { + return BufferedBinary(buffer.array()) + } catch (e: IOException) { + throw WriteAbortedException("Failed to get byte buffer", e) + } + + } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/data/binary/StreamBinary.kt b/dataforge-core/src/main/kotlin/hep/dataforge/data/binary/StreamBinary.kt new file mode 100644 index 00000000..8a4a59cf --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/data/binary/StreamBinary.kt @@ -0,0 +1,36 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.data.binary + +import java.io.IOException +import java.io.InputStream +import java.io.ObjectStreamException +import java.io.WriteAbortedException +import java.nio.channels.Channels +import java.nio.channels.ReadableByteChannel + +class StreamBinary(private val sup: () -> InputStream) : Binary { + //TODO limit inputStream size + override val stream: InputStream by lazy(sup) + + override val channel: ReadableByteChannel by lazy { + Channels.newChannel(stream) + } + + override val size: Long + get() = -1 + + @Throws(ObjectStreamException::class) + private fun writeReplace(): Any { + try { + return BufferedBinary(buffer.array()) + } catch (e: IOException) { + throw WriteAbortedException("Failed to get byte buffer", e) + } + + } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/description/ActionDescriptor.kt b/dataforge-core/src/main/kotlin/hep/dataforge/description/ActionDescriptor.kt new file mode 100644 index 00000000..9f494805 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/description/ActionDescriptor.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.description + +import hep.dataforge.actions.Action +import hep.dataforge.actions.GenericAction +import hep.dataforge.actions.ManyToOneAction +import hep.dataforge.io.output.Output +import hep.dataforge.io.output.SelfRendered +import hep.dataforge.io.output.TextOutput +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import java.awt.Color + +/** + * + * + * ActionDescriptor class. + * + * @author Alexander Nozik + * @version $Id: $Id + */ +class ActionDescriptor(meta: Meta) : NodeDescriptor(meta), SelfRendered { + + override val info: String + get() = meta.getString("actionDef.description", "") + + val inputType: String + get() = meta.getString("actionDef.inputType", "") + + val outputType: String + get() = meta.getString("actionDef.outputType", "") + + override val name: String + get() = meta.getString("actionDef.name", super.name) + + override fun render(output: Output, meta: Meta) { + if (output is TextOutput) { + output.renderText(name, Color.GREEN) + output.renderText(" {input : ") + output.renderText(inputType, Color.CYAN) + output.renderText(", output : ") + output.renderText(outputType, Color.CYAN) + output.renderText(String.format("}: %s", info)) + + } else { + output.render(String.format("Action %s (input: %s, output: %s): %s%n", name, inputType, outputType, info), meta) + } + } + + companion object { + + fun build(action: Action<*, *>): ActionDescriptor { + val builder = Descriptors.forType(action.name, action::class).meta.builder + + val actionDef = MetaBuilder("actionDef").putValue("name", action.name) + if (action is GenericAction<*, *>) { + actionDef + .putValue("inputType", action.inputType.simpleName) + .putValue("outputType", action.outputType.simpleName) + if (action is ManyToOneAction<*, *>) { + actionDef.setValue("inputType", (action as GenericAction<*, *>).outputType.simpleName + "[]") + } + } + + val def = action.javaClass.getAnnotation(TypedActionDef::class.java) + + if (def != null) { + actionDef.putValue("description", def.info) + } + builder.putNode(actionDef) + return ActionDescriptor(builder) + } + + fun build(actionClass: Class>): ActionDescriptor { + val builder = Descriptors.forType("action", actionClass.kotlin).meta.builder + + val def = actionClass.getAnnotation(TypedActionDef::class.java) + if (def != null) { + val actionDef = MetaBuilder("actionDef") + .putValue("name", def.name) + .putValue("inputType", def.inputType.simpleName) + .putValue("outputType", def.outputType.simpleName) + .putValue("description", def.info) + + if (actionClass.isAssignableFrom(ManyToOneAction::class.java)) { + actionDef.setValue("inputType", def.inputType.simpleName + "[]") + } + + builder.putNode(actionDef) + + } + return ActionDescriptor(builder) + } + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/description/Described.kt b/dataforge-core/src/main/kotlin/hep/dataforge/description/Described.kt new file mode 100644 index 00000000..9c86d7b0 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/description/Described.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.description + +/** + * A general interface for something with meta description + * + * @author Alexander Nozik + */ +interface Described { + /** + * Provide a description + * + * @return + */ + @JvmDefault + val descriptor: NodeDescriptor + get() = Descriptors.forJavaType("node", this.javaClass) +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/description/Description.kt b/dataforge-core/src/main/kotlin/hep/dataforge/description/Description.kt new file mode 100644 index 00000000..09257e30 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/description/Description.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.description + +import hep.dataforge.values.ValueType +import kotlin.reflect.KClass + +/** + * Description text for meta property, node or whole object + */ +@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class Description(val value: String) + +/** + * Annotation for value property which states that lists are expected + */ +@Target(AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class Multiple + +/** + * Descriptor target + */ +@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class Descriptor(val value: String) + + +/** + * Aggregator class for descriptor nodes + */ +@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION, AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class DescriptorNodes(vararg val nodes: NodeDef) + +/** + * Aggregator class for descriptor values + */ +@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION, AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class DescriptorValues(vararg val nodes: ValueDef) + +/** + * Alternative name for property descriptor declaration + */ +@Target(AnnotationTarget.PROPERTY, AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class DescriptorName(val name: String) + +@Target(AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class DescriptorValue(val def: ValueDef) +//TODO enter fields directly? + +@Target(AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class ValueProperty( + val name: String = "", + val type: Array = [ValueType.STRING], + val multiple: Boolean = false, + val def: String = "", + val enumeration: KClass<*> = Any::class, + val tags: Array = [] +) + + +@Target(AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class NodeProperty(val name: String = "") diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/description/DescriptorBuilder.kt b/dataforge-core/src/main/kotlin/hep/dataforge/description/DescriptorBuilder.kt new file mode 100644 index 00000000..c728d60e --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/description/DescriptorBuilder.kt @@ -0,0 +1,205 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.description + +import hep.dataforge.findNode +import hep.dataforge.meta.* +import hep.dataforge.names.Name +import hep.dataforge.set +import hep.dataforge.values.Value +import hep.dataforge.values.ValueType +import org.slf4j.LoggerFactory + +/** + * Helper class to builder descriptors + * @author Alexander Nozik + */ +class DescriptorBuilder(name: String, override val meta: Configuration = Configuration("node")) : Metoid { + + var name by meta.mutableStringValue() + var required by meta.mutableBooleanValue() + var multiple by meta.mutableBooleanValue() + var default by meta.mutableNode() + var info by meta.mutableStringValue() + var tags: List by meta.customMutableValue(read = { it.list.map { it.string } }, write = { Value.of(it) }) + + init { + this.name = name + } + + //TODO add caching for node names? + /** + * Check if this node condtains descriptor node with given name + */ + private fun hasNodeDescriptor(name: String): Boolean { + return meta.getMetaList("node").find { it.getString("name") == name } != null + } + + /** + * Check if this node contains value with given name + */ + private fun hasValueDescriptor(name: String): Boolean { + return meta.getMetaList("value").find { it.getString("name") == name } != null + } + + /** + * append child descriptor builder + */ + fun node(childDescriptor: NodeDescriptor): DescriptorBuilder { + if (!hasNodeDescriptor(childDescriptor.name)) { + meta.putNode(childDescriptor.meta) + } else { + LoggerFactory.getLogger(javaClass).warn("Trying to replace existing node descriptor ${childDescriptor.name}") + } + return this + } + + /** + * Append node to this descriptor respecting the path + */ + fun node(name: Name, childBuilder: DescriptorBuilder.() -> Unit): DescriptorBuilder { + val parent = if (name.length == 1) { + this + } else { + buildChild(name.cutLast()) + } + parent.node(DescriptorBuilder(name.last.toString()).apply(childBuilder).build()) + return this + } + + /** + * Add a node using DSL builder. Name could be non-atomic + */ + fun node(name: String, childBuilder: DescriptorBuilder.() -> Unit): DescriptorBuilder { + return node(Name.of(name), childBuilder) + } + + /** + * Build a descrptor builder for child node. Changes in child descriptor reflect on this descriptor builder + */ + private fun buildChild(name: Name): DescriptorBuilder { + return when (name.length) { + 0 -> this + 1 -> { + val existingChild = meta.getMetaList("node").find { it.getString("name") == name.first.unescaped } + return if (existingChild == null) { + DescriptorBuilder(name.first.unescaped).also { this.meta.attachNode(it.meta) } + } else { + DescriptorBuilder(name.first.unescaped, existingChild) + } + } + else -> { + buildChild(name.first).buildChild(name.cutFirst()) + } + } + } + + /** + * Add node from annotation + */ + fun node(nodeDef: NodeDef): DescriptorBuilder { + return node(Descriptors.forDef(nodeDef)) + } + + /** + * Add a value respecting its path inside descriptor + */ + fun value(descriptor: ValueDescriptor): DescriptorBuilder { + val name = Name.of(descriptor.name) + val parent = if (name.length == 1) { + this + } else { + buildChild(name.cutLast()) + } + + if (!parent.hasValueDescriptor(name.last.unescaped)) { + parent.meta.putNode(descriptor.toMeta().builder.apply { this["name"] = name.last.unescaped }) + } else { + LoggerFactory.getLogger(javaClass).warn("Trying to replace existing value descriptor ${descriptor.name}") + } + return this + } + + fun value(def: ValueDef): DescriptorBuilder { + return value(ValueDescriptor.build(def)) + } + + /** + * Create value descriptor from its fields. Name could be non-atomic + */ + fun value( + name: String, + info: String = "", + defaultValue: Any? = null, + required: Boolean = false, + multiple: Boolean = false, + types: List = emptyList(), + allowedValues: List = emptyList() + ): DescriptorBuilder { + return value(ValueDescriptor.build(name, info, defaultValue, required, multiple, types, allowedValues)) + } + + fun update(descriptor: NodeDescriptor): DescriptorBuilder { + //TODO update primary fields + descriptor.valueDescriptors().forEach { + this.value(it.value) + } + descriptor.childrenDescriptors().forEach { + this.node(it.value) + } + return this + } + + /** + * Get existing node descriptor with given name + */ + fun findNode(name: Name): DescriptorBuilder? { + return when { + name.length == 0 -> this + name.length == 1 -> meta.findNode("node") { getString("name") == name.unescaped }?.let { DescriptorBuilder(name.unescaped, it.self()) } + else -> findNode(name.first)?.findNode(name.cutFirst()) + } + } + + /** + * Get existing value descriptor configuration with given name + */ + fun findValue(name: Name): Configuration? { + return findNode(name.cutLast())?.meta?.findNode("value") { getString("name") == name.last.unescaped }?.self() + } + + /** + * Override default for value descriptor if it is present + */ + fun setDefault(name: Name, def: Any) { + findValue(name)?.setValue("default", def)?: logger.warn("Can't set a default for value $name. Descriptor does not exist.") + } + + fun build(): NodeDescriptor { + return NodeDescriptor(meta) + } + + companion object { + val logger = LoggerFactory.getLogger(DescriptorBuilder::class.java) + } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/description/Descriptors.kt b/dataforge-core/src/main/kotlin/hep/dataforge/description/Descriptors.kt new file mode 100644 index 00000000..81256c5e --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/description/Descriptors.kt @@ -0,0 +1,316 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.description + +import hep.dataforge.context.Global +import hep.dataforge.exceptions.NameNotFoundException +import hep.dataforge.io.MetaFileReader +import hep.dataforge.io.render +import hep.dataforge.listAnnotations +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.meta.buildMeta +import hep.dataforge.providers.Path +import hep.dataforge.utils.Misc +import hep.dataforge.values.ValueFactory +import org.slf4j.LoggerFactory +import java.io.IOException +import java.lang.reflect.AnnotatedElement +import java.net.URISyntaxException +import java.nio.file.Paths +import java.text.ParseException +import kotlin.reflect.KAnnotatedElement +import kotlin.reflect.KClass +import kotlin.reflect.full.declaredMemberProperties +import kotlin.reflect.full.findAnnotation +import kotlin.reflect.full.memberFunctions +import kotlin.reflect.full.memberProperties + +object Descriptors { + + private val descriptorCache = Misc.getLRUCache(1000) + + private fun buildMetaFromResource(name: String, resource: String): MetaBuilder { + try { + val file = Paths.get(Descriptors::class.java.classLoader.getResource(resource)!!.toURI()) + return buildMetaFromFile(name, file) + } catch (ex: IOException) { + throw RuntimeException("Can't read resource file for descriptor", ex) + } catch (ex: URISyntaxException) { + throw RuntimeException("Can't read resource file for descriptor", ex) + } catch (ex: ParseException) { + throw RuntimeException("Can't parse resource file for descriptor", ex) + } + + } + + @Throws(IOException::class, ParseException::class) + private fun buildMetaFromFile(name: String, file: java.nio.file.Path): MetaBuilder { + return MetaFileReader.read(file).builder.rename(name) + } + + /** + * Find a class or method designated by NodeDef `target` value + * + * @param path + * @return + */ + private fun findAnnotatedElement(path: Path): KAnnotatedElement? { + try { + when { + path.target.isEmpty() || path.target == "class" -> return Class.forName(path.name.toString()).kotlin + path.target == "method" -> { + val className = path.name.cutLast().toString() + val methodName = path.name.last.toString() + val dClass = Class.forName(className).kotlin + val res = dClass.memberFunctions.find { it.name == methodName } + if (res == null) { + LoggerFactory.getLogger(Descriptors::class.java).error("Annotated method not found by given path: $path") + } + return res + + } + path.target == "property" -> { + val className = path.name.cutLast().toString() + val methodName = path.name.last.toString() + val dClass = Class.forName(className).kotlin + val res = dClass.memberProperties.find { it.name == methodName } + if (res == null) { + LoggerFactory.getLogger(Descriptors::class.java).error("Annotated property not found by given path: $path") + } + return res + } + else -> { + LoggerFactory.getLogger(Descriptors::class.java).error("Unknown target for descriptor finder: " + path.target) + return null + } + } + } catch (ex: ClassNotFoundException) { + LoggerFactory.getLogger(Descriptors::class.java).error("Class not fond by given path: $path", ex) + return null + } + } + + private fun KAnnotatedElement.listNodeDefs(): Iterable{ + return (listAnnotations(true).flatMap { it.value.asIterable() } + listAnnotations(true)).distinctBy { it.key } + } + + private fun KAnnotatedElement.listValueDefs(): Iterable{ + return (listAnnotations(true).flatMap { it.value.asIterable() } + listAnnotations(true)).distinctBy { it.key } + } + + private fun describe(name: String, element: KAnnotatedElement): DescriptorBuilder { + val reference = element.findAnnotation() + + if (reference != null) { + return forReference(name, reference.value).builder() + } + + val builder = DescriptorBuilder(name) + + //Taking a group annotation if it is present and individual annotations if not + element.listNodeDefs() + .filter { it -> !it.key.startsWith("@") } + .forEach { + builder.node(it) + } + + //Filtering hidden values + element.listValueDefs() + .filter { it -> !it.key.startsWith("@") } + .forEach { valueDef -> + builder.value(ValueDescriptor.build(valueDef)) + } + + element.findAnnotation()?.let { + builder.info = it.value + } + + if (element is KClass<*>) { + element.declaredMemberProperties.forEach { property -> + try { + property.findAnnotation()?.let { + val propertyName = if (it.name.isEmpty()) { + property.name + } else { + it.name + } + builder.value( + name = propertyName, + info = property.description, + multiple = it.multiple, + defaultValue = ValueFactory.parse(it.def), + required = it.def.isEmpty(), + allowedValues = if (it.enumeration == Any::class) { + emptyList() + } else { + it.enumeration.java.enumConstants.map { it.toString() } + }, + types = it.type.toList() + ) + } + + property.findAnnotation()?.let { + val nodeName = if (it.name.isEmpty()) property.name else it.name + builder.node(describe(nodeName, property).build()) + } + } catch (ex: Exception) { + LoggerFactory.getLogger(Descriptors::class.java).warn("Failed to construct descriptor from property {}", property.name) + } + } + } + + + return builder + } + + private fun describe(name: String, element: AnnotatedElement): NodeDescriptor { + val reference = element.getAnnotation(Descriptor::class.java) + + if (reference != null) { + return forReference(name, reference.value) + } + val builder = DescriptorBuilder(name) + + element.listAnnotations(NodeDef::class.java, true) + .stream() + .filter { it -> !it.key.startsWith("@") } + .forEach { nodeDef -> + builder.node(nodeDef) + } + + //Filtering hidden values + element.listAnnotations(ValueDef::class.java, true) + .stream() + .filter { it -> !it.key.startsWith("@") } + .forEach { valueDef -> + builder.value(ValueDescriptor.build(valueDef)) + } + + return builder.build() + } + + private val KAnnotatedElement.description: String + get() = findAnnotation()?.value ?: "" + + /** + * Build Meta that contains all the default nodes and values from given node + * descriptor + * + * @param descriptor + * @return + */ + @JvmStatic + fun buildDefaultNode(descriptor: NodeDescriptor): Meta { + val builder = MetaBuilder(descriptor.name) + descriptor.valueDescriptors().values.stream().filter { vd -> vd.hasDefault() }.forEach { vd -> + if (vd.hasDefault()) { + builder.setValue(vd.name, vd.default) + } + } + + descriptor.childrenDescriptors().values.forEach { nd: NodeDescriptor -> + if (nd.hasDefault()) { + builder.setNode(nd.name, nd.default) + } else { + val defaultNode = buildDefaultNode(nd) + if (!defaultNode.isEmpty) { + builder.setNode(defaultNode) + } + } + } + return builder + } + + + fun forDef(def: NodeDef): NodeDescriptor { + val element = when { + def.type == Any::class -> if (def.descriptor.isEmpty()) { + null + } else { + findAnnotatedElement(Path.of(def.descriptor)) + } + else -> def.type + } + return (element?.let { describe(def.key, it) } ?: DescriptorBuilder(def.key)).apply { + info = def.info + multiple = def.multiple + required = def.required + this.tags = def.tags.toList() + def.values.forEach { + value(it) + } + }.build() + + } + + /** + * Build a descriptor for given Class or Method using Java annotations or restore it from cache if it was already used recently + * + * @param element + * @return + */ + @JvmStatic + fun forType(name: String, element: KAnnotatedElement): NodeDescriptor { + return descriptorCache.getOrPut(element) { describe(name, element).build() } + } + + @JvmStatic + fun forJavaType(name: String, element: AnnotatedElement): NodeDescriptor { + return descriptorCache.getOrPut(element) { describe(name, element) } + } + + @JvmStatic + fun forReference(name: String, reference: String): NodeDescriptor { + return try { + val path = Path.of(reference) + when (path.target) { + "", "class", "method", "property" -> { + val target = findAnnotatedElement(path) + ?: throw RuntimeException("Target element $path not found") + forType(name, target) + } + "file" -> descriptorCache.getOrPut(reference) { + NodeDescriptor(MetaFileReader.read(Global.getFile(path.name.toString()).absolutePath).builder.setValue("name", name)) + } + "resource" -> descriptorCache.getOrPut(reference) { + NodeDescriptor(buildMetaFromResource("node", path.name.toString()).builder.setValue("name", name)) + } + else -> throw NameNotFoundException("Cant create descriptor from given target", reference) + } + } catch (ex: Exception) { + LoggerFactory.getLogger(Descriptors::class.java).error("Failed to build descriptor", ex) + NodeDescriptor(Meta.empty()) + } + } + + /** + * Debug function to print all descriptors currently in cache + */ + fun printDescriptors(){ + + Global.output.render(buildMeta { + descriptorCache.forEach{ + node("descriptor"){ + "reference" to it.key + "value" to it.value.meta + } + } + }) + } + +} \ No newline at end of file diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/description/NodeDescriptor.kt b/dataforge-core/src/main/kotlin/hep/dataforge/description/NodeDescriptor.kt new file mode 100644 index 00000000..dd1f894e --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/description/NodeDescriptor.kt @@ -0,0 +1,190 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.description + +import hep.dataforge.Named +import hep.dataforge.meta.* +import hep.dataforge.names.Name +import java.util.* + +/** + * Descriptor for meta node. Could contain additional information for viewing + * and editing. + * + * @author Alexander Nozik + */ +open class NodeDescriptor(meta: Meta) : SimpleMetaMorph(meta.sealed), Named { + + /** + * True if multiple children with this nodes name are allowed. Anonymous + * nodes are always single + * + * @return + */ + val multiple: Boolean by booleanValue(def = false) + + /** + * True if the node is required + * + * @return + */ + val required: Boolean by booleanValue(def = false) + + /** + * The node description + * + * @return + */ + open val info: String by stringValue(def = "") + + /** + * A list of tags for this node. Tags used to customize node usage + * + * @return + */ + val tags: List by customValue(def = emptyList()) { it.list.map { it.string } } + + /** + * The name of this node + * + * @return + */ + override val name: String by stringValue(def = meta.name) + + /** + * The list of value descriptors + * + * @return + */ + fun valueDescriptors(): Map { + val map = HashMap() + if (meta.hasMeta("value")) { + for (valueNode in meta.getMetaList("value")) { + val vd = ValueDescriptor(valueNode) + map[vd.name] = vd + } + } + return map + } + + /** + * The child node descriptor for given name. Name syntax is supported. + * + * @param name + * @return + */ + fun getNodeDescriptor(name: String): NodeDescriptor? { + return getNodeDescriptor(Name.of(name)) + } + + fun getNodeDescriptor(name: Name): NodeDescriptor? { + return if (name.length == 1) { + childrenDescriptors()[name.unescaped] + } else { + getNodeDescriptor(name.cutLast())?.getNodeDescriptor(name.last) + } + } + + /** + * The value descriptor for given value name. Name syntax is supported. + * + * @param name + * @return + */ + fun getValueDescriptor(name: String): ValueDescriptor? { + return getValueDescriptor(Name.of(name)) + } + + fun getValueDescriptor(name: Name): ValueDescriptor? { + return if (name.length == 1) { + valueDescriptors()[name.unescaped] + } else { + getNodeDescriptor(name.cutLast())?.getValueDescriptor(name.last) + } + } + + /** + * The map of children node descriptors + * + * @return + */ + fun childrenDescriptors(): Map { + val map = HashMap() + if (meta.hasMeta("node")) { + for (node in meta.getMetaList("node")) { + val nd = NodeDescriptor(node) + map[nd.name] = nd + } + } + return map + } + + /** + * Check if this node has default + * + * @return + */ + fun hasDefault(): Boolean { + return meta.hasMeta("default") + } + + /** + * The default meta for this node (could be multiple). Null if not defined + * + * @return + */ + val default: List by nodeList(def = emptyList()) + + /** + * Identify if this descriptor has child value descriptor with default + * + * @param name + * @return + */ + fun hasDefaultForValue(name: String): Boolean { + return getValueDescriptor(name)?.hasDefault() ?: false + } + + /** + * The key of the value which is used to display this node in case it is + * multiple. By default, the key is empty which means that node index is + * used. + * + * @return + */ + val key: String by stringValue(def = "") + + override fun toMeta(): Meta { + return meta + } + + fun builder(): DescriptorBuilder = DescriptorBuilder(this.name, Configuration(this.meta)) + + //override val descriptor: NodeDescriptor = empty("descriptor") + + companion object { + + fun empty(nodeName: String): NodeDescriptor { + return NodeDescriptor(Meta.buildEmpty(nodeName)) + } + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/description/ValueDescriptor.kt b/dataforge-core/src/main/kotlin/hep/dataforge/description/ValueDescriptor.kt new file mode 100644 index 00000000..2ef23f80 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/description/ValueDescriptor.kt @@ -0,0 +1,207 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.description + +import hep.dataforge.Named +import hep.dataforge.meta.* +import hep.dataforge.names.AnonymousNotAlowed +import hep.dataforge.values.BooleanValue +import hep.dataforge.values.Value +import hep.dataforge.values.ValueType + +/** + * A descriptor for meta value + * + * Descriptor can have non-atomic path. It is resolved when descriptor is added to the node + * + * @author Alexander Nozik + */ +@AnonymousNotAlowed +class ValueDescriptor(meta: Meta) : SimpleMetaMorph(meta.sealed), Named { + + /** + * Check if there is default for this value + * + * @return + */ + fun hasDefault(): Boolean { + return meta.hasValue("default") + } + + /** + * The default for this value. Null if there is no default. + * + * @return + */ + val default: Value by value(def = Value.NULL) + + /** + * True if multiple values with this name are allowed. + * + * @return + */ + val multiple: Boolean by booleanValue(def = false) + + /** + * True if the value is required + * + * @return + */ + val required: Boolean by booleanValue(def = !hasDefault()) + + /** + * Value name + * + * @return + */ + override val name: String by stringValue(def = "") + + /** + * The value info + * + * @return + */ + val info: String by stringValue(def = "") + + /** + * A list of allowed ValueTypes. Empty if any value type allowed + * + * @return + */ + val type: List by customValue(def = emptyList()) { + it.list.map { v -> ValueType.valueOf(v.string) } + } + + val tags: List by customValue(def = emptyList()) { + meta.getStringArray("tags").toList() + } + + /** + * Check if given value is allowed for here. The type should be allowed and + * if it is value should be within allowed values + * + * @param value + * @return + */ + fun isValueAllowed(value: Value): Boolean { + return (type.isEmpty() || type.contains(ValueType.STRING) || type.contains(value.type)) && (allowedValues.isEmpty() || allowedValues.contains(value)) + } + + /** + * A list of allowed values with descriptions. If empty than any value is + * allowed. + * + * @return + */ + val allowedValues: List by customValue( + def = if (type.size == 1 && type[0] === ValueType.BOOLEAN) { + listOf(BooleanValue.TRUE, BooleanValue.FALSE) + } else { + emptyList() + } + ) { it.list } + + companion object { + + /** + * Build a value descriptor from annotation + */ + fun build(def: ValueDef): ValueDescriptor { + val builder = MetaBuilder("value") + .setValue("name", def.key) + + if (def.type.isNotEmpty()) { + builder.setValue("type", def.type) + } + + if (def.multiple) { + builder.setValue("multiple", def.multiple) + } + + if (!def.info.isEmpty()) { + builder.setValue("info", def.info) + } + + if (def.allowed.isNotEmpty()) { + builder.setValue("allowedValues", def.allowed) + } else if (def.enumeration != Any::class) { + if (def.enumeration.java.isEnum) { + val values = def.enumeration.java.enumConstants + builder.setValue("allowedValues", values.map { it.toString() }) + } else { + throw RuntimeException("Only enumeration classes are allowed in 'enumeration' annotation property") + } + } + + if (def.def.isNotEmpty()) { + builder.setValue("default", def.def) + } else if(!def.required){ + builder.setValue("required", def.required) + } + + if (def.tags.isNotEmpty()) { + builder.setValue("tags", def.tags) + } + return ValueDescriptor(builder) + } + + /** + * Build a value descriptor from its fields + */ + fun build( + name: String, + info: String = "", + defaultValue: Any? = null, + required: Boolean = false, + multiple: Boolean = false, + types: List = emptyList(), + allowedValues: List = emptyList() + ): ValueDescriptor { + val valueBuilder = buildMeta("value") { + "name" to name + if (!types.isEmpty()) "type" to types + if (required) "required" to required + if (multiple) "multiple" to multiple + if (!info.isEmpty()) "info" to info + if (defaultValue != null) "default" to defaultValue + if (!allowedValues.isEmpty()) "allowedValues" to allowedValues + }.build() + return ValueDescriptor(valueBuilder) + } + + /** + * Build empty value descriptor + */ + fun empty(valueName: String): ValueDescriptor { + val builder = MetaBuilder("value") + .setValue("name", valueName) + return ValueDescriptor(builder) + } + + /** + * Merge two separate value descriptors + */ + fun merge(primary: ValueDescriptor, secondary: ValueDescriptor): ValueDescriptor { + return ValueDescriptor(Laminate(primary.meta, secondary.meta)) + } + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/goals/Coal.kt b/dataforge-core/src/main/kotlin/hep/dataforge/goals/Coal.kt new file mode 100644 index 00000000..ddc71e99 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/goals/Coal.kt @@ -0,0 +1,162 @@ +package hep.dataforge.goals + +import hep.dataforge.await +import hep.dataforge.context.Context +import hep.dataforge.utils.ReferenceRegistry +import kotlinx.coroutines.* +import kotlinx.coroutines.future.asCompletableFuture +import kotlinx.coroutines.time.withTimeout +import org.slf4j.LoggerFactory +import java.time.Duration +import java.util.* +import java.util.concurrent.CompletableFuture +import java.util.concurrent.TimeUnit +import java.util.stream.Stream + +/** + * Coroutine implementation of Goal + * @param id - string id of the Coal + * @param deps - dependency goals + * @param scope custom coroutine dispatcher. By default common pool + * @param block execution block. Could be suspending + */ +class Coal( + val scope: CoroutineScope, + private val deps: Collection> = Collections.emptyList(), + val id: String = "", + block: suspend () -> R) : Goal { + +// /** +// * Construct using context +// */ +// constructor(deps: Collection> = Collections.emptyList(), +// context: Context, +// id: String = "", +// block: suspend () -> R) : this(context.coroutineContext, deps, id, block) + + private val listeners = ReferenceRegistry>(); + + private var deferred: Deferred = scope.async(start = CoroutineStart.LAZY) { + try { + notifyListeners { onGoalStart() } + if (!id.isEmpty()) { + Thread.currentThread().name = "Goal:$id" + } + block.invoke().also { + notifyListeners { onGoalComplete(it) } + } + } catch (ex: Throwable) { + notifyListeners { onGoalFailed(ex) } + //rethrow exception + throw ex + } + } + + private fun CoroutineScope.notifyListeners(action: suspend GoalListener.() -> Unit) { + listeners.forEach { + scope.launch { + try { + action.invoke(it) + } catch (ex: Exception) { + LoggerFactory.getLogger(javaClass).error("Failed to notify goal listener", ex) + } + } + } + } + + + suspend fun await(): R { + run() + return deferred.await(); + } + + override fun run() { + deps.forEach { it.run() } + deferred.start() + } + + override fun get(): R { + return runBlocking { await() } + } + + override fun get(timeout: Long, unit: TimeUnit): R { + return runBlocking { + withTimeout(Duration.ofMillis(timeout)) { await() } + } + } + + override fun cancel(mayInterruptIfRunning: Boolean): Boolean { + deferred.cancel() + return true + } + + override fun isCancelled(): Boolean { + return deferred.isCancelled; + } + + override fun isDone(): Boolean { + return deferred.isCompleted + } + + override fun isRunning(): Boolean { + return deferred.isActive + } + + override fun asCompletableFuture(): CompletableFuture { + return deferred.asCompletableFuture(); + } + + override fun registerListener(listener: GoalListener) { + listeners.add(listener, true) + } + + override fun dependencies(): Stream> { + return deps.stream() + } +} + + +fun Context.goal(deps: Collection> = Collections.emptyList(), id: String = "", block: suspend () -> R): Coal { + return Coal(this, deps, id, block); +} + +/** + * Create a simple generator Coal (no dependencies) + */ +fun Context.generate(id: String = "", block: suspend () -> R): Coal { + return Coal(this, Collections.emptyList(), id, block); +} + +/** + * Join a uniform list of goals + */ +fun List>.join(scope: CoroutineScope, block: suspend (List) -> R): Coal { + return Coal(scope, this) { + block.invoke(this.map { + it.await() + }) + } +} + +/** + * Transform using map of goals as a dependency + */ +fun Map>.join(scope: CoroutineScope, block: suspend (Map) -> R): Coal { + return Coal(scope, this.values) { + block.invoke(this.mapValues { it.value.await() }) + } +} + + +/** + * Pipe goal + */ +fun Goal.pipe(scope: CoroutineScope, block: suspend (T) -> R): Coal { + return Coal(scope, listOf(this)) { + block.invoke(this.await()) + } +} + +fun Collection>.group(): GoalGroup { + return GoalGroup(this); +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/goals/GoalGroup.kt b/dataforge-core/src/main/kotlin/hep/dataforge/goals/GoalGroup.kt new file mode 100644 index 00000000..4cb7e7af --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/goals/GoalGroup.kt @@ -0,0 +1,56 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.goals + +import hep.dataforge.utils.ReferenceRegistry +import java.util.concurrent.CompletableFuture +import java.util.stream.Stream +import kotlin.streams.toList + +/** + * A goal with no result which is completed when all its dependencies are + * completed. Stopping this goal does not stop dependencies. Staring goal does start dependencies. + * + * + * On start hooks works only if this group was specifically started. All of its dependencies could be started and completed without triggering it. + * + * @author Alexander Nozik + */ +class GoalGroup(private val dependencies: Collection>) : Goal { + private val listeners = ReferenceRegistry>() + + private var res: CompletableFuture = CompletableFuture + .allOf(*dependencies.stream().map> { it.asCompletableFuture() }.toList().toTypedArray()) + .whenComplete { aVoid, throwable -> + if (throwable != null) { + listeners.forEach { l -> l.onGoalFailed(throwable) } + } else { + listeners.forEach { l -> l.onGoalComplete(null) } + } + } + + override fun dependencies(): Stream> { + return dependencies.stream() + } + + override fun run() { + listeners.forEach { it.onGoalStart() } + dependencies.forEach { it.run() } + } + + override fun asCompletableFuture(): CompletableFuture? { + return res + } + + override fun isRunning(): Boolean { + return dependencies.stream().anyMatch { it.isRunning } + } + + override fun registerListener(listener: GoalListener) { + listeners.add(listener, true) + } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/io/AbstractOutputManager.kt b/dataforge-core/src/main/kotlin/hep/dataforge/io/AbstractOutputManager.kt new file mode 100644 index 00000000..f01b29cd --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/io/AbstractOutputManager.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.io + +import ch.qos.logback.classic.Logger +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.Appender +import ch.qos.logback.core.UnsynchronizedAppenderBase +import hep.dataforge.context.BasicPlugin +import hep.dataforge.context.Context +import hep.dataforge.io.OutputManager.Companion.LOGGER_APPENDER_NAME +import hep.dataforge.io.output.Output +import hep.dataforge.meta.Meta + +/** + * + * @author Alexander Nozik + * @version $Id: $Id + */ +abstract class AbstractOutputManager(meta: Meta = Meta.empty()) : OutputManager, BasicPlugin(meta) { + /** + * Create logger appender for this manager + * + * @return + */ + open fun createLoggerAppender(): Appender { + return object : UnsynchronizedAppenderBase() { + override fun append(eventObject: ILoggingEvent) { + get("@log").render(eventObject) + } + } + } + + private fun addLoggerAppender(logger: Logger) { + val loggerContext = logger.loggerContext + val appender = createLoggerAppender() + appender.name = LOGGER_APPENDER_NAME + appender.context = loggerContext + appender.start() + logger.addAppender(appender) + } + + private fun removeLoggerAppender(logger: Logger) { + val app = logger.getAppender(LOGGER_APPENDER_NAME) + if (app != null) { + logger.detachAppender(app) + app.stop() + } + } + + override fun attach(context: Context) { + super.attach(context) + if (context.logger is ch.qos.logback.classic.Logger) { + addLoggerAppender(context.logger as ch.qos.logback.classic.Logger) + } + } + + override fun detach() { + if (logger is ch.qos.logback.classic.Logger) { + removeLoggerAppender(logger as ch.qos.logback.classic.Logger) + } + super.detach() + } +} + +/** + * The simple output manager, which redirects everything to a single output stream + */ +class SimpleOutputManager(val default: Output) : AbstractOutputManager() { + override fun get(meta: Meta): Output = default +} + diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/io/BufferChannel.kt b/dataforge-core/src/main/kotlin/hep/dataforge/io/BufferChannel.kt new file mode 100644 index 00000000..d67fdc30 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/io/BufferChannel.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.io + +import java.nio.ByteBuffer +import java.nio.channels.SeekableByteChannel + + +/** + * A channel that reads o writes inside large buffer + */ +class BufferChannel(val buffer: ByteBuffer) : SeekableByteChannel { + override fun isOpen(): Boolean { + return true + } + + override fun position(): Long { + return buffer.position().toLong() + } + + override fun position(newPosition: Long): SeekableByteChannel { + buffer.position(newPosition.toInt()) + return this + } + + override fun write(src: ByteBuffer): Int { + buffer.put(src) + return src.remaining() + } + + override fun size(): Long { + return buffer.limit().toLong() + } + + override fun close() { + //do nothing + } + + override fun truncate(size: Long): SeekableByteChannel { + if(size< buffer.limit()){ + buffer.limit(size.toInt()) + } + return this + } + + override fun read(dst: ByteBuffer): Int { + val array = ByteArray(dst.remaining()) + buffer.get(array) + dst.put(array) + return array.size; + } + +} \ No newline at end of file diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/io/DirectoryOutput.kt b/dataforge-core/src/main/kotlin/hep/dataforge/io/DirectoryOutput.kt new file mode 100644 index 00000000..9060c14d --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/io/DirectoryOutput.kt @@ -0,0 +1,81 @@ +package hep.dataforge.io + +import ch.qos.logback.classic.LoggerContext +import ch.qos.logback.classic.encoder.PatternLayoutEncoder +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.Appender +import ch.qos.logback.core.FileAppender +import hep.dataforge.context.FileReference +import hep.dataforge.context.Plugin +import hep.dataforge.context.PluginDef +import hep.dataforge.context.PluginFactory +import hep.dataforge.io.OutputManager.Companion.OUTPUT_NAME_KEY +import hep.dataforge.io.OutputManager.Companion.OUTPUT_STAGE_KEY +import hep.dataforge.io.OutputManager.Companion.OUTPUT_TYPE_KEY +import hep.dataforge.io.output.FileOutput +import hep.dataforge.io.output.Output +import hep.dataforge.meta.Meta +import hep.dataforge.names.Name +import org.slf4j.LoggerFactory +import java.io.File + +/** + * A directory based IO manager. Any named output is redirected to file in corresponding directory inside work directory + */ +@PluginDef(name = "output.dir", group = "hep.dataforge", info = "Directory based output plugin") +class DirectoryOutput : AbstractOutputManager() { + + //internal var registry = ReferenceRegistry() + // FileAppender appender; + + private val map = HashMap() + + + override fun createLoggerAppender(): Appender { + val lc = LoggerFactory.getILoggerFactory() as LoggerContext + val ple = PatternLayoutEncoder() + + ple.pattern = "%date %level [%thread] %logger{10} [%file:%line] %msg%n" + ple.context = lc + ple.start() + val appender = FileAppender() + appender.file = File(context.workDir.toFile(), meta.getString("logFileName", "${context.name}.log")).toString() + appender.encoder = ple + return appender + } + + override fun detach() { + super.detach() + map.values.forEach { + //TODO add catch + it.close() + } + } + + /** + * Get file extension for given content type + */ + private fun getExtension(type: String): String { + return when (type) { + Output.BINARY_TYPE -> "df" + else -> "out" + } + } + + override fun get(meta: Meta): Output { + val name = meta.getString(OUTPUT_NAME_KEY) + val stage = Name.of(meta.getString(OUTPUT_STAGE_KEY, "")) + val extension = meta.optString("file.extension").orElseGet { getExtension(meta.getString(OUTPUT_TYPE_KEY, Output.TEXT_TYPE)) } + val reference = FileReference.newWorkFile(context, name, extension, stage) + return FileOutput(reference) + } + + class Factory : PluginFactory() { + override val type: Class = DirectoryOutput::class.java + + override fun build(meta: Meta): Plugin { + return DirectoryOutput() + } + } + +} \ No newline at end of file diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/io/MetaFileReader.kt b/dataforge-core/src/main/kotlin/hep/dataforge/io/MetaFileReader.kt new file mode 100644 index 00000000..2bfa5f7e --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/io/MetaFileReader.kt @@ -0,0 +1,119 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.io + +import hep.dataforge.context.Context +import hep.dataforge.context.Global +import hep.dataforge.io.envelopes.EnvelopeReader +import hep.dataforge.io.envelopes.MetaType +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import java.io.IOException +import java.nio.file.Files +import java.nio.file.Path +import java.text.ParseException +import java.util.* + +//TODO add examples for transformations + +/** + * A reader for meta file in any supported format. Additional file formats could + * be statically registered by plug-ins. + * + * Basically reader performs two types of "on read" transformations: + * + * Includes: include a meta from given file instead of given node + * + * Substitutions: replaces all occurrences of `${}` in child meta nodes by given value. Substitutions are made as strings. + * @author [Alexander Nozik](mailto:altavir@gmail.com) + */ +class MetaFileReader { + + fun read(context: Context, path: String): Meta { + return read(context, context.rootDir.resolve(path)) + } + + fun read(context: Context, file: Path): Meta { + val fileName = file.fileName.toString() + for (type in context.serviceStream(MetaType::class.java)) { + if (type.fileNameFilter.invoke(fileName)) { + return transform(context, type.reader.readFile(file)) + } + } + //Fall back and try to resolve meta as an envelope ignoring extension + return EnvelopeReader.readFile(file).meta + } + + /** + * Evaluate parameter substitution and include substitution + * + * @param builder + * @return + */ + private fun transform(context: Context, builder: MetaBuilder): MetaBuilder { + return builder.substituteValues(context) + } + + private fun evaluateSubst(context: Context, subst: String): String { + return subst + } + + companion object { + + const val SUBST_ELEMENT = "df:subst" + const val INCLUDE_ELEMENT = "df:include" + + private val instance = MetaFileReader() + + fun instance(): MetaFileReader { + return instance + } + + fun read(file: Path): Meta { + try { + return instance().read(Global, file) + } catch (e: IOException) { + throw RuntimeException("Failed to read meta file " + file.toString(), e) + } catch (e: ParseException) { + throw RuntimeException("Failed to read meta file " + file.toString(), e) + } + + } + + /** + * Resolve the file with given name (without extension) in the directory and read it as meta. If multiple files with the same name exist in the directory, the ran + * + * @param directory + * @param name + * @return + */ + fun resolve(directory: Path, name: String): Optional { + try { + return Files.list(directory).filter { it -> it.startsWith(name) }.findFirst().map{ read(it) } + } catch (e: IOException) { + throw RuntimeException("Failed to list files in the directory " + directory.toString(), e) + } + + } + } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/io/OutputManager.kt b/dataforge-core/src/main/kotlin/hep/dataforge/io/OutputManager.kt new file mode 100644 index 00000000..ea905da7 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/io/OutputManager.kt @@ -0,0 +1,146 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.io + +import hep.dataforge.Types +import hep.dataforge.context.BasicPlugin +import hep.dataforge.context.Context +import hep.dataforge.context.Plugin +import hep.dataforge.description.ValueDef +import hep.dataforge.description.ValueDefs +import hep.dataforge.io.OutputManager.Companion.OUTPUT_NAME_KEY +import hep.dataforge.io.OutputManager.Companion.OUTPUT_STAGE_KEY +import hep.dataforge.io.OutputManager.Companion.OUTPUT_TYPE_KEY +import hep.dataforge.io.OutputManager.Companion.split +import hep.dataforge.io.output.Output +import hep.dataforge.io.output.Output.Companion.splitOutput +import hep.dataforge.meta.KMetaBuilder +import hep.dataforge.meta.Meta +import hep.dataforge.meta.buildMeta + +/** + * + * + * IOManager interface. + * + * @author Alexander Nozik + * @version $Id: $Id + */ +interface OutputManager : Plugin { + + /** + * Get secondary output for this context + * @param stage + * @param name + * @return + */ + @ValueDefs( + ValueDef(key = OUTPUT_STAGE_KEY, def = "", info = "Fully qualified name of the output stage"), + ValueDef(key = OUTPUT_NAME_KEY, required = true, info = "Fully qualified name of the output inside the stage if it is present"), + ValueDef(key = OUTPUT_TYPE_KEY, def = "", info = "Type of the output container") + ) + fun get(meta: Meta): Output + + /** + * + */ + @JvmDefault + operator fun get(stage: String, name: String, type: String? = null): Output { + return get { + OUTPUT_NAME_KEY to name + OUTPUT_STAGE_KEY to stage + OUTPUT_TYPE_KEY to type + } + } + + + @JvmDefault + operator fun get(name: String): Output { + return get { + OUTPUT_NAME_KEY to name + } + } + + companion object { + + const val LOGGER_APPENDER_NAME = "df.output" + const val OUTPUT_STAGE_KEY = "stage" + const val OUTPUT_NAME_KEY = "name" + const val OUTPUT_TYPE_KEY = "type" + + /** + * Produce a split [OutputManager] + */ + fun split(vararg channels: OutputManager): OutputManager = SplitOutputManager(setOf(*channels)) + } +} + +/** + * Kotlin helper to get [Output] using meta builder + */ +fun OutputManager.get(builder: KMetaBuilder.() -> Unit): Output = get(buildMeta("output", builder)) + +fun OutputManager.render(obj: Any, stage: String? = null, name: String? = null, type: String? = null, meta: Meta) { + val outputMeta = meta.builder.apply { + if (name != null) { + setValue(OUTPUT_NAME_KEY, name) + } + if (stage != null) { + setValue(OUTPUT_STAGE_KEY, stage) + } + if (type != null || !meta.hasValue(OUTPUT_TYPE_KEY)) { + setValue(OUTPUT_TYPE_KEY, type ?: Types[obj]) + } + } + get(outputMeta).render(obj, outputMeta) +} + +/** + * Helper to directly render an object using optional stage and name an meta builder + */ +fun OutputManager.render(obj: Any, stage: String? = null, name: String? = null, type: String? = null, builder: KMetaBuilder.() -> Unit = {}) { + render(obj, stage, name, type, buildMeta("output", builder)) +} + + +/** + * An [OutputManager] that supports multiple outputs simultaneously + */ +class SplitOutputManager(val managers: Set = HashSet(), meta: Meta = Meta.empty()) : OutputManager, BasicPlugin(meta) { + + override fun get(meta: Meta): Output = splitOutput(*managers.map { it.get(meta) }.toTypedArray()) + + override fun attach(context: Context) { + super.attach(context) + managers.forEach { + context.plugins.loadDependencies(it) + it.attach(context) + } + } + + override fun detach() { + super.detach() + managers.forEach { it.detach() } + } +} + +operator fun OutputManager.plus(other: OutputManager): OutputManager { + return when { + this is SplitOutputManager -> SplitOutputManager(managers + other) + else -> split(this, other) + } +} + diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/BinaryMetaType.kt b/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/BinaryMetaType.kt new file mode 100644 index 00000000..03f0f37b --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/BinaryMetaType.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.io.envelopes + +import hep.dataforge.io.MetaStreamReader +import hep.dataforge.io.MetaStreamWriter +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaUtils +import java.io.* + + +val binaryMetaType = BinaryMetaType() + +/** + * Binary meta type + * Created by darksnake on 02-Mar-17. + */ +class BinaryMetaType : MetaType { + + override val codes: List = listOf(0x4249, 10)//BI + + override val name: String = "binary" + + override val fileNameFilter: (String)->Boolean = { str -> str.toLowerCase().endsWith(".meta") } + + + override val reader: MetaStreamReader = MetaStreamReader { stream, length -> + val actualStream = if (length > 0) { + val bytes = ByteArray(length.toInt()) + stream.read(bytes) + ByteArrayInputStream(bytes) + } else { + stream + } + val ois = ObjectInputStream(actualStream) + MetaUtils.readMeta(ois) + } + + override val writer = object : MetaStreamWriter { + + @Throws(IOException::class) + override fun write(stream: OutputStream, meta: Meta) { + MetaUtils.writeMeta(ObjectOutputStream(stream), meta) + stream.write('\r'.toInt()) + stream.write('\n'.toInt()) + } + } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/DefaultEnvelopeReader.kt b/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/DefaultEnvelopeReader.kt new file mode 100644 index 00000000..966e6f32 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/DefaultEnvelopeReader.kt @@ -0,0 +1,152 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.io.envelopes + +import hep.dataforge.data.binary.Binary +import hep.dataforge.data.binary.BufferedBinary +import hep.dataforge.data.binary.FileBinary +import hep.dataforge.exceptions.EnvelopeFormatException +import hep.dataforge.io.envelopes.DefaultEnvelopeType.Companion.SEPARATOR +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaNode.DEFAULT_META_NAME +import org.slf4j.LoggerFactory +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.InputStream +import java.nio.ByteBuffer +import java.nio.channels.Channels +import java.nio.channels.ReadableByteChannel +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardOpenOption.READ +import java.text.ParseException + +/** + * @author darksnake + */ +open class DefaultEnvelopeReader : EnvelopeReader { + + protected open fun newTag(): EnvelopeTag { + return EnvelopeTag() + } + + override fun read(channel: ReadableByteChannel): Envelope { + return read(Channels.newInputStream(channel)) + } + + @Throws(IOException::class) + override fun read(stream: InputStream): Envelope { + val tag = newTag().read(stream) + val parser = tag.metaType.reader + val metaLength = tag.metaSize + val meta: Meta = if (metaLength == 0) { + Meta.buildEmpty(DEFAULT_META_NAME) + } else { + try { + parser.read(stream, metaLength.toLong()) + } catch (ex: ParseException) { + throw EnvelopeFormatException("Error parsing meta", ex) + } + + } + + val binary: Binary + val dataLength = tag.dataSize + //skipping separator for automatic meta reading + if (metaLength == -1) { + stream.skip(separator().size.toLong()) + } + binary = readData(stream, dataLength) + + return SimpleEnvelope(meta, binary) + } + + /** + * The envelope is lazy meaning it will be calculated on demand. If the + * stream will be closed before that, than an error will be thrown. In order + * to avoid this problem, it is wise to call `getData` after read. + * + * @return + */ + override fun read(file: Path): Envelope { + val channel = Files.newByteChannel(file, READ) + val tag = newTag().read(channel) + val metaLength = tag.metaSize + val dataLength = tag.dataSize + if (metaLength < 0 || dataLength < 0) { + LoggerFactory.getLogger(javaClass).error("Can't lazy read infinite data or meta. Returning non-lazy envelope") + return read(file) + } + + val metaBuffer = ByteBuffer.allocate(metaLength) + channel.position(tag.length.toLong()) + channel.read(metaBuffer) + val parser = tag.metaType.reader + + val meta: Meta = if (metaLength == 0) { + Meta.buildEmpty(DEFAULT_META_NAME) + } else { + try { + parser.readBuffer(metaBuffer) + } catch (ex: ParseException) { + throw EnvelopeFormatException("Error parsing annotation", ex) + } + + } + channel.close() + + return SimpleEnvelope(meta, FileBinary(file, (tag.length + metaLength).toLong())) + } + + protected fun separator(): ByteArray { + return SEPARATOR + } + + @Throws(IOException::class) + private fun readData(stream: InputStream, length: Int): Binary { + return if (length == -1) { + val baos = ByteArrayOutputStream() + while (stream.available() > 0) { + baos.write(stream.read()) + } + BufferedBinary(baos.toByteArray()) + } else { + val buffer = ByteBuffer.allocate(length) + Channels.newChannel(stream).read(buffer) + BufferedBinary(buffer) + } + } + + /** + * Read envelope with data (without lazy reading) + * + * @param stream + * @return + * @throws IOException + */ + @Throws(IOException::class) + fun readWithData(stream: InputStream): Envelope { + val res = read(stream) + res.data + return res + } + + companion object { + + val INSTANCE = DefaultEnvelopeReader() + } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/DefaultEnvelopeType.kt b/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/DefaultEnvelopeType.kt new file mode 100644 index 00000000..f0061c59 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/DefaultEnvelopeType.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.io.envelopes + + +/** + * @author darksnake + */ +open class DefaultEnvelopeType : EnvelopeType { + + override val code: Int = DEFAULT_ENVELOPE_CODE + + override val name: String = DEFAULT_ENVELOPE_NAME + + override fun description(): String = "Standard envelope type. Meta and data end auto detection are not supported. Tag is mandatory." + + override fun getReader(properties: Map): EnvelopeReader = DefaultEnvelopeReader.INSTANCE + + override fun getWriter(properties: Map): EnvelopeWriter = DefaultEnvelopeWriter(this, MetaType.resolve(properties)) + + + /** + * True if metadata length auto detection is allowed + * + * @return + */ + open fun infiniteDataAllowed(): Boolean = false + + /** + * True if data length auto detection is allowed + * + * @return + */ + open fun infiniteMetaAllowed(): Boolean = false + + companion object { + + val INSTANCE = DefaultEnvelopeType() + + const val DEFAULT_ENVELOPE_CODE = 0x44463032 + const val DEFAULT_ENVELOPE_NAME = "default" + + /** + * The set of symbols that separates tag from metadata and data + */ + val SEPARATOR = byteArrayOf('\r'.toByte(), '\n'.toByte()) + } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/DefaultEnvelopeWriter.kt b/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/DefaultEnvelopeWriter.kt new file mode 100644 index 00000000..9bb6be24 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/DefaultEnvelopeWriter.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.io.envelopes + +import hep.dataforge.io.envelopes.DefaultEnvelopeType.Companion.SEPARATOR +import hep.dataforge.io.envelopes.Envelope.Companion.DATA_LENGTH_PROPERTY +import hep.dataforge.io.envelopes.Envelope.Companion.META_LENGTH_PROPERTY +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.OutputStream +import java.nio.channels.Channels + +/** + * @author darksnake + */ +class DefaultEnvelopeWriter(private val envelopeType: EnvelopeType, private val metaType: MetaType) : EnvelopeWriter { + + @Throws(IOException::class) + override fun write(stream: OutputStream, envelope: Envelope) { + val tag = EnvelopeTag().also { + it.envelopeType = envelopeType + it.metaType = metaType + } + write(stream, tag, envelope) + } + + /** + * Automatically define meta size and data size if it is not defined already + * and write envelope to the stream + * + * @param stream + * @param envelope + * @throws IOException + */ + @Throws(IOException::class) + private fun write(stream: OutputStream, tag: EnvelopeTag, envelope: Envelope) { + + val writer = tag.metaType.writer + val meta: ByteArray + val metaSize: Int + if (envelope.meta.isEmpty) { + meta = ByteArray(0) + metaSize = 0 + } else { + val baos = ByteArrayOutputStream() + writer.write(baos, envelope.meta) + meta = baos.toByteArray() + metaSize = meta.size + 2 + } + tag.setValue(META_LENGTH_PROPERTY, metaSize) + + tag.setValue(DATA_LENGTH_PROPERTY, envelope.data.size) + + stream.write(tag.toBytes().array()) + + stream.write(meta) + if (meta.isNotEmpty()) { + stream.write(SEPARATOR) + } + + Channels.newChannel(stream).write(envelope.data.buffer) + } + +// companion object { +// private val TAG_PROPERTIES = HashSet( +// Arrays.asList(Envelope.TYPE_PROPERTY, Envelope.META_TYPE_PROPERTY, Envelope.META_LENGTH_PROPERTY, Envelope.DATA_LENGTH_PROPERTY) +// ) +// } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/Envelope.kt b/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/Envelope.kt new file mode 100644 index 00000000..c7747c5b --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/Envelope.kt @@ -0,0 +1,134 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.io.envelopes + +import hep.dataforge.data.Data +import hep.dataforge.data.binary.Binary +import hep.dataforge.description.NodeDef +import hep.dataforge.meta.Meta +import hep.dataforge.meta.Metoid +import hep.dataforge.nullable +import org.slf4j.LoggerFactory +import java.io.IOException +import java.io.Serializable +import java.time.Instant +import java.util.function.Function + +/** + * The message is a pack that can include two principal parts: + * + * * Envelope meta-data + * * binary data + * + * + * @author Alexander Nozik + */ +@NodeDef(key = "@envelope", info = "An optional envelope service info node") +interface Envelope : Metoid, Serializable { + + /** + * Read data into buffer. This operation could take a lot of time so be + * careful when performing it synchronously + * + * @return + */ + val data: Binary + + /** + * The purpose of the envelope + * + * @return + */ + val type: String? + get() = meta.optString(ENVELOPE_TYPE_KEY).nullable + + /** + * The type of data encoding + * + * @return + */ + val dataType: String? + get() = meta.optString(ENVELOPE_DATA_TYPE_KEY).nullable + + /** + * Textual user friendly description + * + * @return + */ + val description: String? + get() = meta.optString(ENVELOPE_DESCRIPTION_KEY).nullable + + /** + * Time of creation of the envelope + * + * @return + */ + val time: Instant? + get() = meta.optTime(ENVELOPE_TIME_KEY).nullable + + /** + * Meta part of the envelope + * + * @return + */ + override val meta: Meta + + fun hasMeta(): Boolean { + return !meta.isEmpty + } + + fun hasData(): Boolean { + return try { + data.size > 0 + } catch (e: IOException) { + LoggerFactory.getLogger(javaClass).error("Failed to estimate data size in the envelope", e) + false + } + + } + + /** + * Transform Envelope to Lazy data using given transformation. + * In case transformation failed an exception will be thrown in call site. + * + * @param type + * @param transform + * @param + * @return + */ + fun map(type: Class, transform: Function): Data { + return Data.generate(type, meta) { transform.apply(data) } + } + + companion object { + /** + * Property keys + */ + const val TYPE_PROPERTY = "type" + const val META_TYPE_PROPERTY = "metaType" + const val META_LENGTH_PROPERTY = "metaLength" + const val DATA_LENGTH_PROPERTY = "dataLength" + + /** + * meta keys + */ + const val ENVELOPE_NODE = "@envelope" + const val ENVELOPE_TYPE_KEY = "@envelope.type" + const val ENVELOPE_DATA_TYPE_KEY = "@envelope.dataType" + const val ENVELOPE_DESCRIPTION_KEY = "@envelope.description" + const val ENVELOPE_TIME_KEY = "@envelope.time" + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/EnvelopeBuilder.kt b/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/EnvelopeBuilder.kt new file mode 100644 index 00000000..b26929d0 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/EnvelopeBuilder.kt @@ -0,0 +1,148 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.io.envelopes + +import hep.dataforge.data.binary.Binary +import hep.dataforge.data.binary.BufferedBinary +import hep.dataforge.io.envelopes.Envelope.Companion.ENVELOPE_DATA_TYPE_KEY +import hep.dataforge.io.envelopes.Envelope.Companion.ENVELOPE_DESCRIPTION_KEY +import hep.dataforge.io.envelopes.Envelope.Companion.ENVELOPE_TIME_KEY +import hep.dataforge.io.envelopes.Envelope.Companion.ENVELOPE_TYPE_KEY +import hep.dataforge.meta.KMetaBuilder +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.meta.buildMeta + +import java.io.ByteArrayOutputStream +import java.io.ObjectStreamException +import java.io.OutputStream +import java.nio.ByteBuffer +import java.time.Instant + +/** + * The convenient builder for envelopes + * + * @author Alexander Nozik + */ +class EnvelopeBuilder : Envelope { + + /** + * Get modifiable meta builder for this envelope + * + * @return + */ + override var meta = MetaBuilder() + + //initializing with empty buffer + override var data: Binary = BufferedBinary(ByteArray(0)) + private set + + constructor(envelope: Envelope) { + this.meta = envelope.meta.builder + this.data = envelope.data + } + + constructor() { + + } + + fun meta(annotation: Meta): EnvelopeBuilder { + this.meta = annotation.builder + return this + } + + fun meta(builder: KMetaBuilder.()->Unit) : EnvelopeBuilder{ + return meta(buildMeta("meta", builder)) + } + + /** + * Helper to fast put node to envelope meta + * + * @param element + * @return + */ + fun putMetaNode(nodeName: String, element: Meta): EnvelopeBuilder { + this.meta.putNode(nodeName, element) + return this + } + + fun putMetaNode(element: Meta): EnvelopeBuilder { + this.meta.putNode(element) + return this + } + + /** + * Helper to fast put value to meta root + * + * @param name + * @param value + * @return + */ + fun setMetaValue(name: String, value: Any): EnvelopeBuilder { + this.meta.setValue(name, value) + return this + } + + fun data(data: Binary): EnvelopeBuilder { + this.data = data + return this + } + + fun data(data: ByteBuffer): EnvelopeBuilder { + this.data = BufferedBinary(data) + return this + } + + fun data(data: ByteArray): EnvelopeBuilder { + this.data = BufferedBinary(data) + return this + } + + fun data(consumer: (OutputStream) -> Unit): EnvelopeBuilder { + val baos = ByteArrayOutputStream() + consumer(baos) + return data(baos.toByteArray()) + } + + //TODO kotlinize + + fun setEnvelopeType(type: String): EnvelopeBuilder { + setMetaValue(ENVELOPE_TYPE_KEY, type) + return this + } + + fun setDataType(type: String): EnvelopeBuilder { + setMetaValue(ENVELOPE_DATA_TYPE_KEY, type) + return this + } + + fun setDescription(description: String): EnvelopeBuilder { + setMetaValue(ENVELOPE_DESCRIPTION_KEY, description) + return this + } + + fun build(): Envelope { + setMetaValue(ENVELOPE_TIME_KEY, Instant.now()) + return SimpleEnvelope(this.meta, data) + } + + @Throws(ObjectStreamException::class) + private fun writeReplace(): Any { + return SimpleEnvelope(this.meta, data) + } +} + +fun buildEnvelope(action: EnvelopeBuilder.()-> Unit): Envelope = EnvelopeBuilder().apply(action).build() diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/EnvelopeReader.kt b/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/EnvelopeReader.kt new file mode 100644 index 00000000..fe1faa86 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/EnvelopeReader.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.io.envelopes + + +import hep.dataforge.io.BufferChannel +import java.io.IOException +import java.io.InputStream +import java.nio.ByteBuffer +import java.nio.channels.Channels +import java.nio.channels.ReadableByteChannel +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardOpenOption.READ + +/** + * interface for reading envelopes + * + * @author Alexander Nozik + */ +interface EnvelopeReader { + + /** + * Read the whole envelope from the stream. + * + * @param stream + * @return + * @throws IOException + */ + fun read(stream: InputStream): Envelope + + /** + * Read the envelope from channel + */ + @JvmDefault + fun read(channel: ReadableByteChannel): Envelope { + return read(Channels.newInputStream(channel)) + } + + /** + * Read the envelope from buffer (could produce lazy envelope) + */ + @JvmDefault + fun read(buffer: ByteBuffer): Envelope { + return read(BufferChannel(buffer))//read(ByteArrayInputStream(buffer.array())) + } + + /** + * Read the envelope from NIO file (could produce lazy envelope) + */ + @JvmDefault + fun read(file: Path): Envelope { + return Files.newByteChannel(file, READ).use { read(it) } + } + + companion object { + + /** + * Resolve envelope type and use it to read the file as envelope + * + * @param path + * @return + */ + @Throws(IOException::class) + fun readFile(path: Path): Envelope { + val type = EnvelopeType.infer(path) ?: error("The file is not envelope") + return type.reader.read(path) + } + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/EnvelopeTag.kt b/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/EnvelopeTag.kt new file mode 100644 index 00000000..bbf3c745 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/EnvelopeTag.kt @@ -0,0 +1,194 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.io.envelopes + +import hep.dataforge.values.Value +import hep.dataforge.values.ValueType +import hep.dataforge.values.asValue +import org.slf4j.LoggerFactory +import java.io.IOException +import java.io.InputStream +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.channels.SeekableByteChannel +import java.util.* + +/** + * Envelope tag converter v2 + * Created by darksnake on 25-Feb-17. + */ +open class EnvelopeTag { + + val values = HashMap() + var metaType: MetaType = xmlMetaType + var envelopeType: EnvelopeType = DefaultEnvelopeType.INSTANCE + + + protected open val startSequence: ByteArray = "#~".toByteArray() + + protected open val endSequence: ByteArray = "~#\r\n".toByteArray() + + /** + * Get the length of tag in bytes. -1 means undefined size in case tag was modified + * + * @return + */ + open val length: Int = 20 + + val metaSize: Int + get() = values[Envelope.META_LENGTH_PROPERTY]?.int ?: 0 + + var dataSize: Int + get() = values[Envelope.DATA_LENGTH_PROPERTY]?.int ?: 0 + set(value) { + values[Envelope.DATA_LENGTH_PROPERTY] = value.asValue() + } + + /** + * Read header line + * + * @throws IOException + */ + @Throws(IOException::class) + protected open fun readHeader(buffer: ByteBuffer): Map { + + val res = HashMap() + + val lead = ByteArray(2) + + buffer.get(lead) + + if (!Arrays.equals(lead, startSequence)) { + throw IOException("Wrong start sequence for envelope tag") + } + + //reading type + val type = buffer.getInt(2) + val envelopeType = EnvelopeType.resolve(type) + + if (envelopeType != null) { + res[Envelope.TYPE_PROPERTY] = envelopeType.name.asValue() + } else { + LoggerFactory.getLogger(EnvelopeTag::class.java).warn("Could not resolve envelope type code. Using default") + } + + //reading meta type + val metaTypeCode = buffer.getShort(6) + val metaType = MetaType.resolve(metaTypeCode) + + if (metaType != null) { + res[Envelope.META_TYPE_PROPERTY] = metaType.name.asValue() + } else { + LoggerFactory.getLogger(EnvelopeTag::class.java).warn("Could not resolve meta type. Using default") + } + + //reading meta length + val metaLength = Integer.toUnsignedLong(buffer.getInt(8)) + res[Envelope.META_LENGTH_PROPERTY] = Value.of(metaLength) + //reading data length + val dataLength = Integer.toUnsignedLong(buffer.getInt(12)) + res[Envelope.DATA_LENGTH_PROPERTY] = Value.of(dataLength) + + val endSequence = ByteArray(4) + buffer.position(16) + buffer.get(endSequence) + + if (!Arrays.equals(endSequence, endSequence)) { + throw IOException("Wrong ending sequence for envelope tag") + } + return res + } + + /** + * Convert tag to properties + * + * @return + */ + fun getValues(): Map { + return values + } + + /** + * Update existing properties + * + * @param props + */ + fun setValues(props: Map) { + props.forEach { name, value -> this.setValue(name, value) } + } + + fun setValue(name: String, value: Any) { + setValue(name, Value.of(value)) + } + + fun setValue(name: String, value: Value) { + if (Envelope.TYPE_PROPERTY == name) { + val type = if (value.type == ValueType.NUMBER) EnvelopeType.resolve(value.int) else EnvelopeType.resolve(value.string) + if (type != null) { + envelopeType = type + } else { + LoggerFactory.getLogger(javaClass).trace("Can't resolve envelope type") + } + } else if (Envelope.META_TYPE_PROPERTY == name) { + val type = if (value.type == ValueType.NUMBER) MetaType.resolve(value.int.toShort()) else MetaType.resolve(value.string) + if (type != null) { + metaType = type + } else { + LoggerFactory.getLogger(javaClass).error("Can't resolve meta type") + } + } else { + values[name] = value + } + } + + @Throws(IOException::class) + fun read(channel: SeekableByteChannel): EnvelopeTag { + val header: Map + + val bytes = ByteBuffer.allocate(length) + channel.read(bytes) + bytes.flip() + header = readHeader(bytes) + + setValues(header) + return this + } + + @Throws(IOException::class) + fun read(stream: InputStream): EnvelopeTag { + val header: Map + val body = ByteArray(length) + stream.read(body) + header = readHeader(ByteBuffer.wrap(body).order(ByteOrder.BIG_ENDIAN)) + setValues(header) + return this + } + + fun toBytes(): ByteBuffer { + val buffer = ByteBuffer.allocate(20) + buffer.put(startSequence) + + buffer.putInt(envelopeType.code) + buffer.putShort(metaType.codes[0]) + buffer.putInt(values[Envelope.META_LENGTH_PROPERTY]!!.long.toInt()) + buffer.putInt(values[Envelope.DATA_LENGTH_PROPERTY]!!.long.toInt()) + buffer.put(endSequence) + buffer.position(0) + return buffer + } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/EnvelopeType.kt b/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/EnvelopeType.kt new file mode 100644 index 00000000..00c30449 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/EnvelopeType.kt @@ -0,0 +1,103 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.io.envelopes + +import hep.dataforge.context.Context +import hep.dataforge.context.Global +import org.slf4j.LoggerFactory +import java.nio.channels.FileChannel +import java.nio.file.Path +import java.nio.file.StandardOpenOption + +/** + * Envelope io format description + * + * @author Alexander Nozik + */ +interface EnvelopeType { + + val code: Int + + val name: String + + val reader: EnvelopeReader + get() = getReader(emptyMap()) + + val writer: EnvelopeWriter + get() = getWriter(emptyMap()) + + fun description(): String + + /** + * Get reader with properties override + * + * @param properties + * @return + */ + fun getReader(properties: Map): EnvelopeReader + + /** + * Get writer with properties override + * + * @param properties + * @return + */ + fun getWriter(properties: Map): EnvelopeWriter + + companion object { + + + /** + * Infer envelope type from file reading only first line (ignoring empty and sha-bang) + * + * @param path + * @return + */ + fun infer(path: Path): EnvelopeType? { + return try { + FileChannel.open(path, StandardOpenOption.READ).use { + val buffer = it.map(FileChannel.MapMode.READ_ONLY, 0, 6) + val array = ByteArray(6) + buffer.get(array) + val header = String(array) + when { + //TODO use templates from appropriate types + header.startsWith("#!") -> error("Legacy dataforge tags are not supported") + header.startsWith("#~DFTL") -> TaglessEnvelopeType.INSTANCE + header.startsWith("#~") -> DefaultEnvelopeType.INSTANCE + else -> null + } + } + } catch (ex: Exception) { + LoggerFactory.getLogger(EnvelopeType::class.java).warn("Could not infer envelope type of file {} due to exception: {}", path, ex) + null + } + + } + + fun resolve(code: Int, context: Context = Global): EnvelopeType? { + synchronized(context) { + return context.findService(EnvelopeType::class.java) { it -> it.code == code } + } + } + + fun resolve(name: String, context: Context = Global): EnvelopeType? { + synchronized(context) { + return context.findService(EnvelopeType::class.java) { it -> it.name == name } + } + } + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/EnvelopeWriter.kt b/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/EnvelopeWriter.kt new file mode 100644 index 00000000..864eff31 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/EnvelopeWriter.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.io.envelopes + +import java.io.IOException +import java.io.OutputStream + +/** + * The writer interface for the envelope + * + * @author Alexander Nozik + */ +interface EnvelopeWriter { + + /** + * Write the envelope to an OutputStream + * + * @param stream + * @param envelope + * @throws IOException + */ + @Throws(IOException::class) + fun write(stream: OutputStream, envelope: Envelope) +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/JavaObjectWrapper.kt b/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/JavaObjectWrapper.kt new file mode 100644 index 00000000..77b1db7f --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/JavaObjectWrapper.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.io.envelopes + +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.ObjectInputStream +import java.io.ObjectOutputStream + +/** + * @author Alexander Nozik + */ +object JavaObjectWrapper : Wrapper { + const val JAVA_CLASS_KEY = "javaClass" + const val JAVA_OBJECT_TYPE = "hep.dataforge.java" + const val JAVA_SERIAL_DATA = "java.serial" + + override val type: Class + get() = Any::class.java + + override val name: String = JAVA_OBJECT_TYPE + + override fun wrap(obj: Any): Envelope { + val builder = EnvelopeBuilder() + .setDataType(JAVA_SERIAL_DATA) + .setEnvelopeType(JAVA_OBJECT_TYPE) + .setMetaValue(JAVA_CLASS_KEY, obj.javaClass.name) + val baos = ByteArrayOutputStream() + try { + ObjectOutputStream(baos).use { stream -> + stream.writeObject(obj) + builder.data(baos.toByteArray()) + return builder.build() + } + } catch (ex: IOException) { + throw RuntimeException(ex) + } + + } + + override fun unWrap(envelope: Envelope): Any { + if (name != envelope.type) { + throw Error("Wrong envelope type: " + envelope.type) + } + try { + val stream = ObjectInputStream(envelope.data.stream) + return stream.readObject() + } catch (ex: IOException) { + throw RuntimeException(ex) + } catch (ex: ClassNotFoundException) { + throw RuntimeException(ex) + } + + } + + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/LazyEnvelope.kt b/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/LazyEnvelope.kt new file mode 100644 index 00000000..8368cf6c --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/LazyEnvelope.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.io.envelopes + +import hep.dataforge.data.binary.Binary +import hep.dataforge.meta.Meta + +import java.io.ObjectStreamException +import java.util.function.Supplier + +/** + * The envelope that does not store data part in memory but reads it on demand. + * + * @property Return supplier of data for lazy calculation. The supplier is supposed to + * @author darksnake + */ +class LazyEnvelope(override val meta: Meta, private val dataSupplier: Supplier) : Envelope { + + constructor(meta: Meta, sup: () -> Binary) : this(meta, Supplier(sup)) + + /** + * Calculate data buffer if it is not already calculated and return result. + * + * @return + */ + override val data: Binary by lazy { dataSupplier.get() } + + @Throws(ObjectStreamException::class) + private fun writeReplace(): Any { + return SimpleEnvelope(meta, data) + } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/MetaType.kt b/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/MetaType.kt new file mode 100644 index 00000000..1839d3d8 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/MetaType.kt @@ -0,0 +1,68 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.io.envelopes + +import hep.dataforge.context.Global +import hep.dataforge.io.MetaStreamReader +import hep.dataforge.io.MetaStreamWriter +import hep.dataforge.io.envelopes.Envelope.Companion.META_TYPE_PROPERTY +import hep.dataforge.toList + +/** + * + * @author Alexander Nozik + */ +interface MetaType { + + + val codes: List + + val name: String + + val reader: MetaStreamReader + + val writer: MetaStreamWriter + + /** + * A file name filter for meta encoded in this format + * @return + */ + val fileNameFilter: (String) -> Boolean + + companion object { + + /** + * Lazy cache of meta types to improve performance + */ + private val metaTypes by lazy{ + Global.serviceStream(MetaType::class.java).toList() + } + + /** + * Resolve a meta type code and return null if code could not be resolved + * @param code + * @return + */ + fun resolve(code: Short): MetaType? { + return metaTypes.firstOrNull { it -> it.codes.contains(code) } + + } + + /** + * Resolve a meta type and return null if it could not be resolved + * @param name + * @return + */ + fun resolve(name: String): MetaType? { + return Global.serviceStream(MetaType::class.java) + .filter { it -> it.name.equals(name, ignoreCase = true) }.findFirst().orElse(null) + } + + fun resolve(properties: Map): MetaType { + return properties[META_TYPE_PROPERTY]?.let { MetaType.resolve(it) } ?: xmlMetaType + } + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/SimpleEnvelope.kt b/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/SimpleEnvelope.kt new file mode 100644 index 00000000..6170dc6f --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/SimpleEnvelope.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.io.envelopes + +import hep.dataforge.data.binary.Binary +import hep.dataforge.meta.Meta + +import java.io.IOException +import java.io.ObjectInputStream +import java.io.ObjectOutputStream + +/** + * The simplest in-memory envelope + * + * @author Alexander Nozik + */ +open class SimpleEnvelope(meta: Meta = Meta.empty(), data: Binary = Binary.EMPTY) : Envelope { + + override var meta: Meta = meta + protected set + + override var data: Binary = data + protected set + + @Throws(IOException::class) + private fun writeObject(out: ObjectOutputStream) { + DefaultEnvelopeWriter(DefaultEnvelopeType.INSTANCE, binaryMetaType).write(out, this) + } + + @Throws(IOException::class, ClassNotFoundException::class) + private fun readObject(`in`: ObjectInputStream) { + val envelope = DefaultEnvelopeReader.INSTANCE.read(`in`) + + this.meta = envelope.meta + this.data = envelope.data + } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/TaglessEnvelopeType.kt b/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/TaglessEnvelopeType.kt new file mode 100644 index 00000000..640a41fc --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/TaglessEnvelopeType.kt @@ -0,0 +1,216 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.io.envelopes + +import hep.dataforge.data.binary.BufferedBinary +import hep.dataforge.io.envelopes.Envelope.Companion.DATA_LENGTH_PROPERTY +import hep.dataforge.meta.Meta +import java.io.* +import java.nio.ByteBuffer +import java.nio.channels.Channels +import java.nio.channels.ReadableByteChannel +import java.text.ParseException +import java.util.* +import java.util.regex.Pattern + +/** + * A tagless envelope. No tag. Data infinite by default + */ +class TaglessEnvelopeType : EnvelopeType { + + override val code: Int = 0x4446544c //DFTL + + override val name: String = TAGLESS_ENVELOPE_TYPE + + override fun description(): String { + return "Tagless envelope. Text only. By default uses XML meta with utf encoding and data end auto-detection." + } + + override fun getReader(properties: Map): EnvelopeReader { + return TaglessReader(properties) + } + + override fun getWriter(properties: Map): EnvelopeWriter { + return TaglessWriter(properties) + } + + class TaglessWriter(var properties: Map = emptyMap()) : EnvelopeWriter { + + @Throws(IOException::class) + override fun write(stream: OutputStream, envelope: Envelope) { + val writer = PrintWriter(stream) + + //printing header + writer.println(TAGLESS_ENVELOPE_HEADER) + + //printing all properties + properties.forEach { key, value -> writer.printf("#? %s: %s;%n", key, value) } + writer.printf("#? %s: %s;%n", DATA_LENGTH_PROPERTY, envelope.data.size) + + //Printing meta + if (envelope.hasMeta()) { + //print optional meta start tag + writer.println(properties.getOrDefault(META_START_PROPERTY, DEFAULT_META_START)) + writer.flush() + + //define meta type + val metaType = MetaType.resolve(properties) + + //writing meta + metaType.writer.write(stream, envelope.meta) + } + + //Printing data + if (envelope.hasData()) { + //print mandatory data start tag + writer.println(properties.getOrDefault(DATA_START_PROPERTY, DEFAULT_DATA_START)) + writer.flush() + Channels.newChannel(stream).write(envelope.data.buffer) + } + stream.flush() + + } + } + + class TaglessReader(private val override: Map) : EnvelopeReader { + + private val BUFFER_SIZE = 1024 + + @Throws(IOException::class) + override fun read(stream: InputStream): Envelope { + return read(Channels.newChannel(stream)) + } + + override fun read(channel: ReadableByteChannel): Envelope { + val properties = HashMap(override) + val buffer = ByteBuffer.allocate(BUFFER_SIZE).apply { position(BUFFER_SIZE) } + val meta = readMeta(channel, buffer, properties) + return LazyEnvelope(meta) { BufferedBinary(readData(channel, buffer, properties)) } + } + + + /** + * Read lines using provided channel and buffer. Buffer state is changed by this operation + */ + private fun readLines(channel: ReadableByteChannel, buffer: ByteBuffer): Sequence { + return sequence { + val builder = ByteArrayOutputStream() + while (true) { + if (!buffer.hasRemaining()) { + if (!channel.isOpen) { + break + } + buffer.flip() + val count = channel.read(buffer) + buffer.flip() + if (count < BUFFER_SIZE) { + channel.close() + } + } + val b = buffer.get() + builder.write(b.toInt()) + if (b == '\n'.toByte()) { + yield(String(builder.toByteArray(), Charsets.UTF_8)) + builder.reset() + } + } + } + } + + @Throws(IOException::class) + private fun readMeta(channel: ReadableByteChannel, buffer: ByteBuffer, properties: MutableMap): Meta { + val sb = StringBuilder() + val metaEnd = properties.getOrDefault(DATA_START_PROPERTY, DEFAULT_DATA_START) + readLines(channel, buffer).takeWhile { it.trim { it <= ' ' } != metaEnd }.forEach { line -> + if (line.startsWith("#?")) { + readProperty(line.trim(), properties) + } else if (line.isEmpty() || line.startsWith("#~")) { + //Ignore headings, do nothing + } else { + sb.append(line).append("\r\n") + } + } + + + return if (sb.isEmpty()) { + Meta.empty() + } else { + val metaType = MetaType.resolve(properties) + try { + metaType.reader.readString(sb.toString()) + } catch (e: ParseException) { + throw RuntimeException("Failed to parse meta", e) + } + + } + } + + + @Throws(IOException::class) + private fun readData(channel: ReadableByteChannel, buffer: ByteBuffer, properties: Map): ByteBuffer { + val array = ByteArray(buffer.remaining()); + buffer.get(array) + if (properties.containsKey(DATA_LENGTH_PROPERTY)) { + val result = ByteBuffer.allocate(Integer.parseInt(properties[DATA_LENGTH_PROPERTY])) + result.put(array)//TODO fix it to not use direct array access + channel.read(result) + return result + } else { + val baos = ByteArrayOutputStream() + baos.write(array) + while (channel.isOpen) { + val read = channel.read(buffer) + buffer.flip() + if (read < BUFFER_SIZE) { + channel.close() + } + + baos.write(buffer.array()) + } + val remainingArray: ByteArray = ByteArray(buffer.remaining()) + buffer.get(remainingArray) + baos.write(remainingArray) + return ByteBuffer.wrap(baos.toByteArray()) + } + } + + private fun readProperty(line: String, properties: MutableMap) { + val pattern = Pattern.compile("#\\?\\s*(?[\\w.]*)\\s*:\\s*(?[^;]*);?") + val matcher = pattern.matcher(line) + if (matcher.matches()) { + val key = matcher.group("key") + val value = matcher.group("value") + properties.putIfAbsent(key, value) + } else { + throw RuntimeException("Custom property definition does not match format") + } + } + } + + companion object { + const val TAGLESS_ENVELOPE_TYPE = "tagless" + + const val TAGLESS_ENVELOPE_HEADER = "#~DFTL~#" + const val META_START_PROPERTY = "metaSeparator" + const val DEFAULT_META_START = "#~META~#" + const val DATA_START_PROPERTY = "dataSeparator" + const val DEFAULT_DATA_START = "#~DATA~#" + + val INSTANCE = TaglessEnvelopeType() + } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/Wrapper.kt b/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/Wrapper.kt new file mode 100644 index 00000000..2c410c71 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/Wrapper.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.io.envelopes + +import hep.dataforge.Named +import hep.dataforge.context.Context +import hep.dataforge.context.Global + +/** + * The class to unwrap object of specific type from envelope. Generally, T is + * supposed to be Wrappable, but it is not guaranteed. + * + * @author Alexander Nozik + */ +interface Wrapper : Named { + + val type: Class + + fun wrap(obj: T): Envelope + + fun unWrap(envelope: Envelope): T + + companion object { + const val WRAPPER_CLASS_KEY = "@wrapper" + + @Suppress("UNCHECKED_CAST") + @Throws(Exception::class) + fun unwrap(context: Context, envelope: Envelope): T { + val wrapper: Wrapper = when { + envelope.meta.hasValue(WRAPPER_CLASS_KEY) -> + Class.forName(envelope.meta.getString(WRAPPER_CLASS_KEY)).getConstructor().newInstance() as Wrapper + envelope.meta.hasValue(Envelope.ENVELOPE_TYPE_KEY) -> + context.findService(Wrapper::class.java) { it -> it.name == envelope.meta.getString(Envelope.ENVELOPE_TYPE_KEY) } as Wrapper? + ?: throw RuntimeException("Unwrapper not found") + else -> throw IllegalArgumentException("Not a wrapper envelope") + } + return wrapper.unWrap(envelope) + } + + @Throws(Exception::class) + fun unwrap(envelope: Envelope): T { + return unwrap(Global, envelope) + } + + @Suppress("UNCHECKED_CAST") + fun wrap(context: Context, obj: Any): Envelope { + val wrapper: Wrapper = context.findService(Wrapper::class.java) { it -> it.type != Any::class.java && it.type.isInstance(obj) } as Wrapper? + ?: JavaObjectWrapper + return wrapper.wrap(obj) + } + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/XMLMetaType.kt b/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/XMLMetaType.kt new file mode 100644 index 00000000..453ffd07 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/io/envelopes/XMLMetaType.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.io.envelopes + +import hep.dataforge.io.MetaStreamReader +import hep.dataforge.io.MetaStreamWriter +import hep.dataforge.io.XMLMetaReader +import hep.dataforge.io.XMLMetaWriter + +val xmlMetaType = XMLMetaType() + +class XMLMetaType : MetaType { + + override val codes: List = listOf(0x584d, 0)//XM + + override val name: String = XML_META_TYPE + + override val reader: MetaStreamReader = XMLMetaReader() + + override val writer: MetaStreamWriter = XMLMetaWriter() + + override val fileNameFilter: (String) -> Boolean = { str -> str.toLowerCase().endsWith(".xml") } + + companion object { + const val XML_META_TYPE = "XML" + } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/io/output/Output.kt b/dataforge-core/src/main/kotlin/hep/dataforge/io/output/Output.kt new file mode 100644 index 00000000..c030a263 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/io/output/Output.kt @@ -0,0 +1,67 @@ +package hep.dataforge.io.output + +import hep.dataforge.context.Context +import hep.dataforge.context.ContextAware +import hep.dataforge.context.FileReference +import hep.dataforge.meta.Meta +import java.io.OutputStream + +/** + * An interface for generic display and ouput capabilities. + */ +interface Output : ContextAware { + /** + * Display an object with given configuration. Throw an exception if object type not supported + */ + fun render(obj: Any, meta: Meta = Meta.empty()) + + companion object { + const val TEXT_TYPE = "text" + const val BINARY_TYPE = "binary" + + + fun splitOutput(vararg outputs: Output): Output { + val context = outputs.first().context + return object : Output { + override val context: Context + get() = context + + override fun render(obj: Any, meta: Meta) { + outputs.forEach { it.render(obj, meta) } + } + + } + } + + fun fileOutput(ref: FileReference): Output { + return FileOutput(ref) + } + + fun streamOutput(context: Context, stream: OutputStream): Output { + return StreamOutput(context, stream) + } + } +} + +/** + * The object that knows best how it should be rendered + */ +interface SelfRendered { + fun render(output: Output, meta: Meta) +} + +/** + * Custom renderer for specific type of object + */ +interface OutputRenderer { + val type: String + fun render(output: Output, obj: Any, meta: Meta) +} + + +val Output.stream: OutputStream + get() = if(this is StreamOutput){ + this.stream + } else{ + StreamConsumer(this) + } \ No newline at end of file diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/io/output/StreamConsumer.kt b/dataforge-core/src/main/kotlin/hep/dataforge/io/output/StreamConsumer.kt new file mode 100644 index 00000000..59c1c368 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/io/output/StreamConsumer.kt @@ -0,0 +1,35 @@ +package hep.dataforge.io.output + +import hep.dataforge.meta.Meta +import java.io.ByteArrayOutputStream +import java.io.OutputStream + +/** + * A stream wrapping the output object. Used for backward compatibility + */ +class StreamConsumer(val output: Output, val meta: Meta = Meta.empty()) : OutputStream() { + val buffer = ByteArrayOutputStream() + + override fun write(b: Int) { + synchronized(buffer) { + when(b.toChar()){ + '\r' -> {} + '\n' -> flush() + else -> buffer.write(b) + } + } + } + + override fun flush() { + synchronized(buffer) { + if(buffer.size()>0) { + output.render(String(buffer.toByteArray(), Charsets.UTF_8), meta) + buffer.reset() + } + } + } + + override fun close() { + flush() + } +} \ No newline at end of file diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/io/output/TextOutput.kt b/dataforge-core/src/main/kotlin/hep/dataforge/io/output/TextOutput.kt new file mode 100644 index 00000000..77eb373d --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/io/output/TextOutput.kt @@ -0,0 +1,283 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.io.output + +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.LoggerContext +import ch.qos.logback.classic.encoder.PatternLayoutEncoder +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.encoder.Encoder +import ch.qos.logback.core.encoder.EncoderBase +import hep.dataforge.asMap +import hep.dataforge.context.Context +import hep.dataforge.context.FileReference +import hep.dataforge.description.NodeDescriptor +import hep.dataforge.description.ValueDescriptor +import hep.dataforge.io.ColumnedDataWriter +import hep.dataforge.io.IOUtils +import hep.dataforge.io.envelopes.Envelope +import hep.dataforge.io.envelopes.EnvelopeType +import hep.dataforge.io.envelopes.TaglessEnvelopeType +import hep.dataforge.io.envelopes.buildEnvelope +import hep.dataforge.io.history.Record +import hep.dataforge.meta.Meta +import hep.dataforge.meta.Metoid +import hep.dataforge.tables.Table +import hep.dataforge.useValue +import hep.dataforge.values.ValueType +import org.slf4j.LoggerFactory +import java.awt.Color +import java.io.OutputStream +import java.io.PrintWriter +import java.time.Instant +import java.util.concurrent.Executors + + +/** + * A n output that could display plain text with attributes + */ +interface TextOutput : Output { + fun renderText(text: String, vararg attributes: TextAttribute) + + /** + * Render new line honoring offsets and bullets + */ + fun newLine(meta: Meta) { + renderText("\n") + render("", meta) + } + + @JvmDefault + fun renderText(text: String, color: Color) { + renderText(text, TextColor(color)) + } +} + +/** + * A display based on OutputStream. The stream must be closed by caller + */ +open class StreamOutput(override val context: Context, val stream: OutputStream) : Output, AutoCloseable, TextOutput { + private val printer = PrintWriter(stream) + private val executor = Executors.newSingleThreadExecutor() + + var isOpen: Boolean = true + protected set + + protected open val logEncoder: Encoder by lazy { + PatternLayoutEncoder().apply { + this.pattern = "%date %level [%thread] %logger{10} [%file:%line] %msg%n" + this.context = LoggerFactory.getILoggerFactory() as LoggerContext + start() + } + } + + override fun render(obj: Any, meta: Meta) { + if (!isOpen) { + error("Can't write to closed output") + } + executor.run { + meta.useValue("text.offset") { repeat(it.int) { renderText("\t") } } + meta.useValue("text.bullet") { renderText(it.string + " ") } + when (obj) { + is Meta -> renderMeta(obj, meta) + is SelfRendered -> { + obj.render(this@StreamOutput, meta) + } + is Table -> { + //TODO add support for tab-stops + renderText(obj.format.names.joinToString(separator = "\t"), TextColor(Color.BLUE)) + obj.rows.forEach { values -> + printer.println(obj.format.names.map { values[it] }.joinToString(separator = "\t")) + } + } + is Envelope -> { + val envelopeType = EnvelopeType.resolve(meta.getString("envelope.encoding", TaglessEnvelopeType.TAGLESS_ENVELOPE_TYPE)) + ?: throw RuntimeException("Unknown envelope encoding") + val envelopeProperties = meta.getMeta("envelope.properties", Meta.empty()).asMap { it.string } + envelopeType.getWriter(envelopeProperties).write(stream, obj) + } + is ILoggingEvent -> { + printer.println(String(logEncoder.encode(obj))) + } + is CharSequence -> printer.println(obj) + is Record -> printer.println(obj) + is ValueDescriptor -> { + if (obj.required) renderText("(*) ", Color.CYAN) + renderText(obj.name, Color.RED) + if (obj.multiple) renderText(" (mult)", Color.CYAN) + renderText(" (${obj.type.first()})") + if (obj.hasDefault()) { + val def = obj.default + if (def.type == ValueType.STRING) { + renderText(" = \"") + renderText(def.string, Color.YELLOW) + renderText("\": ") + } else { + renderText(" = ") + renderText(def.string, Color.YELLOW) + renderText(": ") + } + } else { + renderText(": ") + } + renderText(obj.info) + } + is NodeDescriptor -> { + obj.childrenDescriptors().forEach { key, value -> + val newMeta = meta.builder + .setValue("text.offset", meta.getInt("text.offset", 0) + 1) + .setValue("text.bullet", "+") + renderText(key + "\n", Color.BLUE) + if (value.required) renderText("(*) ", Color.CYAN) + + renderText(value.name, Color.MAGENTA) + + if (value.multiple) renderText(" (mult)", Color.CYAN) + + if (!value.info.isEmpty()) { + renderText(": ${value.info}") + } + render(value, newMeta) + } + + obj.valueDescriptors().forEach { key, value -> + val newMeta = meta.builder + .setValue("text.offset", meta.getInt("text.offset", 0) + 1) + .setValue("text.bullet", "-") + renderText(key + "\n", Color.BLUE) + render(value, newMeta) + } + } + is Metoid -> { // render custom metoid + val renderType = obj.meta.getString("@output.type", "@default") + context.findService(OutputRenderer::class.java) { it.type == renderType } + ?.render(this@StreamOutput, obj, meta) + ?: renderMeta(obj.meta, meta) + } + else -> printer.println(obj) + } + printer.flush() + } + } + + open fun renderMeta(meta: Meta, options: Meta) { + printer.println(meta.toString()) + } + + override fun renderText(text: String, vararg attributes: TextAttribute) { + printer.println(text) + } + + override fun close() { + isOpen = false + stream.close() + } +} + +/** + * A stream output with ANSI colors enabled + */ +class ANSIStreamOutput(context: Context, stream: OutputStream) : StreamOutput(context, stream) { + + override val logEncoder: Encoder by lazy { + object : EncoderBase() { + override fun headerBytes(): ByteArray = ByteArray(0) + + override fun footerBytes(): ByteArray = ByteArray(0) + + override fun encode(event: ILoggingEvent): ByteArray { + return buildString { + append(Instant.ofEpochMilli(event.timeStamp).toString() + "\t") + //%level [%thread] %logger{10} [%file:%line] %msg%n + if (event.threadName != Thread.currentThread().name) { + append("[${event.threadName}]\t") + } + append(IOUtils.wrapANSI(event.loggerName, IOUtils.ANSI_BLUE) + "\t") + + when (event.level) { + Level.ERROR -> appendln(IOUtils.wrapANSI(event.message, IOUtils.ANSI_RED)) + Level.WARN -> appendln(IOUtils.wrapANSI(event.message, IOUtils.ANSI_YELLOW)) + else -> appendln(event.message) + } + }.toByteArray() + } + + }.apply { + this.context = LoggerFactory.getILoggerFactory() as LoggerContext + start() + } + } + + private fun wrapText(text: String, vararg attributes: TextAttribute): String { + return attributes.find { it is TextColor }?.let { + when ((it as TextColor).color) { + Color.BLACK -> IOUtils.wrapANSI(text, IOUtils.ANSI_BLACK) + Color.RED -> IOUtils.wrapANSI(text, IOUtils.ANSI_RED) + Color.GREEN -> IOUtils.wrapANSI(text, IOUtils.ANSI_GREEN) + Color.YELLOW -> IOUtils.wrapANSI(text, IOUtils.ANSI_YELLOW) + Color.BLUE -> IOUtils.wrapANSI(text, IOUtils.ANSI_BLUE) + //Color. -> IOUtils.wrapANSI(text, IOUtils.ANSI_PURPLE) + Color.CYAN -> IOUtils.wrapANSI(text, IOUtils.ANSI_CYAN) + Color.WHITE -> IOUtils.wrapANSI(text, IOUtils.ANSI_WHITE) + else -> { + //Color is not resolved + text + } + } + } ?: text + } + + override fun renderText(text: String, vararg attributes: TextAttribute) { + super.renderText(wrapText(text, *attributes), *attributes) + } +} + +class FileOutput(val file: FileReference) : Output, AutoCloseable { + override val context: Context + get() = file.context + + private val streamOutput by lazy { + StreamOutput(context, file.outputStream) + } + + override fun render(obj: Any, meta: Meta) { + when (obj) { + is Table -> { + streamOutput.render( + buildEnvelope { + meta(meta) + data { + ColumnedDataWriter.writeTable(it, obj, "") + } + } + ) + } + else -> streamOutput.render(obj, meta) + } + } + + override fun close() { + streamOutput.close() + } + +} + +sealed class TextAttribute + +class TextColor(val color: Color) : TextAttribute() +class TextStrong : TextAttribute() +class TextEmphasis : TextAttribute() \ No newline at end of file diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/meta/KMetaBuilder.kt b/dataforge-core/src/main/kotlin/hep/dataforge/meta/KMetaBuilder.kt new file mode 100644 index 00000000..25bcf4e4 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/meta/KMetaBuilder.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.meta + +import hep.dataforge.meta.MetaNode.DEFAULT_META_NAME +import hep.dataforge.values.NamedValue + +@DslMarker +annotation class MetaDSL + +/** + * Kotlin meta builder extension + */ +@MetaDSL +class KMetaBuilder(name: String = MetaBuilder.DEFAULT_META_NAME) : MetaBuilder(name) { + + operator fun Meta.unaryPlus() { + putNode(this); + } + + operator fun String.unaryMinus() { + removeNode(this); + removeValue(this); + } + + operator fun NamedValue.unaryPlus() { + putValue(this.name, this.anonymous) + } + + /** + * Add value + */ + infix fun String.to(value: Any?) { + setValue(this, value); + } + + infix fun String.to(metaBuilder: KMetaBuilder.() -> Unit) { + setNode(this, KMetaBuilder(this).apply(metaBuilder)) + } + + infix fun String.to(meta: Meta) { + setNode(this, meta) + } + +// /** +// * Short infix notation to put value +// */ +// infix fun String.v(value: Any) { +// putValue(this, value); +// } +// +// /** +// * Short infix notation to put node +// */ +// infix fun String.n(node: Meta) { +// putNode(this, node) +// } +// +// /** +// * Short infix notation to put any object that could be converted to meta +// */ +// infix fun String.n(node: MetaID) { +// putNode(this, node.toMeta()) +// } + + fun putNode(node: MetaID) { + putNode(node.toMeta()) + } + + fun putNode(key: String, node: MetaID) { + putNode(key, node.toMeta()) + } + + /** + * Attach new node + */ + @MetaDSL + fun node(name: String, vararg values: Pair, transform: (KMetaBuilder.() -> Unit)? = null) { + val node = KMetaBuilder(name); + values.forEach { + node.putValue(it.first, it.second) + } + transform?.invoke(node) + attachNode(node) + } +} + +fun buildMeta(name: String = DEFAULT_META_NAME, transform: (KMetaBuilder.() -> Unit)? = null): KMetaBuilder { + val node = KMetaBuilder(name); + transform?.invoke(node) + return node +} + +fun buildMeta(name: String, vararg values: Pair, transform: (KMetaBuilder.() -> Unit)? = null): KMetaBuilder { + val node = KMetaBuilder(name); + values.forEach { + node.putValue(it.first, it.second) + } + transform?.invoke(node) + return node +} \ No newline at end of file diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/meta/Laminate.kt b/dataforge-core/src/main/kotlin/hep/dataforge/meta/Laminate.kt new file mode 100644 index 00000000..4fd35207 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/meta/Laminate.kt @@ -0,0 +1,330 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.meta + +import hep.dataforge.description.Described +import hep.dataforge.description.Descriptors +import hep.dataforge.description.NodeDescriptor +import hep.dataforge.io.XMLMetaWriter +import hep.dataforge.nullable +import hep.dataforge.values.Value +import hep.dataforge.values.ValueFactory +import org.jetbrains.annotations.Contract +import java.time.Instant +import java.util.* +import java.util.stream.Collector +import java.util.stream.Collectors +import java.util.stream.Stream + +/** + * A chain of immutable meta. The value is taken from the first meta in list + * that contains it. The list itself is immutable. + * + * @author darksnake + */ +class Laminate(layers: Iterable, descriptor: NodeDescriptor? = null) : Meta(), Described { + + //TODO consider descriptor merging + override val descriptor: NodeDescriptor = descriptor ?: layers.asSequence() + .filter { it -> it is Laminate }.map { Laminate::class.java.cast(it) } + .map { it.descriptor } + .firstOrNull() ?: NodeDescriptor(Meta.empty()) + + + /** + * Create laminate from layers. Deepest first. + * + * @param layers + */ + constructor(vararg layers: Meta?) : this(Arrays.asList(*layers)) {} + + + val layers: List = layers.filterNotNull().flatMap { + when { + it.isEmpty -> emptyList() + it is Laminate -> it.layers // no need to check deeper since laminate is always has only direct members + else -> listOf(it) + } + } + + private val descriptorLayer: Meta? by lazy { + descriptor?.let { Descriptors.buildDefaultNode(it) } + } + + override val name: String + get() = layers.stream().map { it.name }.findFirst().orElse(MetaNode.DEFAULT_META_NAME) + + + @Contract(pure = true) + fun hasDescriptor(): Boolean { + return !this.descriptor.meta.isEmpty + } + + /** + * Attach descriptor to this laminate to use for default values and aliases + * (ALIASES NOT IMPLEMENTED YET!). + */ + fun withDescriptor(descriptor: NodeDescriptor): Laminate { + return Laminate(this.layers, descriptor) + } + + /** + * Add primary (first layer) + * + * @param layer + * @return + */ + fun withFirstLayer(layer: Meta): Laminate { + return if (layer.isEmpty) { + this + } else { + return Laminate(listOf(layer) + this.layers, this.descriptor) + } + } + + /** + * Add layer to stack + * + * @param layers + * @return + */ + fun withLayer(vararg layers: Meta): Laminate { + return Laminate(this.layers + layers, descriptor) + } + + /** + * Add layer to the end of laminate + */ + operator fun plus(layer: Meta): Laminate { + return withLayer(layer) + } + + /** + * Get laminate layers in inverse order + * + * @return + */ + fun layersInverse(): List { + return layers.reversed() + } + + override fun optMeta(path: String): Optional { + val childLayers = ArrayList() + layers.stream().filter { layer -> layer.hasMeta(path) }.forEach { m -> + //FIXME child elements are not chained! + childLayers.add(m.getMeta(path)) + } + return if (!childLayers.isEmpty()) { + Optional.of(Laminate(childLayers, descriptor.childrenDescriptors()[path])) + } else { + //if node not found, using descriptor layer if it is defined + descriptorLayer?.optMeta(path) ?: Optional.empty() + } + } + + + /** + * Get the first occurrence of meta node with the given name without merging. If not found, uses description. + * + * @param path + * @return + */ + override fun getMetaList(path: String): List { + val stream: Stream = if (descriptorLayer == null) { + layers.stream() + } else { + Stream.concat(layers.stream(), Stream.of(descriptorLayer)) + } + + return stream + .filter { m -> m.hasMeta(path) } + .map { m -> m.getMetaList(path) }.findFirst() + .orElse(emptyList()) + } + + /** + * Node names includes descriptor nodes + * + * @return + */ + override fun getNodeNames(includeHidden: Boolean): Stream { + return getNodeNames(includeHidden, true) + } + + + fun getNodeNames(includeHidden: Boolean, includeDefaults: Boolean): Stream { + val names = layers.stream().flatMap { layer -> layer.getNodeNames(includeHidden) } + return if (includeDefaults && descriptorLayer != null) { + Stream.concat(names, descriptorLayer!!.getNodeNames(includeHidden)).distinct() + } else { + names.distinct() + } + } + + /** + * Value names includes descriptor values, + * + * @return + */ + override fun getValueNames(includeHidden: Boolean): Stream { + return getValueNames(includeHidden, true) + } + + fun getValueNames(includeHidden: Boolean, includeDefaults: Boolean): Stream { + val names = layers.stream().flatMap { layer -> layer.getValueNames(includeHidden) } + return if (includeDefaults && descriptorLayer != null) { + Stream.concat(names, descriptorLayer!!.getValueNames(includeHidden)).distinct() + } else { + names.distinct() + } + } + + override fun optValue(path: String): Optional { + //searching layers for value + for (m in layers) { + val opt = m.optValue(path) + if (opt.isPresent) { + return opt.map { it -> MetaUtils.transformValue(it) } + } + } + + // if descriptor layer is definded, searching it for value + return if (descriptorLayer != null) { + descriptorLayer!!.optValue(path).map { it -> MetaUtils.transformValue(it) } + } else Optional.empty() + + } + + override fun isEmpty(): Boolean { + return this.layers.isEmpty() && (this.descriptorLayer == null || this.descriptorLayer!!.isEmpty) + } + + /** + * Combine values in layers using provided collector. Default values from provider and description are ignored + * + * @param valueName + * @param collector + * @return + */ + fun collectValue(valueName: String, collector: Collector): Value { + return layers.stream() + .filter { layer -> layer.hasValue(valueName) } + .map { layer -> layer.getValue(valueName) } + .collect(collector) + } + + /** + * Merge nodes using provided collector (good idea to use [MergeRule]). + * + * @param nodeName + * @param collector + * @param + * @return + */ + fun collectNode(nodeName: String, collector: Collector): Meta { + return layers.stream() + .filter { layer -> layer.hasMeta(nodeName) } + .map { layer -> layer.getMeta(nodeName) } + .collect(collector) + } + + /** + * Merge node lists grouping nodes by provided classifier and then merging each group independently + * + * @param nodeName the name of node + * @param classifier grouping function + * @param collector used to each group + * @param intermediate collector accumulator type + * @param classifier key type + * @return + */ + fun collectNodes(nodeName: String, classifier: (Meta) -> K, collector: Collector): Collection { + return layers.stream() + .filter { layer -> layer.hasMeta(nodeName) } + .flatMap { layer -> layer.getMetaList(nodeName).stream() } + .collect(Collectors.groupingBy(classifier, { LinkedHashMap() }, collector)).values + //linkedhashmap ensures ordering + } + + /** + * Same as above, but uses fixed replace rule to merge meta + * + * @param nodeName + * @param classifier + * @param + * @return + */ + fun collectNodes(nodeName: String, classifier: (Meta) -> K): Collection { + return collectNodes(nodeName, classifier, MergeRule.replace()) + } + + /** + * Same as above but uses fixed meta value with given key as identity + * + * @param nodeName + * @param key + * @return + */ + fun collectNodes(nodeName: String, key: String): Collection { + return collectNodes(nodeName) { getValue(key, ValueFactory.NULL) } + } + + /** + * Calculate sum of numeric values with given name. Values in all layers must be numeric. + * + * @param valueName + * @return + */ + fun sumValue(valueName: String): Double { + return layers.stream().mapToDouble { layer -> layer.getDouble(valueName, 0.0) }.sum() + } + + /** + * Press all of the Laminate layers together creating single immutable meta + * + * @return + */ + fun merge(): Meta { + return SealedNode(this) + } + + override fun toString(): String { + return XMLMetaWriter().writeString(this.merge()) + } + + companion object { + fun join(meta: Iterable, descriptor: NodeDescriptor? = null): Laminate { + return Laminate(meta.toList(), descriptor) + } + + fun join(vararg meta: Meta): Laminate = join(meta.asIterable()) + } +} + +fun Iterable.toLaminate(descriptor: NodeDescriptor? = null) = Laminate.join(this, descriptor) + +inline operator fun Meta.get(name: String) : T? = when(T::class){ + String::class -> optString(name).nullable as T? + Double::class -> optNumber(name).map { it.toDouble() }.nullable as T? + Int::class -> optNumber(name).map { it.toInt() }.nullable as T? + Short::class -> optNumber(name).map { it.toShort() }.nullable as T? + Long::class -> optNumber(name).map { it.toLong() }.nullable as T? + Float::class -> optNumber(name).map { it.toFloat() }.nullable as T? + Boolean::class -> optBoolean(name).nullable as T? + Instant::class -> optTime(name).nullable as T? + Meta::class -> optMeta(name).nullable as T? + else -> error("Type ${T::class} is not recognized as a meta member") +} \ No newline at end of file diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/meta/MetaDelegates.kt b/dataforge-core/src/main/kotlin/hep/dataforge/meta/MetaDelegates.kt new file mode 100644 index 00000000..7899fa12 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/meta/MetaDelegates.kt @@ -0,0 +1,369 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.meta + +import hep.dataforge.description.Described +import hep.dataforge.description.DescriptorName +import hep.dataforge.values.Value +import hep.dataforge.values.parseValue +import java.time.Instant +import kotlin.properties.ReadOnlyProperty +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KClass +import kotlin.reflect.KProperty +import kotlin.reflect.full.findAnnotation + +/* Meta delegate classes */ + +interface MetaDelegate { + val target: Meta + val name: String? + + fun targetName(property: KProperty<*>): String{ + //TODO maybe it should be optimized + return name ?:property.findAnnotation()?.name ?: property.name + } +} + +/** + * The delegate for value in meta + * Values for sealed meta are automatically cached + * @property name name of the property. If null, use property name + */ +open class ValueDelegate( + override val target: Meta, + override val name: String? = null, + val def: T? = null, + val write: (T) -> Any = { it }, + val read: (Value) -> T +) : ReadOnlyProperty, MetaDelegate { + + private var cached: T? = null + + private fun getValueInternal(thisRef: Any?, property: KProperty<*>): T { + val key = targetName(property) + return when { + target.hasValue(key) -> read(target.getValue(key)) + def != null -> def + thisRef is Described -> thisRef.descriptor.getValueDescriptor(key)?.default?.let(read) + ?: throw RuntimeException("Neither value, not default found for value $key in $thisRef") + else -> throw RuntimeException("Neither value, not default found for value $key in $thisRef") + } + } + + override operator fun getValue(thisRef: Any?, property: KProperty<*>): T { + if (target is SealedNode && cached == null) { + cached = getValueInternal(thisRef, property) + } + return cached ?: getValueInternal(thisRef, property) + } + +} + +class EnumValueDelegate>( + target: Meta, + val type: KClass, + name: String? = null, + def: T? = null +) : ValueDelegate(target, name, def, write = { it.name }, read = { java.lang.Enum.valueOf(type.java, it.string) }) + +open class NodeDelegate( + override val target: Meta, + override val name: String? = null, + val def: T? = null, + val read: (Meta) -> T +) : ReadOnlyProperty, MetaDelegate { + + private var cached: T? = null + + private fun getNodeInternal(thisRef: Any, property: KProperty<*>): T { + val key = targetName(property) + return when { + target.hasMeta(key) -> read(target.getMeta(key)) + def != null -> def + thisRef is Described -> thisRef.descriptor.getNodeDescriptor(key)?.default?.firstOrNull()?.let(read) + ?: throw RuntimeException("Neither value, not default found for node $key in $thisRef") + else -> throw RuntimeException("Neither value, not default found for node $key in $thisRef") + } + } + + + override operator fun getValue(thisRef: Any, property: KProperty<*>): T { + if (target is SealedNode && cached == null) { + cached = getNodeInternal(thisRef, property) + } + return cached ?: getNodeInternal(thisRef, property) + } +} + +class NodeListDelegate( + override val target: Meta, + override val name: String?, + val def: List? = null, + private val read: (Meta) -> T +) : ReadOnlyProperty>, MetaDelegate { + + private var cached: List? = null + + private fun getNodeInternal(thisRef: Any?, property: KProperty<*>): List { + val key = targetName(property) + return when { + target.hasMeta(key) -> target.getMetaList(key).map(read) + def != null -> def + thisRef is Described -> thisRef.descriptor.getNodeDescriptor(key)?.default?.map(read) + ?: throw RuntimeException("Neither value, not default found for node $key in $thisRef") + else -> throw RuntimeException("Neither value, not default found for node $key in $thisRef") + } + } + + + override operator fun getValue(thisRef: Any?, property: KProperty<*>): List { + if (target is SealedNode && cached == null) { + cached = getNodeInternal(thisRef, property) + } + return cached ?: getNodeInternal(thisRef, property) + } +} + +open class MutableValueDelegate( + override val target: Configuration, + name: String? = null, + def: T? = null, + write: (T) -> Any = { it }, + read: (Value) -> T +) : ReadWriteProperty, ValueDelegate(target, name, def, write, read) { + + override operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + //TODO set for allowed values + target.setValue(targetName(property), write(value)) + } +} + +class MutableEnumValueDelegate>( + target: Configuration, + type: KClass, + name: String? = null, + def: T? = null +) : MutableValueDelegate(target, name, def, write = { it.name }, read = { java.lang.Enum.valueOf(type.java, it.string) }) + +class MutableNodeDelegate( + override val target: Configuration, + name: String?, + def: T? = null, + val write: (T) -> Meta, + read: (Meta) -> T +) : ReadWriteProperty, NodeDelegate(target, name, def, read) { + + override operator fun setValue(thisRef: Any, property: KProperty<*>, value: T) { + val nodeName = targetName(property) + val node = write(value) + // if node exists, update it + if (target.hasMeta(nodeName)) { + target.getMeta(nodeName).update(write(value)) + } else { + // if it is configuration, use it as is + if (node is Configuration) { + target.attachNode(targetName(property), node) + } else { + // otherwise transform it to configuration + target.setNode(nodeName, node) + } + } + } +} + +/* + * Delegate value and meta getter to target meta using thisref description + */ + +fun Meta.value(valueName: String? = null, def: Value? = null): ReadOnlyProperty = + ValueDelegate(this, valueName, def) { it } + +fun Meta.stringValue(valueName: String? = null, def: String? = null): ReadOnlyProperty = + ValueDelegate(this, valueName, def) { it.string } + +fun Meta.booleanValue(valueName: String? = null, def: Boolean? = null): ReadOnlyProperty = + ValueDelegate(this, valueName, def) { it.boolean } + +fun Meta.timeValue(valueName: String? = null, def: Instant? = null): ReadOnlyProperty = + ValueDelegate(this, valueName, def) { it.time } + +fun Meta.numberValue(valueName: String? = null, def: Number? = null): ReadOnlyProperty = + ValueDelegate(this, valueName, def) { it.number } + +fun Meta.doubleValue(valueName: String? = null, def: Double? = null): ReadOnlyProperty = + ValueDelegate(this, valueName, def) { it.double } + +fun Meta.intValue(valueName: String? = null, def: Int? = null): ReadOnlyProperty = + ValueDelegate(this, valueName, def) { it.int } + +fun Meta.customValue(valueName: String? = null, def: T? = null, conv: (Value) -> T): ReadOnlyProperty = + ValueDelegate(this, valueName, def, read = conv) + +inline fun > Meta.enumValue(valueName: String? = null, def: T? = null): ReadOnlyProperty = + EnumValueDelegate(this, T::class, valueName, def) + +fun Meta.node(nodeName: String? = null, def: Meta? = null): ReadOnlyProperty = + NodeDelegate(this, nodeName, def) { it } + +fun Meta.nodeList(nodeName: String? = null, def: List? = null): ReadOnlyProperty> = + NodeListDelegate(this, nodeName, def) { it } + +fun Meta.customNode(nodeName: String? = null, def: T? = null, conv: (Meta) -> T): ReadOnlyProperty = + NodeDelegate(this, nodeName, def, conv) + +fun Meta.morphNode(type: KClass, nodeName: String? = null, def: T? = null): ReadOnlyProperty = + NodeDelegate(this, nodeName, def) { MetaMorph.morph(type, it) } + +fun Meta.morphList(type: KClass, nodeName: String? = null, def: List? = null): ReadOnlyProperty> = + NodeListDelegate(this, nodeName, def) { MetaMorph.morph(type, it) } + +inline fun Meta.morphNode(nodeName: String? = null, def: T? = null): ReadOnlyProperty = + this.morphNode(T::class, nodeName, def) + +inline fun Meta.morphList(nodeName: String? = null, def: List? = null): ReadOnlyProperty> = + NodeListDelegate(this, nodeName, def) { MetaMorph.morph(T::class, it) } + +//Metoid extensions + +fun Metoid.value(valueName: String? = null, def: Value? = null) = meta.value(valueName, def) +fun Metoid.stringValue(valueName: String? = null, def: String? = null) = meta.stringValue(valueName, def) +fun Metoid.booleanValue(valueName: String? = null, def: Boolean? = null) = meta.booleanValue(valueName, def) +fun Metoid.timeValue(valueName: String? = null, def: Instant? = null) = meta.timeValue(valueName, def) +fun Metoid.numberValue(valueName: String? = null, def: Number? = null) = meta.numberValue(valueName, def) +fun Metoid.doubleValue(valueName: String? = null, def: Double? = null) = meta.doubleValue(valueName, def) +fun Metoid.intValue(valueName: String? = null, def: Int? = null) = meta.intValue(valueName, def) +fun Metoid.customValue(valueName: String? = null, def: T? = null, read: (Value) -> T) = meta.customValue(valueName, def, read) +inline fun > Metoid.enumValue(valueName: String? = null, def: T? = null) = meta.enumValue(valueName, def) +fun Metoid.node(nodeName: String? = null, def: Meta? = null) = meta.node(nodeName, def) +fun Metoid.nodeList(nodeName: String? = null, def: List? = null) = meta.nodeList(nodeName, def) +fun Metoid.customNode(nodeName: String? = null, def: T? = null, conv: (Meta) -> T) = meta.customNode(nodeName, def, conv) +fun Metoid.morphNode(type: KClass, nodeName: String? = null, def: T? = null) = meta.morphNode(type, nodeName, def) +inline fun Metoid.morphNode(nodeName: String? = null, def: T? = null) = meta.morphNode(nodeName, def) +inline fun Metoid.morphList(nodeName: String? = null, def: List? = null) = meta.morphList(nodeName, def) + +//Configuration extension + +/** [Configuration] extensions */ + +fun Configuration.mutableValue(valueName: String? = null, def: Value? = null): ReadWriteProperty = + MutableValueDelegate(this, valueName, def, { it }, { it }) + +fun Configuration.mutableStringValue(valueName: String? = null, def: String? = null): ReadWriteProperty = + MutableValueDelegate(this, valueName, def, { it.parseValue() }, Value::string) + +fun Configuration.mutableBooleanValue(valueName: String? = null, def: Boolean? = null): ReadWriteProperty = + MutableValueDelegate(this, valueName, def) { it.boolean } + +fun Configuration.mutableTimeValue(valueName: String? = null, def: Instant? = null): ReadWriteProperty = + MutableValueDelegate(this, valueName, def) { it.time } + +fun Configuration.mutableNumberValue(valueName: String? = null, def: Number? = null): ReadWriteProperty = + MutableValueDelegate(this, valueName, def) { it.number } + +fun Configuration.mutableDoubleValue(valueName: String? = null, def: Double? = null): ReadWriteProperty = + MutableValueDelegate(this, valueName, def) { it.double } + +fun Configuration.mutableIntValue(valueName: String? = null, def: Int? = null): ReadWriteProperty = + MutableValueDelegate(this, valueName, def) { it.int } + +fun Configuration.customMutableValue(valueName: String? = null, def: T? = null, read: (Value) -> T, write: (T) -> Any): ReadWriteProperty = + MutableValueDelegate(this, valueName, def, write, read) + +inline fun > Configuration.mutableEnumValue(valueName: String? = null, def: T? = null): ReadWriteProperty = + MutableEnumValueDelegate(this, T::class, valueName, def) + +/** + * Returns a child node of given meta that could be edited in-place + */ +fun Configuration.mutableNode(metaName: String? = null, def: Meta? = null): ReadWriteProperty = + MutableNodeDelegate(this, metaName, Configuration(def ?: Meta.empty()), + read = { + it as? Configuration ?: Configuration(it) + }, + write = { it } + ) + +fun Configuration.mutableCustomNode(metaName: String? = null, def: T? = null, write: (T) -> Meta, read: (Meta) -> T): ReadWriteProperty = + MutableNodeDelegate(this, metaName, def, write, read) + +/** + * Create a property that is delegate for configurable + */ +fun Configuration.mutableMorph(type: KClass, metaName: String? = null, def: T? = null): ReadWriteProperty { + return MutableNodeDelegate(this, metaName, def, { it.toMeta() }, { MetaMorph.morph(type, it) }) +} + +inline fun Configuration.mutableMorph(metaName: String? = null, def: T? = null): ReadWriteProperty = + mutableMorph(T::class, metaName, def) + + +/** [Configurable] extensions */ + +fun Configurable.configValue(valueName: String? = null, def: Value? = null) = config.mutableValue(valueName, def) + +fun Configurable.configString(valueName: String? = null, def: String? = null) = config.mutableStringValue(valueName, def) +fun Configurable.configBoolean(valueName: String? = null, def: Boolean? = null) = config.mutableBooleanValue(valueName, def) +fun Configurable.configTime(valueName: String? = null, def: Instant? = null) = config.mutableTimeValue(valueName, def) +fun Configurable.configNumber(valueName: String? = null, def: Number? = null) = config.mutableNumberValue(valueName, def) +fun Configurable.configDouble(valueName: String? = null, def: Double? = null) = config.mutableDoubleValue(valueName, def) +fun Configurable.configInt(valueName: String? = null, def: Int? = null) = config.mutableIntValue(valueName, def) +fun Configurable.customConfigValue(valueName: String? = null, def: T? = null, read: (Value) -> T, write: (T) -> Any) = + config.customMutableValue(valueName, def, read, write) + +inline fun > Configurable.configEnum(valueName: String? = null, def: T? = null) = config.mutableEnumValue(valueName, def) + +fun Configurable.configNode(nodeName: String? = null, def: Meta? = null) = config.mutableNode(nodeName, def) +fun Configurable.customConfigNode(nodeName: String? = null, def: T? = null, write: (T) -> Meta, read: (Meta) -> T) = + config.mutableCustomNode(nodeName, def, write, read) + +fun Configurable.morphConfigNode(type: KClass, nodeName: String? = null, def: T? = null) = config.mutableMorph(type, nodeName, def) +inline fun Configurable.morphConfigNode(nodeName: String? = null, def: T? = null) = config.mutableMorph(nodeName, def) + + +//ValueProvider delegates + +/** + * Delegate class for valueProvider + */ + + +//private class ValueProviderDelegate(private val valueName: String?, val conv: (Value) -> T) : ReadOnlyProperty { +// override operator fun getValue(thisRef: ValueProvider, property: KProperty<*>): T = +// conv(thisRef.getValue(valueName ?: property.name)) +//} +// +///** +// * Delegate ValueProvider element to read only property +// */ +//fun ValueProvider.valueDelegate(valueName: String? = null): ReadOnlyProperty = ValueProviderDelegate(valueName) { it } + +// +//fun ValueProvider.getString(valueName: String? = null): ReadOnlyProperty = ValueProviderDelegate(valueName) { it.getString() } +//fun ValueProvider.booleanValue(valueName: String? = null): ReadOnlyProperty = ValueProviderDelegate(valueName) { it.booleanValue() } +//fun ValueProvider.getTime(valueName: String? = null): ReadOnlyProperty = ValueProviderDelegate(valueName) { it.getTime() } +//fun ValueProvider.numberValue(valueName: String? = null): ReadOnlyProperty = ValueProviderDelegate(valueName) { it.numberValue() } +//fun ValueProvider.customValue(valueName: String? = null, conv: (Value) -> T): ReadOnlyProperty = ValueProviderDelegate(valueName, conv) + +//Meta provider delegate + +//private class MetaDelegate(private val metaName: String?) : ReadOnlyProperty { +// override operator fun getValue(thisRef: MetaProvider, property: KProperty<*>): Meta = +// thisRef.optMeta(metaName ?: property.name).orElse(null); +//} +// +//fun MetaProvider.metaNode(metaName: String? = null): ReadOnlyProperty = MetaDelegate(metaName) \ No newline at end of file diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/meta/MetaHolder.kt b/dataforge-core/src/main/kotlin/hep/dataforge/meta/MetaHolder.kt new file mode 100644 index 00000000..c19a2aac --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/meta/MetaHolder.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.meta + +import hep.dataforge.description.Described +import hep.dataforge.description.NodeDescriptor +import hep.dataforge.optional +import hep.dataforge.utils.Optionals +import hep.dataforge.values.Value +import hep.dataforge.values.ValueProvider +import java.util.* + +/** + * The base class for `Meta` objects with immutable meta which also + * implements ValueProvider and Described interfaces + * + * @author Alexander Nozik + */ +open class MetaHolder(override val meta: Meta) : Metoid, Described, ValueProvider { + + override val descriptor: NodeDescriptor by lazy { + super.descriptor + } + + /** + * If this object's meta provides given value, return it, otherwise, use + * descriptor + * + * @param path + * @return + */ + override fun optValue(path: String): Optional { + return Optionals + .either(meta.optValue(path)) + .or { descriptor.getValueDescriptor(path)?.default.optional } + .opt() + } + + /** + * true if this object's meta or description contains the value + * + * @param path + * @return + */ + override fun hasValue(path: String): Boolean { + return meta.hasValue(path) || descriptor.hasDefaultForValue(path) + } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/meta/MetaMorph.kt b/dataforge-core/src/main/kotlin/hep/dataforge/meta/MetaMorph.kt new file mode 100644 index 00000000..26b5c7e9 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/meta/MetaMorph.kt @@ -0,0 +1,160 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.meta + +import java.io.ObjectStreamException +import java.io.Serializable +import java.lang.annotation.Inherited +import kotlin.reflect.KClass +import kotlin.reflect.full.* +import kotlin.reflect.jvm.javaType + + +/** + * An object that could be identified by its meta. The contract is that two MetaID are equal if their {@code toMeta()} methods produce equal meta + * Created by darksnake on 17.06.2017. + */ +interface MetaID { + fun toMeta(): Meta +} + +@MustBeDocumented +@Inherited +annotation class MorphTarget(val target: KClass<*>) + +interface MorphProvider { + fun morph(meta: Meta): T +} + +/** + * An exception to be thrown when automatic class cast is failed + */ +class MorphException(val from: Class<*>, val to: Class<*>, message: String? = null, cause: Throwable? = null) : RuntimeException(message, cause) { + override val message: String = String.format("Meta morph from %s to %s failed", from, to) + super.message +} + +private class MetaMorphProxy(val type: Class<*>, val meta: Meta) : Serializable { + private fun readResolve(): Any { + return MetaMorph.morph(type, meta) + } +} + +/** + * Ab object that could be represented as meta. Serialized via meta serializer and deserialized back + * Created by darksnake on 12-Nov-16. + */ +interface MetaMorph : Serializable, MetaID { + + /** + * Convert this object to Meta + * + * @return + */ + override fun toMeta(): Meta + + companion object { + /** + * Create an instance of some class from its meta representation + */ + @Suppress("UNCHECKED_CAST") + fun morph(source: KClass, meta: Meta): T { + try { + //Using morphing redirect for default implementation + val type: KClass = source.findAnnotation()?.target as KClass? ?: source + + //trying to use constructor with single meta parameter + val constructor = type.constructors.find { it.parameters.size == 1 && it.parameters.first().type.javaType == Meta::class.java } + return when { + constructor != null -> constructor.call(meta) + type.companionObjectInstance is MorphProvider<*> -> (type.companionObjectInstance as MorphProvider).morph(meta) + else -> throw RuntimeException("An instance of class $source could not be morphed") + } + } catch (ex: Exception) { + throw MorphException(Meta::class.java, source.java, cause = ex) + } + } + + fun morph(source: Class, meta: Meta): T { + return morph(source.kotlin, meta) + } + } +} + +/** + * A simple metamorph implementation based on [MetaHolder]. + * It is supposed, that there is no state fields beside meta itself + * Created by darksnake on 20-Nov-16. + */ +open class SimpleMetaMorph(meta: Meta) : MetaHolder(meta), MetaMorph { + + override fun toMeta(): Meta { + return meta + } + + @Throws(ObjectStreamException::class) + fun writeReplace(): Any { + return MetaMorphProxy(this::class.java, toMeta()) + } + + override fun hashCode(): Int { + return meta.hashCode() + } + + override fun equals(other: Any?): Boolean { + return javaClass == other?.javaClass && (other as Metoid).meta == meta + } +} + +/** + * A specific case of metamorph that could be mutated. If input meta is configuration, changes it on mutation. + * Otherwise creates new configuration from meta + * On deserialization converts to immutable [SimpleMetaMorph] + */ +open class ConfigMorph(meta: Meta) : SimpleMetaMorph(meta), Configurable { + private val _config = (meta as? Configuration)?: Configuration(meta) + + override fun getConfig(): Configuration = _config +} + +/** + * Convert a meta to given MetaMorph type. It is preferable to directly call the MetaMorph constructor. + */ +inline fun Meta.morph(): T { + return MetaMorph.morph(T::class, this); +} + + +fun MetaMorph.morph(type: KClass): T { + return when { + type.isSuperclassOf(this::class) -> type.cast(this) + type.isSuperclassOf(Meta::class) -> type.cast(toMeta()) + type.isSubclassOf(MetaMorph::class) -> toMeta().morph(type) + else -> throw MorphException(javaClass, type.java) + } +} + +/** + * Converts MetaMorph to Meta or another metamorph using transformation to meta and back. + * If the conversion is failed, catch the exception and rethrow it as [MorphException] + * + * @param type + * @param + * @return + */ +inline fun MetaMorph.morph(): T { + return this.morph(T::class) +} \ No newline at end of file diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/meta/Metoid.kt b/dataforge-core/src/main/kotlin/hep/dataforge/meta/Metoid.kt new file mode 100644 index 00000000..334d2754 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/meta/Metoid.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.meta + +/** + * A general convention on object with meta-data + * + * @author Alexander Nozik + */ +interface Metoid { + + /** + * Get the meta-data for this object. By convention null is not allowed. If + * there is no meta-data, empty meta is returned. The name of returned meta + * is currently not restricted. + * + * @since 0.4.0 + * @return + */ + val meta: Meta +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/names/CompositeName.kt b/dataforge-core/src/main/kotlin/hep/dataforge/names/CompositeName.kt new file mode 100644 index 00000000..f334788b --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/names/CompositeName.kt @@ -0,0 +1,134 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.names + +import hep.dataforge.exceptions.NamingException +import java.util.* +import java.util.stream.Collectors +import kotlin.streams.toList + +/** + * The name path composed of tokens + * + * @author Alexander Nozik + */ +internal class CompositeName(private val names: LinkedList) : Name { + + override val first: Name + get() = names.first + + override val last: Name + get() = names.last + + override val query: String + get() = names.last.query + + override val length: Int + get() = names.size + + override fun isEmpty(): Boolean = false + + override val tokens: List + get() = Collections.unmodifiableList(names) + + override fun cutFirst(): Name { + when (length) { + 2 -> return names.last + 1 -> throw NamingException("Can not cut name token") + else -> { + val tokens = LinkedList(names) + tokens.removeFirst() + return CompositeName(tokens) + } + } + } + + override fun cutLast(): Name { + return when (length) { + 2 -> names.first + 1 -> throw NamingException("Can not cut name token") + else -> { + val tokens = LinkedList(names) + tokens.removeLast() + CompositeName(tokens) + } + } + } + + override fun hasQuery(): Boolean { + return names.last.hasQuery() + } + + override fun ignoreQuery(): Name { + //Replace last element if needed + if (hasQuery()) { + val tokens = LinkedList(names) + tokens.removeLast() + tokens.addLast(names.last.ignoreQuery()) + return CompositeName(tokens) + } else { + return this + } + } + + + override fun toString(): String { + val it = Iterable { names.stream().map { it.toString() }.iterator() } + return it.joinToString(Name.NAME_TOKEN_SEPARATOR) + } + + override fun asArray(): Array { + return names.stream().map { it.toString() }.toList().toTypedArray() + } + + override val unescaped: String + get() { + val it = Iterable { names.stream().map { it.unescaped }.iterator() } + return it.joinToString(Name.NAME_TOKEN_SEPARATOR) + } + + override fun entry(): String { + return first.entry() + } + + override fun hashCode(): Int { + var hash = 3 + hash = 19 * hash + Objects.hashCode(this.names) + return hash + } + + override fun equals(other: Any?): Boolean { + if (other == null) { + return false + } + if (javaClass != other.javaClass) { + return false + } + + return (other as? CompositeName)?.names == this.names + } + + companion object { + + fun of(tokens: List): CompositeName { + val list = tokens.stream() + .flatMap { it -> it.tokens.stream() } + .map { NameToken::class.java.cast(it) }.collect(Collectors.toCollection { LinkedList() }) + return CompositeName(list) + } + } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/names/EmptyName.kt b/dataforge-core/src/main/kotlin/hep/dataforge/names/EmptyName.kt new file mode 100644 index 00000000..cb57c849 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/names/EmptyName.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.names + +import hep.dataforge.exceptions.NamingException + +/** + * Created by darksnake on 26-Aug-16. + */ +internal class EmptyName : Name { + + override val query: String + get() = "" + + override val length: Int + get() = 0 + + override val first: Name + get() = this + + override val last: Name + get() = this + + override val tokens: List + get() = emptyList() + + override fun isEmpty(): Boolean = true + + override fun hasQuery(): Boolean { + return false + } + + override fun ignoreQuery(): Name { + return this + } + + override fun cutFirst(): Name { + throw NamingException("Can not cut name token") + } + + override fun cutLast(): Name { + throw NamingException("Can not cut name token") + } + + override fun entry(): String { + return "" + } + + + override fun asArray(): Array { + return arrayOf() + } + + override fun toString(): String { + return "" + } + + override val unescaped: String + get() { + return "" + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/names/Name.kt b/dataforge-core/src/main/kotlin/hep/dataforge/names/Name.kt new file mode 100644 index 00000000..475ac310 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/names/Name.kt @@ -0,0 +1,250 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.names + +import java.util.stream.Stream +import java.util.stream.StreamSupport +import kotlin.streams.toList + +/** + * + * The general interface for working with names. + * The name is a dot separated list of strings like `token1.token2.token3`. + * Each token could contain additional query in square brackets. Following symbols are prohibited in name tokens: `{}.:\`. + * Escaped dots (`\.`) are ignored. + * Square brackets are allowed only to designate queries. + * + * + * The [Name] is not connected with [javax.naming.Name] because DataForge does not need JNDI. No need to declare the dependency. + * + * + * @author Alexander Nozik + * @version $Id: $Id + */ +interface Name : Comparable { + + /** + * Query for last elements without brackets + * + * @return a [java.lang.String] object. + */ + val query: String + + /** + * The number of tokens in this name + * + * @return a int. + */ + val length: Int + + /** + * First token + * + * @return a [hep.dataforge.names.Name] object. + */ + val first: Name + + /** + * Last token + * + * @return a [hep.dataforge.names.Name] object. + */ + val last: Name + + /** + * Get the list of contained tokens + * + * @return + */ + val tokens: List + + /** + * Returns true only for EMPTY name + * + * @return + */ + fun isEmpty(): Boolean + + + /** + * The name as a String including query and escaping + * + * @return + */ + override fun toString(): String + + /** + * if has query for the last element + * + * @return a boolean. + */ + fun hasQuery(): Boolean + + /** + * This name without last element query. If there is no query, returns + * itself + * + * @return + */ + fun ignoreQuery(): Name + + /** + * The whole name but the first token + * + * @return a [hep.dataforge.names.Name] object. + */ + fun cutFirst(): Name + + /** + * The whole name but the lat token + * + * @return a [hep.dataforge.names.Name] object. + */ + fun cutLast(): Name + + + /** + * Return the leading name without query + * + * @return a [java.lang.String] object. + */ + fun entry(): String + + /** + * Create a new name with given name appended to the end of this one + * + * @param name + * @return + */ + @JvmDefault + operator fun plus(name: Name): Name { + return join(this, name) + } + + /** + * Append a name to the end of this name treating new name as a single name segment + * + * @param name + * @return + */ + @JvmDefault + operator fun plus(name: String): Name { + return join(this, ofSingle(name)) + } + + fun asArray(): Array + + @JvmDefault + fun equals(name: String): Boolean { + return this.toString() == name + } + + override fun compareTo(other: Name): Int { + return this.toString().compareTo(other.toString()) + } + + /** + * Convert to string without escaping separators + * + * @return + */ + val unescaped: String + + companion object { + + + const val NAME_TOKEN_SEPARATOR = "." + + val EMPTY: Name = EmptyName() + + /** + * + */ + fun empty(): Name { + return EMPTY + } + + fun of(name: String?): Name { + if (name == null || name.isEmpty()) { + return EMPTY + } + val tokens = name.split("(?{ NameToken(it) }.toList()) + } + } + + /** + * Build name from string ignoring name token separators and treating it as a single name token + * + * @param name + * @return + */ + fun ofSingle(name: String): Name { + return if (name.isEmpty()) { + EMPTY + } else { + NameToken(name) + } + } + + /** + * Join all segments in the given order. Segments could be composite. + * + * @param segments + * @return a [hep.dataforge.names.Name] object. + */ + fun join(vararg segments: String): Name { + if (segments.isEmpty()) { + return EMPTY + } else if (segments.size == 1) { + return of(segments[0]) + } + + return of(Stream.of(*segments).filter { it -> !it.isEmpty() }.map{ of(it) }.toList()) + } + + fun joinString(vararg segments: String): String { + return segments.joinToString(NAME_TOKEN_SEPARATOR) + } + + fun join(vararg segments: Name): Name { + if (segments.isEmpty()) { + return EMPTY + } else if (segments.size == 1) { + return segments[0] + } + + return of(Stream.of(*segments).filter { it -> !it.isEmpty() }.toList()) + } + + fun of(tokens: Iterable): Name { + return of(StreamSupport.stream(tokens.spliterator(), false) + .filter { str -> !str.isEmpty() } + .map{ NameToken(it) }.toList()) + } + + fun of(tokens: List): Name { + return when { + tokens.isEmpty() -> EMPTY + tokens.size == 1 -> tokens[0] + else -> CompositeName.of(tokens) + } + } + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/names/NameList.kt b/dataforge-core/src/main/kotlin/hep/dataforge/names/NameList.kt new file mode 100644 index 00000000..c813ea2f --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/names/NameList.kt @@ -0,0 +1,166 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.names + +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.meta.MetaMorph +import java.util.* +import java.util.stream.Stream +import kotlin.streams.toList + +/** + * The list of strings with cached index retrieval + * + * @author Alexander Nozik + */ +class NameList(list: Iterable) : MetaMorph, Iterable { + + private val nameList = ArrayList().apply { + //TODO check for duplicates + addAll(list) + } + + /** + * An index cache to make calls of `getNumberByName` faster + */ + @Transient + private val indexCache = HashMap() + + constructor(vararg list: String) : this(list.toList()) + + constructor(stream: Stream) : this(stream.toList()) + + constructor(meta: Meta) : this(*meta.getStringArray("names")) + + /** + * Checks if this Names contains all the names presented in the input array + * + * @param names + * @return true only if all names a presented in this Names. + */ + fun contains(vararg names: String): Boolean { + val list = asList() + var res = true + for (name in names) { + res = res && list.contains(name) + } + return res + } + + /** + * {@inheritDoc} + */ + operator fun contains(names: Iterable): Boolean { + val list = asList() + var res = true + for (name in names) { + res = res && list.contains(name) + } + return res + } + + /** + * {@inheritDoc} + */ + fun size(): Int { + return nameList.size + } + + /** + * {@inheritDoc} + */ + operator fun get(i: Int): String { + return this.nameList[i] + } + + /** + * Finds the number of the given name in list if numbering is supported + * + * @param str a [java.lang.String] object. + * @return a int. + * @throws hep.dataforge.exceptions.NameNotFoundException if any. + */ + fun getNumberByName(str: String): Int { + return indexCache.computeIfAbsent(str) { nameList.indexOf(it) } + } + + /** + * {@inheritDoc} + */ + override fun iterator(): Iterator { + return this.nameList.iterator() + } + + /** + * {@inheritDoc} + */ + fun asArray(): Array { + return nameList.toTypedArray() + } + + /** + * {@inheritDoc} + */ + fun asList(): List { + return Collections.unmodifiableList(this.nameList) + } + + fun stream(): Stream { + return this.nameList.stream() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + return nameList == (other as? NameList)?.nameList + } + + override fun hashCode(): Int { + return Objects.hash(nameList) + } + + override fun toString(): String { + return nameList.toString() + } + + /** + * Create new Names containing all the names in this, but for the strings in argument. The order of names is preserved + * + * @param minusNames + * @return + */ + fun minus(vararg minusNames: String): NameList { + val newNames = ArrayList(asList()) + newNames.removeAll(Arrays.asList(*minusNames)) + return NameList(newNames) + } + + /** + * Create new Names with additional names preserving order. + * + * @param plusNames + * @return + */ + internal fun plus(vararg plusNames: String): NameList { + val newNames = LinkedHashSet(asList()) + newNames.addAll(Arrays.asList(*plusNames)) + return NameList(newNames) + } + + override fun toMeta(): Meta { + return MetaBuilder("names").putValue("names", this.asList()) + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/names/NameToken.kt b/dataforge-core/src/main/kotlin/hep/dataforge/names/NameToken.kt new file mode 100644 index 00000000..bf542966 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/names/NameToken.kt @@ -0,0 +1,123 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.names + +import java.util.* + +/** + * Единичное Ð¸Ð¼Ñ Ñ Ð²Ð¾Ð·Ð¼Ð¾Ð¶Ð½Ñ‹Ð¼ запроÑом. Ðа данный момент проверки правильноÑти + * Ð·Ð°Ð´Ð°Ð½Ð¸Ñ Ð¸Ð¼ÐµÐ½Ð¸ при Ñоздании не производитÑÑ + * + * @author Alexander Nozik + */ +internal class NameToken(singlet: String) : Name { + + + private val theName: String + + private val theQuery: String? + + override val first: Name + get() = this + + override val last: Name + get() = this + + override val query: String + get() = theQuery ?: "" + + override val length: Int + get() = 1 + + override val tokens: List + get() = listOf(this) + + override fun isEmpty(): Boolean = false + + init { + //unescape string + val unescaped = singlet.replace("\\.", ".") + if (unescaped.matches(".*\\[.*]".toRegex())) { + val bracketIndex = unescaped.indexOf("[") + this.theName = unescaped.substring(0, bracketIndex) + this.theQuery = unescaped.substring(bracketIndex + 1, unescaped.lastIndexOf("]")) + } else { + this.theName = unescaped + this.theQuery = null + } + } + + override fun cutFirst(): Name { + return Name.EMPTY + } + + override fun cutLast(): Name { + return Name.EMPTY + } + + override fun hasQuery(): Boolean { + return theQuery != null + } + + override fun ignoreQuery(): NameToken { + return if (!hasQuery()) { + this + } else { + NameToken(theName) + } + } + + override fun toString(): String { + return unescaped.replace(".", "\\.") + } + + override fun entry(): String { + return theName + } + + + override fun asArray(): Array { + return arrayOf(unescaped) + } + + override fun hashCode(): Int { + var hash = 7 + hash = 79 * hash + Objects.hashCode(this.unescaped) + return hash + } + + override fun equals(other: Any?): Boolean { + if (other == null) { + return false + } + if (javaClass != other.javaClass) { + return false + } + return unescaped == (other as? NameToken)?.unescaped + } + + /** + * The full name including query but without escaping + */ + override val unescaped: String + get() { + return if (theQuery != null) { + String.format("%s[%s]", theName, theQuery) + } else { + theName + } + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/states/State.kt b/dataforge-core/src/main/kotlin/hep/dataforge/states/State.kt new file mode 100644 index 00000000..24565bb8 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/states/State.kt @@ -0,0 +1,397 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.states + +import hep.dataforge.Named +import hep.dataforge.description.* +import hep.dataforge.meta.* +import hep.dataforge.values.Value +import hep.dataforge.values.parseValue +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.BroadcastChannel +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.time.withTimeout +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.time.Duration +import java.time.Instant +import java.util.concurrent.atomic.AtomicReference +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KClass +import kotlin.reflect.KProperty + +/** + * A logical state possibly backed by physical state + */ +@Suppress("EXPERIMENTAL_API_USAGE") +sealed class State( + final override val name: String, + def: T? = null, + val owner: Stateful? = null, + private val scope: CoroutineScope = GlobalScope, + private val getter: (suspend () -> T)? = null, + private val setter: (suspend State.(T?, T) -> Unit)? = null) : Named, MetaID { + private var valid: Boolean = false + + val logger: Logger get() = owner?.logger ?: LoggerFactory.getLogger("state::$name") + + private val ref = AtomicReference() + val channel = BroadcastChannel(BUFFER_SIZE) + + init { + @Suppress("LeakingThis") + owner?.states?.init(this) + } + + /** + * Open subscription for updates of this state + */ + fun subscribe(): ReceiveChannel { + return channel.openSubscription() + } + + fun onChange(scope: CoroutineScope = GlobalScope, action: suspend (T) -> Unit) { + val subscription = subscribe() + scope.launch { + try { + while (true) { + action(subscription.receive()) + } + } catch (ex: CancellationException) { + subscription.cancel() + } + } + } + + init { + if (def != null) { + channel.offer(def) + ref.set(def) + valid = true + } + } + + /** + * Update the logical value without triggering the change of backing physical state + */ + private fun updateValue(value: T) { + ref.set(value) + //TODO evict on full + channel.offer(value) + valid = true + logger.debug("State {} changed to {}", name, value) + } + + protected abstract fun transform(value: Any): T + + /** + * Update state with any object automatically casting it to required type or throwing exception + */ + fun update(value: Any?) { + if (value == null) { + invalidate() + } else { + updateValue(transform(value)) + } + } + + /** + * If setter is provided, launch it asynchronously without changing logical state. + * If the setter produces non-null result, it is asynchronously updated logical value. + * Otherwise just change the logical state. + */ + fun set(value: Any?) { + if (value == null) { + invalidate() + } else { + val transformed = transform(value) + setter?.let { + scope.launch { + it.invoke(this@State, ref.get(), transformed) + } + } ?: update(value) + } + } + + /** + * Set the value and block calling thread until it is set or until timeout expires + */ + fun setValueAndWait(value: T, timeout: Duration? = null): T { + if (setter == null) { + update(value) + return value + } else { + val deferred = scope.async { + val subscription = subscribe() + setter.invoke(this@State, ref.get(), value) + return@async subscription.receive().also { subscription.cancel() } + } + + return runBlocking { + if (timeout == null) { + deferred.await() + } else { + withTimeout(timeout) { deferred.await() } + } + } + } + } + + fun setAndWait(value: Any?, timeout: Duration? = null): T { + return if (value == null) { + invalidate() + runBlocking { read(timeout) } + } else { + setValueAndWait(transform(value), timeout) + } + } + + /** + * Get current value or invoke getter if it is present. Getter is invoked in blocking mode. If state is invalid + */ + private fun get(): T { + return if (valid) { + ref.get() + } else { + runBlocking { read() } + } + } + + /** + * Invalidate current state value and force it to be re-aquired from physical state on next call. + * If getter is not defined, then subsequent calls will produce error. + */ + fun invalidate() { + valid = false + } + + /** + * read the state if the getter is available and update logical + */ + suspend fun read(): T { + if (getter == null) { + throw RuntimeException("The getter for state $name not defined") + } else { + val res = getter.invoke() + updateValue(res) + return res + } + } + + suspend fun read(timeout: Duration?): T { + return if (timeout == null) { + read() + } else { + withTimeout(timeout) { + read() + } + } + } + + fun readBlocking(): T { + return runBlocking { + read() + } + } + + /** + * Read == get() + * Write == set() + */ + var value: T + get() = get() + set(value) = set(value) + + val delegate = object : ReadWriteProperty { + override fun getValue(thisRef: Any?, property: KProperty<*>): T { + return value + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + this@State.value = value + } + } + + companion object { + const val BUFFER_SIZE = 100 + } +} + +private fun (suspend () -> Any).toValue(): (suspend () -> Value) { + return { Value.of(this.invoke()) } +} + +class ValueState( + name: String, + val descriptor: ValueDescriptor = ValueDescriptor.empty(name), + def: Value = Value.NULL, + owner: Stateful? = null, + getter: (suspend () -> Any)? = null, + setter: (suspend State.(Value?, Value) -> Unit)? = null +) : State(name, def, owner, getter = getter?.toValue(), setter = setter) { + + constructor( + def: ValueDef, + owner: Stateful? = null, + getter: (suspend () -> Any)? = null, + setter: (suspend State.(Value?, Value) -> Unit)? = null + ) : this(def.key, ValueDescriptor.build(def), def.def.parseValue(), owner, getter, setter) + + override fun transform(value: Any): Value { + return Value.of(value) + } + + override fun toMeta(): Meta { + return buildMeta("state", "name" to name, "value" to value) + } + + val booleanDelegate: ReadWriteProperty = object : ReadWriteProperty { + override fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean) { + set(value) + } + + override fun getValue(thisRef: Any?, property: KProperty<*>): Boolean { + return value.boolean + } + } + + val booleanValue + get() = value.boolean + + val stringDelegate: ReadWriteProperty = object : ReadWriteProperty { + override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { + set(value) + } + + override fun getValue(thisRef: Any?, property: KProperty<*>): String { + return value.string + } + } + + val stringValue: String + get() = value.string + + val timeDelegate: ReadWriteProperty = object : ReadWriteProperty { + override fun setValue(thisRef: Any?, property: KProperty<*>, value: Instant) { + set(value) + } + + override fun getValue(thisRef: Any?, property: KProperty<*>): Instant { + return value.time + } + } + + val timeValue: Instant + get() = value.time + + val intDelegate: ReadWriteProperty = object : ReadWriteProperty { + override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) { + set(value) + } + + override fun getValue(thisRef: Any?, property: KProperty<*>): Int { + return value.int + } + } + + val intValue + get() = value.int + + val doubleDelegate: ReadWriteProperty = object : ReadWriteProperty { + override fun setValue(thisRef: Any?, property: KProperty<*>, value: Double) { + set(value) + } + + override fun getValue(thisRef: Any?, property: KProperty<*>): Double { + return value.double + } + } + + val doubleValue + get() = value.double + + inline fun > enumDelegate(): ReadWriteProperty = object : ReadWriteProperty { + override fun getValue(thisRef: Any?, property: KProperty<*>): T { + return enumValueOf(value.string) + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + set(value.name) + } + } + + inline fun > enumValue(): T { + return enumValueOf(value.string) + } + +} + +fun ValueState(def: StateDef): ValueState { + return ValueState(def.value) +} + +class MetaState( + name: String, + val descriptor: NodeDescriptor = NodeDescriptor.empty(name), + def: Meta = Meta.empty(), + owner: Stateful? = null, + getter: (suspend () -> Meta)? = null, + setter: (suspend State.(Meta?, Meta) -> Unit)? = null +) : State(name, def, owner, getter = getter, setter = setter) { + + constructor( + def: NodeDef, + owner: Stateful? = null, + getter: (suspend () -> Meta)? = null, + setter: (suspend State.(Meta?, Meta) -> Unit)? = null + ) : this(def.key, Descriptors.forDef(def), Meta.empty(), owner, getter, setter) + + override fun transform(value: Any): Meta { + return (value as? MetaID)?.toMeta() + ?: throw RuntimeException("The state $name requires meta-convertible value, but found ${value::class}") + } + + override fun toMeta(): Meta { + return buildMeta("state", "name" to name) { + putNode("value", value) + } + } +} + +fun MetaState(def: MetaStateDef): MetaState { + return MetaState(def.value) +} + +class MorphState( + name: String, + val type: KClass, + def: T? = null, + owner: Stateful? = null, + getter: (suspend () -> T)? = null, + setter: (suspend State.(T?, T) -> Unit)? = null +) : State(name, def, owner, getter = getter, setter = setter) { + override fun transform(value: Any): T { + return (value as? MetaMorph)?.morph(type) + ?: throw RuntimeException("The state $name requires metamorph value, but found ${value::class}") + } + + override fun toMeta(): Meta { + return buildMeta("state", "name" to name) { + putNode("value", value) + } + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/states/StateAnnotations.kt b/dataforge-core/src/main/kotlin/hep/dataforge/states/StateAnnotations.kt new file mode 100644 index 00000000..3eff8fd4 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/states/StateAnnotations.kt @@ -0,0 +1,75 @@ +///* +// * Copyright 2018 Alexander Nozik. +// * +// * 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 hep.dataforge.states + +//import hep.dataforge.description.NodeDef +//import hep.dataforge.description.ValueDef +//import java.lang.annotation.Inherited +// +///** +// * The definition of state for a stateful object. +// * +// * @property value The definition for state value +// * @property readable This state could be read +// * @property writable This state could be written +// * @author Alexander Nozik +// */ +//@Target(AnnotationTarget.CLASS, AnnotationTarget.FILE) +//@MustBeDocumented +//@Inherited +//@Repeatable +//annotation class MetaStateDef( +// val value: NodeDef, +// val readable: Boolean = true, +// val writable: Boolean = false +//) +// +///** +// * +// * @author Alexander Nozik +// */ +//@Target(AnnotationTarget.CLASS, AnnotationTarget.FILE) +//@MustBeDocumented +//@Inherited +//annotation class MetaStateDefs(vararg val value: MetaStateDef) +// +///** +// * The definition of state for a stateful object. +// * +// * @property value The definition for state value +// * @property readable This state could be read +// * @property writable This state could be written +// * @author Alexander Nozik +// */ +//@Target(AnnotationTarget.CLASS, AnnotationTarget.FILE) +//@MustBeDocumented +//@Inherited +//@Repeatable +//annotation class StateDef( +// val value: ValueDef, +// val readable: Boolean = true, +// val writable: Boolean = false +//) +// +///** +// * +// * @author Alexander Nozik +// */ +//@Target(AnnotationTarget.CLASS, AnnotationTarget.FILE) +//@MustBeDocumented +//@Inherited +//annotation class StateDefs(vararg val value: StateDef) \ No newline at end of file diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/states/Stateful.kt b/dataforge-core/src/main/kotlin/hep/dataforge/states/Stateful.kt new file mode 100644 index 00000000..ddcf9040 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/states/Stateful.kt @@ -0,0 +1,221 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.states + +import hep.dataforge.exceptions.NameNotFoundException +import hep.dataforge.listAnnotations +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaMorph +import hep.dataforge.optional +import hep.dataforge.providers.Provider +import hep.dataforge.providers.Provides +import hep.dataforge.providers.ProvidesNames +import hep.dataforge.values.Value +import hep.dataforge.values.ValueProvider +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.selects.select +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.util.* +import java.util.stream.Stream +import kotlin.collections.HashMap +import kotlin.reflect.KClass + + +/** + * An object that could have a set of readonly or read/write states + */ +interface Stateful : Provider { + + val logger: Logger + val states: StateHolder + + @Provides(STATE_TARGET) + fun optState(stateName: String): State<*>? { + return states[stateName] + } + + @get:ProvidesNames(STATE_TARGET) + val stateNames: Collection + get() = states.names + + companion object { + const val STATE_TARGET = "state" + } +} + +/** + * Create a new meta state using class MetaState annotation if it is present and register it + */ +fun Stateful.metaState( + name: String, + owner: Stateful? = null, + getter: (suspend () -> Meta)? = null, + setter: (suspend State.(Meta?, Meta) -> Unit)? = null +): MetaState { + val def: MetaStateDef? = this::class.listAnnotations(true).find { it.value.key == name } + return if (def == null) { + MetaState(name = name, owner = this, getter = getter, setter = setter) + } else { + MetaState(def.value, this, getter, setter) + } +} + +fun Stateful.metaState( + name: String, + getter: (suspend () -> Meta)? = null, + setter: (suspend (Meta) -> Unit) +): MetaState { + return metaState(name, getter = getter, setter = { old, value -> if (old != value) setter.invoke(value) }) +} + +fun Stateful.valueState( + name: String, + getter: (suspend () -> Any)? = null, + setter: (suspend State.(Value?, Value) -> Unit)? = null +): ValueState { + val def: StateDef? = this::class.listAnnotations(true).find { it.value.key == name } + return if (def == null) { + ValueState(name = name, owner = this, getter = getter, setter = setter) + } else { + ValueState(def.value, this, getter, setter) + } +} + +/** + * Simplified version of value state generator, applies setter only if value is changed + */ +fun Stateful.valueState( + name: String, + getter: (suspend () -> Any)? = null, + setter: (suspend State.(Value) -> Unit) +): ValueState { + return valueState(name, getter = getter, setter = { old, value -> if (old != value) setter.invoke(this, value) }) +} + +fun Stateful.morphState( + name: String, + type: KClass, + def: T? = null, + getter: (suspend () -> T)? = null, + setter: (suspend State.(T?, T) -> Unit)? = null +): MorphState { + return MorphState(name, type, def, this, getter, setter) +} + +class StateHolder(val logger: Logger = LoggerFactory.getLogger(StateHolder::class.java)) : Provider, Iterable>, + ValueProvider, AutoCloseable { + private val stateMap: MutableMap> = HashMap() + + operator fun get(stateName: String): State<*>? { + return stateMap[stateName] + } + + /** + * Type checked version of the get method + */ + inline fun > getState(stateName: String): S? { + return get(stateName) as? S + } + + /** + * null invalidates the state + */ + operator fun set(stateName: String, value: Any?) { + stateMap[stateName]?.set(value) ?: throw NameNotFoundException(stateName) + } + + val names: Collection + get() = stateMap.keys + + fun stream(): Stream> { + return stateMap.values.stream() + } + + override fun iterator(): Iterator> { + return stateMap.values.iterator() + } + + /** + * Register a new state + */ + fun init(state: State<*>) { + this.stateMap[state.name] = state + } + + /** + * Reset state to its default value if it is present + */ + fun invalidate(stateName: String) { + stateMap[stateName]?.invalidate() + } + + /** + * Update logical state if it is changed. If argument is Meta or MetaMorph, then redirect to {@link updateLogicalMetaState} + * + * @param stateName + * @param stateValue + */ + fun update(stateName: String, stateValue: Any?) { + val state = stateMap.getOrPut(stateName) { + logger.warn("State with name $stateName is not registered. Creating new logical state") + when (stateValue) { + is Meta -> MetaState(stateName).also { init(it) } + is MetaMorph -> MorphState(stateName, (stateValue as MetaMorph)::class) + else -> ValueState(stateName).also { init(it) } + } + } + + state.update(stateValue) +// logger.info("State {} changed to {}", stateName, stateValue) + } + + override fun optValue(path: String): Optional { + return (get(path) as? ValueState)?.value.optional + } + + /** + * Subscribe on updates of specific states. By default subscribes on all updates. + * Subscription is formed when the method is called, so states initialized after that are ignored. + */ + fun changes(pattern: Regex = ".*".toRegex()): Flow> { + val subscriptions = stateMap.filter { it.key.matches(pattern) }.mapValues { it.value.subscribe() } + return flow { + try { + while (true) { + select { + subscriptions.forEach { key, value -> + value.onReceive { + emit(Pair(key, it)) + } + } + } + } + } catch (ex: CancellationException) { + subscriptions.values.forEach { + it.cancel() + } + } + } + } + + override fun close() { + + } +} \ No newline at end of file diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/tables/Column.kt b/dataforge-core/src/main/kotlin/hep/dataforge/tables/Column.kt new file mode 100644 index 00000000..0f9389b7 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/tables/Column.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.tables + +import hep.dataforge.Named +import hep.dataforge.values.Value + +import java.io.Serializable +import java.util.stream.Stream +import java.util.stream.StreamSupport + +/** + * Column of values with format meta + * + * @author Alexander Nozik + * @version $Id: $Id + */ + +interface Column : Named, Iterable, Serializable { + + val format: ColumnFormat + + @JvmDefault + override val name: String + get() = format.name + + /** + * Get the value with the given index + * @param n + * @return + */ + operator fun get(n: Int): Value + + //TODO add custom value type accessors + + /** + * Get values as list + * @return + */ + fun asList(): List + + /** + * The length of the column + * @return + */ + fun size(): Int + + /** + * Get the values as a stream + * @return + */ + @JvmDefault + fun stream(): Stream { + return StreamSupport.stream(spliterator(), false) + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/tables/ColumnFormat.kt b/dataforge-core/src/main/kotlin/hep/dataforge/tables/ColumnFormat.kt new file mode 100644 index 00000000..d0d2fb44 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/tables/ColumnFormat.kt @@ -0,0 +1,94 @@ +package hep.dataforge.tables + +import hep.dataforge.Named +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.meta.SimpleMetaMorph +import hep.dataforge.toList +import hep.dataforge.values.Value +import hep.dataforge.values.ValueType +import java.util.* +import java.util.stream.Stream + +/** + * Created by darksnake on 29-Dec-16. + */ +class ColumnFormat(meta: Meta) : SimpleMetaMorph(meta), Named { + + override val name: String + get() = getString("name") + + /** + * Return primary type. By default primary type is `STRING` + * + * @return + */ + val primaryType: ValueType + get() = ValueType.valueOf(getString("type", ValueType.STRING.name)) + + /** + * Get displayed title for this column. By default returns column name + * + * @return + */ + val title: String + get() = getString("title") { this.name } + + /** + * @return + */ + val tags: List + get() = Arrays.asList(*getStringArray(TAG_KEY)) + + /** + * Check if value is allowed by the format. It 'type' field of meta is empty then any type is allowed. + * + * @param value + * @return + */ + fun isAllowed(value: Value): Boolean { + //TODO add complex analysis here including enum-values + return !hasValue("type") || Arrays.asList(*getStringArray("type")).contains(value.type.name) + } + + companion object { + + const val TAG_KEY = "tag" + + // /** + // * mark column as optional so its value is replaced by {@code null} in table builder if it is not present + // */ + // public static final String OPTIONAL_TAG = "optional"; + + /** + * Construct simple column format + * + * @param name + * @param type + * @return + */ + fun build(name: String, type: ValueType, vararg tags: String): ColumnFormat { + return ColumnFormat(MetaBuilder("column") + .putValue("name", name) + .putValue("type", type) + .putValue(TAG_KEY, Stream.of(*tags).toList()) + ) + } + + /** + * Create a new format instance with changed name. Returns argument if name is not changed + * + * @param name + * @param columnFormat + * @return + */ + fun rename(name: String, columnFormat: ColumnFormat): ColumnFormat { + return if (name == columnFormat.name) { + columnFormat + } else { + ColumnFormat(columnFormat.toMeta().builder.setValue("name", name).build()) + } + } + } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/tables/ColumnTable.kt b/dataforge-core/src/main/kotlin/hep/dataforge/tables/ColumnTable.kt new file mode 100644 index 00000000..4df26427 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/tables/ColumnTable.kt @@ -0,0 +1,183 @@ +package hep.dataforge.tables + +import hep.dataforge.exceptions.NameNotFoundException +import hep.dataforge.meta.MetaMorph +import hep.dataforge.meta.MorphTarget +import hep.dataforge.values.* +import java.util.* +import java.util.stream.IntStream +import java.util.stream.Stream +import kotlin.streams.toList + +/** + * A column based table. Column access is fast, but row access is slow. Best memory efficiency. + * Column table is immutable all operations create new tables. + * Created by darksnake on 12.07.2017. + */ +@MorphTarget(target = ListTable::class) +class ColumnTable : Table { + + private val map = LinkedHashMap() + + override val columns: Collection + get() = map.values + + private val size: Int + + override val format: TableFormat + get() = TableFormat { columns.stream().map { it.format }} + + override val rows: Stream + get() = IntStream.range(0, size).mapToObj{ this.getRow(it) } + + /** + * Build a table from pre-constructed columns + * + * @param columns + */ + constructor(columns: Collection) { + columns.forEach { it -> map[it.name] = ListColumn.copy(it) } + if (map.values.stream().mapToInt{ it.size() }.distinct().count() != 1L) { + throw IllegalArgumentException("Column dimension mismatch") + } + size = map.values.stream().findFirst().map { it.size() }.orElse(0) + } + + /** + * Create empty column table + */ + constructor() { + size = 0 + } + + override fun getRow(i: Int): Values { + return ValueMap(columns.associate { it.name to get(it.name,i) }) + } + + override fun size(): Int { + return size + } + + override fun getColumn(name: String): Column { + return map[name]?: error("Column with name $name not found") + } + + override fun get(columnName: String, rowNumber: Int): Value { + return getColumn(columnName).get(rowNumber) + } + + override fun iterator(): Iterator { + return rows.iterator() + } + + + /** + * Add or replace column + * + * @param column + * @return + */ + fun addColumn(column: Column): ColumnTable { + val map = LinkedHashMap(map) + map[column.name] = column + return ColumnTable(map.values) + } + + /** + * Add a new column built from object stream + * + * @param name + * @param type + * @param data + * @param tags + * @return + */ + fun addColumn(name: String, type: ValueType, data: Stream<*>, vararg tags: String): ColumnTable { + val format = ColumnFormat.build(name, type, *tags) + val column = ListColumn(format, data.map { ValueFactory.of(it) }) + return addColumn(column) + } + + /** + * Create a new table with values derived from appropriate rows. The operation does not consume a lot of memory + * and time since existing columns are immutable and are reused. + * + * + * If column with given name exists, it is replaced. + * + * @param format + * @param transform + * @return + */ + fun buildColumn(format: ColumnFormat, transform: Values.() -> Any): ColumnTable { + val list = ArrayList(columns) + val newColumn = ListColumn.build(format, rows.map(transform)) + list.add(newColumn) + return ColumnTable(list) + } + + fun buildColumn(name: String, type: ValueType, transform: Values.() -> Any): ColumnTable { + val format = ColumnFormat.build(name, type) + return buildColumn(format, transform) + } + + /** + * Replace existing column with new values (without changing format) + * + * @param columnName + * @param transform + * @return + */ + fun replaceColumn(columnName: String, transform: (Values)->Any): ColumnTable { + if (!map.containsKey(columnName)) { + throw NameNotFoundException(columnName) + } + val newColumn = ListColumn.build(getColumn(columnName).format, rows.map(transform)) + map[columnName] = newColumn + return ColumnTable(map.values) + } + + /** + * Return a new Table with given columns being removed + * + * @param columnName + * @return + */ + fun removeColumn(vararg columnName: String): ColumnTable { + val map = LinkedHashMap(map) + for (c in columnName) { + map.remove(c) + } + return ColumnTable(map.values) + } + + override fun equals(other: Any?): Boolean { + return other != null && javaClass == other.javaClass && (other as MetaMorph).toMeta() == this.toMeta() + } + + companion object { + + + fun copy(table: Table): ColumnTable { + return ColumnTable(table.columns) + } + + /** + * Create instance of column table using given columns with appropriate names + * + * @param columns + * @return + */ + fun of(columns: Map): ColumnTable { + return ColumnTable( + columns.entries + .stream() + .map { entry -> ListColumn.copy(entry.key, entry.value) } + .toList() + ) + } + } +} + + +fun Table.asColumnTable(): ColumnTable = ColumnTable.copy(this) \ No newline at end of file diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/tables/ListColumn.kt b/dataforge-core/src/main/kotlin/hep/dataforge/tables/ListColumn.kt new file mode 100644 index 00000000..bd2656b6 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/tables/ListColumn.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.tables + +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.meta.MetaMorph +import hep.dataforge.values.Value +import hep.dataforge.values.ValueFactory +import java.util.* +import java.util.stream.Stream +import kotlin.streams.toList + +/** + * A simple immutable Column implementation using list of values + * + * @author Alexander Nozik + * @version $Id: $Id + */ +class ListColumn : Column, MetaMorph { + + override lateinit var format: ColumnFormat + private var values: List? = null + + constructor() {} + + internal constructor(meta: Meta) { + this.format = MetaMorph.morph(ColumnFormat::class.java, meta.getMeta("format")) + this.values = meta.getValue("data").list + } + + constructor(format: ColumnFormat, values: Stream) { + this.format = format + this.values = values.toList() + if (!this.values!!.stream().allMatch{ format.isAllowed(it) }) { + throw IllegalArgumentException("Not allowed value in the column") + } + } + + override fun asList(): List { + return Collections.unmodifiableList(values!!) + } + + /** + * {@inheritDoc} + */ + override fun get(n: Int): Value { + return values!![n] + } + + override fun stream(): Stream { + return values!!.stream() + } + + override fun iterator(): Iterator { + return values!!.iterator() + } + + override fun size(): Int { + return values!!.size + } + + override fun toMeta(): Meta { + return MetaBuilder("column") + .putNode("format", format.toMeta()) + .putValue("data", values) + } + + companion object { + + /** + * Create a copy of given column if it is not ListColumn. + * + * @param column + * @return + */ + fun copy(column: Column): ListColumn { + return column as? ListColumn ?: ListColumn(column.format, column.stream()) + } + + /** + * Create a copy of given column renaming it in process + * + * @param name + * @param column + * @return + */ + fun copy(name: String, column: Column): ListColumn { + return if (name == column.name) { + copy(column) + } else { + ListColumn(ColumnFormat.rename(name, column.format), column.stream()) + } + } + + fun build(format: ColumnFormat, values: Stream<*>): ListColumn { + return ListColumn(format, values.map{ ValueFactory.of(it) }) + } + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/tables/ListOfPoints.kt b/dataforge-core/src/main/kotlin/hep/dataforge/tables/ListOfPoints.kt new file mode 100644 index 00000000..991e141b --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/tables/ListOfPoints.kt @@ -0,0 +1,82 @@ +package hep.dataforge.tables + +import hep.dataforge.exceptions.NameNotFoundException +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.meta.MetaMorph +import hep.dataforge.values.Value +import hep.dataforge.values.ValueMap +import hep.dataforge.values.Values +import java.util.* + +/** + * Created by darksnake on 18-Apr-17. + */ +open class ListOfPoints(private val data: List) : MetaMorph, NavigableValuesSource { + + constructor(points: Iterable): this(points.toList()) + + constructor(meta: Meta): this(buildFromMeta(meta)) + + /** + * {@inheritDoc} + * + * @param i + * @return + */ + override fun getRow(i: Int): Values { + return data[i] + } + + /** + * {@inheritDoc} + */ + @Throws(NameNotFoundException::class) + override fun get(name: String, index: Int): Value { + return this.data[index].getValue(name) + } + + /** + * {@inheritDoc} + */ + override fun iterator(): MutableIterator { + return data.toMutableList().iterator() + } + + /** + * {@inheritDoc} + */ + override fun size(): Int { + return data.size + } + + + override fun toMeta(): Meta { + val dataNode = MetaBuilder("data") + forEach { dp -> dataNode.putNode("point", dp.toMeta()) } + return dataNode + } + + override fun equals(other: Any?): Boolean { + return other != null && javaClass == other.javaClass && (other as MetaMorph).toMeta() == this.toMeta() + } + + override fun hashCode(): Int { + var result = data.hashCode() + result = 31 * result + data.hashCode() + return result + } + + companion object { + + fun buildFromMeta(annotation: Meta): List { + val res = ArrayList() + for (pointMeta in annotation.getMetaList("point")) { + val map = HashMap() + pointMeta.valueNames.forEach { key -> map[key] = pointMeta.getValue(key) } + res.add(ValueMap(map)) + } + return res + } + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/tables/ListTable.kt b/dataforge-core/src/main/kotlin/hep/dataforge/tables/ListTable.kt new file mode 100644 index 00000000..fdc5c11c --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/tables/ListTable.kt @@ -0,0 +1,196 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.tables + +import hep.dataforge.exceptions.NameNotFoundException +import hep.dataforge.exceptions.NamingException +import hep.dataforge.meta.Meta +import hep.dataforge.values.* +import java.util.stream.Stream +import java.util.stream.StreamSupport +import kotlin.streams.toList + +/** + * An immutable row-based Table based on ArrayList. Row access is fast, but + * column access could be complicated + * + * @param format Формат опиÑывает набор полей, которые ОБЯЗÐТЕЛЬÐО приÑутÑтвуют в каждой + * точке из набора данных. Ðабор полей каждой точки может быть шире, но не + * уже. + * + * @param unsafe if `true`, skip format compatibility on the init. + * @author Alexander Nozik + */ +class ListTable @JvmOverloads constructor(override val format: TableFormat, points: List, unsafe: Boolean = false) : ListOfPoints(points), Table { + + + constructor(meta: Meta) : this( + MetaTableFormat(meta.getMeta("format")), + ListOfPoints.buildFromMeta(meta.getMeta("data")) + ) + + init { + if (!unsafe) { + points.forEach { + if (!it.names.contains(format.names)) { + throw NamingException("Row $it does not contain all off the fields declared in ${format.names}") + } + } + } + } + + /** + * {@inheritDoc} + * + * @param name + * @return + */ + @Throws(NameNotFoundException::class) + override fun getColumn(name: String): Column { + if (!this.format.names.contains(name)) { + throw NameNotFoundException(name) + } + return object : Column { + override val format: ColumnFormat = this@ListTable.format.getColumn(name) + + override fun get(n: Int): Value { + return asList()[n] + } + + override fun asList(): List { + return StreamSupport.stream(this.spliterator(), false).toList() + } + + override fun stream(): Stream { + return this@ListTable.rows.map { point -> point.getValue(name) } + } + + override fun iterator(): MutableIterator { + return stream().iterator() + } + + override fun size(): Int { + return this@ListTable.size() + } + } + } + + override val columns: Collection by lazy { + format.names.map { getColumn(it) } + } + + override fun get(name: String, index: Int): Value { + return getRow(index).getValue(name) + } + + override fun toMeta(): Meta { + return super.toMeta() + } + + class Builder(private var _format: TableFormat? = null) { + + private val points: MutableList = ArrayList() + + var format: TableFormat + get() = _format ?: throw RuntimeException("Format not defined") + set(value) { + _format = value + } + + constructor(format: Iterable) : this(MetaTableFormat.forNames(format)) + + constructor(vararg format: String) : this(MetaTableFormat.forNames(*format)) + + /** + * ЕÑли formatter == null, то могут быть любые точки + * + * @param e + * @throws hep.dataforge.exceptions.NamingException if any. + */ + fun row(e: Values): Builder { + if (_format == null) { + _format = MetaTableFormat.forValues(e) + } + points.add(e) + return this + } + + /** + * Add new point constructed from a list of objects using current table format + * + * @param values + * @return + * @throws NamingException + */ + @Throws(NamingException::class) + fun row(vararg values: Any): Builder { + return row(ValueMap.of(format.namesAsArray(), *values)) + } + + fun row(values: ValueProvider): Builder { + val names = format.namesAsArray() + val map = names.associateBy({ it }) { values.getValue(it) } + return row(ValueMap(map)) + } + + fun row(vararg values: NamedValue): Builder { + return row(ValueMap.of(*values)) + } + + fun row(vararg values: Pair): Builder { + return row(ValueMap.of(values.map { NamedValue.of(it.first, it.second) })) + } + + fun row(map: Map): Builder { + return row(ValueMap.ofMap(map)) + } + + fun rows(points: Iterable): Builder { + for (point in points) { + row(point) + } + return this + } + + fun rows(stream: Stream): Builder { + stream.forEach { this.row(it) } + return this + } + + fun build(): Table { + return ListTable(format, points) + } + + /** + * Build table without points name check + */ + fun buildUnsafe(): Table { + return ListTable(format, points, true) + } + } + + companion object { + + fun copy(table: Table): ListTable { + return table as? ListTable ?: ListTable(table.format, table.rows.toList()) + } + } + +} + +fun buildTable(format: TableFormat? = null, builder: ListTable.Builder.() -> Unit): Table { + return ListTable.Builder(format).apply(builder).build() +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/tables/MetaTableFormat.kt b/dataforge-core/src/main/kotlin/hep/dataforge/tables/MetaTableFormat.kt new file mode 100644 index 00000000..1845a75d --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/tables/MetaTableFormat.kt @@ -0,0 +1,111 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.tables + +import hep.dataforge.description.NodeDef +import hep.dataforge.description.NodeDefs +import hep.dataforge.exceptions.NameNotFoundException +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.meta.MetaNode.DEFAULT_META_NAME +import hep.dataforge.meta.MetaUtils +import hep.dataforge.meta.SimpleMetaMorph +import hep.dataforge.names.NameList +import hep.dataforge.values.Values +import java.util.stream.Stream + +/** + * A class for point set visualization + * + * @author Alexander Nozik + */ +@NodeDefs( + NodeDef(key = "column", multiple = true, required = true, info = "A column format", descriptor = "class::hep.dataforge.tables.ColumnFormat"), + NodeDef(key = "defaultColumn", info = "Default column format. Used when format for specific column is not given"), + NodeDef(key = DEFAULT_META_NAME, info = "Custom table information") +) +class MetaTableFormat(meta: Meta) : SimpleMetaMorph(meta), TableFormat { + //TODO add transformation to use short column description + + val isEmpty: Boolean + get() = !meta.hasMeta("column") + + private val _names: NameList by lazy { + NameList(columns.map { it.name }) + } + + override fun getNames(): NameList { + return _names + } + + private fun getColumnMeta(column: String): Meta { + return MetaUtils.findNodeByValue(meta, "column", "name", column).orElseThrow { NameNotFoundException(column) } + } + + override fun getColumn(column: String): ColumnFormat { + return ColumnFormat(getColumnMeta(column)) + } + + override fun getColumns(): Stream { + return meta.getMetaList("column").stream().map { ColumnFormat(it) } + } + + + override fun iterator(): MutableIterator { + return columns.iterator() + } + + override fun toMeta(): Meta { + return super.toMeta() + } + + companion object { + + /** + * An empty format holding information only about the names of columns + * + * @param names + * @return + */ + fun forNames(vararg names: String): TableFormat { + val builder = MetaBuilder("format") + for (n in names) { + builder.putNode(MetaBuilder("column").setValue("name", n)) + } + return MetaTableFormat(builder.build()) + } + + + fun forNames(names: Iterable): TableFormat { + return forNames(*names.toList().toTypedArray()) + } + + /** + * Build a table format using given data point as reference + * + * @param dataPoint + * @return + */ + fun forValues(dataPoint: Values): TableFormat { + val builder = MetaBuilder("format") + for (n in dataPoint.names) { + builder.putNode(MetaBuilder("column").setValue("name", n).setValue("type", dataPoint.getValue(n).type.name)) + } + return MetaTableFormat(builder.build()) + } + } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/tables/Table.kt b/dataforge-core/src/main/kotlin/hep/dataforge/tables/Table.kt new file mode 100644 index 00000000..bd3a8e46 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/tables/Table.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.tables + +import hep.dataforge.Type +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.meta.MetaMorph +import hep.dataforge.tables.Table.Companion.TABLE_TYPE + + +/** + * An immutable table of values + * + * @author Alexander Nozik + */ +@Type(TABLE_TYPE) +interface Table : NavigableValuesSource, MetaMorph { + + /** + * Get columns as a stream + * + * @return + */ + val columns: Collection + + /** + * A minimal set of fields to be displayed in this table. Could return empty format if source is unformatted + * + * @return + */ + val format: TableFormat + + /** + * Get an immutable column from this table + * + * @param name + * @return + */ + fun getColumn(name: String): Column + + + @JvmDefault + override fun toMeta(): Meta { + val res = MetaBuilder("table") + res.putNode("format", format.toMeta()) + val dataNode = MetaBuilder("data") + forEach { dp -> dataNode.putNode("point", dp.toMeta()) } + res.putNode(dataNode) + return res + } + + companion object { + const val TABLE_TYPE = "hep.dataforge.table" + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/tables/Tables.kt b/dataforge-core/src/main/kotlin/hep/dataforge/tables/Tables.kt new file mode 100644 index 00000000..93cb2b7d --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/tables/Tables.kt @@ -0,0 +1,219 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.tables + +import hep.dataforge.exceptions.NamingException +import hep.dataforge.nullable +import hep.dataforge.toList +import hep.dataforge.values.* +import java.util.function.Predicate +import java.util.stream.Stream + + +object Tables { + + @JvmStatic + fun sort(table: Table, comparator: java.util.Comparator): Table { + return ListTable(table.format, table.rows.sorted(comparator).toList()) + } + + @JvmStatic + fun sort(table: Table, name: String, ascending: Boolean): Table { + return sort( + table, + Comparator { o1: Values, o2: Values -> + val signum = if (ascending) +1 else -1 + o1.getValue(name).compareTo(o2.getValue(name)) * signum + } + ) + } + + /** + * Фильтрует набор данных и оÑтавлÑет только те точки, что удовлетоврÑÑŽÑ‚ + * уÑловиÑм + * + * @param condition a [java.util.function.Predicate] object. + * @return a [hep.dataforge.tables.Table] object. + * @throws hep.dataforge.exceptions.NamingException if any. + */ + @Throws(NamingException::class) + @JvmStatic + fun filter(table: Table, condition: Predicate): Table { + return ListTable(table.format, table.rows.filter(condition).toList()) + } + + /** + * БыÑтрый фильтр Ð´Ð»Ñ Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¸Ð¹ одного Ð¿Ð¾Ð»Ñ + * + * @param valueName + * @param a + * @param b + * @return + * @throws hep.dataforge.exceptions.NamingException + */ + @Throws(NamingException::class) + @JvmStatic + fun filter(table: Table, valueName: String, a: Value, b: Value): Table { + return filter(table, Filtering.getValueCondition(valueName, a, b)) + } + + @Throws(NamingException::class) + @JvmStatic + fun filter(table: Table, valueName: String, a: Number, b: Number): Table { + return filter(table, Filtering.getValueCondition(valueName, ValueFactory.of(a), ValueFactory.of(b))) + } + + /** + * БыÑтрый фильтр по меткам + * + * @param tags + * @return a [hep.dataforge.tables.Column] object. + * @throws hep.dataforge.exceptions.NamingException + * @throws hep.dataforge.exceptions.NameNotFoundException if any. + */ + @Throws(NamingException::class) + @JvmStatic + fun filter(table: Table, vararg tags: String): Table { + return filter(table, Filtering.getTagCondition(*tags)) + } + + + /** + * Create a ListTable for list of rows and infer format from the first row + */ + @JvmStatic + fun infer(points: List): ListTable { + if (points.isEmpty()) { + throw IllegalArgumentException("Can't create ListTable from the empty list. Format required.") + } + return ListTable(MetaTableFormat.forValues(points[0]), points) + } +} + +/** + * Extension methods for tables + */ + +/* Column operations */ + +/** + * Return a new table with additional column. + * Warning: if initial table is not a column table, then the whole amount of data will be copied, which could be ineffective for large tables + */ +operator fun Table.plus(column: Column): Table { + return ColumnTable.copy(this).addColumn(column) +} + +/** + * Warning: if initial table is not a column table, then the whole amount of data will be copied, which could be ineffective for large tables + */ +fun Table.addColumn(name: String, type: ValueType, data: Stream<*>, vararg tags: String): Table { + return ColumnTable.copy(this).addColumn(name, type, data, *tags) +} + +/** + * Warning: if initial table is not a column table, then the whole amount of data will be copied, which could be ineffective for large tables + */ +fun Table.addColumn(format: ColumnFormat, transform: Values.() -> Any): Table { + return ColumnTable.copy(this).buildColumn(format, transform) +} + +fun Table.addColumn(name: String, type: ValueType, transform: Values.() -> Any): Table = addColumn(ColumnFormat.build(name, type), transform) + +fun Table.replaceColumn(name: String, transform: Values.() -> Any): Table { + return ColumnTable.copy(this).replaceColumn(name, transform) +} + +/* Row filtering and sorting */ + +fun Table.filter(condition: (Values) -> Boolean): Table { + return ListTable(format, rows.filter(condition).toList()) +} + +fun Table.sort(comparator: Comparator): Table { + return ListTable(format, rows.sorted(comparator).toList()) +} + +fun Table.sort(name: String = format.first().name, ascending: Boolean = true): Table { + return sort( + Comparator { o1: Values, o2: Values -> + val signum = if (ascending) +1 else -1 + o1.getValue(name).compareTo(o2.getValue(name)) * signum + } + ) +} + + +/* Row reduction */ + +fun Table.reduceRows(format: TableFormat? = null, keySelector: (Values) -> K, mapper: (K, List) -> Values) = + ListTable(format ?: this.format, this.groupBy(keySelector).map { (key, value) -> mapper(key, value) }, false) + +/** + * A helper for table row reduction + * @param default the default reduction performed for columns that are not explicitly mentioned + */ +class RowReducer(val default: (Iterable) -> Value) { + private val reducers = HashMap) -> Value>() + + /** + * Add custom rule + */ + fun rule(key: String, reducer: (Iterable) -> Value) { + reducers[key] = reducer + } + + fun sumByDouble(key: String) = rule(key) { rows -> rows.sumByDouble { it.double }.asValue() } + fun sumByInt(key: String) = rule(key) { rows -> rows.sumBy { it.int }.asValue() } + + fun averageByDouble(key: String) = rule(key) { rows -> rows.map { it.double }.average().asValue() } + fun averageByInt(key: String) = rule(key) { rows -> rows.map { it.int }.average().asValue() } + + fun reduce(key: String, values: Iterable): Value { + return reducers.getOrDefault(key, default).invoke(values) + } + + /** + * Reduce list of rows to a single row + */ + fun reduce(keys: Iterable, rows: Iterable): Values { + val map = keys.associate { key -> + key to rows.map { it.optValue(key).nullable ?: Value.NULL } + }.mapValues { reduce(it.key, it.value) } + return ValueMap.ofMap(map) + } +} + +/** + * Rows are grouped by specific column value and step and then reduced by group. + * By default, uses averaging operation for key column and sum for others, but could be customized by [customizer]. + * Missing values are treated as zeroes + */ +fun Table.sumByStep(key: String, step: Double, customizer: (RowReducer) -> Unit = {}): Table { + assert(step > 0) { "Step must be positive" } + + val reducer = RowReducer { rows -> rows.sumByDouble { it.double }.asValue() }.apply { + averageByDouble(key) + }.apply(customizer) + + val rows = this.groupBy { + kotlin.math.ceil((it.optValue(key).nullable?.double ?: 0.0) / step) + }.map { (_, value) -> + reducer.reduce(format.names, value) + } + return ListTable(format, rows, true) +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/tables/ValuesParser.java b/dataforge-core/src/main/kotlin/hep/dataforge/tables/ValuesParser.java new file mode 100644 index 00000000..cbeae8b8 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/tables/ValuesParser.java @@ -0,0 +1,17 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.tables; + diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/tables/ValuesReader.kt b/dataforge-core/src/main/kotlin/hep/dataforge/tables/ValuesReader.kt new file mode 100644 index 00000000..4d3d22d3 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/tables/ValuesReader.kt @@ -0,0 +1,140 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.tables + +import hep.dataforge.io.LineIterator +import hep.dataforge.values.LateParseValue +import hep.dataforge.values.ValueMap +import hep.dataforge.values.Values +import java.io.InputStream + +/** + * + * ValuesParser interface. + * + * @author Alexander Nozik + * @version $Id: $Id + */ +interface ValuesParser { + /** + * + * parse. + * + * @param str a [java.lang.String] object. + * @return + */ + fun parse(str: String): Values +} + + +/** + * + * + * SimpleValuesParser class. + * + * @author Alexander Nozik + * @version $Id: $Id + */ +class SimpleValuesParser : ValuesParser { + + private val format: Array + + /** + * + * + * Constructor for SimpleDataParser. + * + * @param format an array of [java.lang.String] objects. + */ + constructor(format: Array) { + this.format = format + } + + /** + * Создаем парÑер по заголовной Ñтроке + * + * @param line a [java.lang.String] object. + */ + constructor(line: String) { + this.format = line.trim { it <= ' ' }.split("[^\\w']*".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + } + + constructor(format: TableFormat) { + this.format = format.namesAsArray() + } + + /** + * {@inheritDoc} + * + * @param str + */ + override fun parse(str: String): Values { + val strings = str.split("\\s".toRegex()) + return ValueMap((0 until format.size).associate { format[it] to LateParseValue(strings[it]) }) + } + +} + + +/** + * + * Считаем, что формат файла Ñледующий: Ñначала идут метаданные, потом данные + * + * @author Alexander Nozik + * @version $Id: $Id + */ +class ValuesReader(private val reader: Iterator, private val parser: ValuesParser) : Iterator { + + @Volatile + var pos = 0 + private set + + constructor(stream: InputStream, parser: ValuesParser) : this(LineIterator(stream), parser) + + constructor(stream: InputStream, names: Array) : this(LineIterator(stream), SimpleValuesParser(names)) + + constructor(reader: Iterator, names: Array) : this(reader, SimpleValuesParser(names)) + + constructor(reader: Iterator, headline: String) : this(reader, SimpleValuesParser(headline)) + + /** + * {@inheritDoc} + * + * @return + */ + override fun hasNext(): Boolean { + return reader.hasNext() + } + + /** + * {@inheritDoc} + * + * @return + */ + override fun next(): Values { + return parser.parse(reader.next()).also { + pos++ + } + } + + fun skip(n: Int) { + for (i in 0 until n) { + reader.next() + pos++ + } + } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/tables/ValuesSource.kt b/dataforge-core/src/main/kotlin/hep/dataforge/tables/ValuesSource.kt new file mode 100644 index 00000000..a0ad851f --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/tables/ValuesSource.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.tables + +import hep.dataforge.values.Values + +import java.util.stream.Stream +import java.util.stream.StreamSupport + +/** + * A finite or infinite source of DataPoints + * + * @author Alexander Nozik + */ +interface ValuesSource : Iterable { + + @JvmDefault + val rows: Stream + get() = StreamSupport.stream(this.spliterator(), false) + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/values/AbstractValue.kt b/dataforge-core/src/main/kotlin/hep/dataforge/values/AbstractValue.kt new file mode 100644 index 00000000..b331b1f9 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/values/AbstractValue.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.values + +import hep.dataforge.exceptions.ValueConversionException + +import java.time.Instant + +/** + * Created by darksnake on 05-Aug-16. + */ +abstract class AbstractValue : Value { + + /** + * Smart equality condition + * @param other + * @return + */ + override fun equals(other: Any?): Boolean { + //TODO add list values equality condition + return when (other) { + is Value -> { + try { + when (type) { + ValueType.BOOLEAN -> this.boolean == other.boolean + ValueType.TIME -> this.time == other.time + ValueType.STRING -> this.string == other.string + ValueType.NUMBER -> ValueUtils.NUMBER_COMPARATOR.compare(this.number, other.number) == 0 + ValueType.NULL -> other.type == ValueType.NULL + else -> + //unreachable statement, but using string comparison just to be sure + this.string == other.string + } + } catch (ex: ValueConversionException) { + false + } + } + + is Double -> this.double == other + is Int -> this.int == other + is Number -> ValueUtils.NUMBER_COMPARATOR.compare(this.number, other as Number?) == 0 + is String -> this.string == other + is Boolean -> this.boolean == other + is Instant -> this.time == other + null -> this.type == ValueType.NULL + else -> super.equals(other) + } + } + + /** + * Groovy smart cast support + * + * @param type + * @return + */ + fun asType(type: Class<*>): Any { + return when { + type.isAssignableFrom(String::class.java) -> this.string + type.isAssignableFrom(Double::class.java) -> this.double + type.isAssignableFrom(Int::class.java) -> this.int + type.isAssignableFrom(Number::class.java) -> this.number + type.isAssignableFrom(Boolean::class.java) -> this.boolean + type.isAssignableFrom(Instant::class.java) -> this.time + else -> type.cast(this) + } + } + + override fun toString(): String { + return string + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/values/BinaryValue.kt b/dataforge-core/src/main/kotlin/hep/dataforge/values/BinaryValue.kt new file mode 100644 index 00000000..aad4137b --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/values/BinaryValue.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.values + +import java.nio.ByteBuffer +import java.time.Instant + +class BinaryValue(override val value: ByteBuffer): AbstractValue(){ + override val number: Number + get() = TODO("not implemented") //To change initializer of created properties use File | Settings | File Templates. + override val boolean: Boolean + get() = TODO("not implemented") //To change initializer of created properties use File | Settings | File Templates. + override val time: Instant + get() = TODO("not implemented") //To change initializer of created properties use File | Settings | File Templates. + override val string: String + get() = TODO("not implemented") //To change initializer of created properties use File | Settings | File Templates. + + override val type: ValueType = ValueType.BINARY +} \ No newline at end of file diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/values/BooleanValue.kt b/dataforge-core/src/main/kotlin/hep/dataforge/values/BooleanValue.kt new file mode 100644 index 00000000..feea8831 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/values/BooleanValue.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.values + +import hep.dataforge.exceptions.ValueConversionException + +import java.time.Instant + +/** + * @author Alexander Nozik + */ +internal class BooleanValue private constructor(override val boolean: Boolean) : AbstractValue() { + + /** + * {@inheritDoc} + */ + override val number: Number + get() = if (boolean) { + 1 + } else { + 0 + } + + /** + * {@inheritDoc} + */ + override val string: String + get() = java.lang.Boolean.toString(boolean) + + /** + * {@inheritDoc} + */ + override val time: Instant + get() = throw ValueConversionException(this, ValueType.TIME) + + /** + * {@inheritDoc} + */ + override val type: ValueType + get() = ValueType.BOOLEAN + + /** + * {@inheritDoc} + */ + override fun hashCode(): Int { + var hash = 3 + hash = 11 * hash + if (this.boolean) 1 else 0 + return hash + } + + /** + * {@inheritDoc} + */ + override fun equals(other: Any?): Boolean { + return when (other) { + null -> false + is Value -> this.boolean == other.boolean + else -> false + } + } + + override val value: Any + get() { + return this.boolean + } + + companion object { + + var TRUE: Value = BooleanValue(true) + var FALSE: Value = BooleanValue(false) + + fun ofBoolean(b: Boolean): Value { + return if (b) { + TRUE + } else { + FALSE + } + } + + fun ofBoolean(b: String): Value { + return ofBoolean(java.lang.Boolean.parseBoolean(b)) + } + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/values/LateParseValue.kt b/dataforge-core/src/main/kotlin/hep/dataforge/values/LateParseValue.kt new file mode 100644 index 00000000..ddc64328 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/values/LateParseValue.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.values + +import java.time.Instant + +/** + * A value that delays its parsing allowing to parse meta from text much faster, since the parsing is the most expensive operation + */ +class LateParseValue(str: String) : AbstractValue() { + + private val _value: Value by lazy { str.parseValue() } + override val value: Any + get() = _value.value + + override val number: Number + get() = _value.number + override val boolean: Boolean + get() = _value.boolean + override val time: Instant + get() = _value.time + override val string: String + get() = _value.string + override val type: ValueType + get() = _value.type + +} \ No newline at end of file diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/values/LazyValue.kt b/dataforge-core/src/main/kotlin/hep/dataforge/values/LazyValue.kt new file mode 100644 index 00000000..4a0520ad --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/values/LazyValue.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.values + +import java.time.Instant + +/** + * A lazy calculated value defined by some supplier. The value is calculated + * only once on first call, after that it is stored and not recalculated. + * + * + * **WARNING** Since the value is calculated on demand it is not + * strictly immutable. Use it only then it is impossible to avoid or ensure that + * supplier does not depend on external state. + * + * + * @author Darksnake + */ +class LazyValue(override val type: ValueType, supplier: () -> Value) : AbstractValue() { + + override val value: Value by lazy(supplier) + + override val number: Number + get() = value.number + + override val boolean: Boolean + get() = value.boolean + + override val time: Instant + get() = value.time + + override val string: String + get() = value.string + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/values/ListValue.kt b/dataforge-core/src/main/kotlin/hep/dataforge/values/ListValue.kt new file mode 100644 index 00000000..2f4b62ee --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/values/ListValue.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.values + +import java.time.Instant +import java.util.* +import kotlin.collections.ArrayList + +/** + * A wrapper for value lists which could be used both as listValue and as value. + * When used as value only first element of listValue is used. If the listValue + * is empty, than ListValue is equivalent of Null value. + * + * @author Alexander Nozik + */ +class ListValue(values: Collection) : Value { + + private val values: List = ArrayList(values) + + override val number: Number + get() = if (values.isNotEmpty()) { + values[0].number + } else { + 0 + } + + override val boolean: Boolean + get() = values.isNotEmpty() && values[0].boolean + + override val time: Instant + get() = if (values.size > 0) { + values[0].time + } else { + Instant.ofEpochMilli(0) + } + + override val string: String + get() = if (values.isEmpty()) { + "" + } else if (values.size == 1) { + values[0].string + } else { + values.joinToString(prefix = "[", postfix = "]", separator = ", ") { it.string } + } + + override val type: ValueType + get() = if (values.isNotEmpty()) { + values[0].type + } else { + ValueType.NULL + } + + override val list: List + get() = this.values + + override val isList: Boolean + get() = true + + + override fun hashCode(): Int { + var hash = 3 + hash = 53 * hash + Objects.hashCode(this.values) + return hash + } + + override fun equals(other: Any?): Boolean { + if (other == null) { + return false + } + if (javaClass != other.javaClass) { + return false + } + return this.values == (other as ListValue).values + } + + override fun toString(): String { + return string + } + + override val value: Any + get() { + return Collections.unmodifiableList(this.values) + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/values/MapValueProvider.kt b/dataforge-core/src/main/kotlin/hep/dataforge/values/MapValueProvider.kt new file mode 100644 index 00000000..b27c9679 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/values/MapValueProvider.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.values + +import java.util.* + +/** + * Simple valuew provider based on map + * Created by darksnake on 16-Aug-16. + */ +class MapValueProvider(map: Map) : ValueProvider { + private val map: MutableMap + + init { + this.map = HashMap() + map.forEach { key, value -> this.map[key] = Value.of(value) } + } + + override fun hasValue(path: String): Boolean { + return map.containsKey(path) + } + + override fun optValue(path: String): Optional { + return Optional.ofNullable(map[path]) + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/values/NamedValue.kt b/dataforge-core/src/main/kotlin/hep/dataforge/values/NamedValue.kt new file mode 100644 index 00000000..24d82fc2 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/values/NamedValue.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.values + +import hep.dataforge.Named + +import java.time.Instant + +/** + * Content value + * + * @author Alexander Nozik + * @version $Id: $Id + */ +class NamedValue(override val name: String, val anonymous: Value) : Named, Value { + + /** + * {@inheritDoc} + */ + override val boolean: Boolean + get() = anonymous.boolean + + /** + * {@inheritDoc} + */ + override val number: Number + get() = anonymous.number + + /** + * {@inheritDoc} + */ + override val string: String + get() = anonymous.string + + /** + * {@inheritDoc} + * + * @return + */ + override val time: Instant + get() = anonymous.time + + /** + * {@inheritDoc} + * + * @return + */ + override val type: ValueType + get() = anonymous.type + + override val value: Any + get() { + return anonymous.value + } + + companion object { + + fun of(name: String, value: Any): NamedValue { + return NamedValue(name, Value.of(value)) + } + } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/values/NumberValue.kt b/dataforge-core/src/main/kotlin/hep/dataforge/values/NumberValue.kt new file mode 100644 index 00000000..563260a9 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/values/NumberValue.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.values + +import java.math.BigDecimal +import java.math.MathContext +import java.time.Instant +import java.util.* + +/** + * + * @author Alexander Nozik + */ +internal class NumberValue(override val number: Number) : AbstractValue() { + + /** + * {@inheritDoc} + */ + override val boolean: Boolean + get() = number.toDouble() > 0 + + /** + * {@inheritDoc} + */ + override val string: String + get() = number.toString() + + /** + * {@inheritDoc} + * + * Ð’Ñ€ÐµÐ¼Ñ Ð² СЕКУÐДÐÐ¥ + */ + override val time: Instant + get() = Instant.ofEpochMilli(number.toLong()) + + /** + * {@inheritDoc} + */ + override val type: ValueType + get() = ValueType.NUMBER + + /** + * {@inheritDoc} + */ + override fun hashCode(): Int { + var hash = 7 + //TODO evaluate infinities + hash = 59 * hash + Objects.hashCode(BigDecimal(this.number.toDouble(), MathContext.DECIMAL32)) + return hash + } + + /** + * {@inheritDoc} + */ + override fun equals(other: Any?): Boolean { + return when (other) { + null -> false + is Value -> ValueUtils.NUMBER_COMPARATOR.compare(this.number, other.number) == 0 + else -> false + } + } + + override val value: Any + get() = this.number +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/values/StringValue.kt b/dataforge-core/src/main/kotlin/hep/dataforge/values/StringValue.kt new file mode 100644 index 00000000..d61be1b9 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/values/StringValue.kt @@ -0,0 +1,143 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.values + +import hep.dataforge.exceptions.ValueConversionException +import java.text.NumberFormat +import java.text.ParseException +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneOffset +import java.time.format.DateTimeParseException +import java.util.* + +/** + * TODO заменить интерфейÑÑ‹ на иÑпользуемые в javax.jcr + * + * @author Alexander Nozik + */ +internal class StringValue +/** + * ЕÑли передаетÑÑ Ñтрока в кавычках, то кавычки откуÑываютÑÑ + * + * @param value a [String] object. + */ +(value: String) : AbstractValue() { + + /** + * {@inheritDoc} + * + * + * Ð’Ñегда возвращаем Ñтроковое значение Ñтроки в ковычках чтобы избежать + * проблем Ñ Ð¿Ñ€Ð¾Ð±ÐµÐ»Ð°Ð¼Ð¸ + */ + override val string: String + + // public StringValue(Boolean value) { + // this.value = value.toString(); + // } + + /** + * {@inheritDoc} + */ + override val boolean: Boolean + get() { + try { + return java.lang.Boolean.valueOf(string) + } catch (ex: NumberFormatException) { + throw ValueConversionException(this, ValueType.BOOLEAN) + } + + } + + /** + * {@inheritDoc} + */ + override val number: Number + get() { + try { + return NumberFormat.getInstance().parse(string) + } catch (ex: ParseException) { + throw ValueConversionException(this, ValueType.NUMBER) + } catch (ex: NumberFormatException) { + throw ValueConversionException(this, ValueType.NUMBER) + } + + } + + /** + * {@inheritDoc} + */ + override val time: Instant + get() { + try { + return if (string.endsWith("Z")) { + Instant.parse(string) + } else { + LocalDateTime.parse(string).toInstant(ZoneOffset.UTC) + } + } catch (ex: DateTimeParseException) { + throw ValueConversionException(this, ValueType.TIME) + } + + } + + /** + * {@inheritDoc} + */ + override val type: ValueType + get() = ValueType.STRING + + /** + * {@inheritDoc} + */ + override fun hashCode(): Int { + var hash = 3 + hash = 47 * hash + Objects.hashCode(this.string) + return hash + } + + /** + * {@inheritDoc} + */ + override fun equals(other: Any?): Boolean { + return when (other) { + null -> false + is Value -> this.string == other.string + else -> false + } + } + + init { + if (value.startsWith("\"") && value.endsWith("\"")) { + this.string = value.substring(1, value.length - 1) + } else { + this.string = value + } + } + + /** + * {@inheritDoc} + */ + override fun toString(): String { + return "\"" + string + "\"" + } + + override val value: Any + get() { + return this.string + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/values/SubstProvider.kt b/dataforge-core/src/main/kotlin/hep/dataforge/values/SubstProvider.kt new file mode 100644 index 00000000..ad522bd0 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/values/SubstProvider.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.values + +import java.util.regex.Pattern + +/** + * Value provider with substitutions + * + * @author Alexander Nozik + */ +abstract class SubstProvider : ValueProvider { + + /** + * {@inheritDoc} + * + * @param path + */ + override fun getValue(path: String): Value { + val value = getValueForName(path) + return if (value.type == ValueType.STRING && value.string.contains("$")) { + var valStr = value.string + val matcher = Pattern.compile("\\$\\{(?.*)}").matcher(valStr) + while (matcher.find()) { + valStr = valStr.replace(matcher.group(), evaluateSubst(matcher.group("sub"))) + } + valStr.parseValue() + } else { + value + } + } + + /** + * Provide the value for name, where name is taken literally + * + * @param name a [String] object. + * @return a [Value] object. + */ + protected abstract fun getValueForName(name: String): Value + + /** + * Evaluate substitution string for ${} query. + * + * TODO Ñделать что-то более умное вроде GString + * + * @param subst a [String] object. + * @return a [String] object. + */ + protected fun evaluateSubst(subst: String): String { + return getValueForName(subst).string + } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/values/TimeValue.kt b/dataforge-core/src/main/kotlin/hep/dataforge/values/TimeValue.kt new file mode 100644 index 00000000..2003cd20 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/values/TimeValue.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.values + +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneOffset +import java.util.* + +/** + * @author Alexander Nozik + */ +internal class TimeValue(override val time: Instant) : AbstractValue() { + + constructor(value: LocalDateTime) : this(value.toInstant(ZoneOffset.UTC)) + + /** + * {@inheritDoc} + */ + override val boolean: Boolean + get() = time.isAfter(Instant.MIN) + + /** + * {@inheritDoc} + */ + override val number: Number + get() = time.toEpochMilli() + + /** + * {@inheritDoc} + */ + override// return LocalDateTime.ofInstant(value, ZoneId.systemDefault()).toString(); + val string: String + get() = time.toString() + + /** + * {@inheritDoc} + */ + override val type: ValueType + get() = ValueType.TIME + + + /** + * {@inheritDoc} + */ + override fun hashCode(): Int { + var hash = 3 + hash = 89 * hash + Objects.hashCode(this.time) + return hash + } + + /** + * {@inheritDoc} + */ + override fun equals(other: Any?): Boolean { + return when (other) { + null -> false + is Value -> this.time == other.time + else -> false + } + } + + override val value: Any + get() { + return this.time + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/values/Value.kt b/dataforge-core/src/main/kotlin/hep/dataforge/values/Value.kt new file mode 100644 index 00000000..b219730f --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/values/Value.kt @@ -0,0 +1,314 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.values + +import hep.dataforge.names.AlphanumComparator +import hep.dataforge.utils.NamingUtils +import java.io.Serializable +import java.nio.ByteBuffer +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneOffset +import java.time.format.DateTimeParseException +import java.util.stream.Stream +import kotlin.streams.toList + +/** + * The list of supported Value types. + * + * @author Alexander Nozik + * @version $Id: $Id + */ +enum class ValueType { + BINARY, NUMBER, BOOLEAN, STRING, TIME, NULL +} + +/** + * An immutable wrapper class that can hold Numbers, Strings and Instant + * objects. The general contract for Value is that it is immutable, more + * specifically, it can't be changed after first call (it could be lazy + * calculated though) + * + * @author Alexander Nozik + * @version $Id: $Id + */ +interface Value : Serializable, Comparable { + + /** + * The number representation of this value + * + * @return a [Number] object. + */ + val number: Number + + /** + * Boolean representation of this Value + * + * @return a boolean. + */ + val boolean: Boolean + + @JvmDefault + val double: Double + get() = number.toDouble() + + @JvmDefault + val int: Int + get() = number.toInt() + + @JvmDefault + val long: Long + get() = number.toLong() + + /** + * Instant representation of this Value + * + * @return + */ + val time: Instant + + @JvmDefault + val binary: ByteBuffer + get() = ByteBuffer.wrap(string.toByteArray()) + + /** + * The String representation of this value + * + * @return a [String] object. + */ + val string: String + + val type: ValueType + + /** + * Return list of values representation of current value. If value is + * instance of ListValue, than the actual list is returned, otherwise + * immutable singleton list is returned. + * + * @return + */ + @JvmDefault + val list: List + get() = listOf(this) + + @JvmDefault + val isNull: Boolean + get() = this.type == ValueType.NULL + + /** + * True if it is a list value + * + * @return + */ + @JvmDefault + val isList: Boolean + get() = false + + /** + * Return underlining object. Used for dynamic calls mostly + */ + val value: Any + + @JvmDefault + override fun compareTo(other: Value): Int { + return when (type) { + ValueType.NUMBER -> ValueUtils.NUMBER_COMPARATOR.compare(number, other.number) + ValueType.BOOLEAN -> boolean.compareTo(other.boolean) + ValueType.STRING -> AlphanumComparator.INSTANCE.compare(this.string, other.string) + ValueType.TIME -> time.compareTo(other.time) + ValueType.NULL -> if (other.type == ValueType.NULL) 0 else -1 + ValueType.BINARY -> binary.compareTo(other.binary) + } + } + + companion object { + const val NULL_STRING = "@null" + + val NULL: Value = object : Value { + override val boolean: Boolean = false + override val double: Double = java.lang.Double.NaN + override val number: Number = 0 + override val time: Instant = Instant.MIN + override val string: String = "@null" + override val type: ValueType = ValueType.NULL + override val value: Any = double + } + + fun of(list: Array): Value { + return list.map(::of).asValue() + } + + fun of(list: Collection): Value { + return list.map(::of).asValue() + } + + /** + * Reflection based Value resolution + * + * @param obj a [Object] object. + * @return a [Value] object. + */ + fun of(value: Any?): Value { + return when (value) { + null -> Value.NULL + is Value -> value + is Number -> NumberValue(value) + is Instant -> TimeValue(value) + is LocalDateTime -> TimeValue(value) + is Boolean -> BooleanValue.ofBoolean(value) + is String -> StringValue(value) + is Collection -> Value.of(value) + is Stream<*> -> Value.of(value.toList()) + is Array<*> -> Value.of(value.map(::of)) + is Enum<*> -> StringValue(value.name) + else -> StringValue(value.toString()) + } + } + } +} + +/** + * Java compatibility layer + */ +object ValueFactory { + @JvmField + val NULL = Value.NULL + + @JvmStatic + fun of(value: Any?): Value = if (value is String) { + value.parseValue() + } else { + Value.of(value) + } + + @JvmStatic + @JvmOverloads + fun parse(value: String, lazy: Boolean = true): Value { + return if (lazy) { + LateParseValue(value) + } else { + value.parseValue() + } + } +} + + +fun String.asValue(): Value { + return StringValue(this) +} + +/** + * create a boolean Value + * + * @param b a boolean. + * @return a [Value] object. + */ +fun Boolean.asValue(): Value { + return BooleanValue.ofBoolean(this) +} + +fun Number.asValue(): Value { + return NumberValue(this) +} + +fun LocalDateTime.asValue(): Value { + return TimeValue(this) +} + +fun Instant.asValue(): Value { + return TimeValue(this) +} + +fun Iterable.asValue(): Value { + val list = this.toList() + return when (list.size) { + 0 -> Value.NULL + 1 -> list[0] + else -> ListValue(list) + } +} + +val Value?.nullableDouble: Double? + get() = this?.let { if (isNull) null else double } + + +//fun asValue(list: Collection): Value { +// return when { +// list.isEmpty() -> Value.NULL +// list.size == 1 -> asValue(list.first()) +// else -> ListValue(list.map { Value.of(it) }) +// } +//} + + +/** + * Create Value from String using closest match conversion + * + * @param str a [String] object. + * @return a [Value] object. + */ +fun String.parseValue(): Value { + + //Trying to get integer + if (isEmpty()) { + return Value.NULL + } + + //string constants + if (startsWith("\"") && endsWith("\"")) { + return StringValue(substring(1, length - 2)) + } + + try { + val `val` = Integer.parseInt(this) + return Value.of(`val`) + } catch (ignored: NumberFormatException) { + } + + //Trying to get double + try { + val `val` = java.lang.Double.parseDouble(this) + return Value.of(`val`) + } catch (ignored: NumberFormatException) { + } + + //Trying to get Instant + try { + val `val` = Instant.parse(this) + return Value.of(`val`) + } catch (ignored: DateTimeParseException) { + } + + //Trying to parse LocalDateTime + try { + val `val` = LocalDateTime.parse(this).toInstant(ZoneOffset.UTC) + return Value.of(`val`) + } catch (ignored: DateTimeParseException) { + } + + if ("true" == this || "false" == this) { + return BooleanValue.ofBoolean(this) + } + + if (startsWith("[") && endsWith("]")) { + //FIXME there will be a problem with nested lists because of splitting + val strings = NamingUtils.parseArray(this) + return Value.of(strings) + } + + //Give up and return a StringValue + return StringValue(this) +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/values/ValueMap.kt b/dataforge-core/src/main/kotlin/hep/dataforge/values/ValueMap.kt new file mode 100644 index 00000000..85600f09 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/values/ValueMap.kt @@ -0,0 +1,218 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.values + +import hep.dataforge.exceptions.NameNotFoundException +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.meta.MetaMorph +import hep.dataforge.names.NameList +import hep.dataforge.utils.GenericBuilder +import java.util.* + +/** + * A simple [Values] implementation using HashMap. + * + * @author Alexander Nozik + */ + +class ValueMap : Values, MetaMorph { + + + private val valueMap = LinkedHashMap() + + /** + * Serialization constructor + */ + constructor(meta: Meta) { + meta.valueNames.forEach { valName -> valueMap[valName] = meta.getValue(valName) } + } + + constructor(map: Map) { + this.valueMap.putAll(map) + } + + /** + * {@inheritDoc} + */ + override fun hasValue(path: String): Boolean { + return this.valueMap.containsKey(path) + } + + /** + * {@inheritDoc} + */ + override fun getNames(): NameList { + return NameList(this.valueMap.keys) + } + + /** + * {@inheritDoc} + */ + @Throws(NameNotFoundException::class) + override fun optValue(path: String): Optional { + return Optional.ofNullable(valueMap[path]) + } + + /** + * {@inheritDoc} + */ + override fun toString(): String { + val res = StringBuilder("[") + var flag = true + for (name in this.names) { + if (flag) { + flag = false + } else { + res.append(", ") + } + res.append(name).append(":").append(getValue(name).string) + } + return res.toString() + "]" + } + + fun builder(): Builder { + return Builder(LinkedHashMap(valueMap)) + } + + override fun toMeta(): Meta { + val builder = MetaBuilder("point") + for (name in namesAsArray()) { + builder.putValue(name, getValue(name)) + } + return builder.build() + } + + // @Override + // public Map asMap() { + // return Collections.unmodifiableMap(this.valueMap); + // } + + class Builder : GenericBuilder { + + private val valueMap = LinkedHashMap() + + constructor(dp: Values) { + for (name in dp.names) { + valueMap[name] = dp.getValue(name) + } + + } + + constructor(map: Map) { + valueMap.putAll(map) + } + + constructor() { + + } + + /** + * if value exists it is replaced + * + * @param name a [java.lang.String] object. + * @param value a [hep.dataforge.values.Value] object. + * @return a [ValueMap] object. + */ + fun putValue(name: String, value: Value = Value.NULL): Builder { + valueMap[name] = value + return this + } + + fun putValue(name: String, value: Any): Builder { + valueMap[name] = Value.of(value) + return this + } + + infix fun String.to(value: Any?) { + if (value == null) { + valueMap.remove(this) + } else { + putValue(this, value) + } + } + + /** + * Put the value at the beginning of the map + * + * @param name + * @param value + * @return + */ + fun putFirstValue(name: String, value: Any): Builder { + synchronized(valueMap) { + val newMap = LinkedHashMap() + newMap[name] = Value.of(value) + newMap.putAll(valueMap) + valueMap.clear() + valueMap.putAll(newMap) + return this + } + } + + fun addTag(tag: String): Builder { + return putValue(tag, true) + } + + override fun build(): ValueMap { + return ValueMap(valueMap) + } + + override fun self(): Builder { + return this + } + } + + companion object { + + @JvmStatic + fun ofMap(map: Map): ValueMap { + return ValueMap(map.mapValues { Value.of(it.value) }) + } + + @SafeVarargs + fun ofPairs(vararg pairs: Pair): ValueMap { + val builder = Builder() + for ((first, second) in pairs) { + builder.putValue(first, second) + } + return builder.build() + } + + @JvmStatic + fun of(values: Iterable): ValueMap { + return ValueMap(values.associateBy(keySelector = { it.name }, valueTransform = { it.anonymous })) + } + + @JvmStatic + fun of(vararg values: NamedValue): ValueMap { + return ValueMap(values.associateBy(keySelector = { it.name }, valueTransform = { it.anonymous })) + } + + @JvmStatic + fun of(list: Array, vararg values: Any): ValueMap { + if (list.size != values.size) { + throw IllegalArgumentException() + } + val valueMap = LinkedHashMap() + for (i in values.indices) { + val `val` = Value.of(values[i]) + valueMap[list[i]] = `val` + } + return ValueMap(valueMap) + } + } +} \ No newline at end of file diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/values/ValueProvider.kt b/dataforge-core/src/main/kotlin/hep/dataforge/values/ValueProvider.kt new file mode 100644 index 00000000..58d87dfa --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/values/ValueProvider.kt @@ -0,0 +1,165 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.values + +import hep.dataforge.exceptions.NameNotFoundException +import hep.dataforge.providers.Provides +import java.time.Instant +import java.util.* + +interface ValueProvider { + + @JvmDefault + fun hasValue(path: String): Boolean { + return optValue(path).isPresent + } + + @Provides(VALUE_TARGET) + fun optValue(path: String): Optional + + @JvmDefault + fun getValue(path: String): Value { + return optValue(path).orElseThrow { NameNotFoundException(path) } + } + + @Provides(BOOLEAN_TARGET) + @JvmDefault + fun optBoolean(name: String): Optional { + return optValue(name).map { it.boolean } + } + + @JvmDefault + fun getBoolean(name: String, def: Boolean): Boolean { + return optValue(name).map { it.boolean }.orElse(def) + } + + @JvmDefault + fun getBoolean(name: String, def: () -> Boolean): Boolean { + return optValue(name).map { it.boolean }.orElseGet(def) + } + + @JvmDefault + fun getBoolean(name: String): Boolean { + return getValue(name).boolean + } + + @Provides(NUMBER_TARGET) + @JvmDefault + fun optNumber(name: String): Optional { + return optValue(name).map { it.number } + } + + @JvmDefault + fun getDouble(name: String, def: Double): Double { + return optValue(name).map { it.double }.orElse(def) + } + + @JvmDefault + fun getDouble(name: String, def: () -> Double): Double { + return optValue(name).map { it.double }.orElseGet(def) + } + + @JvmDefault + fun getDouble(name: String): Double { + return getValue(name).double + } + + @JvmDefault + fun getInt(name: String, def: Int): Int { + return optValue(name).map { it.int }.orElse(def) + } + + @JvmDefault + fun getInt(name: String, def: () -> Int): Int { + return optValue(name).map { it.int }.orElseGet(def) + + } + + @JvmDefault + fun getInt(name: String): Int { + return getValue(name).int + } + + @JvmDefault + @Provides(STRING_TARGET) + fun optString(name: String): Optional { + return optValue(name).map { it.string } + } + + @JvmDefault + fun getString(name: String, def: String): String { + return optString(name).orElse(def) + } + + @JvmDefault + fun getString(name: String, def: () -> String): String { + return optString(name).orElseGet(def) + } + + @JvmDefault + fun getString(name: String): String { + return getValue(name).string + } + + @JvmDefault + fun getValue(name: String, def: Any): Value { + return optValue(name).orElse(Value.of(def)) + } + + @JvmDefault + fun getValue(name: String, def: () -> Value): Value { + return optValue(name).orElseGet(def) + } + + @Provides(TIME_TARGET) + @JvmDefault + fun optTime(name: String): Optional { + return optValue(name).map { it.time } + } + + @JvmDefault + fun getStringArray(name: String): Array { + val vals = getValue(name).list + return Array(vals.size) { vals[it].string } + } + + @JvmDefault + fun getStringArray(name: String, def: () -> Array): Array { + return if (this.hasValue(name)) { + getStringArray(name) + } else { + def() + } + } + + @JvmDefault + fun getStringArray(name: String, def: Array): Array { + return if (this.hasValue(name)) { + getStringArray(name) + } else { + def + } + } + + companion object { + + const val VALUE_TARGET = "value" + const val STRING_TARGET = "string" + const val NUMBER_TARGET = "number" + const val BOOLEAN_TARGET = "boolean" + const val TIME_TARGET = "time" + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/values/ValueUtils.kt b/dataforge-core/src/main/kotlin/hep/dataforge/values/ValueUtils.kt new file mode 100644 index 00000000..0dd97db0 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/values/ValueUtils.kt @@ -0,0 +1,354 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.values + +import hep.dataforge.io.IOUtils +import hep.dataforge.providers.Path +import hep.dataforge.providers.Provider +import hep.dataforge.toBigDecimal +import java.io.DataInput +import java.io.DataOutput +import java.io.IOException +import java.io.Serializable +import java.math.BigDecimal +import java.math.BigInteger +import java.nio.ByteBuffer +import java.time.Instant +import java.util.* + +data class ValueRange(override val start: Value, override val endInclusive: Value) : ClosedRange { + operator fun contains(any: Any): Boolean { + return contains(Value.of(any)) + } +} + +operator fun Value.rangeTo(other: Value): ValueRange = ValueRange(this, other) + +/** + * Read a string value as an enum + */ +inline fun > ValueProvider.getEnum(name: String): T { + return enumValueOf(getString(name)) +} + +/** + * Read a string value as an enum with default. If string field does not represent valid enum, an exception is thrown and default is ignored + */ +inline fun > ValueProvider.getEnum(name: String, default: T): T { + return optString(name).map { enumValueOf(it) }.orElse(default) +} + +/** + * Created by darksnake on 06-Aug-16. + */ +object ValueUtils { + + val NUMBER_COMPARATOR: Comparator = NumberComparator() + + + /** + * Build a meta provider from given general provider + * + * @param provider + * @return + */ + @JvmStatic + fun Provider.asValueProvider(): ValueProvider { + return this as? ValueProvider ?: object : ValueProvider { + override fun optValue(path: String): Optional { + return this@asValueProvider.provide(Path.of(path, ValueProvider.VALUE_TARGET)).map { Value::class.java.cast(it) } + } + } + } + + private class NumberComparator : Comparator, Serializable { + + override fun compare(x: Number, y: Number): Int { + val d1 = x.toDouble() + val d2 = y.toDouble() + return if ((d1 != 0.0 || d2 != 0.0) && Math.abs(d1 - d2) / Math.max(d1, d2) < RELATIVE_NUMERIC_PRECISION) { + 0 + } else if (isSpecial(x) || isSpecial(y)) { + java.lang.Double.compare(d1, d2) + } else { + toBigDecimal(x).compareTo(toBigDecimal(y)) + } + } + + companion object { + private const val RELATIVE_NUMERIC_PRECISION = 1e-5 + + private fun isSpecial(x: Number): Boolean { + val specialDouble = x is Double && (java.lang.Double.isNaN(x) || java.lang.Double.isInfinite(x)) + val specialFloat = x is Float && (java.lang.Float.isNaN(x) || java.lang.Float.isInfinite(x)) + return specialDouble || specialFloat + } + + private fun toBigDecimal(number: Number): BigDecimal { + if (number is BigDecimal) { + return number + } + if (number is BigInteger) { + return BigDecimal(number) + } + if (number is Byte || number is Short + || number is Int || number is Long) { + return BigDecimal(number.toLong()) + } + if (number is Float || number is Double) { + return BigDecimal(number.toDouble()) + } + + try { + return BigDecimal(number.toString()) + } catch (e: NumberFormatException) { + throw RuntimeException("The given number (\"" + number + + "\" of class " + number.javaClass.name + + ") does not have a parsable string representation", e) + } + + } + } + } + +} + +/** + * Fast and compact serialization for values + * + * @param oos + * @param value + * @throws IOException + */ +@Throws(IOException::class) +fun DataOutput.writeValue(value: Value) { + if (value.isList) { + writeByte('*'.toInt()) // List designation + writeShort(value.list.size) + for (subValue in value.list) { + writeValue(subValue) + } + } else { + when (value.type) { + ValueType.NULL -> writeChar('0'.toInt()) // null + ValueType.TIME -> { + writeByte('T'.toInt())//Instant + writeLong(value.time.epochSecond) + writeLong(value.time.nano.toLong()) + } + ValueType.STRING -> { + this.writeByte('S'.toInt())//String + IOUtils.writeString(this, value.string) + } + ValueType.NUMBER -> { + val num = value.number + when (num) { + is Double -> { + writeByte('D'.toInt()) // double + writeDouble(num.toDouble()) + } + is Int -> { + writeByte('I'.toInt()) // integer + writeInt(num.toInt()) + } + is Long -> { + writeByte('L'.toInt()) + writeLong(num.toLong()) + } + else -> { + writeByte('N'.toInt()) // BigDecimal + val decimal = num.toBigDecimal() + val bigInt = decimal.unscaledValue().toByteArray() + val scale = decimal.scale() + writeShort(bigInt.size) + write(bigInt) + writeInt(scale) + } + } + } + ValueType.BOOLEAN -> if (value.boolean) { + writeByte('+'.toInt()) //true + } else { + writeByte('-'.toInt()) // false + } + ValueType.BINARY -> { + val binary = value.binary + writeByte('X'.toInt()) + writeInt(binary.limit()) + write(binary.array()) + } + } + } +} + +/** + * Value deserialization + */ +fun DataInput.readValue(): Value { + val type = readByte().toChar() + when (type) { + '*' -> { + val listSize = readShort() + val valueList = ArrayList() + for (i in 0 until listSize) { + valueList.add(readValue()) + } + return Value.of(valueList) + } + '0' -> return Value.NULL + 'T' -> { + val time = Instant.ofEpochSecond(readLong(), readLong()) + return time.asValue() + } + 'S' -> return IOUtils.readString(this).asValue() + 'D' -> return readDouble().asValue() + 'I' -> return readInt().asValue() + 'L' -> return readLong().asValue() + 'N' -> { + val intSize = readShort() + val intBytes = ByteArray(intSize.toInt()) + readFully(intBytes) + val scale = readInt() + val bdc = BigDecimal(BigInteger(intBytes), scale) + return bdc.asValue() + } + 'X' -> { + val length = readInt() + val buffer = ByteArray(length) + readFully(buffer) + return BinaryValue(ByteBuffer.wrap(buffer)) + } + '+' -> return BooleanValue.TRUE + '-' -> return BooleanValue.FALSE + else -> throw RuntimeException("Wrong value serialization format. Designation $type is unexpected") + } +} + + +fun ByteBuffer.getValue(): Value { + val type = get().toChar() + when (type) { + '*' -> { + val listSize = getShort() + val valueList = ArrayList() + for (i in 0 until listSize) { + valueList.add(getValue()) + } + return Value.of(valueList) + } + '0' -> return Value.NULL + 'T' -> { + val time = Instant.ofEpochSecond(getLong(), getLong()) + return time.asValue() + } + 'S' -> { + val length = getInt() + val buffer = ByteArray(length) + get(buffer) + return String(buffer, Charsets.UTF_8).asValue() + } + 'D' -> return getDouble().asValue() + 'I' -> return getInt().asValue() + 'L' -> return getLong().asValue() + 'N' -> { + val intSize = getShort() + val intBytes = ByteArray(intSize.toInt()) + get() + val scale = getInt() + val bdc = BigDecimal(BigInteger(intBytes), scale) + return bdc.asValue() + } + 'X' -> { + val length = getInt() + val buffer = ByteArray(length) + get(buffer) + return BinaryValue(ByteBuffer.wrap(buffer)) + } + '+' -> return BooleanValue.TRUE + '-' -> return BooleanValue.FALSE + else -> throw RuntimeException("Wrong value serialization format. Designation $type is unexpected") + } +} + +fun ByteBuffer.putValue(value: Value) { + if (value.isList) { + put('*'.toByte()) // List designation + if (value.list.size > Short.MAX_VALUE) { + throw RuntimeException("The array values of size more than ${Short.MAX_VALUE} could not be serialized") + } + putShort(value.list.size.toShort()) + value.list.forEach { putValue(it) } + } else { + when (value.type) { + ValueType.NULL -> put('0'.toByte()) // null + ValueType.TIME -> { + put('T'.toByte())//Instant + putLong(value.time.epochSecond) + putLong(value.time.nano.toLong()) + } + ValueType.STRING -> { + put('S'.toByte())//String + if (value.string.length > Int.MAX_VALUE) { + throw RuntimeException("The string valuse of size more than ${Int.MAX_VALUE} could not be serialized") + } + put(value.string.toByteArray(Charsets.UTF_8)) + } + ValueType.NUMBER -> { + val num = value.number + when (num) { + is Double -> { + put('D'.toByte()) // double + putDouble(num.toDouble()) + } + is Int -> { + put('I'.toByte()) // integer + putInt(num.toInt()) + } + is Long -> { + put('L'.toByte()) + putLong(num.toLong()) + } + is BigDecimal -> { + put('N'.toByte()) // BigDecimal + val bigInt = num.unscaledValue().toByteArray() + val scale = num.scale() + if (bigInt.size > Short.MAX_VALUE) { + throw RuntimeException("Too large BigDecimal") + } + putShort(bigInt.size.toShort()) + put(bigInt) + putInt(scale) + } + else -> { + throw RuntimeException("Custom number serialization is not allowed. Yet") + } + } + } + ValueType.BOOLEAN -> if (value.boolean) { + put('+'.toByte()) //true + } else { + put('-'.toByte()) // false + } + ValueType.BINARY -> { + put('X'.toByte()) + val binary = value.binary + putInt(binary.limit()) + put(binary.array()) + } + } + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/values/Values.kt b/dataforge-core/src/main/kotlin/hep/dataforge/values/Values.kt new file mode 100644 index 00000000..1e86cb74 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/values/Values.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.values + +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.meta.MetaMorph +import hep.dataforge.names.NameSetContainer +import java.util.* + +/** + * A named set of values with fixed name list. + */ +interface Values : NameSetContainer, ValueProvider, MetaMorph, Iterable { + + /** + * Faster search for existing values + * + * @param path + * @return + */ + @JvmDefault + override fun hasValue(path: String): Boolean { + return this.names.contains(path) + } + + /** + * A convenient method to access value by its index. Has generally worse performance. + * + * @param num + * @return + */ + @JvmDefault + operator fun get(num: Int): Value { + return getValue(this.names.get(num)) + } + + @JvmDefault + operator fun get(key: String): Value { + return getValue(key) + } + + /** + * Convert a DataPoint to a Map. Order is not guaranteed + * @return + */ + @JvmDefault + fun asMap(): Map { + val res = HashMap() + for (field in this.names) { + res[field] = getValue(field) + } + return res + } + + @JvmDefault + override fun iterator(): Iterator { + return names.map { NamedValue(it, get(it)) }.iterator() + } + + /** + * Simple check for boolean tag + * + * @param name + * @return + */ + @JvmDefault + fun hasTag(name: String): Boolean { + return names.contains(name) && getValue(name).boolean + } + + @JvmDefault + override fun toMeta(): Meta { + val builder = MetaBuilder("point") + for (name in namesAsArray()) { + builder.putValue(name, getValue(name)) + } + return builder.build() + } +} + +fun Values.builder() = ValueMap.Builder(this) + +fun Values.edit(block: ValueMap.Builder.()->Unit) = builder().apply(block).build() \ No newline at end of file diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/workspace/AbstractWorkspace.kt b/dataforge-core/src/main/kotlin/hep/dataforge/workspace/AbstractWorkspace.kt new file mode 100644 index 00000000..a91fde86 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/workspace/AbstractWorkspace.kt @@ -0,0 +1,98 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.workspace + +import hep.dataforge.cache.CachePlugin +import hep.dataforge.context.Context +import hep.dataforge.data.DataNode +import hep.dataforge.meta.Laminate +import hep.dataforge.meta.Meta +import hep.dataforge.workspace.tasks.Task +import hep.dataforge.workspace.tasks.TaskModel + +/** + * @author Alexander Nozik + */ +abstract class AbstractWorkspace( + override val context: Context, + protected val taskMap: Map>, + protected val targetMap: Map +) : Workspace { + + + override fun optTask(taskName: String): Task<*>? { + return taskMap[taskName] + } + + override val tasks: Collection> + get() = taskMap.values + + override val targets: Collection + get() = targetMap.values + + /** + * Automatically constructs a laminate if `@parent` value if defined + * @param name + * @return + */ + override fun optTarget(name: String): Meta? { + val target = targetMap[name] + return if (target == null) { + null + } else { + if (target.hasValue(PARENT_TARGET_KEY)) { + Laminate(target, optTarget(target.getString(PARENT_TARGET_KEY)) ?: Meta.empty()) + } else { + target + } + } + } + + private val cache: CachePlugin by lazy { + context.getOrLoad(CachePlugin::class.java) + } + + private val cacheEnabled: Boolean + get() = context[CachePlugin::class.java] != null && context.getBoolean("cache.enabled", true) + + override fun runTask(model: TaskModel): DataNode<*> { + //Cache result if cache is available and caching is not blocked by task + val result = getTask(model.name).run(model) + return if (cacheEnabled && model.meta.getBoolean("cache.enabled", true)) { + cacheTaskResult(model, result) + } else { + result + } + } + + /** + * Put given data node into cache one by one + */ + private fun cacheTaskResult(model: TaskModel, node: DataNode): DataNode { + return cache.cacheNode(model.name, node) { model.getID(it) } + } + + override fun clean() { + logger.info("Cleaning up cache...") + invalidateCache() + } + + private fun invalidateCache() { + if (cacheEnabled) { + cache.invalidate() + } + } + + companion object { + + /** + * The key in the meta designating parent target. The resulting target is obtained by overlaying parent with this one + */ + const val PARENT_TARGET_KEY = "@parent" + + } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/workspace/ActiveWorkspace.kt b/dataforge-core/src/main/kotlin/hep/dataforge/workspace/ActiveWorkspace.kt new file mode 100644 index 00000000..0cf05052 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/workspace/ActiveWorkspace.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.workspace + +import hep.dataforge.context.Context +import hep.dataforge.data.Data +import hep.dataforge.data.DataNode +import hep.dataforge.meta.Meta +import hep.dataforge.utils.ReferenceRegistry +import hep.dataforge.workspace.tasks.Task + +typealias ActiveStateListener = () -> Unit + +class ActiveState(val producer: () -> DataNode) { + private val listeners = ReferenceRegistry() + + val data: DataNode + get() = producer.invoke() + + fun addListener(listener: ActiveStateListener) { + listeners.add(listener) + } + + fun removeListener(listener: ActiveStateListener) { + listeners.remove { listener } + } + +} + +class ActiveWorkspace( + context: Context, + taskMap: Map>, + targetMap: Map, + initialData: DataNode? = null +) : AbstractWorkspace(context, taskMap, targetMap) { + + override var data: DataNode = initialData ?: DataNode.empty() + set(value) { + field = value + dataChanged() + } + + private fun dataChanged() { + + } + + fun pushData(map: Map?>) { + data = data.edit().apply { + map.forEach { key, value -> + if (value == null) { + this.removeData(key) + } else { + this.putData(key, value, true) + } + } + }.build() + } + +} \ No newline at end of file diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/workspace/BasicWorkspace.kt b/dataforge-core/src/main/kotlin/hep/dataforge/workspace/BasicWorkspace.kt new file mode 100644 index 00000000..653b070b --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/workspace/BasicWorkspace.kt @@ -0,0 +1,88 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.workspace + +import hep.dataforge.context.Context +import hep.dataforge.context.Global +import hep.dataforge.data.Data +import hep.dataforge.data.DataNode +import hep.dataforge.data.DataNodeBuilder +import hep.dataforge.data.DataTree +import hep.dataforge.meta.Meta +import hep.dataforge.workspace.tasks.Task + +/** + * A basic workspace with fixed data + * + * @author Alexander Nozik + */ +class BasicWorkspace( + context: Context, + taskMap: Map>, + targetMap: Map, + override val data: DataNode<*> +) : AbstractWorkspace(context, taskMap, targetMap) { + + class Builder(override var context: Context = Global) : Workspace.Builder { + private var data: DataNodeBuilder = DataTree.edit(Any::class.java).apply { name = "data" } + + private val taskMap: MutableMap> = HashMap() + private val targetMap: MutableMap = HashMap() + + //internal var workspace = BasicWorkspace() + + override fun self(): Builder { + return this + } + + override fun data(key: String, data: Data): Builder { +// if (this.data.optNode(key) != null) { +// logger.warn("Overriding non-empty data during workspace data fill") +// } + this.data.putData(key, data) + return self() + } + + override fun data(key: String?, dataNode: DataNode): Builder { + if (key == null || key.isEmpty()) { + if (!data.isEmpty) { + logger.warn("Overriding non-empty root data node during workspace construction") + } + data = dataNode.edit() as DataNodeBuilder + } else { + data.putNode(key, dataNode) + } + return self() + } + + override fun task(task: Task<*>): Builder { + taskMap[task.name] = task + return self() + } + + override fun target(name: String, meta: Meta): Builder { + targetMap[name] = meta + return self() + } + + override fun build(): Workspace { + context.plugins.stream(true) + .flatMap { plugin -> plugin.provideAll(Task.TASK_TARGET, Task::class.java) } + .forEach { taskMap.putIfAbsent(it.name, it) } + return BasicWorkspace(context, taskMap, targetMap, data.build()) + } + + } + + companion object { + + fun builder(): Builder { + return Builder() + } + + } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/workspace/DynamicWorkspace.kt b/dataforge-core/src/main/kotlin/hep/dataforge/workspace/DynamicWorkspace.kt new file mode 100644 index 00000000..98486336 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/workspace/DynamicWorkspace.kt @@ -0,0 +1,80 @@ +package hep.dataforge.workspace + +import hep.dataforge.context.Context +import hep.dataforge.data.DataNode +import hep.dataforge.meta.Meta +import hep.dataforge.workspace.tasks.Task +import hep.dataforge.workspace.tasks.TaskModel + +/** + * A dynamic workspace can update workspace specification dynamically from external source. It fully delegates all tasks to loaded workspace. + * Loading new workspace during calculations do not affect current progress because backing workspace is not affected by update. + */ +abstract class DynamicWorkspace : Workspace { + + /** + * Check if backing workspace is loaded + * + * @return + */ + protected var isValid: Boolean = false + private set + + private lateinit var _workspace: Workspace + + /** + * Get backing workspace instance + * + * @return + */ + protected open val workspace: Workspace + get() { + if (!isValid) { + _workspace = buildWorkspace() + isValid = true + } + return _workspace + } + + override val data: DataNode<*> + get() = workspace.data + + override val tasks: Collection> + get() = workspace.tasks + + override val targets: Collection + get() = workspace.targets + + override val context: Context + get() = workspace.context + + /** + * Build new workspace instance + * + * @return + */ + protected abstract fun buildWorkspace(): Workspace + + /** + * Invalidate current backing workspace + */ + protected fun invalidate() { + isValid = false + } + + override fun optTask(taskName: String): Task<*>? { + return workspace.optTask(taskName) + } + + override fun optTarget(name: String): Meta? { + return workspace.optTarget(name) + } + + override fun clean() { + workspace.clean() + } + + override fun runTask(model: TaskModel): DataNode<*> { + return workspace.runTask(model) + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/workspace/FileBasedWorkspace.kt b/dataforge-core/src/main/kotlin/hep/dataforge/workspace/FileBasedWorkspace.kt new file mode 100644 index 00000000..4ddc61f6 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/workspace/FileBasedWorkspace.kt @@ -0,0 +1,87 @@ +package hep.dataforge.workspace + +import hep.dataforge.context.Context +import hep.dataforge.context.Global +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardWatchEventKinds +import java.nio.file.WatchKey +import java.security.MessageDigest + +/** + * Dynamic workspace that is parsed from file using external algorithm. Workspace is reloaded only if file is changed + */ +class FileBasedWorkspace(private val path: Path, private val parser: (Path) -> Workspace) : DynamicWorkspace(), AutoCloseable { + + private var watchJob: Job? = null + + private val fileMonitor: WatchKey by lazy { + val service = path.fileSystem.newWatchService() + path.parent.register(service, StandardWatchEventKinds.ENTRY_MODIFY) + } + + override fun buildWorkspace(): Workspace { + if (watchJob == null) { + watchJob = GlobalScope.launch { + while (true) { + fileMonitor.pollEvents().forEach { + if(it.context() == path) { + logger.info("Workspace configuration changed. Invalidating.") + invalidate() + } + } + fileMonitor.reset() + } + } + } + return parser(path) + } + + + private fun getCheckSum(): ByteArray { + try { + val md = MessageDigest.getInstance("MD5") + md.update(Files.readAllBytes(path)) + return md.digest() + } catch (ex: Exception) { + throw RuntimeException("Failed to generate file checksum", ex) + } + + } + + override fun close() { + fileMonitor.cancel() + watchJob?.cancel() + } + + companion object { + + /** + * Find appropriate parser and builder a workspace + * + * @param context a parent context for workspace. Workspace usually creates its own context. + * @param path path of the file to create workspace from + * @param transformation a finalization transformation applied to workspace after loading + * @return + */ + @JvmOverloads + @JvmStatic + fun build(context: Context, path: Path, transformation: (Workspace.Builder) -> Workspace = { it.build() }): FileBasedWorkspace { + val fileName = path.fileName.toString() + return context.serviceStream(WorkspaceParser::class.java) + .filter { parser -> parser.listExtensions().stream().anyMatch { fileName.endsWith(it) } } + .findFirst() + .map { parser -> + FileBasedWorkspace(path) { p -> transformation(parser.parse(context, p)) } + } + .orElseThrow { RuntimeException("Workspace parser for $path not found") } + } + + fun build(path: Path): Workspace { + return build(Global, path) { it.build() } + } + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/workspace/Workspace.kt b/dataforge-core/src/main/kotlin/hep/dataforge/workspace/Workspace.kt new file mode 100644 index 00000000..8e4186f9 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/workspace/Workspace.kt @@ -0,0 +1,319 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.workspace + +import hep.dataforge.context.Context +import hep.dataforge.context.ContextAware +import hep.dataforge.context.ContextBuilder +import hep.dataforge.context.Global +import hep.dataforge.data.* +import hep.dataforge.exceptions.NameNotFoundException +import hep.dataforge.meta.Laminate +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaNode.DEFAULT_META_NAME +import hep.dataforge.providers.Provider +import hep.dataforge.providers.Provides +import hep.dataforge.providers.ProvidesNames +import hep.dataforge.utils.GenericBuilder +import hep.dataforge.workspace.tasks.Task +import hep.dataforge.workspace.tasks.TaskModel + +/** + * A place to store tasks and their results + * + * @author Alexander Nozik + * @version $Id: $Id + */ +interface Workspace : ContextAware, Provider { + + /** + * Get the whole data tree + * + * @return + */ + val data: DataNode<*> + + /** + * The stream of available tasks + * + * @return + */ + @get:ProvidesNames(Task.TASK_TARGET) + val tasks: Collection> + + /** + * Get stream of meta objects stored in the Workspace. Not every target is valid for every task. + * + * @return + */ + @get:ProvidesNames(Meta.META_TARGET) + val targets: Collection + + // String DATA_STAGE_NAME = "@data"; + + /** + * Get specific static data. Null if no data with given name is found + * + * @param dataPath Fully qualified data name + * @return + */ + @JvmDefault + fun getData(dataPath: String): Data<*> { + return data.getData(dataPath) + } + + /** + * Opt task by name + * + * @param taskName + * @return + */ + @Provides(Task.TASK_TARGET) + fun optTask(taskName: String): Task<*>? + + /** + * Get task by name. Throw [hep.dataforge.exceptions.NameNotFoundException] if task with given name does not exist. + * + * @param taskName + * @return + */ + @JvmDefault + fun getTask(taskName: String): Task<*> { + return optTask(taskName) ?: throw NameNotFoundException(taskName) + } + + /** + * Check task dependencies and run it with given configuration or load + * result from cache if it is available + * + * @param taskName + * @param config + * @param overlay use given meta as overaly for existing meta with the same name + * @return + */ + @JvmDefault + fun runTask(taskName: String, config: Meta, overlay: Boolean): DataNode<*> { + val task = getTask(taskName) + val taskConfig = if (overlay && hasTarget(config.name)) { + Laminate(config, getTarget(config.name)) + } else { + config + } + val model = task.build(this, taskConfig) + return runTask(model) + } + + @JvmDefault + fun runTask(taskName: String, config: Meta): DataNode<*> { + return this.runTask(taskName, config, true) + } + + /** + * Use config root node name as task name + * + * @param config + * @return + */ + @JvmDefault + fun runTask(config: Meta): DataNode<*> { + return runTask(config.name, config) + } + + /** + * Run task using meta previously stored in workspace. + * + * @param taskName + * @param target + * @return + */ + @JvmDefault + fun runTask(taskName: String, target: String = taskName): DataNode<*> { + return runTask(taskName, optTarget(target) ?: Meta.empty()) + } + + /** + * Run task with given model. + * + * @param model + * @return + */ + @JvmDefault + fun runTask(model: TaskModel): DataNode<*> { + return this.getTask(model.name).run(model) + } + + /** + * Opt a predefined meta with given name + * + * @return + */ + @Provides(Meta.META_TARGET) + fun optTarget(name: String): Meta? + + /** + * Get a predefined meta with given name + * + * @param name + * @return + */ + @JvmDefault + fun getTarget(name: String): Meta { + return optTarget(name) ?: throw NameNotFoundException(name) + } + + /** + * Check if workspace contains given target + * + * @param name + * @return + */ + @JvmDefault + fun hasTarget(name: String): Boolean { + return optTarget(name) != null + } + + /** + * Clean up workspace. Invalidate caches etc. + */ + fun clean() + + interface Builder : GenericBuilder, ContextAware { + + override var context: Context + + @JvmDefault + fun loadFrom(meta: Meta): Workspace.Builder { + if (meta.hasValue("context")) { + context = Global.getContext(meta.getString("context")) + } + if (meta.hasMeta("data")) { + meta.getMetaList("data").forEach { dataMeta: Meta -> + val factory: DataFactory = if (dataMeta.hasValue("dataFactoryClass")) { + try { + Class.forName(dataMeta.getString("dataFactoryClass")).newInstance() as DataFactory<*> + } catch (ex: Exception) { + throw RuntimeException("Error while initializing data factory", ex) + } + } else { + FileDataFactory() + } + val key = dataMeta.getString("as", "") + data(key, factory.build(context, dataMeta)) + } + } + if (meta.hasMeta("target")) { + meta.getMetaList("target").forEach { configMeta: Meta -> + target(configMeta.getString("name"), + configMeta.getMeta(DEFAULT_META_NAME)) + } + } + + return self() + } + + fun data(key: String, data: Data): Workspace.Builder + + /** + * Load a data node to workspace data tree. + * + * @param key path to the new node in the data tree could be empty + * @param dataNode + * @return + */ + fun data(key: String?, dataNode: DataNode): Workspace.Builder + + + /** + * Load data using universal data loader + * + * @param place + * @param dataConfig + * @return + */ + @JvmDefault + fun data(place: String, dataConfig: Meta): Workspace.Builder { + return data(place, DataLoader.SMART.build(context, dataConfig)) + } + + /** + * Load a data node generated by given DataLoader + * + * @param place + * @param factory + * @param dataConfig + * @return + */ + @JvmDefault + fun data(place: String, factory: DataLoader, dataConfig: Meta): Workspace.Builder { + return data(place, factory.build(context, dataConfig)) + } + + /** + * Add static data to the workspace + * + * @param name + * @param `obj` + * @param meta + * @return + */ + @JvmDefault + fun staticData(name: String, obj: Any, meta: Meta): Workspace.Builder { + return data(name, Data.buildStatic(obj, meta)) + } + + @JvmDefault + fun staticData(name: String, obj: Any): Workspace.Builder { + return data(name, Data.buildStatic(obj)) + } + + @JvmDefault + fun fileData(place: String, filePath: String, meta: Meta): Workspace.Builder { + return data(place, DataUtils.readFile(context.getFile(filePath), meta)) + } + + @JvmDefault + fun fileData(dataName: String, filePath: String): Workspace.Builder { + return fileData(dataName, filePath, Meta.empty()) + } + + fun target(name: String, meta: Meta): Workspace.Builder + + @JvmDefault + fun target(meta: Meta): Workspace.Builder { + return target(meta.name, meta) + } + + fun task(task: Task<*>): Workspace.Builder + + @Throws(IllegalAccessException::class, InstantiationException::class) + @JvmDefault + fun task(type: Class>): Workspace.Builder { + return task(type.getConstructor().newInstance()) + } + } +} + +inline fun Workspace.Builder.data(block: DataNodeBuilder<*>.() -> Unit) { + data(null, DataTree(block)) +} + +fun Workspace.Builder.context(name: String = "WORKSPACE", builder: ContextBuilder.() -> Unit) { + context = ContextBuilder(name, context).apply(builder).build() +} + +fun Workspace(context: Context = Global, block: Workspace.Builder.() -> Unit): Workspace { + return BasicWorkspace.Builder(context).apply(block).build() +} \ No newline at end of file diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/workspace/WorkspaceParser.kt b/dataforge-core/src/main/kotlin/hep/dataforge/workspace/WorkspaceParser.kt new file mode 100644 index 00000000..5ff075ed --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/workspace/WorkspaceParser.kt @@ -0,0 +1,37 @@ +package hep.dataforge.workspace + +import hep.dataforge.context.Context + +import java.io.IOException +import java.io.Reader +import java.nio.file.Files +import java.nio.file.Path + +/** + * A parser for a workspace + */ +interface WorkspaceParser { + /** + * List all extensions managed by this parser + * + * @return + */ + fun listExtensions(): List + + /** + * Parse a file as a workspace + * + * @param path + * @return + */ + fun parse(parentContext: Context, path: Path): Workspace.Builder { + try { + return parse(parentContext, Files.newBufferedReader(path)) + } catch (e: IOException) { + throw RuntimeException(e) + } + + } + + fun parse(parentContext: Context, reader: Reader): Workspace.Builder +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/workspace/tasks/AbstractTask.kt b/dataforge-core/src/main/kotlin/hep/dataforge/workspace/tasks/AbstractTask.kt new file mode 100644 index 00000000..a1e70133 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/workspace/tasks/AbstractTask.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.workspace.tasks + +import hep.dataforge.data.DataNode +import hep.dataforge.data.DataNodeBuilder +import hep.dataforge.data.DataTree +import hep.dataforge.description.NodeDescriptor +import hep.dataforge.meta.Meta +import hep.dataforge.workspace.Workspace + + +/** + * @param type the upper boundary type of a node, returned by this task. + * @param descriptor the descriptor override for this task. If null, construct descriptor from annotations. + * Created by darksnake on 21-Aug-16. + */ +abstract class AbstractTask(override val type: Class, descriptor: NodeDescriptor? = null) : Task { + + override val descriptor = descriptor?:super.descriptor + + + protected open fun gather(model: TaskModel): DataNode { + val builder: DataNodeBuilder = DataTree.edit() + model.dependencies.forEach { dep -> + dep.apply(builder, model.workspace) + } + return builder.build() + } + + override fun run(model: TaskModel): DataNode { + //validate model + validate(model) + + // gather data + val input = gather(model) + + //execute + val output = run(model, input) + + //handle result + output.handle(model.context.dispatcher) { this.handle(it) } + + return output + } + + /** + * Result handler for the task + */ + protected open fun handle(output: DataNode) { + //do nothing + } + + protected abstract fun run(model: TaskModel, data: DataNode): DataNode + + /** + * Apply model transformation to include custom dependencies or change + * existing ones. + * + * @param model the model to be transformed + * @param meta the whole configuration (not only for this particular task) + */ + protected abstract fun buildModel(model: TaskModel.Builder, meta: Meta) + + /** + * Build new TaskModel and apply specific model transformation for this + * task. By default model uses the meta node with the same node as the name of the task. + * + * @param workspace + * @param taskConfig + * @return + */ + override fun build(workspace: Workspace, taskConfig: Meta): TaskModel { + val taskMeta = taskConfig.getMeta(name, taskConfig) + val builder = TaskModel.builder(workspace, name, taskMeta) + buildModel(builder, taskConfig) + return builder.build() + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/workspace/tasks/KTask.kt b/dataforge-core/src/main/kotlin/hep/dataforge/workspace/tasks/KTask.kt new file mode 100644 index 00000000..49181233 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/workspace/tasks/KTask.kt @@ -0,0 +1,198 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.workspace.tasks + +import hep.dataforge.actions.* +import hep.dataforge.data.DataNode +import hep.dataforge.data.DataNodeBuilder +import hep.dataforge.data.DataTree +import hep.dataforge.description.DescriptorBuilder +import hep.dataforge.description.NodeDescriptor +import hep.dataforge.meta.Meta +import hep.dataforge.names.Name +import kotlin.reflect.KClass + +class KTask( + override val name: String, + type: KClass, + descriptor: NodeDescriptor? = null, + private val modelTransform: TaskModel.Builder.(Meta) -> Unit, + private val dataTransform: TaskModel.(DataNode) -> DataNode +) : AbstractTask(type.java, descriptor) { + + override fun run(model: TaskModel, data: DataNode): DataNode { + model.context.logger.info("Starting task '$name' on data node ${data.name} with meta: \n${model.meta}") + return dataTransform.invoke(model, data); + } + + override fun buildModel(model: TaskModel.Builder, meta: Meta) { + modelTransform.invoke(model, meta); + } + + //TODO add validation +} + +class KTaskBuilder(val name: String) { + private var modelTransform: TaskModel.Builder.(Meta) -> Unit = { data("*") } + var descriptor: NodeDescriptor? = null + + private class DataTransformation( + val from: String = "", + val to: String = "", + val transform: TaskModel.(DataNode) -> DataNode + ) { + operator fun invoke(model: TaskModel, node: DataNode): DataNode { + val localData = if (from.isEmpty()) { + node + } else { + node.getNode(from) + } + return transform.invoke(model, localData); + } + } + + private val dataTransforms: MutableList = ArrayList(); + + fun model(modelTransform: TaskModel.Builder.(Meta) -> Unit) { + this.modelTransform = modelTransform + } + + fun transform(inputType: Class, from: String = "", to: String = "", transform: TaskModel.(DataNode) -> DataNode) { + dataTransforms += DataTransformation(from, to) { data: DataNode -> + transform.invoke(this, data.checked(inputType)) + } + } + + inline fun transform(from: String = "", to: String = "", noinline transform: TaskModel.(DataNode) -> DataNode) { + transform(T::class.java, from, to, transform) + } + + /** + * Perform given action on data elements in `from` node in input and put the result to `to` node + */ + inline fun action(action: Action, from: String = "", to: String = "") { + transform(from, to){ data: DataNode -> + action.run(context, data, meta) + } + } + + inline fun pipeAction( + actionName: String = "pipe", + from: String = "", + to: String = "", + noinline action: PipeBuilder.() -> Unit) { + val pipe: Action = KPipe( + actionName = Name.joinString(name, actionName), + inputType = T::class.java, + outputType = R::class.java, + action = action + ) + action(pipe, from, to); + } + + inline fun pipe( + actionName: String = "pipe", + from: String = "", + to: String = "", + noinline action: suspend ActionEnv.(T) -> R) { + val pipe: Action = KPipe( + actionName = Name.joinString(name, actionName), + inputType = T::class.java, + outputType = R::class.java, + action = { result(action) } + ) + action(pipe, from, to); + } + + + inline fun joinAction( + actionName: String = "join", + from: String = "", + to: String = "", + noinline action: JoinGroupBuilder.() -> Unit) { + val join: Action = KJoin( + actionName = Name.joinString(name, actionName), + inputType = T::class.java, + outputType = R::class.java, + action = action + ) + action(join, from, to); + } + + inline fun join( + actionName: String = name, + from: String = "", + to: String = "", + noinline action: suspend ActionEnv.(Map) -> R) { + val join: Action = KJoin( + actionName = Name.joinString(name, actionName), + inputType = T::class.java, + outputType = R::class.java, + action = { + result(meta.getString("@target", actionName), action) + } + ) + action(join, from, to); + } + + inline fun splitAction( + actionName: String = "split", + from: String = "", + to: String = "", + noinline action: SplitBuilder.() -> Unit) { + val split: Action = KSplit( + actionName = Name.joinString(name, actionName), + inputType = T::class.java, + outputType = R::class.java, + action = action + ) + action(split, from, to); + } + + /** + * Use DSL to create a descriptor for this task + */ + fun descriptor(transform: DescriptorBuilder.() -> Unit) { + this.descriptor = DescriptorBuilder(name).apply(transform).build() + } + + fun build(): KTask { + val transform: TaskModel.(DataNode) -> DataNode = { data -> + if (dataTransforms.isEmpty()) { + //return data node as is + logger.warn("No transformation present, returning input data") + data.checked(Any::class.java) + } else { + val builder: DataNodeBuilder = DataTree.edit(Any::class.java) + dataTransforms.forEach { + val res = it(this, data) + if (it.to.isEmpty()) { + builder.update(res) + } else { + builder.putNode(it.to, res) + } + } + builder.build() + } + } + return KTask(name, Any::class, descriptor, modelTransform, transform); + } +} + +fun task(name: String, builder: KTaskBuilder.() -> Unit): KTask { + return KTaskBuilder(name).apply(builder).build(); +} \ No newline at end of file diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/workspace/tasks/MultiStageTask.kt b/dataforge-core/src/main/kotlin/hep/dataforge/workspace/tasks/MultiStageTask.kt new file mode 100644 index 00000000..ada79721 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/workspace/tasks/MultiStageTask.kt @@ -0,0 +1,142 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.workspace.tasks + +import hep.dataforge.data.DataNode +import java.util.* + +/** + * A generic implementation of task with 4 phases: + * + * * gathering + * * transformation + * * reporting + * * result generation + * + * + * @author Alexander Nozik + */ +abstract class MultiStageTask(type: Class) : AbstractTask(type) { + + override fun run(model: TaskModel, data: DataNode): DataNode { + val state = MultiStageTaskState(data) + val logger = model.logger + // Work work = getWork(model, data.getName()); + + logger.debug("Starting transformation phase") + // work.setStatus("Data transformation..."); + transform(model, state) + if (!state.isFinished) { + logger.warn("Task state is not finalized. Using last applied state as a result") + state.finish() + } + logger.debug("Starting result phase") + + // work.setStatus("Task result generation..."); + + return result(model, state) + } + + /** + * The main task body + * + * @param model + * @param state + */ + protected abstract fun transform(model: TaskModel, state: MultiStageTaskState): MultiStageTaskState + + /** + * Generating finish and storing it in workspace. + * + * @param state + * @return + */ + protected fun result(model: TaskModel, state: MultiStageTaskState): DataNode { + return state.getResult()!!.checked(type) + } + + /** + * The mutable data content of a task. + * + * @author Alexander Nozik + */ + protected class MultiStageTaskState { + + /** + * list of stages results + */ + private val stages = LinkedHashMap>() + internal var isFinished = false + /** + * final finish of task + */ + private var result: DataNode<*>? = null + + /** + * Return initial data + * + * @return + */ + val data: DataNode<*> + get() = getData(INITAIL_DATA_STAGE)!! + + private constructor() {} + + constructor(data: DataNode<*>) { + this.stages[INITAIL_DATA_STAGE] = data + } + + fun getData(stage: String): DataNode<*>? { + return stages[stage] + } + + fun getResult(): DataNode<*>? { + return result + } + + fun setData(stage: String, data: DataNode<*>): MultiStageTaskState { + if (isFinished) { + throw IllegalStateException("Can't edit task state after result is finalized") + } else { + this.stages[stage] = data + result = data + return this + } + } + + @Synchronized + fun finish(result: DataNode<*>): MultiStageTaskState { + if (isFinished) { + throw IllegalStateException("Can't edit task state after result is finalized") + } else { + this.result = result + isFinished = true + return this + } + } + + fun finish(): MultiStageTaskState { + this.isFinished = true + return this + } + + companion object { + + private val INITAIL_DATA_STAGE = "@data" + } + + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/workspace/tasks/PipeTask.kt b/dataforge-core/src/main/kotlin/hep/dataforge/workspace/tasks/PipeTask.kt new file mode 100644 index 00000000..16d54b3a --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/workspace/tasks/PipeTask.kt @@ -0,0 +1,24 @@ +package hep.dataforge.workspace.tasks + +import hep.dataforge.actions.KPipe +import hep.dataforge.context.Context +import hep.dataforge.data.DataNode +import hep.dataforge.meta.Meta + +abstract class PipeTask protected constructor(final override val name: String, private val inputType: Class, outputType: Class) : AbstractTask(outputType) { + + private val action = KPipe(this.name, inputType, outputType) { + result { input -> + result(context, name, input, meta) + } + } + + + override fun run(model: TaskModel, data: DataNode): DataNode { + return action.run(model.context, data.checked(inputType), model.meta) + } + + abstract override fun buildModel(model: TaskModel.Builder, meta: Meta) + + protected abstract fun result(context: Context, name: String, input: T, meta: Meta): R +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/workspace/tasks/SingleActionTask.kt b/dataforge-core/src/main/kotlin/hep/dataforge/workspace/tasks/SingleActionTask.kt new file mode 100644 index 00000000..50057571 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/workspace/tasks/SingleActionTask.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.workspace.tasks + +import hep.dataforge.actions.Action +import hep.dataforge.actions.GenericAction +import hep.dataforge.data.DataNode +import hep.dataforge.meta.Meta +import org.jetbrains.annotations.Contract + +/** + * A task wrapper for single action + * Created by darksnake on 21-Aug-16. + */ +abstract class SingleActionTask(type: Class) : AbstractTask(type) { + + protected open fun gatherNode(data: DataNode): DataNode { + return data as DataNode + } + + protected abstract fun getAction(model: TaskModel): Action + + protected open fun transformMeta(model: TaskModel): Meta { + return model.meta + } + + override fun run(model: TaskModel, data: DataNode): DataNode { + val actionMeta = transformMeta(model) + val checkedData = gatherNode(data) + return getAction(model).run(model.context, checkedData, actionMeta) + } + + companion object { + + @Contract(pure = true) + fun from(action: GenericAction, dependencyBuilder: (TaskModel.Builder, Meta) -> Unit): Task { + return object : SingleActionTask(action.outputType) { + override val name: String = action.name + + override fun buildModel(model: TaskModel.Builder, meta: Meta) { + dependencyBuilder(model, meta) + } + + override fun getAction(model: TaskModel): Action { + return action + } + } + } + + @Contract(pure = true) + fun from(action: GenericAction): Task { + return from(action) { model, meta -> model.allData() } + } + } + +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/workspace/tasks/Task.kt b/dataforge-core/src/main/kotlin/hep/dataforge/workspace/tasks/Task.kt new file mode 100644 index 00000000..7015e6ad --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/workspace/tasks/Task.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.workspace.tasks + +import hep.dataforge.Named +import hep.dataforge.data.DataNode +import hep.dataforge.description.Described +import hep.dataforge.meta.Meta +import hep.dataforge.workspace.Workspace + +/** + * The main building block of "pull" data flow model. + * + * @param + * @author Alexander Nozik + */ +interface Task : Named, Described { + + /** + * If true, the task is designated as terminal. + * Terminal task is executed immediately after `run` is called, without any lazy calculations. + * @return + */ + val isTerminal: Boolean + get() = false + + /** + * The type of the node returned by the task + */ + val type: Class + + /** + * Build a model for this task + * + * @param workspace + * @param taskConfig + * @return + */ + fun build(workspace: Workspace, taskConfig: Meta): TaskModel + + /** + * Check if the model is valid and is acceptable by the task. Throw exception if not. + * + * @param model + */ + @JvmDefault + fun validate(model: TaskModel) { + //do nothing + } + + /** + * Run given task model. Type check expected to be performed before actual + * calculation. + * + * @param model + * @return + */ + fun run(model: TaskModel): DataNode + + companion object { + const val TASK_TARGET = "task" + } +} \ No newline at end of file diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/workspace/tasks/TaskModel.kt b/dataforge-core/src/main/kotlin/hep/dataforge/workspace/tasks/TaskModel.kt new file mode 100644 index 00000000..6a9867b5 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/workspace/tasks/TaskModel.kt @@ -0,0 +1,500 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.workspace.tasks + +import hep.dataforge.Named +import hep.dataforge.context.Context +import hep.dataforge.context.ContextAware +import hep.dataforge.data.DataNode +import hep.dataforge.data.DataNodeBuilder +import hep.dataforge.data.NamedData +import hep.dataforge.exceptions.AnonymousNotAlowedException +import hep.dataforge.exceptions.NameNotFoundException +import hep.dataforge.meta.* +import hep.dataforge.meta.MetaNode.DEFAULT_META_NAME +import hep.dataforge.utils.GenericBuilder +import hep.dataforge.utils.NamingUtils +import hep.dataforge.values.Value +import hep.dataforge.values.ValueProvider +import hep.dataforge.workspace.Workspace +import org.slf4j.LoggerFactory +import java.util.* +import java.util.stream.Stream + +/** + * The model for task execution. Is computed without actual task invocation. + * + * @param workspace The workspace this model belongs to + * @param name The unique name of the task + * @param taskMeta + * @param dependencies + * @author Alexander Nozik + */ +class TaskModel private constructor( + val workspace: Workspace, + override var name: String, + taskMeta: Meta, + dependencies: Set = emptySet()) : Named, Metoid, ValueProvider, MetaID, ContextAware { + + /** + * Meta for this specific task + */ + override var meta: Meta = taskMeta + private set + + /** + * A set of dependencies + */ + private val _dependencies: MutableSet = LinkedHashSet(dependencies) + + /** + * An ordered collection of dependencies + * + * @return + */ + val dependencies: Collection = _dependencies + + override val context: Context = workspace.context + + /** + * Create a copy of this model an delegate it to builder + * + * @return + */ + fun builder(): Builder { + return Builder(workspace, name, meta) + } + + /** + * Shallow copy + * + * @return + */ + fun copy(): TaskModel { + return TaskModel(workspace, name, meta, _dependencies) + } + + override fun toMeta(): Meta { + val id = MetaBuilder("task") + .setNode(context.toMeta()) + .setValue("name", name) + .setNode(DEFAULT_META_NAME, meta) + + val depNode = MetaBuilder("dependencies") + + dependencies.forEach { dependency -> depNode.putNode(dependency.toMeta()) } + id.putNode(depNode) + + return id + } + + /** + * Convenience method. Equals `meta().getValue(path)` + * + * @param path + * @return + */ + override fun optValue(path: String): Optional { + return meta.optValue(path) + } + + /** + * Convenience method. Equals `meta().hasValue(path)` + * + * @param path + * @return + */ + override fun hasValue(path: String): Boolean { + return meta.hasValue(path) + } + + + /** + * Generate an unique ID for output data for caching + */ + fun getID(data: NamedData<*>): Meta{ + //TODO calculate dependencies + return data.id + } + +// /** +// * Find all data that is used to construct data with given name +// */ +// fun resolveData(name: Name): List>{ +// return dependencies.flatMap { +// +// } +// } + + + /** + * A rule to add calculate dependency data from workspace + */ + interface Dependency : MetaID { + + /** + * Apply data to data dree. Could throw exceptions caused by either + * calculation or placement procedures. + * + * @param tree + * @param workspace + */ + fun apply(tree: DataNodeBuilder, workspace: Workspace) + } + + /** + * Data dependency + */ + internal class DataDependency : Dependency { + + /** + * The gathering function for data + */ + private val gatherer: (Workspace) -> Stream> + @Transient + private val id: Meta + + /** + * The rule to andThen from workspace data name to DataTree path + */ + private val pathTransformationRule: (String) -> String + + // public DataDependency(Function>> gatherer, UnaryOperator rule) { + // this.gatherer = gatherer; + // this.pathTransformationRule = rule; + // } + + constructor(mask: String, rule: (String) -> String) { + this.gatherer = { workspace -> + workspace.data.dataStream() + .filter { data -> + NamingUtils.wildcardMatch(mask, data.name) + } + } + this.pathTransformationRule = rule + id = MetaBuilder("data").putValue("mask", mask) + } + + /** + * Data dependency w + * + * @param type + * @param mask + * @param rule + */ + constructor(type: Class<*>, mask: String, rule: (String) -> String) { + this.gatherer = { workspace -> + workspace.data.dataStream().filter { data -> + NamingUtils.wildcardMatch(mask, data.name) && type.isAssignableFrom(data.type) + } + } + this.pathTransformationRule = rule + id = MetaBuilder("data").putValue("mask", mask).putValue("type", type.name) + } + + /** + * Place data + * + * @param tree + * @param workspace + */ + override fun apply(tree: DataNodeBuilder, workspace: Workspace) { + gatherer(workspace).forEach { data -> + tree.putData(pathTransformationRule(data.name), data.anonymize()) + } + } + + override fun toMeta(): Meta { + return id + } + } + + internal class DataNodeDependency(private val type: Class, private val sourceNodeName: String, private val targetNodeName: String) : Dependency { + + override fun apply(tree: DataNodeBuilder, workspace: Workspace) { + tree.putNode(targetNodeName, workspace.data.getCheckedNode(sourceNodeName, type)) + } + + override fun toMeta(): Meta { + return MetaBuilder("dataNode") + .putValue("source", sourceNodeName) + .putValue("target", targetNodeName) + .putValue("type", type.name) + } + } + + /** + * Task dependency + * @param taskModel The model of task + */ + internal class TaskDependency(var taskModel: TaskModel, val key: String) : Dependency { + //TODO make meta configurable + + /** + * Attach result of task execution to the data tree + * + * @param tree + * @param workspace + */ + override fun apply(tree: DataNodeBuilder, workspace: Workspace) { + val result = workspace.runTask(taskModel) + if (key.isEmpty()) { + if (!result.meta.isEmpty) { + if (tree.meta.isEmpty()) { + tree.meta = result.meta + } else { + LoggerFactory.getLogger(javaClass).error("Root node meta already exists.") + } + } + result.dataStream().forEach { tree.add(it) } + } else { + tree.putNode(key, result) + } + } + + override fun toMeta(): Meta { + return taskModel.toMeta() + } + + } + + /** + * A builder for immutable model + */ + class Builder(workspace: Workspace, taskName: String, taskMeta: Meta) : GenericBuilder { + private val model: TaskModel = TaskModel(workspace, taskName, Meta.empty()) + private var taskMeta = taskMeta.builder + + val workspace: Workspace + get() = model.workspace + + val name: String? + get() = this.model.name + + + // constructor(model: TaskModel) { +// this.model = model.copy() +// this.taskMeta = model.meta.builder +// } + + override fun self(): Builder { + return this + } + + override fun build(): TaskModel { + model.meta = taskMeta.build() + return model + } + + // public Meta getMeta() { + // return this.model.getMeta(); + // } + + /** + * Apply meta transformation to model meta + * + * @param transform + * @return + */ + fun configure(transform: MetaBuilder.() -> Unit): Builder { + transform(taskMeta) + return self() + } + + /** + * replace model meta + * + * @param meta + * @return + */ + fun configure(meta: Meta): Builder { + this.taskMeta = meta.builder + return self() + } + + /** + * Rename model + * + * @param name + * @return + */ + fun rename(name: String): Builder { + if (name.isEmpty()) { + throw AnonymousNotAlowedException() + } else { + model.name = name + return self() + } + } + + /** + * Add dependency on Model with given task + * + * @param dep + * @param `as` + */ + @JvmOverloads + fun dependsOn(dep: TaskModel, key: String = dep.name): Builder { + model._dependencies.add(TaskDependency(dep, key)) + return self() + } + + /** + * dependsOn(new TaskModel(taskName, taskMeta), as); + * + * @param taskName + * @param taskMeta + * @param `as` + */ + @JvmOverloads + fun dependsOn(taskName: String, taskMeta: Meta, key: String = ""): Builder { + try { + return dependsOn(model.workspace.getTask(taskName).build(model.workspace, taskMeta), key) + } catch (ex: NameNotFoundException) { + throw RuntimeException("Task with name " + ex.name + " not found", ex) + } + + } + + @JvmOverloads + fun dependsOn(task: Task<*>, taskMeta: Meta, `as`: String = ""): Builder { + return dependsOn(task.build(model.workspace, taskMeta), `as`) + } + + /** + * Add data dependency rule using data path mask and name transformation + * rule. + * + * + * Name change rule should be "pure" to avoid runtime model changes + * + * @param mask + * @param rule + */ + @JvmOverloads + fun data(mask: String, rule: (String) -> String = { it }): Builder { + model._dependencies.add(DataDependency(mask, rule)) + return self() + } + + /** + * Type checked data dependency + * + * @param type + * @param mask + * @param rule + * @return + */ + fun data(type: Class<*>, mask: String, rule: (String) -> String): Builder { + model._dependencies.add(DataDependency(type, mask, rule)) + return self() + } + + /** + * data(mask, `str -> as`); + * + * @param mask + * @param `as` + */ + fun data(mask: String, key: String): Builder { + //FIXME make smart name transformation here + return data(mask) { key } + } + + /** + * Add all data in the workspace as a dependency + * + * @return + */ + fun allData(): Builder { + model._dependencies.add(DataDependency("*") { it }) + return self() + } + + /** + * Add a dependency on a type checked node + * + * @param type + * @param sourceNodeName + * @param targetNodeName + * @return + */ + fun dataNode(type: Class<*>, sourceNodeName: String, targetNodeName: String): Builder { + model._dependencies.add(DataNodeDependency(type, sourceNodeName, targetNodeName)) + return this + } + + /** + * Source and target node have the same name + * + * @param type + * @param nodeName + * @return + */ + fun dataNode(type: Class<*>, nodeName: String): Builder { + model._dependencies.add(DataNodeDependency(type, nodeName, nodeName)) + return this + } + + } + /** + * dependsOn(model, model.getName()); + * + * @param dep + */ + /** + * dependsOn(new TaskModel(workspace, taskName, taskMeta)) + * + * @param name + * @param taskMeta + */ + /** + * data(mask, UnaryOperator.identity()); + * + * @param mask + */ + + companion object { + + /** + * Create an empty model builder + * + * @param workspace + * @param taskName + * @param taskMeta + * @return + */ + fun builder(workspace: Workspace, taskName: String, taskMeta: Meta): TaskModel.Builder { + return TaskModel.Builder(workspace, taskName, taskMeta) + } + + /** + * Generate id for NamedData + */ + val NamedData<*>.id: Meta + get() = meta.builder.apply { + "name" to name + "type" to this@id.type + } + + /** + * Generate id for the DataNode, describing its content + */ + val DataNode<*>.id: Meta + get() = buildMeta { + if (!meta.isEmpty) { + "name" to name + "meta" to meta + } + nodeStream(false).forEach { + "node" to it.id + } + dataStream(false).forEach { + "data" to it.id + } + } + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/workspace/templates/GatherTaskTemplate.kt b/dataforge-core/src/main/kotlin/hep/dataforge/workspace/templates/GatherTaskTemplate.kt new file mode 100644 index 00000000..7a5cb505 --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/workspace/templates/GatherTaskTemplate.kt @@ -0,0 +1,44 @@ +package hep.dataforge.workspace.templates + +import hep.dataforge.context.Context +import hep.dataforge.data.DataNode +import hep.dataforge.meta.Meta +import hep.dataforge.workspace.tasks.AbstractTask +import hep.dataforge.workspace.tasks.Task +import hep.dataforge.workspace.tasks.TaskModel + +/** + * The task that gathers data from workspace and returns it as is. + * The task configuration is considered to be dependency configuration. + */ +class GatherTaskTemplate : TaskTemplate { + override val name: String = "gather" + + override fun build(context: Context, meta: Meta): Task<*> { + return object : AbstractTask(Any::class.java) { + override fun run(model: TaskModel, data: DataNode): DataNode { + return data + } + + override fun buildModel(model: TaskModel.Builder, meta: Meta) { + if (meta.hasMeta("data")) { + meta.getMetaList("data").forEach { dataElement -> + val dataPath = dataElement.getString("name") + model.data(dataPath, dataElement.getString("as", dataPath)) + } + } + //Iterating over task dependancies + if (meta.hasMeta("task")) { + meta.getMetaList("task").forEach { taskElement -> + val taskName = taskElement.getString("name") + val task = model.workspace.getTask(taskName) + //Building model with default data construction + model.dependsOn(task.build(model.workspace, taskElement), taskElement.getString("as", taskName)) + } + } + } + + override val name: String =meta.getString("name", this@GatherTaskTemplate.name) + } + } +} diff --git a/dataforge-core/src/main/kotlin/hep/dataforge/workspace/templates/TaskTemplate.kt b/dataforge-core/src/main/kotlin/hep/dataforge/workspace/templates/TaskTemplate.kt new file mode 100644 index 00000000..c4500d8d --- /dev/null +++ b/dataforge-core/src/main/kotlin/hep/dataforge/workspace/templates/TaskTemplate.kt @@ -0,0 +1,10 @@ +package hep.dataforge.workspace.templates + +import hep.dataforge.Named +import hep.dataforge.utils.ContextMetaFactory +import hep.dataforge.workspace.tasks.Task + +/** + * A factory to create a task from meta + */ +interface TaskTemplate : ContextMetaFactory>, Named diff --git a/dataforge-core/src/main/resources/META-INF/services/ch.qos.logback.classic.spi.Configurator b/dataforge-core/src/main/resources/META-INF/services/ch.qos.logback.classic.spi.Configurator new file mode 100644 index 00000000..440bc90a --- /dev/null +++ b/dataforge-core/src/main/resources/META-INF/services/ch.qos.logback.classic.spi.Configurator @@ -0,0 +1 @@ +hep.dataforge.io.LogbackConfigurator \ No newline at end of file diff --git a/dataforge-core/src/main/resources/META-INF/services/hep.dataforge.context.PluginFactory b/dataforge-core/src/main/resources/META-INF/services/hep.dataforge.context.PluginFactory new file mode 100644 index 00000000..4013339a --- /dev/null +++ b/dataforge-core/src/main/resources/META-INF/services/hep.dataforge.context.PluginFactory @@ -0,0 +1,3 @@ +hep.dataforge.actions.ActionManager$Factory +hep.dataforge.cache.CachePlugin$Factory +hep.dataforge.io.DirectoryOutput$Factory \ No newline at end of file diff --git a/dataforge-core/src/main/resources/META-INF/services/hep.dataforge.data.DataLoader b/dataforge-core/src/main/resources/META-INF/services/hep.dataforge.data.DataLoader new file mode 100644 index 00000000..906b15a5 --- /dev/null +++ b/dataforge-core/src/main/resources/META-INF/services/hep.dataforge.data.DataLoader @@ -0,0 +1 @@ +hep.dataforge.data.FileDataFactory diff --git a/dataforge-core/src/main/resources/META-INF/services/hep.dataforge.io.envelopes.EnvelopeType b/dataforge-core/src/main/resources/META-INF/services/hep.dataforge.io.envelopes.EnvelopeType new file mode 100644 index 00000000..4ba21155 --- /dev/null +++ b/dataforge-core/src/main/resources/META-INF/services/hep.dataforge.io.envelopes.EnvelopeType @@ -0,0 +1,2 @@ +hep.dataforge.io.envelopes.DefaultEnvelopeType +hep.dataforge.io.envelopes.TaglessEnvelopeType \ No newline at end of file diff --git a/dataforge-core/src/main/resources/META-INF/services/hep.dataforge.io.envelopes.MetaType b/dataforge-core/src/main/resources/META-INF/services/hep.dataforge.io.envelopes.MetaType new file mode 100644 index 00000000..a41ec634 --- /dev/null +++ b/dataforge-core/src/main/resources/META-INF/services/hep.dataforge.io.envelopes.MetaType @@ -0,0 +1,2 @@ +hep.dataforge.io.envelopes.XMLMetaType +hep.dataforge.io.envelopes.BinaryMetaType diff --git a/dataforge-core/src/main/resources/META-INF/services/hep.dataforge.io.envelopes.Wrapper b/dataforge-core/src/main/resources/META-INF/services/hep.dataforge.io.envelopes.Wrapper new file mode 100644 index 00000000..2a45fde7 --- /dev/null +++ b/dataforge-core/src/main/resources/META-INF/services/hep.dataforge.io.envelopes.Wrapper @@ -0,0 +1 @@ +hep.dataforge.io.envelopes.JavaObjectWrapper diff --git a/dataforge-core/src/main/resources/META-INF/services/hep.dataforge.workspace.WorkspaceParser b/dataforge-core/src/main/resources/META-INF/services/hep.dataforge.workspace.WorkspaceParser new file mode 100644 index 00000000..e69de29b diff --git a/dataforge-core/src/test/kotlin/hep/dataforge/description/DescriptorsTest.kt b/dataforge-core/src/test/kotlin/hep/dataforge/description/DescriptorsTest.kt new file mode 100644 index 00000000..c0c09274 --- /dev/null +++ b/dataforge-core/src/test/kotlin/hep/dataforge/description/DescriptorsTest.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.description + +import hep.dataforge.values.ValueType +import org.junit.Assert.assertEquals +import org.junit.Test + + +class DescriptorsTest{ + + class DescribedTestClass{ + @Description("The title of the axis") + @ValueProperty(type = [ValueType.STRING], def = "aaa") + val title: String = "aaa" + } + + + @Test + fun testClassDescriptors(){ + val descriptor = Descriptors.forType("plot",DescribedTestClass::class) + val valueDescriptor = descriptor.getValueDescriptor("title") + assertEquals("aaa", valueDescriptor?.default?.string) + } +} \ No newline at end of file diff --git a/dataforge-core/src/test/kotlin/hep/dataforge/goals/AbstractGoalTest.kt b/dataforge-core/src/test/kotlin/hep/dataforge/goals/AbstractGoalTest.kt new file mode 100644 index 00000000..e8bc26c7 --- /dev/null +++ b/dataforge-core/src/test/kotlin/hep/dataforge/goals/AbstractGoalTest.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.goals + +import org.junit.Test + +import java.util.concurrent.CompletableFuture +import java.util.concurrent.FutureTask + +/** + * + * @author Alexander Nozik + */ +class AbstractGoalTest { + + @Test + @Throws(InterruptedException::class) + fun testComplete() { + val future = CompletableFuture.supplyAsync { + try { + Thread.sleep(500) + } catch (ex: InterruptedException) { + println("canceled") + throw RuntimeException(ex) + } + + println("finished") + "my delayed result" + } + future.whenComplete { res, err -> + println(res) + if (err != null) { + println(err) + } + } + + future.complete("my firs result") + future.complete("my second result") + } + + @Test + @Throws(InterruptedException::class) + fun testCancel() { + val future = FutureTask { + try { + Thread.sleep(300) + } catch (ex: InterruptedException) { + println("canceled") + throw RuntimeException(ex) + } + + println("finished") + "my delayed result" + } + future.run() + future.cancel(true) + Thread.sleep(500) + } + + +} diff --git a/dataforge-core/src/test/kotlin/hep/dataforge/io/XMLIOTest.groovy b/dataforge-core/src/test/kotlin/hep/dataforge/io/XMLIOTest.groovy new file mode 100644 index 00000000..6e053e65 --- /dev/null +++ b/dataforge-core/src/test/kotlin/hep/dataforge/io/XMLIOTest.groovy @@ -0,0 +1,79 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.io + +import hep.dataforge.meta.MetaBuilder +import spock.lang.Specification + +class XMLIOTest extends Specification { + + + def "XML IO"() { + given: + def testMeta = + new MetaBuilder("test") + .putValue("numeric", 22.5) + .putValue("other", "otherValue") + .putValue("some.path", true) + .putNode( + new MetaBuilder("child") + .putValue("childValue", "childValue") + .putNode( + new MetaBuilder("grandChild") + .putValue("grandChildValue", "grandChildValue") + )) + .putNode( + new MetaBuilder("child") + .putValue("childValue", "otherChildValue") + .putNode( + new MetaBuilder("grandChild") + .putValue("grandChildValue", "otherGrandChildValue") + ) + ).build(); + when: + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + new XMLMetaWriter().write(baos, testMeta) + def bytes = baos.toByteArray(); + def res = new XMLMetaReader().read(new ByteArrayInputStream(bytes)) + then: + res == testMeta + } + + def "XMlinput"(){ + given: + def source = "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "" + when: + def res = new XMLMetaReader().read(new ByteArrayInputStream(source.bytes)) + then: + res.getInt("format.column[2].name") == 3 + } +} diff --git a/dataforge-core/src/test/kotlin/hep/dataforge/io/envelopes/EnvelopeFormatSpec.groovy b/dataforge-core/src/test/kotlin/hep/dataforge/io/envelopes/EnvelopeFormatSpec.groovy new file mode 100644 index 00000000..a458f45d --- /dev/null +++ b/dataforge-core/src/test/kotlin/hep/dataforge/io/envelopes/EnvelopeFormatSpec.groovy @@ -0,0 +1,42 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.io.envelopes + +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import spock.lang.Specification + +/** + * Created by darksnake on 25-Feb-17. + */ +class EnvelopeFormatSpec extends Specification { + def "Test read/write"() { + given: + byte[] data = "This is my data".bytes + Meta meta = new MetaBuilder().setValue("myValue", "This is my meta") + Envelope envelope = new EnvelopeBuilder().setMeta(meta).setData(data).build() + when: + def baos = new ByteArrayOutputStream(); + new DefaultEnvelopeWriter().write(baos, envelope) + byte[] reaArray = baos.toByteArray(); + println new String(reaArray) + def bais = new ByteArrayInputStream(reaArray) + Envelope res = new DefaultEnvelopeReader().read(bais) + then: + res.data.buffer.array() == data + } +} diff --git a/dataforge-core/src/test/kotlin/hep/dataforge/io/envelopes/TaglessEnvelopeTest.kt b/dataforge-core/src/test/kotlin/hep/dataforge/io/envelopes/TaglessEnvelopeTest.kt new file mode 100644 index 00000000..34a50294 --- /dev/null +++ b/dataforge-core/src/test/kotlin/hep/dataforge/io/envelopes/TaglessEnvelopeTest.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.io.envelopes + +import hep.dataforge.meta.MetaBuilder +import org.junit.Assert.assertEquals +import org.junit.Test +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.nio.charset.Charset + +class TaglessEnvelopeTest { + private val envelope = EnvelopeBuilder() + .meta(MetaBuilder() + .putValue("myValue", 12) + ).data("Ð’Ñем привет!".toByteArray(Charset.forName("UTF-8"))) + + private val envelopeType = TaglessEnvelopeType.INSTANCE + + @Test + @Throws(IOException::class) + fun testWriteRead() { + val baos = ByteArrayOutputStream() + envelopeType.writer.write(baos, envelope) + + println(String(baos.toByteArray())) + + val bais = ByteArrayInputStream(baos.toByteArray()) + val restored = envelopeType.reader.read(bais) + + assertEquals("Ð’Ñем привет!", String(restored.data.buffer.array(), Charsets.UTF_8)) + } + + @Test + @Throws(IOException::class) + fun testShortForm() { + val envString = "\n" + + "#~DATA~#\n" + + "Ð’Ñем привет!" + println(envString) + val bais = ByteArrayInputStream(envString.toByteArray(charset("UTF-8"))) + val restored = envelopeType.reader.read(bais) + + assertEquals(12, restored.meta.getInt("myValue")) + assertEquals("Ð’Ñем привет!", String(restored.data.buffer.array(), Charsets.UTF_8)) + } +} \ No newline at end of file diff --git a/dataforge-core/src/test/kotlin/hep/dataforge/kodex/CoalTest.kt b/dataforge-core/src/test/kotlin/hep/dataforge/kodex/CoalTest.kt new file mode 100644 index 00000000..74efc36e --- /dev/null +++ b/dataforge-core/src/test/kotlin/hep/dataforge/kodex/CoalTest.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.kodex + +import hep.dataforge.context.Global +import hep.dataforge.goals.generate +import hep.dataforge.goals.join +import hep.dataforge.goals.pipe +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class CoalTest { + val firstLevel = (1..10).map { index -> + Global.generate { + Thread.sleep(100) + println("this is coal $index") + "this is coal $index" + } + } + val secondLevel = firstLevel.map { + it.pipe(Global) { + Thread.sleep(200) + val res = it + ":Level 2" + println(res) + res + } + } + val thirdLevel = secondLevel.map { + it.pipe(Global) { + Thread.sleep(300) + val res = it.replace("Level 2", "Level 3") + println(res) + res + } + } + val joinGoal = thirdLevel.join(Global) { Pair("joining ${it.size} elements", 10) } + + @Test + fun testSingle() { + assertEquals(firstLevel [3].get(), "this is coal 4") + } + + @Test + fun testDep() { + assertTrue(secondLevel [3].get().endsWith("Level 2")) + } + + @Test + fun testJoin() { + val (_, num) = joinGoal.get(); + assertEquals(num, 10); + + } +} \ No newline at end of file diff --git a/dataforge-core/src/test/kotlin/hep/dataforge/kodex/KActionTest.kt b/dataforge-core/src/test/kotlin/hep/dataforge/kodex/KActionTest.kt new file mode 100644 index 00000000..3ac3e1ca --- /dev/null +++ b/dataforge-core/src/test/kotlin/hep/dataforge/kodex/KActionTest.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.kodex + +import hep.dataforge.actions.KPipe +import hep.dataforge.context.Global +import hep.dataforge.data.DataSet +import hep.dataforge.meta.Meta +import hep.dataforge.meta.buildMeta +import hep.dataforge.workspace.BasicWorkspace +import hep.dataforge.workspace.tasks.task +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.time.delay +import org.junit.Test +import java.time.Duration + +class KActionTest { + val data: DataSet = DataSet.edit(String::class.java).apply { + (1..10).forEach { + putData("$it", "this is my data $it", buildMeta { "index" to it }); + } + }.build() + + val pipe = KPipe("testPipe", String::class.java, String::class.java) { + name = "newName_${meta.getInt("index")}" + if (meta.getInt("index") % 2 == 0) { + meta.putValue("odd", true); + } + result { + println("performing action on $name") + delay(Duration.ofMillis(400)); + it + ": stage1"; + } + } + + @Test + fun testPipe() { + println("test pipe") + val res = pipe.run(Global, data, Meta.empty()); + val datum = res.getData("newName_4") + val value = datum.goal.get() + assertTrue(datum.meta.getBoolean("odd")) + assertEquals("this is my data 4: stage1", value) + } + + @Test + fun testPipeTask() { + println("test pipe task") + + val testTask = task("test") { + model { + data("*") + } + action(pipe) + } + + + + Global.setValue("cache.enabled", false) + val workspace = BasicWorkspace.builder() + .data("test", data) + .task(testTask) + .target("test", Meta.empty()) + .apply { context = Global} + .build() + + val res = workspace.runTask("test") + + val datum = res.getData("newName_4") + val value = datum.goal.get() + assertTrue(datum.meta.getBoolean("odd")) + assertEquals("this is my data 4: stage1", value) + + } +} \ No newline at end of file diff --git a/dataforge-core/src/test/kotlin/hep/dataforge/kodex/ListAnnotationsTest.kt b/dataforge-core/src/test/kotlin/hep/dataforge/kodex/ListAnnotationsTest.kt new file mode 100644 index 00000000..a6961210 --- /dev/null +++ b/dataforge-core/src/test/kotlin/hep/dataforge/kodex/ListAnnotationsTest.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.kodex + +import hep.dataforge.description.ValueDef +import hep.dataforge.listAnnotations +import hep.dataforge.states.StateDef +import hep.dataforge.states.StateDefs +import org.junit.Assert.assertEquals +import org.junit.Test + +class ListAnnotationsTest { + + @StateDef(ValueDef(key = "test")) + class Test1 + + + @Test + fun testSingleAnnotation() { + val annotations = Test1::class.java.listAnnotations(StateDef::class.java) + assertEquals(1, annotations.size) + } + + @StateDefs( + StateDef(ValueDef(key = "test1")), + StateDef(ValueDef(key = "test2")) + ) + class Test2 + + @Test + fun testMultipleAnnotations() { + val annotations = Test2::class.java.listAnnotations(StateDef::class.java) + assertEquals(2, annotations.size) + } + +} \ No newline at end of file diff --git a/dataforge-core/src/test/kotlin/hep/dataforge/meta/LaminateTest.kt b/dataforge-core/src/test/kotlin/hep/dataforge/meta/LaminateTest.kt new file mode 100644 index 00000000..58b866f9 --- /dev/null +++ b/dataforge-core/src/test/kotlin/hep/dataforge/meta/LaminateTest.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.meta + +import org.junit.Assert.assertEquals +import org.junit.Test + +class LaminateTest { + + internal var meta: Meta = MetaBuilder("test") + .putNode(MetaBuilder("child").putValue("a", 22)) + + internal var laminate = Laminate(meta, meta) + + @Test + fun testToString() { + println(laminate.toString()) + assertEquals(3, laminate.toString().split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray().size.toLong()) + } + + @Test + fun testMerge() { + val meta1 = buildMeta { + "a" to 11 + "aNode" to { + "a1" to 11 + } + } + + val meta2 = buildMeta { + "b" to 22 + "bNode" to { + "b1" to 22 + } + } + + val laminate = Laminate.join(meta1, meta2) + + val sealed = laminate.sealed + + assertEquals(11, sealed["a"]!!) + assertEquals(11, sealed["aNode.a1"]!!) + assertEquals(22, sealed["b"]!!) + assertEquals(22, sealed["bNode.b1"]!!) + } + +} \ No newline at end of file diff --git a/dataforge-core/src/test/kotlin/hep/dataforge/meta/MetaMorphTest.kt b/dataforge-core/src/test/kotlin/hep/dataforge/meta/MetaMorphTest.kt new file mode 100644 index 00000000..ca0779c9 --- /dev/null +++ b/dataforge-core/src/test/kotlin/hep/dataforge/meta/MetaMorphTest.kt @@ -0,0 +1,44 @@ +package hep.dataforge.meta + +import hep.dataforge.tables.ColumnTable +import hep.dataforge.tables.ListTable +import org.junit.Assert.assertEquals +import org.junit.Test +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.ObjectInputStream +import java.io.ObjectOutputStream + +class MetaMorphTest { + + private fun reconstruct(obj: Any): Any { + val baos = ByteArrayOutputStream() + val oos = ObjectOutputStream(baos) + oos.writeObject(obj) + val ois = ObjectInputStream(ByteArrayInputStream(baos.toByteArray())) + return ois.readObject() + } + + @Test + fun tableListTable() { + val table = ListTable.Builder("a", "b", "c").apply { + row(1, 2, 3) + row(4, 5, 6) + }.build() + + + assertEquals(table, reconstruct(table)) + } + + @Test + fun tableColumnTable() { + val table = ColumnTable.copy(ListTable.Builder("a", "b", "c").apply { + row(1, 2, 3) + row(4, 5, 6) + }.build()) + + + assertEquals(table, reconstruct(table)) + } + +} \ No newline at end of file diff --git a/dataforge-core/src/test/kotlin/hep/dataforge/meta/MetaTest.kt b/dataforge-core/src/test/kotlin/hep/dataforge/meta/MetaTest.kt new file mode 100644 index 00000000..f9f6b200 --- /dev/null +++ b/dataforge-core/src/test/kotlin/hep/dataforge/meta/MetaTest.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.meta + +import hep.dataforge.exceptions.NamingException +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +/** + * + * @author darksnake + */ +class MetaTest { + + internal val testAnnotation = MetaBuilder("test") + .putValue("some", "\${other}") + .putValue("numeric", 22.5) + .putValue("other", "otherValue") + .putValue("some.path", true) + .putNode(MetaBuilder("child") + .putValue("childValue", "childValue") + .putNode(MetaBuilder("grandChild") + .putValue("grandChildValue", "grandChildValue") + ) + ) + .putNode(MetaBuilder("child") + .putValue("childValue", "otherChildValue") + .putNode(MetaBuilder("grandChild") + .putValue("grandChildValue", "otherGrandChildValue") + ) + ) + .build() + + @Before + fun setUp() { + + } + + @After + fun tearDown() { + } + + // @Test + // public void testSubst() { + // System.onComplete.println("Value substitution via AnnotationReader"); + // assertEquals("otherValue", testAnnotation.getString("some")); + // } + + @Test + fun testPath() { + println("Path search") + assertEquals("childValue", testAnnotation.getString("child.childValue")) + assertEquals("grandChildValue", testAnnotation.getString("child.grandChild.grandChildValue")) + assertEquals("otherGrandChildValue", testAnnotation.getString("child[1].grandChild.grandChildValue")) + } + + @Test(expected = NamingException::class) + fun testWrongPath() { + println("Missing path search") + assertEquals("otherGrandChildValue", testAnnotation.getString("child[2].grandChild.grandChildValue")) + } +} diff --git a/dataforge-core/src/test/kotlin/hep/dataforge/meta/MetaUtilsTest.groovy b/dataforge-core/src/test/kotlin/hep/dataforge/meta/MetaUtilsTest.groovy new file mode 100644 index 00000000..3999d1fa --- /dev/null +++ b/dataforge-core/src/test/kotlin/hep/dataforge/meta/MetaUtilsTest.groovy @@ -0,0 +1,89 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.meta + +import hep.dataforge.io.IOUtils +import hep.dataforge.io.XMLMetaReader +import hep.dataforge.io.XMLMetaWriter +import spock.lang.Specification + +/** + * Created by darksnake on 12-Nov-16. + */ +class MetaUtilsTest extends Specification { + def "serialization test"() { + given: + + Meta meta = new MetaBuilder("test") + .setValue("childValue", 18.5) + .setValue("numeric", 6.2e-8) + .setNode(new MetaBuilder("childNode").setValue("listValue", [2, 4, 6]).setValue("grandChildValue", true)) + + println "initial meta: \n${meta.toString()}" + when: + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + MetaUtils.writeMeta(new ObjectOutputStream(baos), meta); + byte[] bytes = baos.toByteArray(); + + println "Serialized string: \n${new String(bytes, IOUtils.ASCII_CHARSET)}\n" + + ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + + Meta reconstructed = MetaUtils.readMeta(new ObjectInputStream(bais)) + println "reconstructed meta: \n${reconstructed.toString()}" + then: + reconstructed == meta + } + + def "XML reconstruction test"() { + given: + + Meta meta = new MetaBuilder("test") + .setValue("childValue", 18.5) + .setValue("numeric", 6.2e-8) + .setNode(new MetaBuilder("childNode").setValue("listValue", [2, 4, 6]).setValue("grandChildValue", true)) + + println "initial meta: \n${meta.toString()}" + when: + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + new XMLMetaWriter().write(baos, meta); + byte[] bytes = baos.toByteArray(); + + println "XML : \n${new String(bytes, IOUtils.UTF8_CHARSET)}\n" + + ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + + Meta reconstructed = new XMLMetaReader().read(bais) + println "reconstructed meta: \n${reconstructed.toString()}" + then: + reconstructed == meta + + } + + def "test query"() { + when: + Meta meta = new MetaBuilder("test") + .putNode(new MetaBuilder("child").putValue("value", 2)) + .putNode(new MetaBuilder("child").putValue("value", 3).putValue("check",true)) + .putNode(new MetaBuilder("child").putValue("value", 4)) + .putNode(new MetaBuilder("child").putValue("value", 5)) + then: + meta.getMeta("child[value = 3, check = true]").getValue("check") + } +} diff --git a/dataforge-core/src/test/kotlin/hep/dataforge/meta/MutableAnnotationSpec.groovy b/dataforge-core/src/test/kotlin/hep/dataforge/meta/MutableAnnotationSpec.groovy new file mode 100644 index 00000000..d95d131a --- /dev/null +++ b/dataforge-core/src/test/kotlin/hep/dataforge/meta/MutableAnnotationSpec.groovy @@ -0,0 +1,73 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.meta + +import hep.dataforge.names.Name +import hep.dataforge.values.Value +import org.jetbrains.annotations.NotNull +import spock.lang.Specification + +/** + * + * @author Alexander Nozik + */ +class MutableAnnotationSpec extends Specification { + Configuration testAnnotation; + + def setup() { + testAnnotation = new Configuration(new MetaBuilder("test") + .putNode(new MetaBuilder("child") + .putValue("child_value", 88) + ) + .putValue("my_value", 48) + .putValue("other_value", "ёлка") + .putValue("path.to.my_value", true) + ) + } + + + def "test MutableAnnotation Value observer"() { + when: + testAnnotation.addListener(new Observer()); + testAnnotation.putValue("some_new_value", 13.3) + then: + testAnnotation.hasValue("some_new_value") + + } + + def "test child Value observer"() { + when: + testAnnotation.addListener(new Observer()); + testAnnotation.getMeta("child").putValue("new_child_value", 89); + then: + testAnnotation.hasValue("child.new_child_value") + } + + + private class Observer implements ConfigChangeListener { + void notifyValueChanged(@NotNull Name name, Value oldItem, Value newItem) { + println "the value with name ${name} changed from ${oldItem} to ${newItem}" + } + + @Override + void notifyNodeChanged(@NotNull Name name, @NotNull List oldItem, @NotNull List newItem) { + println "the element with name ${name} changed from ${oldItem} to ${newItem}" + } + + + } +} + diff --git a/dataforge-core/src/test/kotlin/hep/dataforge/meta/TemplateTest.kt b/dataforge-core/src/test/kotlin/hep/dataforge/meta/TemplateTest.kt new file mode 100644 index 00000000..f2399252 --- /dev/null +++ b/dataforge-core/src/test/kotlin/hep/dataforge/meta/TemplateTest.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.meta + +import hep.dataforge.io.XMLMetaReader +import org.junit.Assert.assertEquals +import org.junit.Test +import java.io.IOException +import java.text.ParseException + +/** + * + * @author Alexander Nozik + */ +class TemplateTest { + + /** + * Test of compileTemplate method, of class MetaUtils. + */ + @Test + @Throws(IOException::class, ParseException::class) + fun testCompileTemplate() { + println("compileTemplate") + val template = XMLMetaReader().read(javaClass.getResourceAsStream("/meta/template.xml")) + val data = XMLMetaReader().read(javaClass.getResourceAsStream("/meta/templateData.xml")) + val result = Template.compileTemplate(template, data) + assertEquals(result.getString("someNode.myValue"), "crocodile") + assertEquals(result.getString("someNode.subNode[0].ggg"), "81.5") + assertEquals(result.getString("someNode.subNode[1].subNode.subNodeValue"), "ccc") + } + +} diff --git a/dataforge-core/src/test/kotlin/hep/dataforge/names/NameTest.kt b/dataforge-core/src/test/kotlin/hep/dataforge/names/NameTest.kt new file mode 100644 index 00000000..11c2b903 --- /dev/null +++ b/dataforge-core/src/test/kotlin/hep/dataforge/names/NameTest.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.names + +import org.junit.Assert.assertEquals +import org.junit.Test + +class NameTest { + @Test + fun testNameFromString() { + val name = Name.of("first.second[28].third\\.andahalf") + assertEquals(3, name.length.toLong()) + assertEquals("third.andahalf", name.last.unescaped) + } + + @Test + fun testQuery(){ + val name = Name.of("name[f = 4]") + assertEquals("name", name.ignoreQuery().toString()) + assertEquals("f = 4", name.query) + assertEquals("name[f = 4]",name.toString()) + assertEquals("name[f = 4]",name.unescaped) + } + + @Test + fun testReconstruction() { + val name = Name.join(Name.of("first.second"), Name.ofSingle("name.with.dot"), Name.ofSingle("end[22]")) + val str = name.toString() + val reconstructed = Name.of(str) + assertEquals(name, reconstructed) + assertEquals("first", reconstructed.tokens[0].unescaped) + assertEquals("name.with.dot", reconstructed.tokens[2].unescaped) + assertEquals("end[22]", reconstructed.tokens[3].unescaped) + } + + @Test + fun testJoin() { + val name = Name.join("first", "second", "", "another") + assertEquals(3, name.length.toLong()) + } + +} \ No newline at end of file diff --git a/dataforge-core/src/test/kotlin/hep/dataforge/states/StateHolderTest.kt b/dataforge-core/src/test/kotlin/hep/dataforge/states/StateHolderTest.kt new file mode 100644 index 00000000..e6079f38 --- /dev/null +++ b/dataforge-core/src/test/kotlin/hep/dataforge/states/StateHolderTest.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.states + +import hep.dataforge.context.Global +import hep.dataforge.values.Value +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import org.junit.Assert.assertEquals +import org.junit.Test +import java.util.concurrent.ConcurrentHashMap + +class StateHolderTest { + + @Test + fun testStates() { + val states = StateHolder() + + val results = ConcurrentHashMap() + + states.init(ValueState("state1")) + states.init(ValueState("state2")) + + val subscription = states.changes() + Global.launch { + subscription.collect { + println("${it.first}: ${it.second}") + results[it.first] = it.second as Value + } + } + + states["state1"] = 1 + states["state2"] = 2 + states["state1"] = 3 + + Thread.sleep(200) + + assertEquals(2, results["state2"]?.int) + assertEquals(3, results["state1"]?.int) + } +} \ No newline at end of file diff --git a/dataforge-core/src/test/kotlin/hep/dataforge/states/StateTest.kt b/dataforge-core/src/test/kotlin/hep/dataforge/states/StateTest.kt new file mode 100644 index 00000000..03f74899 --- /dev/null +++ b/dataforge-core/src/test/kotlin/hep/dataforge/states/StateTest.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.states + +import hep.dataforge.context.Global +import hep.dataforge.values.Value +import kotlinx.coroutines.launch +import org.junit.Assert.assertEquals +import org.junit.Test +import java.util.concurrent.atomic.AtomicReference + +class StateTest { + @Test + fun testFututre() { + val state = ValueState("state") + val ref = AtomicReference(Value.NULL) + + val receiver = state.channel.openSubscription() + Global.launch { + while (true) { + val res = receiver.receive() + println(res) + ref.set(res) + } + } + state.set(1) + state.set(2) + Thread.sleep(50) + assertEquals(2, ref.get().int) + } +} \ No newline at end of file diff --git a/dataforge-core/src/test/kotlin/hep/dataforge/tables/TablesKtTest.kt b/dataforge-core/src/test/kotlin/hep/dataforge/tables/TablesKtTest.kt new file mode 100644 index 00000000..de914d0c --- /dev/null +++ b/dataforge-core/src/test/kotlin/hep/dataforge/tables/TablesKtTest.kt @@ -0,0 +1,30 @@ +package hep.dataforge.tables + +import org.junit.Assert.assertEquals +import org.junit.Test +import kotlin.math.floor + +class TablesKtTest { + val table = buildTable { + (1..99).forEach { + row( + "a" to (it.toDouble() / 100.0), + "b" to it, + "c" to (it.toDouble() / 2.0) + ) + } + } + + @Test + fun groupTest() { + val res = table.groupBy { floor(it["a"].double / 0.1) } + assertEquals(10, res.size) + } + + @Test + fun testReduction() { + val reduced = table.sumByStep("a", 0.1) + assertEquals(10, reduced.size()) + assertEquals(55, reduced.first()["b"].int) + } +} \ No newline at end of file diff --git a/dataforge-core/src/test/kotlin/hep/dataforge/values/ValueUtilsTest.groovy b/dataforge-core/src/test/kotlin/hep/dataforge/values/ValueUtilsTest.groovy new file mode 100644 index 00000000..f6188919 --- /dev/null +++ b/dataforge-core/src/test/kotlin/hep/dataforge/values/ValueUtilsTest.groovy @@ -0,0 +1,68 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.values + +import spock.lang.Specification + +import java.time.Instant + +/** + * Created by darksnake on 02-Oct-16. + */ +class ValueUtilsTest extends Specification { + def "IsBetween"() { + expect: + ValueUtils.isBetween(0.5, Value.of(false), Value.of(true)); + } + + def "ValueIO"() { + given: + + def timeValue = Value.of(Instant.now()); + def stringValue = Value.of("The string Ñ Ñ€ÑƒÑÑкими буквами"); + def listValue = Value.of(1d, 2d, 3d); + def booleanValue = Value.of(true); + def numberValue = Value.of(BigDecimal.valueOf(22.5d)); + when: + + //writing values + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + ValueUtils.writeValue(oos, timeValue); + ValueUtils.writeValue(oos, stringValue); + ValueUtils.writeValue(oos, listValue); + ValueUtils.writeValue(oos, booleanValue); + ValueUtils.writeValue(oos, numberValue); + + oos.flush() + byte[] bytes = baos.toByteArray(); + println "Serialized preview: \t" + new String(bytes) + + //reading values + ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + ObjectInputStream ois = new ObjectInputStream(bais); + + then: + + ValueUtils.readValue(ois) == timeValue; + ValueUtils.readValue(ois) == stringValue; + ValueUtils.readValue(ois) == listValue; + ValueUtils.readValue(ois) == booleanValue; + ValueUtils.readValue(ois) == numberValue; + + } +} diff --git a/dataforge-core/src/test/kotlin/hep/dataforge/workspace/WorkspaceTest.kt b/dataforge-core/src/test/kotlin/hep/dataforge/workspace/WorkspaceTest.kt new file mode 100644 index 00000000..3ee6ddae --- /dev/null +++ b/dataforge-core/src/test/kotlin/hep/dataforge/workspace/WorkspaceTest.kt @@ -0,0 +1,117 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.workspace + +import hep.dataforge.cache.CachePlugin +import hep.dataforge.context.Context +import hep.dataforge.context.Global +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.workspace.tasks.PipeTask +import hep.dataforge.workspace.tasks.TaskModel +import org.junit.Assert.assertEquals +import org.junit.BeforeClass +import org.junit.Test +import org.slf4j.LoggerFactory +import java.util.concurrent.atomic.AtomicInteger + +class WorkspaceTest { + + @Test//(timeout = 900) + fun testExecution() { + LoggerFactory.getLogger(javaClass).info("Starting execution test") + val res = wsp.runTask("test2", Meta.empty()) + res.computeAll() + assertEquals(6, res.getCheckedData("data_1", Number::class.java).get().toLong()) + assertEquals(8, res.getCheckedData("data_2", Number::class.java).get().toLong()) + assertEquals(10, res.getCheckedData("data_3", Number::class.java).get().toLong()) + } + + @Test + fun testCaching() { + counter.set(0) + wsp.context[CachePlugin::class.java]?.invalidate() + val res1 = wsp.runTask("test2", Meta.empty()) + val res2 = wsp.runTask("test2", Meta.empty()) + res1.computeAll() + res2.computeAll() + assertEquals(6, counter.get().toLong()) + val res3 = wsp.runTask("test2", MetaBuilder().putValue("a", 1)) + .getCheckedData("data_2", Number::class.java).get().toLong() + assertEquals(6, res3) + assertEquals(8, counter.get().toLong()) + } + + companion object { + private val counter = AtomicInteger() + private lateinit var wsp: Workspace + + @BeforeClass + @JvmStatic + fun setup() { + val context = Global.getContext("TEST").apply { + load(CachePlugin::class.java, MetaBuilder().setValue("fileCache.enabled", false)) + } + + + val task1 = object : PipeTask("test1", Number::class.java, Number::class.java) { + override fun buildModel(model: TaskModel.Builder, meta: Meta) { + model.data("*") + } + + override fun result(context: Context, name: String, input: Number, meta: Meta): Number { + try { + Thread.sleep(200) + } catch (e: InterruptedException) { + throw RuntimeException(e) + } + + counter.incrementAndGet() + return input.toInt() + meta.getInt("a", 2) + } + } + + val task2 = object : PipeTask("test2", Number::class.java, Number::class.java) { + override fun buildModel(model: TaskModel.Builder, meta: Meta) { + model.dependsOn("test1", meta) + } + + override fun result(context: Context, name: String, input: Number, meta: Meta): Number { + try { + Thread.sleep(200) + } catch (e: InterruptedException) { + throw RuntimeException(e) + } + + counter.incrementAndGet() + return input.toInt() * meta.getInt("b", 2) + } + } + + wsp = BasicWorkspace.Builder() + .apply { this.context = context } + .staticData("data_1", 1) + .staticData("data_2", 2) + .staticData("data_3", 3) + .task(task1) + .task(task2) + .build() + + } + + } +} \ No newline at end of file diff --git a/dataforge-core/src/test/resources/meta/template.xml b/dataforge-core/src/test/resources/meta/template.xml new file mode 100644 index 00000000..a6c3b2a6 --- /dev/null +++ b/dataforge-core/src/test/resources/meta/template.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/dataforge-core/src/test/resources/meta/templateData.xml b/dataforge-core/src/test/resources/meta/templateData.xml new file mode 100644 index 00000000..3d5d3048 --- /dev/null +++ b/dataforge-core/src/test/resources/meta/templateData.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + diff --git a/dataforge-gui/build.gradle b/dataforge-gui/build.gradle new file mode 100644 index 00000000..98d5657c --- /dev/null +++ b/dataforge-gui/build.gradle @@ -0,0 +1,23 @@ +//apply plugin: 'org.openjfx.javafxplugin' +// +//javafx { +// modules = [ 'javafx.controls', 'javafx.web' ] +//} + +description = "A tornadofx based kotlin library" + + +dependencies { + compile project(':dataforge-plots') + compile project(':dataforge-gui:dataforge-html') + compile 'org.controlsfx:controlsfx:8.40.14' + compile "no.tornado:tornadofx:1.7.17" + compile 'no.tornado:tornadofx-controlsfx:0.1.1' + compile group: 'org.fxmisc.richtext', name: 'richtextfx', version: '0.10.2' + compile 'org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.0.1' + + // optional dependency for JFreeChart + //compileOnly project(":dataforge-plots:plots-jfc") +} + + diff --git a/dataforge-gui/dataforge-html/build.gradle b/dataforge-gui/dataforge-html/build.gradle new file mode 100644 index 00000000..fa65adae --- /dev/null +++ b/dataforge-gui/dataforge-html/build.gradle @@ -0,0 +1,6 @@ +description = "An html rendering core and HTML output" + +dependencies { + compile project(':dataforge-core') + compile 'org.jetbrains.kotlinx:kotlinx-html-jvm:0.6.11' +} diff --git a/dataforge-gui/dataforge-html/src/main/kotlin/hep/dataforge/io/HTMLOutput.kt b/dataforge-gui/dataforge-html/src/main/kotlin/hep/dataforge/io/HTMLOutput.kt new file mode 100644 index 00000000..004c1991 --- /dev/null +++ b/dataforge-gui/dataforge-html/src/main/kotlin/hep/dataforge/io/HTMLOutput.kt @@ -0,0 +1,251 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.io + +import ch.qos.logback.classic.spi.ILoggingEvent +import hep.dataforge.childNodes +import hep.dataforge.context.Context +import hep.dataforge.description.NodeDescriptor +import hep.dataforge.description.ValueDescriptor +import hep.dataforge.io.envelopes.Envelope +import hep.dataforge.io.history.Record +import hep.dataforge.io.output.* +import hep.dataforge.meta.Meta +import hep.dataforge.meta.Metoid +import hep.dataforge.tables.Table +import hep.dataforge.values +import hep.dataforge.values.ValueType +import kotlinx.html.* +import org.w3c.dom.Node + +open class HTMLOutput(override val context: Context, private val consumer: TagConsumer<*>) : Output, TextOutput { + + fun appendHTML(meta: Meta = Meta.empty(), action: TagConsumer<*>.() -> Unit) { + if (meta.hasMeta("html")) { + consumer.div(classes = meta.getString("html.class")) { + consumer.action() + } + } else { + consumer.action() + } + } + + override fun renderText(text: String, vararg attributes: TextAttribute) { + appendHTML{ + span { + attributes.forEach { + classes += when(it){ + is TextColor -> "color: ${it.color}" + is TextStrong-> "font-weight: bold" + is TextEmphasis -> "font-style: italic" + } + } + +text + } + } + } + + private fun TagConsumer<*>.meta(meta: Meta) { + div(classes = "meta-node") { + p { + +meta.name + } + ul(classes = "meta-node-list") { + meta.childNodes.forEach { + li { + meta(it) + } + } + } + + ul(classes = "meta-values-list") { + meta.values.forEach { + li { + span { + style = "color:green" + +it.key + } + +": ${it.value}" + } + } + } + } + } + + private fun TagConsumer<*>.descriptor(descriptor: ValueDescriptor) { + div(classes = "descriptor-value") { + p(classes = "descripor-head") { + span { + style = "color:red" + +descriptor.name + } + if (descriptor.required) { + span { + style = "color:cyan" + +"(*) " + } + } + descriptor.type.firstOrNull()?.let { + +"[it]" + } + + if (descriptor.hasDefault()) { + val def = descriptor.default + if (def.type == ValueType.STRING) { + +" = " + span { + style = "color:green" + +"\"${def.string}\"" + } + } else { + +" = " + span { + style = "color:green" + +def.string + } + } + } + p(classes = "descriptor-info") { + +descriptor.info + } + } + } + } + + private fun TagConsumer<*>.descriptor(descriptor: NodeDescriptor) { + div(classes = "descriptor-node") { + p(classes = "descriptor-header") { + +descriptor.name + if (descriptor.required) { + span { + style = "color:cyan" + +"(*) " + } + } + } + p(classes = "descriptor-info") { + descriptor.info + } + div(classes = "descriptor-body") { + descriptor.childrenDescriptors().also { + if (it.isNotEmpty()) { + ul { + it.forEach { _, value -> + li { descriptor(value) } + } + } + } + } + + descriptor.valueDescriptors().also { + if (it.isNotEmpty()) { + ul { + it.forEach { _, value -> + li { descriptor(value) } + } + } + } + } + } + } + } + + + override fun render(obj: Any, meta: Meta) { + when (obj) { + is SelfRendered -> { + obj.render(this, meta) + } + is Node -> render(obj) + is Meta -> { + appendHTML(meta) { + div(classes = "meta") { + meta(obj) + } + } + } + is Table -> { + appendHTML(meta) { + table { + //table header + tr { + obj.format.names.forEach { + th { +it } + } + } + obj.rows.forEach {values-> + tr { + obj.format.names.forEach { + td { + +values[it].string + } + } + } + } + + } + } + } + is Envelope -> { + appendHTML(meta) { + div(classes = "envelope-meta") { + meta(obj.meta) + } + div(classes = "envelope-data") { + +obj.data.toString() + } + } + } + is ILoggingEvent -> { + appendHTML(meta) { + p { + //TODO fix logger + obj.message + } + } + } + is CharSequence, is Record -> { + appendHTML(meta) { + pre { + +obj.toString() + } + } + } + is ValueDescriptor -> { + appendHTML(meta) { + descriptor(obj) + } + } + is NodeDescriptor -> { + appendHTML(meta) { + descriptor(obj) + } + } + is Metoid -> { // render custom metoid + val renderType = obj.meta.getString("@output.type", "@default") + context.findService(OutputRenderer::class.java) { it.type == renderType } + ?.render(this@HTMLOutput, obj, meta) + ?: appendHTML(meta) { meta(obj.meta) } + } + } + } + + companion object { + const val HTML_MODE = "html" + } +} + diff --git a/dataforge-gui/dataforge-html/src/test/kotlin/hep/dataforge/io/HTMLOutputTest.kt b/dataforge-gui/dataforge-html/src/test/kotlin/hep/dataforge/io/HTMLOutputTest.kt new file mode 100644 index 00000000..fbe58747 --- /dev/null +++ b/dataforge-gui/dataforge-html/src/test/kotlin/hep/dataforge/io/HTMLOutputTest.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.io + +import hep.dataforge.context.Global +import hep.dataforge.description.DescriptorBuilder +import hep.dataforge.meta.buildMeta +import kotlinx.html.stream.appendHTML +import org.junit.Test + +class HTMLOutputTest{ + @Test + fun renderTest(){ + val output = HTMLOutput(Global, System.out.appendHTML(prettyPrint = true)).apply { + render( + DescriptorBuilder("test") + .value("a", info = "a value") + .build() + ) + render("Some text", buildMeta { "html.class" to "ddd" }) + } + + output.render("another text") + } +} \ No newline at end of file diff --git a/dataforge-gui/gui-demo/build.gradle b/dataforge-gui/gui-demo/build.gradle new file mode 100644 index 00000000..730449f8 --- /dev/null +++ b/dataforge-gui/gui-demo/build.gradle @@ -0,0 +1,21 @@ +plugins{ + id "application" + id "com.github.johnrengelman.shadow" version "2.0.1" +} +apply plugin: 'kotlin' + +if (!hasProperty('mainClass')) { + ext.mainClass = 'hep.dataforge.plots.demo.DemoApp'//"inr.numass.viewer.test.TestApp" +} + +mainClassName = mainClass + +description = "A demonstration for plots capabilities" + +dependencies { + compile project(':dataforge-plots:plots-jfc') + compile project(':dataforge-gui') +} + + + diff --git a/dataforge-gui/gui-demo/src/main/kotlin/hep/dataforge/plots/demo/DemoApp.kt b/dataforge-gui/gui-demo/src/main/kotlin/hep/dataforge/plots/demo/DemoApp.kt new file mode 100644 index 00000000..250db896 --- /dev/null +++ b/dataforge-gui/gui-demo/src/main/kotlin/hep/dataforge/plots/demo/DemoApp.kt @@ -0,0 +1,11 @@ +package hep.dataforge.plots.demo + +import javafx.application.Application +import tornadofx.* + +class DemoApp: App(DemoView::class) { +} + +fun main(args: Array) { + Application.launch(DemoApp::class.java,*args); +} \ No newline at end of file diff --git a/dataforge-gui/gui-demo/src/main/kotlin/hep/dataforge/plots/demo/DemoView.kt b/dataforge-gui/gui-demo/src/main/kotlin/hep/dataforge/plots/demo/DemoView.kt new file mode 100644 index 00000000..9dc2366d --- /dev/null +++ b/dataforge-gui/gui-demo/src/main/kotlin/hep/dataforge/plots/demo/DemoView.kt @@ -0,0 +1,112 @@ +package hep.dataforge.plots.demo + +import hep.dataforge.fx.plots.PlotContainer +import hep.dataforge.plots.data.DataPlot +import hep.dataforge.plots.jfreechart.JFreeChartFrame +import hep.dataforge.tables.Adapters +import hep.dataforge.values.Values +import javafx.beans.property.SimpleDoubleProperty +import javafx.collections.FXCollections +import javafx.collections.MapChangeListener +import javafx.collections.ObservableList +import javafx.scene.control.TabPane +import javafx.scene.control.TableView +import javafx.util.converter.DoubleStringConverter +import tornadofx.* + +class DemoView : View("Plot demonstration") { + + private val frame = JFreeChartFrame() + + + class PlotData() { + + val yErrProperty = SimpleDoubleProperty() + var yErr: Double? by yErrProperty + + val yProperty = SimpleDoubleProperty() + var y: Double? by yProperty + + val xProperty = SimpleDoubleProperty() + var x: Double? by xProperty + + fun toValues(): Values { + return Adapters.buildXYDataPoint(x ?: 0.0, y ?: 0.0, yErr ?: 0.0) + } + } + + //private val dataMap = FXCollections.observableHashMap>() + val dataMap = FXCollections.observableHashMap>().apply { + addListener { change: MapChangeListener.Change> -> + dataChanged(change.key) + } + }; + + lateinit var dataPane: TabPane; + + override val root = borderpane { + center = PlotContainer(frame).root + top { + toolbar { + val nameField = textfield() + button("+") { + action { + createDataSet(nameField.text) + } + } + } + } + left { + dataPane = tabpane() + + } + } + + fun createDataSet(plotName: String) { + val data = FXCollections.observableArrayList() + dataMap.put(plotName, data) + dataPane.tab(plotName) { + setOnClosed { + dataMap.remove(plotName) + } + borderpane { + top { + toolbar { + button("+") { + action { + data.add(PlotData()) + } + } + } + } + center { + tableview(data) { + isEditable = true + column("x", PlotData::x).useTextField(DoubleStringConverter()) + column("y", PlotData::y).useTextField(DoubleStringConverter()) + column("yErr", PlotData::yErr).useTextField(DoubleStringConverter()) + columnResizePolicy = TableView.CONSTRAINED_RESIZE_POLICY + onEditCommit { + dataChanged(plotName) + } + } + } + } + } + dataChanged(plotName) + } + + private fun dataChanged(plotName: String) { + synchronized(this) { + if (dataMap.containsKey(plotName)) { + if (frame.get(plotName) == null) { + frame.add(DataPlot(plotName)) + } + + (frame.get(plotName) as DataPlot).fillData(dataMap[plotName]!!.stream().map { it.toValues() }) + } else { + frame.remove(plotName) + } + } + } +} diff --git a/dataforge-gui/gui-demo/src/main/kotlin/hep/dataforge/plots/demo/JFreeChartTest.kt b/dataforge-gui/gui-demo/src/main/kotlin/hep/dataforge/plots/demo/JFreeChartTest.kt new file mode 100644 index 00000000..b22be5f4 --- /dev/null +++ b/dataforge-gui/gui-demo/src/main/kotlin/hep/dataforge/plots/demo/JFreeChartTest.kt @@ -0,0 +1,30 @@ +package hep.dataforge.plots.demo + +/** + * Created by darksnake on 29-Apr-17. + */ + +import hep.dataforge.plots.jfreechart.JFreeChartFrame +import javafx.application.Application +import javafx.scene.Scene +import javafx.scene.layout.BorderPane +import javafx.stage.Stage +import tornadofx.* + +class JFreeChartTest : App() { + + override fun start(stage: Stage) { + val root = BorderPane() + val node1 = JFreeChartFrame().fxNode + //val node2 = ChartViewer(JFreeChart("plot", XYPlot(null, NumberAxis(), NumberAxis(), XYLineAndShapeRenderer()))) + root.center = JFreeChartFrame().fxNode + val scene = Scene(root, 800.0, 600.0) + stage.title = "JFC test" + stage.scene = scene + stage.show() + } +} + +fun main(args: Array) { + Application.launch(JFreeChartTest::class.java, *args) +} diff --git a/dataforge-gui/gui-demo/src/main/kotlin/hep/dataforge/plots/demo/JFreeFXTest.java b/dataforge-gui/gui-demo/src/main/kotlin/hep/dataforge/plots/demo/JFreeFXTest.java new file mode 100644 index 00000000..62f93032 --- /dev/null +++ b/dataforge-gui/gui-demo/src/main/kotlin/hep/dataforge/plots/demo/JFreeFXTest.java @@ -0,0 +1,79 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.plots.demo; + +import hep.dataforge.meta.MetaBuilder; +import hep.dataforge.plots.data.DataPlot; +import hep.dataforge.plots.data.XYFunctionPlot; +import hep.dataforge.plots.jfreechart.JFreeChartFrame; +import hep.dataforge.tables.Adapters; +import hep.dataforge.tables.Table; +import hep.dataforge.tables.Tables; +import hep.dataforge.values.ValueMap; +import hep.dataforge.values.Values; +import javafx.application.Application; +import javafx.scene.Scene; +import javafx.scene.layout.BorderPane; +import javafx.stage.Stage; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Alexander Nozik + */ +public class JFreeFXTest extends Application { + + /** + * @param args the command line arguments + */ + public static void main(String[] args) { + launch(args); + } + + @Override + public void start(Stage stage) { + BorderPane root = new BorderPane(); + + JFreeChartFrame frame = new JFreeChartFrame(); + root.setCenter(frame.getFxNode()); + + XYFunctionPlot funcPlot = XYFunctionPlot.Companion.plot("func", 0.1, 4, 200, (x1) -> x1 * x1); + + frame.add(funcPlot); + + String[] names = {"myX", "myY", "myXErr", "myYErr"}; + + List data = new ArrayList<>(); + data.add(ValueMap.of(names, 0.5d, 0.2, 0.1, 0.1)); + data.add(ValueMap.of(names, 1d, 1d, 0.2, 0.5)); + data.add(ValueMap.of(names, 3d, 7d, 0, 0.5)); + Table ds = Tables.infer(data); + + DataPlot dataPlot = DataPlot.plot("dataPlot", ds, Adapters.buildXYAdapter("myX", "myY", "myXErr", "myYErr")); + + frame.getConfig().putNode(new MetaBuilder("yAxis").putValue("logScale", true)); + + frame.add(dataPlot); + + Scene scene = new Scene(root, 800, 600); + + stage.setTitle("my frame"); + stage.setScene(scene); + stage.show(); + } + +} diff --git a/dataforge-gui/gui-demo/src/main/kotlin/hep/dataforge/plots/demo/PlotTest.kt b/dataforge-gui/gui-demo/src/main/kotlin/hep/dataforge/plots/demo/PlotTest.kt new file mode 100644 index 00000000..f6a9fdac --- /dev/null +++ b/dataforge-gui/gui-demo/src/main/kotlin/hep/dataforge/plots/demo/PlotTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.plots.demo + +import hep.dataforge.buildContext +import hep.dataforge.configure +import hep.dataforge.fx.output.FXOutputManager +import hep.dataforge.fx.plots.group +import hep.dataforge.plots.data.DataPlot +import hep.dataforge.plots.data.XYFunctionPlot +import hep.dataforge.plots.jfreechart.JFreeChartPlugin +import hep.dataforge.plots.output.plotFrame +import hep.dataforge.tables.Adapters +import hep.dataforge.tables.Tables +import hep.dataforge.values.ValueMap +import hep.dataforge.values.Values +import java.util.* +import kotlin.concurrent.thread + + +/** + * @param args the command line arguments + */ + +fun main() { + + val context = buildContext("TEST", JFreeChartPlugin::class.java) { + output = FXOutputManager() + } + + val func = { x: Double -> Math.pow(x, 2.0) } + + val funcPlot = XYFunctionPlot.plot("func", 0.1, 4.0, 200, function = func) + + + val names = arrayOf("myX", "myY", "myXErr", "myYErr") + + val data = ArrayList() + data.add(ValueMap.of(names, 0.5, 0.2, 0.1, 0.1)) + data.add(ValueMap.of(names, 1.0, 1.0, 0.2, 0.5)) + data.add(ValueMap.of(names, 3.0, 7.0, 0, 0.5)) + val ds = Tables.infer(data) + + val dataPlot = DataPlot.plot("data.Plot", ds, Adapters.buildXYAdapter("myX", "myXErr", "myY", "myYErr")) + + context.plotFrame("test", stage = "test") { + configure { + "yAxis" to { + "type" to "log" + } + } + thread { + Thread.sleep(5000) + +dataPlot + } + +funcPlot + group("sub") { + +funcPlot + +dataPlot + } + } + + context.plotFrame("test1") { + +funcPlot + } + + + +} + + diff --git a/dataforge-gui/gui-demo/src/main/kotlin/hep/dataforge/plots/demo/SerializationTest.kt b/dataforge-gui/gui-demo/src/main/kotlin/hep/dataforge/plots/demo/SerializationTest.kt new file mode 100644 index 00000000..bfd268bc --- /dev/null +++ b/dataforge-gui/gui-demo/src/main/kotlin/hep/dataforge/plots/demo/SerializationTest.kt @@ -0,0 +1,62 @@ +package hep.dataforge.plots.demo + +import hep.dataforge.buildContext +import hep.dataforge.fx.output.FXOutputManager +import hep.dataforge.plots.data.DataPlot +import hep.dataforge.plots.data.XYFunctionPlot +import hep.dataforge.plots.jfreechart.JFreeChartPlugin +import hep.dataforge.plots.output.plotFrame +import hep.dataforge.tables.Adapters +import hep.dataforge.tables.Tables +import hep.dataforge.values.ValueMap +import hep.dataforge.values.Values +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import java.util.* + +/** + * @param args the command line arguments + */ + +fun main() { + + val context = buildContext("TEST", JFreeChartPlugin::class.java) { + output = FXOutputManager() + } + + val func = { x: Double -> Math.pow(x, 2.0) } + + val funcPlot = XYFunctionPlot.plot("func", 0.1, 4.0, 200, function = func) + + + val names = arrayOf("myX", "myY", "myXErr", "myYErr") + + val data = ArrayList() + data.add(ValueMap.of(names, 0.5, 0.2, 0.1, 0.1)) + data.add(ValueMap.of(names, 1.0, 1.0, 0.2, 0.5)) + data.add(ValueMap.of(names, 3.0, 7.0, 0, 0.5)) + val ds = Tables.infer(data) + + val dataPlot = DataPlot.plot("dataPlot", ds, Adapters.buildXYAdapter("myX", "myXErr", "myY", "myYErr")) + + + context.plotFrame("before"){ + +dataPlot + } + + val baos = ByteArrayOutputStream(); + + ObjectOutputStream(baos).use { + it.writeObject(dataPlot) + } + + val bais = ByteArrayInputStream(baos.toByteArray()); + val restored: DataPlot = ObjectInputStream(bais).readObject() as DataPlot + + context.plotFrame("after"){ + +restored + } + +} \ No newline at end of file diff --git a/dataforge-gui/gui-demo/src/main/kotlin/hep/dataforge/plots/demo/TableDisplayTest.kt b/dataforge-gui/gui-demo/src/main/kotlin/hep/dataforge/plots/demo/TableDisplayTest.kt new file mode 100644 index 00000000..872b42aa --- /dev/null +++ b/dataforge-gui/gui-demo/src/main/kotlin/hep/dataforge/plots/demo/TableDisplayTest.kt @@ -0,0 +1,26 @@ +package hep.dataforge.plots.demo + +import hep.dataforge.fx.table.TableDisplay +import hep.dataforge.tables.ListTable +import javafx.application.Application +import tornadofx.* + + +class TableDisplayTest: App(TableDisplayView::class) { +} + +class TableDisplayView: View() { + + override val root = TableDisplay().apply { + table = ListTable.Builder("x","y") + .row(1,1) + .row(2,2) + .row(3,3) + .build() + }.root + +} + +fun main(args: Array) { + Application.launch(TableDisplayTest::class.java,*args); +} diff --git a/dataforge-gui/gui-workspace/build.gradle b/dataforge-gui/gui-workspace/build.gradle new file mode 100644 index 00000000..5edc60ee --- /dev/null +++ b/dataforge-gui/gui-workspace/build.gradle @@ -0,0 +1,36 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + +plugins{ + id "application" + id "com.github.johnrengelman.shadow" version "2.0.4" +} +apply plugin: 'kotlin' + +if (!hasProperty('mainClass')) { + ext.mainClass = 'hep.dataforge.plots.demo.DemoApp'//"inr.numass.viewer.test.TestApp" +} + +mainClassName = mainClass + +description = "A gui for workspace creation and manipulation" + +dependencies { + compile project(':dataforge-gui') +} + + + diff --git a/dataforge-gui/gui-workspace/src/main/kotlin/hep/dataforge/fx/workspace/ContextEditorView.kt b/dataforge-gui/gui-workspace/src/main/kotlin/hep/dataforge/fx/workspace/ContextEditorView.kt new file mode 100644 index 00000000..62c1ee50 --- /dev/null +++ b/dataforge-gui/gui-workspace/src/main/kotlin/hep/dataforge/fx/workspace/ContextEditorView.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.fx.workspace + diff --git a/dataforge-gui/src/main/kotlin/hep/dataforge/fx/ClipBoardTools.kt b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/ClipBoardTools.kt new file mode 100644 index 00000000..5e60ce6f --- /dev/null +++ b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/ClipBoardTools.kt @@ -0,0 +1,2 @@ +package hep.dataforge.fx + diff --git a/dataforge-gui/src/main/kotlin/hep/dataforge/fx/FXPlugin.kt b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/FXPlugin.kt new file mode 100644 index 00000000..80c14d2b --- /dev/null +++ b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/FXPlugin.kt @@ -0,0 +1,121 @@ +package hep.dataforge.fx + +import hep.dataforge.context.* +import hep.dataforge.description.ValueDef +import hep.dataforge.description.ValueDefs +import hep.dataforge.meta.Meta +import hep.dataforge.values.ValueType.BOOLEAN +import javafx.application.Application +import javafx.application.Platform +import javafx.collections.FXCollections +import javafx.collections.ObservableSet +import javafx.collections.SetChangeListener +import javafx.scene.Scene +import javafx.stage.Stage +import tornadofx.* + +/** + * Plugin holding JavaFX application instance and its root stage + * Created by darksnake on 28-Oct-16. + */ +@PluginDef(name = "fx", group = "hep.dataforge", info = "JavaFX window manager") +@ValueDefs( + ValueDef(key = "consoleMode", type = [BOOLEAN], def = "true", info = "Start an application surrogate if actual application not found") +) +class FXPlugin(meta: Meta = Meta.empty()) : BasicPlugin(meta) { + + private val stages: ObservableSet = FXCollections.observableSet() + + val consoleMode: Boolean = meta.getBoolean("consoleMode", true) + + init { + if (consoleMode) { + stages.addListener(SetChangeListener { change -> + if (change.set.isEmpty()) { + Platform.setImplicitExit(true) + } else { + Platform.setImplicitExit(false) + } + }) + } + } + /** + * Wait for application and toolkit to start if needed + */ + override fun attach(context: Context) { + super.attach(context) + if (FX.getApplication(DefaultScope) == null) { + if (consoleMode) { + Thread { + context.logger.debug("Starting FX application surrogate") + launch() + }.apply { + name = "${context.name} FX application thread" + start() + } + + while (!FX.initialized.get()) { + if (Thread.interrupted()) { + throw RuntimeException("Interrupted application start") + } + } + Platform.setImplicitExit(false) + } else { + throw RuntimeException("FX Application not defined") + } + } + } + + /** + * Define an application to use in this context + */ + fun setApp(app: Application, stage: Stage) { + FX.registerApplication(DefaultScope, app, stage) + } + + /** + * Show something in a pre-constructed stage. Blocks thread until stage is created + * + * @param cons + */ + fun display(action: Stage.() -> Unit) { + runLater { + val stage = Stage() + stage.initOwner(FX.primaryStage) + stage.action() + stage.show() + stages.add(stage) + stage.setOnCloseRequest { stages.remove(stage) } + } + } + + fun display(component: UIComponent, width: Double = 800.0, height: Double = 600.0) { + display { + scene = Scene(component.root, width, height) + title = component.title + } + } + + class Factory : PluginFactory() { + override val type: Class = FXPlugin::class.java + + override fun build(meta: Meta): Plugin { + return FXPlugin(meta) + } + } + +} + +/** + * An application surrogate without any visible primary stage + */ +class ApplicationSurrogate : App() { + override fun start(stage: Stage) { + FX.registerApplication(this, stage) + FX.initialized.value = true + } +} + +fun Context.display(width: Double = 800.0, height: Double = 600.0, component: () -> UIComponent) { + this.load().display(component(), width, height) +} \ No newline at end of file diff --git a/dataforge-gui/src/main/kotlin/hep/dataforge/fx/KodexFX.kt b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/KodexFX.kt new file mode 100644 index 00000000..dd3b322e --- /dev/null +++ b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/KodexFX.kt @@ -0,0 +1,181 @@ +package hep.dataforge.fx + +import hep.dataforge.context.Global +import hep.dataforge.goals.Coal +import hep.dataforge.goals.Goal +import javafx.application.Platform +import javafx.beans.property.BooleanProperty +import javafx.beans.property.SimpleDoubleProperty +import javafx.beans.property.SimpleStringProperty +import javafx.scene.Node +import javafx.scene.control.ToggleButton +import javafx.scene.image.Image +import javafx.scene.image.ImageView +import javafx.scene.layout.Region +import javafx.scene.paint.Color +import javafx.stage.Stage +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.GlobalScope +import tornadofx.* +import java.util.* +import java.util.concurrent.Executor +import java.util.function.BiConsumer +import kotlin.collections.HashMap + +val dfIcon: Image = Image(Global::class.java.getResourceAsStream("/img/df.png")) +val dfIconView = ImageView(dfIcon) + +val uiExecutor = Executor { command -> Platform.runLater(command) } + +class GoalMonitor { + val titleProperty = SimpleStringProperty("") + var title: String by titleProperty + + val messageProperty = SimpleStringProperty("") + var message: String by messageProperty + + val progressProperty = SimpleDoubleProperty(1.0) + var progress by progressProperty + + val maxProgressProperty = SimpleDoubleProperty(1.0) + var maxProgress by maxProgressProperty + + fun updateProgress(progress: Double, maxProgress: Double) { + this.progress = progress + this.maxProgress = maxProgress + } +} + +private val monitors: MutableMap> = HashMap(); + +/** + * Get goal monitor for give UI component + */ +fun UIComponent.getMonitor(id: String): GoalMonitor { + synchronized(monitors) { + return monitors.getOrPut(this) { + HashMap() + }.getOrPut(id) { + GoalMonitor() + } + } +} + +/** + * Clean up monitor + */ +private fun removeMonitor(component: UIComponent, id: String) { + synchronized(monitors) { + monitors[component]?.remove(id) + if (monitors[component]?.isEmpty() == true) { + monitors.remove(component) + } + } +} + +fun UIComponent.runGoal(id: String, scope: CoroutineScope = GlobalScope, block: suspend GoalMonitor.() -> R): Coal { + val monitor = getMonitor(id); + return Coal(scope, Collections.emptyList(), id) { + monitor.progress = -1.0 + block(monitor).also { + monitor.progress = 1.0 + } + }.apply { + onComplete { _, _ -> removeMonitor(this@runGoal, id) } + run() + } +} + +infix fun Goal.ui(action: (R) -> Unit): Goal { + return this.apply { + onComplete(uiExecutor, BiConsumer { res, ex -> + if (res != null) { + action(res); + } + //Always print stack trace if goal is evaluated on UI + ex?.printStackTrace() + }) + } +} + +infix fun Goal.except(action: (Throwable) -> Unit): Goal { + return this.apply { + onComplete(uiExecutor, BiConsumer { _, ex -> + if (ex != null) { + action(ex); + } + }) + } +} + +/** + * Add a listener that performs some update action on any window size change + * + * @param component + * @param action + */ +fun addWindowResizeListener(component: Region, action: Runnable) { + component.widthProperty().onChange { action.run() } + component.heightProperty().onChange { action.run() } +} + +fun colorToString(color: Color): String { + return String.format("#%02X%02X%02X", + (color.red * 255).toInt(), + (color.green * 255).toInt(), + (color.blue * 255).toInt()) +} + +/** + * Check if current thread is FX application thread to avoid runLater from + * UI thread. + * + * @param r + */ +fun runNow(r: Runnable) { + if (Platform.isFxApplicationThread()) { + r.run() + } else { + Platform.runLater(r) + } +} + +/** + * A display window that could be toggled + */ +class ToggleUIComponent( + val component: UIComponent, + val owner: Node, + val toggle: BooleanProperty) { + val stage: Stage by lazy { + val res = component.modalStage ?: component.openWindow(owner = owner.scene.window) + ?: throw RuntimeException("Can'topen window for $component") + res.showingProperty().onChange { + toggle.set(it) + } + res + } + + init { + toggle.onChange { + if (it) { + stage.show() + } else { + stage.hide() + } + } + + } +} + +fun UIComponent.bindWindow(owner: Node, toggle: BooleanProperty): ToggleUIComponent { + return ToggleUIComponent(this, owner, toggle) +} + +fun UIComponent.bindWindow(button: ToggleButton): ToggleUIComponent { + return bindWindow(button, button.selectedProperty()) +} + +//fun TableView.table(table: Table){ +// +//} \ No newline at end of file diff --git a/dataforge-gui/src/main/kotlin/hep/dataforge/fx/StatesFX.kt b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/StatesFX.kt new file mode 100644 index 00000000..a8678b36 --- /dev/null +++ b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/StatesFX.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.fx + +import hep.dataforge.states.State +import hep.dataforge.values.Value +import javafx.beans.property.* +import tornadofx.* + +fun State.asProperty(): ObjectProperty { + val property = SimpleObjectProperty(null, this.name, this.value) + onChange { property.set(it) } + property.onChange { this.set(it) } + return property +} + +fun State.asStringProperty(): StringProperty { + val property = SimpleStringProperty(null, this.name, this.value.string) + onChange { property.set(it.string) } + property.onChange { this.set(it) } + return property +} + +fun State.asBooleanProperty(): BooleanProperty { + val property = SimpleBooleanProperty(null, this.name, this.value.boolean) + onChange { property.set(it.boolean) } + property.onChange { this.set(it) } + return property +} + +fun State.asDoubleProperty(): DoubleProperty { + val property = SimpleDoubleProperty(null, this.name, this.value.double) + onChange { property.set(it.double) } + property.onChange { this.set(it) } + return property +} \ No newline at end of file diff --git a/dataforge-gui/src/main/kotlin/hep/dataforge/fx/fragments/LogFragment.kt b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/fragments/LogFragment.kt new file mode 100644 index 00000000..16433f03 --- /dev/null +++ b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/fragments/LogFragment.kt @@ -0,0 +1,144 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.fx.fragments + +import ch.qos.logback.classic.Logger +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.Appender +import ch.qos.logback.core.AppenderBase +import hep.dataforge.context.Global +import hep.dataforge.fx.output.FXTextOutput +import org.slf4j.LoggerFactory +import tornadofx.* +import java.io.PrintStream +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.util.function.BiConsumer + +/** + * @author Alexander Nozik + */ +class LogFragment : Fragment("DataForge output log") { + + private val timeFormatter = DateTimeFormatter.ISO_LOCAL_TIME + + private var formatter: BiConsumer? = null + private val loggerFormatter = { text: FXTextOutput, eventObject: ILoggingEvent -> + val style = when (eventObject.level.toString()) { + "DEBUG" -> "-fx-fill: green" + "WARN" -> "-fx-fill: orange" + "ERROR" -> "-fx-fill: red" + else -> "-fx-fill: black" + } + + runLater { + val time = Instant.ofEpochMilli(eventObject.timeStamp) + text.append(timeFormatter.format(LocalDateTime.ofInstant(time, ZoneId.systemDefault())) + ": ") + + text.appendColored(eventObject.loggerName, "gray") + + text.appendStyled(eventObject.formattedMessage.replace("\n", "\n\t") + "\r\n", style) + } + + } + + val outputPane = FXTextOutput(Global).apply { + setMaxLines(2000) + } + + private val logAppender: Appender = object : AppenderBase() { + override fun append(eventObject: ILoggingEvent) { + synchronized(this) { + loggerFormatter(outputPane, eventObject) + } + } + }.apply { + name = FX_LOG_APPENDER_NAME + start() + } + + override val root = outputPane.view.root + private var stdHooked = false + + + /** + * Set custom formatter for text + * + * @param formatter + */ + fun setFormatter(formatter: BiConsumer) { + this.formatter = formatter + } + + fun appendText(text: String) { + if (formatter == null) { + outputPane.renderText(text) + } else { + formatter!!.accept(outputPane, text) + } + } + + fun appendLine(text: String) { + appendText(text + "\r\n") + } + + fun addRootLogHandler() { + addLogHandler(Logger.ROOT_LOGGER_NAME) + } + + fun addLogHandler(loggerName: String) { + addLogHandler(LoggerFactory.getLogger(loggerName)) + } + + fun addLogHandler(logger: org.slf4j.Logger) { + if (logger is Logger) { + logger.addAppender(logAppender) + } else { + LoggerFactory.getLogger(javaClass).error("Failed to add log handler. Only Logback loggers are supported.") + } + + } + + /** + * Redirect copy of std streams to this console window + */ + @Deprecated("") + fun hookStd() { + if (!stdHooked) { + System.setOut(PrintStream(outputPane.stream)) + System.setErr(PrintStream(outputPane.stream)) + stdHooked = true + } + } + + /** + * Restore default std streams + */ + @Deprecated("") + fun restoreStd() { + if (stdHooked) { + System.setOut(STD_OUT) + System.setErr(STD_ERR) + stdHooked = false + } + } + + override fun onDelete() { + super.onDelete() + logAppender.stop() + } + + companion object { + + private val STD_OUT = System.out + private val STD_ERR = System.err + + private const val FX_LOG_APPENDER_NAME = "hep.dataforge.fx" + } + +} diff --git a/dataforge-gui/src/main/kotlin/hep/dataforge/fx/meta/ConfigEditor.kt b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/meta/ConfigEditor.kt new file mode 100644 index 00000000..b9929083 --- /dev/null +++ b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/meta/ConfigEditor.kt @@ -0,0 +1,165 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.fx.meta + +import hep.dataforge.description.NodeDescriptor +import hep.dataforge.fx.dfIconView +import hep.dataforge.fx.values.ValueChooserFactory +import hep.dataforge.meta.Configuration +import javafx.scene.control.* +import javafx.scene.control.cell.TextFieldTreeTableCell +import javafx.scene.layout.Priority +import javafx.scene.paint.Color +import javafx.scene.text.Text +import org.controlsfx.glyphfont.Glyph +import tornadofx.* + +/** + * FXML Controller class + * + * @author Alexander Nozik + */ +class ConfigEditor(val configuration: Configuration, title: String = "Configuration editor", val descriptor: NodeDescriptor? = null) : Fragment(title = title, icon = dfIconView) { + + val filter: (ConfigFX) -> Boolean = { cfg -> + when (cfg) { + is ConfigFXNode -> !(cfg.descriptor?.tags?.contains(NO_CONFIGURATOR_TAG) ?: false) + is ConfigFXValue -> !(cfg.descriptor?.tags?.contains(NO_CONFIGURATOR_TAG) ?: false) + } + } + + override val root = borderpane { + center = treetableview { + root = ConfigTreeItem(ConfigFXRoot(configuration, descriptor)) + root.isExpanded = true + sortMode = TreeSortMode.ALL_DESCENDANTS + columnResizePolicy = TreeTableView.CONSTRAINED_RESIZE_POLICY + column("Name") { param: TreeTableColumn.CellDataFeatures -> param.value.value.nameProperty } + .setCellFactory { + object : TextFieldTreeTableCell() { + override fun updateItem(item: String?, empty: Boolean) { + super.updateItem(item, empty) + contextMenu?.items?.removeIf { it.text == "Remove" } + if (!empty) { + if (treeTableRow.item != null) { + textFillProperty().bind(treeTableRow.item.hasValueProperty.objectBinding { + if (it == true) { + Color.BLACK + } else { + Color.GRAY + } + }) + if (treeTableRow.treeItem.value.hasValueProperty.get()) { + contextmenu { + item("Remove") { + action { + treeTableRow.item.remove() + } + } + } + } + } + } + } + } + } + + column("Value") { param: TreeTableColumn.CellDataFeatures -> + param.value.valueProperty() + }.setCellFactory { + ValueCell() + } + + column("Description") { param: TreeTableColumn.CellDataFeatures -> param.value.value.descriptionProperty } + .setCellFactory { param: TreeTableColumn -> + val cell = TreeTableCell() + val text = Text() + cell.graphic = text + cell.prefHeight = Control.USE_COMPUTED_SIZE + text.wrappingWidthProperty().bind(param.widthProperty()) + text.textProperty().bind(cell.itemProperty()) + cell + } + } + } + + private fun showNodeDialog(): String? { + val dialog = TextInputDialog() + dialog.title = "Node name selection" + dialog.contentText = "Enter a name for new node: " + dialog.headerText = null + + val result = dialog.showAndWait() + return result.orElse(null) + } + + private fun showValueDialog(): String? { + val dialog = TextInputDialog() + dialog.title = "Value name selection" + dialog.contentText = "Enter a name for new value: " + dialog.headerText = null + + val result = dialog.showAndWait() + return result.orElse(null) + } + + private inner class ValueCell : TreeTableCell() { + + public override fun updateItem(item: ConfigFX?, empty: Boolean) { + if (!empty) { + if (item != null) { + when (item) { + is ConfigFXValue -> { + text = null + val chooser = ValueChooserFactory.build(item.valueProperty, item.descriptor) { + item.value = it + } + graphic = chooser.node + } + is ConfigFXNode -> { + text = null + graphic = hbox { + button("node", Glyph("FontAwesome", "PLUS_CIRCLE")) { + hgrow = Priority.ALWAYS + maxWidth = Double.POSITIVE_INFINITY + action { + showNodeDialog()?.let { + item.addNode(it) + } + } + } + button("value", Glyph("FontAwesome", "PLUS_SQUARE")) { + hgrow = Priority.ALWAYS + maxWidth = Double.POSITIVE_INFINITY + action { + showValueDialog()?.let { + item.addValue(it) + } + } + } + } + } + } + + } else{ + text = null + graphic = null + } + } else { + text = null + graphic = null + } + } + + } + + companion object { + /** + * The tag not to display node or value in configurator + */ + const val NO_CONFIGURATOR_TAG = "nocfg" + } +} diff --git a/dataforge-gui/src/main/kotlin/hep/dataforge/fx/meta/ConfigFX.kt b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/meta/ConfigFX.kt new file mode 100644 index 00000000..700185e4 --- /dev/null +++ b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/meta/ConfigFX.kt @@ -0,0 +1,283 @@ +package hep.dataforge.fx.meta + +import hep.dataforge.description.NodeDescriptor +import hep.dataforge.description.ValueDescriptor +import hep.dataforge.meta.ConfigChangeListener +import hep.dataforge.meta.Configuration +import hep.dataforge.meta.Meta +import hep.dataforge.names.Name +import hep.dataforge.nullable +import hep.dataforge.orElse +import hep.dataforge.values.Value +import javafx.beans.binding.StringBinding +import javafx.beans.property.SimpleObjectProperty +import javafx.beans.property.SimpleStringProperty +import javafx.beans.value.ObservableBooleanValue +import javafx.beans.value.ObservableStringValue +import javafx.collections.FXCollections +import javafx.collections.ObservableList +import javafx.scene.control.TreeItem +import tornadofx.* +import kotlin.streams.toList + +class ConfigTreeItem(configFX: ConfigFX) : TreeItem(configFX) { + init { + this.children.bind(value.children) { ConfigTreeItem(it) } + } + + override fun isLeaf(): Boolean = value is ConfigFXValue +} + + +/** + * A node, containing relative representation of configuration node and description + * Created by darksnake on 01-May-17. + */ +sealed class ConfigFX(name: String) { + + val nameProperty = SimpleStringProperty(name) + val name by nameProperty + + val parentProperty = SimpleObjectProperty() + val parent by parentProperty + + abstract val hasValueProperty: ObservableBooleanValue + //abstract val hasDefaultProperty: ObservableBooleanValue + + abstract val descriptionProperty: ObservableStringValue + + abstract val children: ObservableList + + /** + * remove itself from parent + */ + abstract fun remove() + + abstract fun invalidate() +} + + +/** + * Tree item for node + * Created by darksnake on 30-Apr-17. + */ +open class ConfigFXNode(name: String, parent: ConfigFXNode? = null) : ConfigFX(name) { + + final override val hasValueProperty = parentProperty.booleanBinding(nameProperty) { + it?.configuration?.hasMeta(this.name) ?: false + } + + + /** + * A descriptor that could be manually set to the node + */ + val descriptorProperty = SimpleObjectProperty() + + /** + * Actual descriptor which holds value inferred from parrent + */ + private val actualDescriptor = objectBinding(descriptorProperty, parentProperty, nameProperty) { + value ?: parent?.descriptor?.getNodeDescriptor(name) + } + + val descriptor: NodeDescriptor? by actualDescriptor + + val configProperty = SimpleObjectProperty() + + private val actualConfig = objectBinding(configProperty, parentProperty, nameProperty) { + value ?: parent?.configuration?.getMetaList(name)?.firstOrNull() + } + + val configuration: Configuration? by actualConfig + + final override val descriptionProperty: ObservableStringValue = stringBinding(actualDescriptor) { + value?.info ?: "" + } + + override val children: ObservableList = FXCollections.observableArrayList() + + init { + parentProperty.set(parent) + hasValueProperty.onChange { + parent?.hasValueProperty?.invalidate() + } + invalidate() + } + + /** + * Get existing configuration node or create and attach new one + * + * @return + */ + private fun getOrBuildNode(): Configuration { + return configuration ?: if (parent == null) { + throw RuntimeException("The configuration for root node is note defined") + } else { + parent.getOrBuildNode().requestNode(name) + } + } + + fun addValue(name: String) { + getOrBuildNode().setValue(name, Value.NULL) + } + + fun setValue(name: String, value: Value) { + getOrBuildNode().setValue(name, value) + } + + fun removeValue(valueName: String) { + configuration?.removeValue(valueName) + children.removeIf { it.name == name } + } + + fun addNode(name: String) { + getOrBuildNode().requestNode(name) + } + + fun removeNode(name: String) { + configuration?.removeNode(name) + } + + override fun remove() { + //FIXME does not work on multinodes + parent?.removeNode(name) + invalidate() + } + + final override fun invalidate() { + actualDescriptor.invalidate() + actualConfig.invalidate() + hasValueProperty.invalidate() + + val nodeNames = ArrayList() + val valueNames = ArrayList() + + configuration?.apply { + nodeNames.addAll(this.nodeNames.toList()) + valueNames.addAll(this.valueNames.toList()) + } + + descriptor?.apply { + nodeNames.addAll(childrenDescriptors().keys) + valueNames.addAll(valueDescriptors().keys) + } + + //removing old values + children.removeIf { !(valueNames.contains(it.name) || nodeNames.contains(it.name)) } + + valueNames.forEach { name -> + children.find { it.name == name }?.invalidate().orElse { + children.add(ConfigFXValue(name, this)) + } + } + + nodeNames.forEach { name -> + children.find { it.name == name }?.invalidate().orElse { + children.add(ConfigFXNode(name, this)) + } + } + children.sortBy { it.name } + } + + fun updateValue(path: Name, value: Value?) { + when { + path.length == 0 -> kotlin.error("Path never could be empty when updating value") + path.length == 1 -> { + val hasDescriptor = descriptor?.getValueDescriptor(path) != null + if (value == null && !hasDescriptor) { + //removing the value if it is present + children.removeIf { it.name == path.unescaped } + } else { + //invalidating value if it is present + children.find { it is ConfigFXValue && it.name == path.unescaped }?.invalidate().orElse { + //adding new node otherwise + children.add(ConfigFXValue(path.unescaped, this)) + } + } + } + path.length > 1 -> children.filterIsInstance().find { it.name == path.first.unescaped }?.updateValue(path.cutFirst(), value) + } + } + + fun updateNode(path: Name, list: List) { + when { + path.isEmpty() -> invalidate() + path.length == 1 -> { + val hasDescriptor = descriptor?.getNodeDescriptor(path.unescaped) != null + if (list.isEmpty() && !hasDescriptor) { + children.removeIf { it.name == path.unescaped } + } else { + children.find { it is ConfigFXNode && it.name == path.unescaped }?.invalidate().orElse { + children.add(ConfigFXNode(path.unescaped, this)) + } + } + } + else -> children.filterIsInstance().find { it.name == path.first.toString() }?.updateNode(path.cutFirst(), list) + } + } +} + +class ConfigFXRoot(rootConfig: Configuration, rootDescriptor: NodeDescriptor? = null) : ConfigFXNode(rootConfig.name), ConfigChangeListener { + + init { + configProperty.set(rootConfig) + descriptorProperty.set(rootDescriptor) + rootConfig.addListener(this) + invalidate() + } + + override fun notifyValueChanged(name: Name, oldItem: Value?, newItem: Value?) { + updateValue(name, newItem) + } + + override fun notifyNodeChanged(nodeName: Name, oldItem: List, newItem: List) { + updateNode(nodeName, newItem) + } +} + + +/** + * Created by darksnake on 01-May-17. + */ +class ConfigFXValue(name: String, parent: ConfigFXNode) : ConfigFX(name) { + + init { + parentProperty.set(parent) + } + + override val hasValueProperty = parentProperty.booleanBinding(nameProperty) { + it?.configuration?.hasValue(this.name) ?: false + } + + + override val children: ObservableList = FXCollections.emptyObservableList() + + val descriptor: ValueDescriptor? = parent.descriptor?.getValueDescriptor(name); + + override val descriptionProperty: ObservableStringValue = object : StringBinding() { + override fun computeValue(): String { + return descriptor?.info ?: "" + } + } + + val valueProperty = parentProperty.objectBinding(nameProperty) { + parent.configuration?.optValue(name).nullable ?: descriptor?.default + } + + var value: Value + set(value) { + parent?.setValue(name, value) + } + get() = valueProperty.value ?: Value.NULL + + + override fun remove() { + parent?.removeValue(name) + invalidate() + } + + override fun invalidate() { + valueProperty.invalidate() + hasValueProperty.invalidate() + } +} diff --git a/dataforge-gui/src/main/kotlin/hep/dataforge/fx/meta/ConfigValueProperty.kt b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/meta/ConfigValueProperty.kt new file mode 100644 index 00000000..4caf1660 --- /dev/null +++ b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/meta/ConfigValueProperty.kt @@ -0,0 +1,113 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.fx.meta + +import hep.dataforge.meta.ConfigChangeListener +import hep.dataforge.meta.Configuration +import hep.dataforge.values.Value +import javafx.beans.InvalidationListener +import javafx.beans.binding.ObjectBinding +import javafx.beans.property.ObjectProperty +import javafx.beans.value.ChangeListener +import javafx.beans.value.ObservableValue + +/** + * Configuration value represented as JavaFX property. Using slightly modified + * JavaFx ObjectPropertyBase code. + * + * @author Alexander Nozik + */ +class ConfigValueProperty(val config: Configuration, val valueName: String, val getter: (Value) -> T) : ObjectProperty() { + + private val cfgListener = ConfigChangeListener { name, oldItem, newItem -> + if (valueName == name.unescaped && oldItem != newItem) { + cachedValue.invalidate() + } + } + + /** + * current value cached to avoid call of configuration parsing + */ + private val cachedValue: ObjectBinding = object : ObjectBinding() { + override fun computeValue(): T { + return getter.invoke(config.getValue(valueName)) + } + } + + init { + //adding a weak observer to configuration + config.addListener(false, cfgListener) + } + + override fun getBean(): Configuration? { + return config + } + + override fun getName(): String? { + return valueName + } + + override fun addListener(listener: InvalidationListener?) { + cachedValue.addListener(listener) + } + + override fun addListener(listener: ChangeListener?) { + cachedValue.addListener(listener) + } + + override fun isBound(): Boolean { + return false + } + + override fun get(): T { + return cachedValue.get() + } + + override fun removeListener(listener: InvalidationListener?) { + return cachedValue.removeListener(listener) + } + + override fun removeListener(listener: ChangeListener?) { + return cachedValue.removeListener(listener) + } + + override fun unbind() { + throw UnsupportedOperationException("Configuration property could not be unbound") + } + + override fun set(value: T) { + config.setValue(valueName, value) + //invalidation not required since it obtained automatically via listener + } + + override fun bind(observable: ObservableValue?) { + throw UnsupportedOperationException("Configuration property could not be bound") + } + + + /** + * Returns a string representation of this `ObjectPropertyBase` + * object. + * + * @return a string representation of this `ObjectPropertyBase` + * object. + */ + override fun toString(): String { + val bean = bean + val name = name + val result = StringBuilder("ConfigurationValueProperty [") + if (bean != null) { + result.append("bean: ").append(bean).append(", ") + } + if (name != null && name != "") { + result.append("name: ").append(name).append(", ") + } + result.append("value: ").append(get()) + result.append("]") + return result.toString() + } + +} \ No newline at end of file diff --git a/dataforge-gui/src/main/kotlin/hep/dataforge/fx/meta/MetaViewer.kt b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/meta/MetaViewer.kt new file mode 100644 index 00000000..3532e4f6 --- /dev/null +++ b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/meta/MetaViewer.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.fx.meta + +import hep.dataforge.fx.dfIconView +import hep.dataforge.meta.Meta +import hep.dataforge.toList +import hep.dataforge.values.Value +import javafx.beans.property.SimpleStringProperty +import javafx.beans.property.StringProperty +import javafx.scene.control.TreeItem +import javafx.scene.control.TreeSortMode +import javafx.scene.control.TreeTableView +import tornadofx.* +import java.util.stream.Stream + +internal sealed class Item { + abstract val titleProperty: StringProperty + abstract val valueProperty: StringProperty +} + +internal class MetaItem(val meta: Meta) : Item() { + override val titleProperty = SimpleStringProperty(meta.name) + override val valueProperty: StringProperty = SimpleStringProperty("") +} + +internal class ValueItem(name: String, val value: Value) : Item() { + override val titleProperty = SimpleStringProperty(name) + override val valueProperty: StringProperty = SimpleStringProperty(value.string) +} + +open class MetaViewer(val meta: Meta, title: String = "Meta viewer: ${meta.name}") : Fragment(title, dfIconView) { + override val root = borderpane { + center { + treetableview { + isShowRoot = false + root = TreeItem(MetaItem(meta)) + populate { + val value: Item = it.value + when (value) { + is MetaItem -> { + val meta = value.meta + Stream.concat( + meta.nodeNames.flatMap { meta.getMetaList(it).stream() }.map { MetaItem(it) }, + meta.valueNames.map { ValueItem(it, meta.getValue(it)) } + ).toList() + } + is ValueItem -> null + } + } + root.isExpanded = true + sortMode = TreeSortMode.ALL_DESCENDANTS + columnResizePolicy = TreeTableView.CONSTRAINED_RESIZE_POLICY + column("Name", Item::titleProperty) + column("Value", Item::valueProperty) + } + } + } +} \ No newline at end of file diff --git a/dataforge-gui/src/main/kotlin/hep/dataforge/fx/output/FXDisplay.kt b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/output/FXDisplay.kt new file mode 100644 index 00000000..e2d522b3 --- /dev/null +++ b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/output/FXDisplay.kt @@ -0,0 +1,78 @@ +package hep.dataforge.fx.output + +import hep.dataforge.context.Context +import hep.dataforge.fx.FXPlugin +import hep.dataforge.fx.dfIcon +import javafx.geometry.Side +import javafx.scene.control.Tab +import javafx.scene.control.TabPane +import javafx.scene.image.ImageView +import javafx.scene.layout.BorderPane +import tornadofx.* + +/** + * An interface to produce border panes for content. + */ +interface FXDisplay { + fun display(stage: String, name: String, action: BorderPane.() -> Unit) +} + +fun buildDisplay(context: Context): FXDisplay { + return TabbedFXDisplay().also { + context.load(FXPlugin::class.java).display(it) + } +} + +class TabbedFXDisplay : View("DataForge display", ImageView(dfIcon)), FXDisplay { + + private val stages: MutableMap = HashMap(); + + private val stagePane = TabPane().apply { + side = Side.LEFT + } + override val root = borderpane { + center = stagePane + } + + override fun display(stage: String, name: String, action: BorderPane.() -> Unit) { + runLater { + stages.getOrPut(stage) { + TabbedStage(stage).apply { + val stageFragment = this + stagePane.tab(stage) { + content = stageFragment.root + isClosable = false + } + } + }.apply { + action.invoke(getTab(name).pane) + } + } + } + + + inner class TabbedStage(val stage: String) : Fragment(stage) { + private var tabs: MutableMap = HashMap() + val tabPane = TabPane() + + override val root = borderpane { + center = tabPane + } + + fun getTab(tabName: String): DisplayTab { + return tabs.getOrPut(tabName) { DisplayTab(tabName) } + } + + + inner class DisplayTab(val name: String) { + private val tab: Tab = Tab(name) + val pane: BorderPane = BorderPane() + + init { + tab.content = pane + tabPane.tabs.add(tab) + } + } + } + +} diff --git a/dataforge-gui/src/main/kotlin/hep/dataforge/fx/output/FXDumbOutput.kt b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/output/FXDumbOutput.kt new file mode 100644 index 00000000..020fd46c --- /dev/null +++ b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/output/FXDumbOutput.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.fx.output + +import hep.dataforge.context.Context +import hep.dataforge.meta.Meta +import javafx.scene.Parent +import tornadofx.* + +class FXDumbOutput(context:Context): FXOutput(context) { + + override val view: Fragment by lazy{ + object: Fragment() { + override val root: Parent + get() = TODO("not implemented") //To change initializer of created properties use File | Settings | File Templates. + } + } + + override fun render(obj: Any, meta: Meta) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + +} \ No newline at end of file diff --git a/dataforge-gui/src/main/kotlin/hep/dataforge/fx/output/FXOuptutManager.kt b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/output/FXOuptutManager.kt new file mode 100644 index 00000000..1f58a888 --- /dev/null +++ b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/output/FXOuptutManager.kt @@ -0,0 +1,201 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.fx.output + +import hep.dataforge.context.BasicPlugin +import hep.dataforge.context.Context +import hep.dataforge.context.PluginDef +import hep.dataforge.context.PluginTag +import hep.dataforge.fx.FXPlugin +import hep.dataforge.fx.dfIconView +import hep.dataforge.io.OutputManager +import hep.dataforge.io.OutputManager.Companion.OUTPUT_STAGE_KEY +import hep.dataforge.io.output.Output +import hep.dataforge.meta.Meta +import hep.dataforge.plots.PlotFactory +import hep.dataforge.plots.Plottable +import hep.dataforge.tables.Table +import javafx.beans.binding.ListBinding +import javafx.collections.FXCollections +import javafx.collections.MapChangeListener +import javafx.collections.ObservableList +import javafx.collections.ObservableMap +import javafx.geometry.Side +import javafx.scene.control.Tab +import javafx.scene.layout.BorderPane +import tornadofx.* + +/** + * Provide a map which is synchronized on UI thread + */ +private fun ObservableMap.ui(): ObservableMap { + val res = FXCollections.observableHashMap() + this.addListener { change: MapChangeListener.Change -> + runLater { + if (change.wasRemoved()) { + res.remove(change.key) + } + if (change.wasAdded()) { + res[change.key] = change.valueAdded + } + } + } + return res +} + +class OutputContainer(val context: Context, val meta: Meta) : + Fragment(title = "[${context.name}] DataForge output container", icon = dfIconView) { + + private val stages: ObservableMap = FXCollections.observableHashMap() + + private val uiStages = stages.ui() + + override val root = tabpane { + //tabs for each stage + side = Side.LEFT + tabs.bind(uiStages) { key, value -> + Tab(key).apply { + content = value.root + isClosable = false + } + } + } + + private fun buildStageContainer(): OutputStageContainer { + return if (meta.getBoolean("treeStage", false)) { + TreeStageContainer() + } else { + TabbedStageContainer() + } + } + + fun get(meta: Meta): Output { + synchronized(this) { + val stage = meta.getString(OUTPUT_STAGE_KEY, "@default") + val container = stages.getOrPut(stage) { buildStageContainer() } + return container.get(meta) + } + } + + /** + * Create a new output + */ + private fun buildOutput(type: String, meta: Meta): FXOutput { + return when { + type.startsWith(Plottable.PLOTTABLE_TYPE) -> if (context.get(PlotFactory::class.java) != null) { + FXPlotOutput(context, meta) + } else { + context.logger.error("Plot output not defined in the context") + FXTextOutput(context) + } + type.startsWith(Table.TABLE_TYPE) -> FXTableOutput(context) + else -> FXWebOutput(context) + } + } + + private abstract inner class OutputStageContainer : Fragment() { + val outputs: ObservableMap = FXCollections.observableHashMap() + + fun get(meta: Meta): FXOutput { + synchronized(outputs) { + val name = meta.getString(OutputManager.OUTPUT_NAME_KEY) + val type = meta.getString(OutputManager.OUTPUT_TYPE_KEY, Output.TEXT_TYPE) + return outputs.getOrPut(name) { buildOutput(type, meta) } + } + } + } + + private inner class TreeStageContainer : OutputStageContainer() { + override val root = borderpane { + left { + // name list + //TODO replace by tree + listview { + items = object : ListBinding() { + init { + bind(outputs) + } + + override fun computeValue(): ObservableList { + return outputs.keys.toList().observable() + } + } + onUserSelect { + this@borderpane.center = outputs[it]!!.view.root + } + } + } + } + } + + private inner class TabbedStageContainer : OutputStageContainer() { + + private val uiOutputs = outputs.ui() + + override val root = tabpane { + //tabs for each output + side = Side.TOP + tabs.bind(uiOutputs) { key, value -> + Tab(key).apply { + content = value.view.root + isClosable = false + } + } + } + } +} + +@PluginDef( + name = "output.fx", + dependsOn = ["hep.dataforge.fx", "hep.dataforge.plots"], + info = "JavaFX based output manager" +) +class FXOutputManager( + meta: Meta = Meta.empty(), + viewConsumer: Context.(OutputContainer) -> Unit = { getOrLoad(FXPlugin::class.java).display(it) } +) : OutputManager, BasicPlugin(meta) { + + override val tag = PluginTag(name = "output.fx", dependsOn = *arrayOf("hep.dataforge:fx")) + + override fun attach(context: Context) { + super.attach(context) + //Check if FX toolkit is started + context.load() + } + + private val container: OutputContainer by lazy { + OutputContainer(context, meta).also { viewConsumer.invoke(context, it) } + } + + val root get() = container + + override fun get(meta: Meta): Output { + return root.get(meta) + } + + companion object { + + @JvmStatic + fun display(): FXOutputManager = FXOutputManager() + + /** + * Display in existing BorderPane + */ + fun display(pane: BorderPane, meta: Meta = Meta.empty()): FXOutputManager = + FXOutputManager(meta) { pane.center = it.root } + } +} \ No newline at end of file diff --git a/dataforge-gui/src/main/kotlin/hep/dataforge/fx/output/FXOutput.kt b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/output/FXOutput.kt new file mode 100644 index 00000000..d1861844 --- /dev/null +++ b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/output/FXOutput.kt @@ -0,0 +1,129 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.fx.output + +import hep.dataforge.context.Context +import hep.dataforge.description.Descriptors +import hep.dataforge.fx.plots.PlotContainer +import hep.dataforge.fx.table.TableDisplay +import hep.dataforge.io.output.Output +import hep.dataforge.meta.Configurable +import hep.dataforge.meta.Configuration +import hep.dataforge.meta.Meta +import hep.dataforge.names.Name +import hep.dataforge.plots.* +import hep.dataforge.plots.output.PlotOutput +import hep.dataforge.tables.Table +import hep.dataforge.useValue +import tornadofx.* + +/** + * An output container represented as FX fragment. The view is initialized lazily to avoid problems with toolkit initialization. + */ +abstract class FXOutput(override val context: Context) : Output { + abstract val view: Fragment +} + +/** + * A specialized output for tables. Pushing new table replaces the old one + */ +class FXTableOutput(context: Context) : FXOutput(context) { + val tableDisplay: TableDisplay by lazy { TableDisplay() } + + override val view: Fragment = object : Fragment() { + override val root = borderpane { + //TODO add meta display + center = tableDisplay.root + } + } + + + override fun render(obj: Any, meta: Meta) { + if (obj is Table) { + runLater { + tableDisplay.table = obj + } + } else { + logger.error("Can't represent ${obj.javaClass} as Table") + } + } + +} + +class FXPlotOutput(context: Context, meta: Meta = Meta.empty()) : FXOutput(context), PlotOutput, Configurable { + + override val frame: PlotFrame by lazy { + context.getOrLoad(PlotFactory::class.java).build(meta.getMetaOrEmpty("frame")) + } + + val container: PlotContainer by lazy { PlotContainer(frame as FXPlotFrame) } + + override val view: Fragment by lazy { + object : Fragment() { + override val root = borderpane { + center = container.root + } + } + } + + override fun getConfig(): Configuration = frame.config + + override fun render(obj: Any, meta: Meta) { + runLater { + if (!meta.isEmpty) { + if (!frame.config.isEmpty) { + logger.warn("Overriding non-empty frame configuration") + } + frame.configure(meta) + // Use descriptor hidden field to update root plot container description + meta.useValue("@descriptor") { + frame.plots.descriptor = Descriptors.forReference("plot", it.string) + } + } + when (obj) { + is PlotFrame -> { + frame.configure(obj.config) + frame.plots.descriptor = obj.plots.descriptor + frame.addAll(obj.plots) + frame.plots.configure(obj.plots.config) + + obj.plots.addListener(object : PlotListener { + + override fun dataChanged(caller: Plottable, path: Name, before: Plottable?, after: Plottable?) { + if (before != after) { + frame.plots[path] = after + } + } + + override fun metaChanged(caller: Plottable, path: Name, plot: Plottable) { + //frame.plots.metaChanged(caller,path) + } + }) + } + is Plottable -> { + frame.add(obj) + } + is Iterable<*> -> { + frame.addAll(obj.filterIsInstance()) + } + else -> { + logger.error("Can't represent ${obj.javaClass} as Plottable") + } + } + } + } +} \ No newline at end of file diff --git a/dataforge-gui/src/main/kotlin/hep/dataforge/fx/output/FXTextOutput.kt b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/output/FXTextOutput.kt new file mode 100644 index 00000000..e9c8a59d --- /dev/null +++ b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/output/FXTextOutput.kt @@ -0,0 +1,216 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.fx.output + +import hep.dataforge.context.Context +import hep.dataforge.io.output.StreamOutput +import hep.dataforge.io.output.TextAttribute +import hep.dataforge.io.output.TextColor +import hep.dataforge.io.output.TextOutput +import hep.dataforge.meta.Meta +import javafx.beans.property.BooleanProperty +import javafx.beans.property.IntegerProperty +import javafx.beans.property.SimpleIntegerProperty +import javafx.collections.ObservableList +import javafx.scene.layout.AnchorPane +import org.fxmisc.richtext.InlineCssTextArea +import tornadofx.* +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.OutputStream +import kotlin.math.max + +/** + * OutputPane for formatted data + * + * @author Alexander Nozik + */ +class FXTextOutput(context: Context) : FXOutput(context), TextOutput { + + private val textArea = InlineCssTextArea() + + private val maxLinesProperty = SimpleIntegerProperty(-1) + + /** + * Tab stop positions + */ + private val tabstops: ObservableList? = null + + /** + * current tab stop + */ + private var currentTab = 0 + + private val tabSize: Int + get() = max(getTabStop(currentTab) - textArea.caretColumn, 2) + + val isEmpty: Boolean + get() = textArea.text.isEmpty() + + val stream: OutputStream + get() = object : ByteArrayOutputStream(1024) { + @Synchronized + @Throws(IOException::class) + override fun flush() { + val text = toString() + if (text.isEmpty()) { + return + } + append(text) + reset() + } + } + + private val output = StreamOutput(context, stream) + + override val view : Fragment by lazy { + object: Fragment() { + override val root = anchorpane(textArea){ + AnchorPane.setBottomAnchor(textArea, 5.0) + AnchorPane.setTopAnchor(textArea, 5.0) + AnchorPane.setLeftAnchor(textArea, 5.0) + AnchorPane.setRightAnchor(textArea, 5.0) + } + } + } + + init { + textArea.isEditable = false + } + + fun setWrapText(wrapText: Boolean) { + textArea.isWrapText = wrapText + } + + fun wrapTextProperty(): BooleanProperty { + return textArea.wrapTextProperty() + } + + fun maxLinesProperty(): IntegerProperty { + return maxLinesProperty + } + + fun setMaxLines(maxLines: Int) { + this.maxLinesProperty.set(maxLines) + } + + /** + * Append a text using given css style. Automatically detect newlines and tabs + * @param text + * @param style + */ + @Synchronized + private fun append(str: String, style: String) { + // Unifying newlines + val text = str.replace("\r\n", "\n") + + runLater { + if (text.contains("\n")) { + val lines = text.split("\n".toRegex()).toTypedArray() + for (i in 0 until lines.size - 1) { + append(lines[i].trim { it <= ' ' }, style) + newline() + } + if(!lines.isEmpty()) { + append(lines[lines.size - 1], style) + } + if (text.endsWith("\n")) { + newline() + } + } else if (text.contains("\t")) { + val tabs = text.split("\t".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + for (i in 0 until tabs.size - 1) { + append(tabs[i], style) + tab() + } + if (tabs.isNotEmpty()) { + append(tabs[tabs.size - 1], style) + } + } else if (style.isEmpty()) { + textArea.appendText(text) + } else { + textArea.appendText(text)//(ReadOnlyStyledDocument.fromString(t, style)) + } + } + } + + /** + * Append tabulation + */ + @Synchronized + private fun tab() { + runLater { + currentTab++ + // textArea.appendText("\t"); + for (i in 0 until tabSize) { + textArea.appendText(" ") + } + } + } + + private fun countLines(): Int { + return textArea.text.chars().filter { value: Int -> value == '\n'.toInt() }.count().toInt() + } + + /** + * Append newLine + */ + @Synchronized + fun newline() { + runLater { + while (maxLinesProperty.get() > 0 && countLines() >= maxLinesProperty.get()) { + //FIXME bad way to count and remove lines + textArea.replaceText(0, textArea.text.indexOf("\n") + 1, "") + } + currentTab = 0 + textArea.appendText("\r\n") + + } + } + + private fun getTabStop(num: Int): Int { + return when { + tabstops == null -> num * DEFAULT_TAB_STOP_SIZE + tabstops.size < num -> tabstops[tabstops.size - 1] + (num - tabstops.size) * DEFAULT_TAB_STOP_SIZE + else -> tabstops[num] + } + } + + fun append(text: String) { + append(text, "") + } + + fun appendColored(text: String, color: String) { + append(text, "-fx-fill: $color;") + } + + fun appendLine(line: String) { + append(line.trim { it <= ' ' }, "") + newline() + } + + fun appendStyled(text: String, style: String) { + append(text, style) + } + + override fun render(obj: Any, meta: Meta) { + //TODO replace by custom rendering + output.render(obj, meta) + } + + override fun renderText(text: String, vararg attributes: TextAttribute) { + val style = StringBuilder() + attributes.find { it is TextColor }?.let { + style.append("-fx-fill: ${(it as TextColor).color};") + } + append(text, style.toString()) + } + + companion object { + + private const val DEFAULT_TAB_STOP_SIZE = 15 + } +} diff --git a/dataforge-gui/src/main/kotlin/hep/dataforge/fx/output/FXWebOutput.kt b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/output/FXWebOutput.kt new file mode 100644 index 00000000..70b2c1a0 --- /dev/null +++ b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/output/FXWebOutput.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.fx.output + +import hep.dataforge.context.Context +import hep.dataforge.context.Global +import hep.dataforge.io.HTMLOutput +import hep.dataforge.io.output.TextAttribute +import hep.dataforge.io.output.TextOutput +import hep.dataforge.meta.Meta +import javafx.scene.Parent +import javafx.scene.web.WebView +import kotlinx.html.body +import kotlinx.html.consumers.onFinalize +import kotlinx.html.dom.append +import kotlinx.html.dom.createHTMLDocument +import kotlinx.html.dom.serialize +import kotlinx.html.head +import kotlinx.html.html +import tornadofx.* + +/** + * An output wrapping html view + */ +class FXWebOutput(context: Context) : FXOutput(context), TextOutput { + + private val webView: WebView by lazy { WebView() } + + + override val view: Fragment by lazy { + object : Fragment() { + override val root: Parent = borderpane { + center = webView + } + } + } + + + private val document = createHTMLDocument().html { + head { } + body { } + } + + private val node = document.getElementsByTagName("body").item(0) + + private val appender = node.append.onFinalize { from, _ -> + runLater { + webView.engine.loadContent(from.ownerDocument.serialize(true)) + } + } + + val out = HTMLOutput(Global, appender) + + override fun render(obj: Any, meta: Meta) = out.render(obj, meta) + + override fun renderText(text: String, vararg attributes: TextAttribute) = out.renderText(text, *attributes) +} + diff --git a/dataforge-gui/src/main/kotlin/hep/dataforge/fx/plots/PlotContainer.kt b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/plots/PlotContainer.kt new file mode 100644 index 00000000..3d3edcfb --- /dev/null +++ b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/plots/PlotContainer.kt @@ -0,0 +1,303 @@ +package hep.dataforge.fx.plots + +import hep.dataforge.description.Descriptors +import hep.dataforge.description.NodeDescriptor +import hep.dataforge.fx.dfIconView +import hep.dataforge.fx.meta.ConfigEditor +import hep.dataforge.fx.table.TableDisplay +import hep.dataforge.meta.Meta +import hep.dataforge.names.Name +import hep.dataforge.plots.* +import hep.dataforge.plots.data.DataPlot +import javafx.beans.property.SimpleBooleanProperty +import javafx.beans.property.SimpleDoubleProperty +import javafx.geometry.Orientation +import javafx.geometry.Pos +import javafx.scene.Node +import javafx.scene.Scene +import javafx.scene.control.TreeItem +import javafx.scene.layout.Priority +import javafx.scene.layout.StackPane +import javafx.scene.layout.VBox +import javafx.scene.paint.Color +import javafx.scene.text.Font +import javafx.scene.text.TextAlignment +import javafx.stage.Stage +import tornadofx.* +import java.util.* +import kotlin.collections.HashMap + +class PlotContainer(val frame: FXPlotFrame) : Fragment(icon = dfIconView), PlotListener { + + private val configWindows = HashMap() + private val dataWindows = HashMap() + + + private val sideBarExpandedProperty = SimpleBooleanProperty(false) + var sideBarExpanded by sideBarExpandedProperty + + private val sideBarPositionProperty = SimpleDoubleProperty(0.7) + var sideBarPoistion by sideBarPositionProperty + + val progressProperty = SimpleDoubleProperty(1.0) + var progress by progressProperty + + private lateinit var sidebar: VBox + + private val treeRoot = fillTree(frame.plots) + + override val root = borderpane { + center { + splitpane(orientation = Orientation.HORIZONTAL) { + stackpane { + borderpane { + minHeight = 300.0 + minWidth = 300.0 + center = frame.fxNode + } + button { + graphicTextGap = 0.0 + opacity = 0.4 + textAlignment = TextAlignment.JUSTIFY + StackPane.setAlignment(this, Pos.TOP_RIGHT) + font = Font.font("System Bold", 12.0) + action { + sideBarExpanded = !sideBarExpanded; + } + sideBarExpandedProperty.addListener { _, _, expanded -> + if (expanded) { + setDividerPosition(0, sideBarPoistion); + } else { + setDividerPosition(0, 1.0); + } + } + textProperty().bind( + sideBarExpandedProperty.stringBinding { + if (it == null || it) { + ">>" + } else { + "<<" + } + } + ) + } + progressindicator(progressProperty) { + maxWidth = 50.0 + prefWidth = 50.0 + visibleWhen(progressProperty.lessThan(1.0)) + } + } + sidebar = vbox { + button(text = "Frame config") { + minWidth = 0.0 + maxWidth = Double.MAX_VALUE + action { + displayConfigurator("Plot frame configuration", frame, Descriptors.forType("plotFrame", frame::class)) + } + } + treeview { + minWidth = 0.0 + root = treeRoot + vgrow = Priority.ALWAYS + + //cell format + cellFormat { item -> + graphic = hbox { + hgrow = Priority.ALWAYS + checkbox(item.title) { + minWidth = 0.0 + if (item == frame.plots) { + text = "<<< All plots >>>" + } + isSelected = item.config.getBoolean("visible", true) + selectedProperty().addListener { _, _, newValue -> + item.config.setValue("visible", newValue) + } + + + if (frame is XYPlotFrame) { + frame.getActualColor(getFullName(this@cellFormat.treeItem)).ifPresent { + textFill = Color.valueOf(it.string) + } + } else if (item.config.hasValue("color")) { + textFill = Color.valueOf(item.config.getString("color")) + } + + item.config.addListener { name, _, newItem -> + when (name.unescaped) { + "title" -> text = if (newItem == null) { + item.title + } else { + newItem.string + } + "color" -> textFill = if (newItem == null) { + Color.BLACK + } else { + try { + Color.valueOf(newItem.string) + } catch (ex: Exception) { + Color.BLACK + } + } + "visible" -> isSelected = newItem?.boolean ?: true + } + } + + contextmenu { + if (item is DataPlot) { + item("Show data") { + action { + displayData(item) + } + } + } else if (item is PlotGroup) { + item("Show all") { + action { item.forEach { it.configureValue("visible", true) } } + } + item("Hide all") { + action { item.forEach { it.configureValue("visible", false) } } + } + } + if (!this@cellFormat.treeItem.isLeaf) { + item("Sort") { + action { this@cellFormat.treeItem.children.sortBy { it.value.title } } + } + } + } + } + + pane { + hgrow = Priority.ALWAYS + } + + button("...") { + minWidth = 0.0 + action { + displayConfigurator(item.title + " configuration", item, Descriptors.forType("plot", item::class)) + } + } + + + } + text = null; + } + + } + } + + dividers[0].position = 1.0 + + dividers[0].positionProperty().onChange { + if (it < 0.9) { + sideBarPositionProperty.set(it) + } + sideBarExpanded = it < 0.99 + } + + this@borderpane.widthProperty().onChange { + if (sideBarExpanded) { + dividers[0].position = sideBarPoistion + } else { + dividers[0].position = 1.0 + } + } + } + } + } + + init { + frame.plots.addListener(this, false) + } + + /** + * Data change listener. Attached always to root plot group + */ + override fun dataChanged(caller: Plottable, path: Name, before: Plottable?, after: Plottable?) { + fun TreeItem.findItem(relativePath: Name): TreeItem? { + return when { + relativePath.isEmpty() -> this + relativePath.length == 1 -> children.find { it.value.name == relativePath.unescaped } + else -> findItem(relativePath.first)?.findItem(relativePath.cutFirst()) + } + } + + val item = treeRoot.findItem(path) + + if (after == null && item != null) { + // remove item + item.parent.children.remove(item) + } else if (after != null && item == null) { + treeRoot.findItem(path.cutLast())?.children?.add(fillTree(after)) + ?: kotlin.error("Parent tree item should exist at the moment") + } + } + + override fun metaChanged(caller: Plottable, path: Name, plot: Plottable) { + //do nothing for now + //TODO update colors etc + } + + fun addToSideBar(index: Int, vararg nodes: Node) { + sidebar.children.addAll(index, Arrays.asList(*nodes)) + } + + fun addToSideBar(vararg nodes: Node) { + sidebar.children.addAll(Arrays.asList(*nodes)) + } + + + /** + * Display configurator in separate scene + * + * @param config + * @param desc + */ + private fun displayConfigurator(header: String, obj: hep.dataforge.meta.Configurable, desc: NodeDescriptor) { + configWindows.getOrPut(obj) { + Stage().apply { + scene = Scene(ConfigEditor(obj.config, "Configuration editor", desc).root) + height = 400.0 + width = 400.0 + title = header + setOnCloseRequest { configWindows.remove(obj) } + initOwner(root.scene.window) + } + }.apply { + show() + toFront() + } + } + + private fun displayData(plot: DataPlot) { + dataWindows.getOrPut(plot) { + Stage().apply { + scene = Scene(TableDisplay().also { it.table = PlotUtils.extractData(plot, Meta.empty()) }.root) + height = 400.0 + width = 400.0 + title = "Data: ${plot.title}" + setOnCloseRequest { dataWindows.remove(plot) } + initOwner(root.scene.window) + } + }.apply { + show() + toFront() + } + } + + private fun fillTree(plot: Plottable): TreeItem { + val item = TreeItem(plot) + if (plot is PlotGroup) { + item.children.setAll(plot.map { fillTree(it) }) + } + item.isExpanded = true + return item + } + + private fun getFullName(item: TreeItem): Name { + return if (item.parent == null || item.parent.value.name.isEmpty()) { + Name.of(item.value.name) + } else { + getFullName(item.parent) + item.value.name + } + } +} \ No newline at end of file diff --git a/dataforge-gui/src/main/kotlin/hep/dataforge/fx/plots/PlotExtensions.kt b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/plots/PlotExtensions.kt new file mode 100644 index 00000000..f7353380 --- /dev/null +++ b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/plots/PlotExtensions.kt @@ -0,0 +1,33 @@ +package hep.dataforge.fx.plots + +import hep.dataforge.plots.PlotFrame +import hep.dataforge.plots.PlotGroup +import hep.dataforge.plots.Plottable +import hep.dataforge.plots.data.DataPlot +import hep.dataforge.values.Values + +fun PlotFrame.plot(plot: Plottable): Plottable { + this.add(plot); + return plot; +} + +operator fun PlotFrame.plusAssign(plot: Plottable) { + this.add(plot) +} + +fun PlotGroup.plot(plot: Plottable): Plottable { + this.add(plot); + return plot; +} + +operator fun PlotGroup.plusAssign(plot: Plottable) { + this.add(plot) +} + +fun PlotFrame.group(name: String, action: PlotGroup.() -> Unit) { + this += PlotGroup(name).apply(action); +} + +operator fun DataPlot.plusAssign(point: Values) { + this.append(point) +} diff --git a/dataforge-gui/src/main/kotlin/hep/dataforge/fx/table/MutableTable.kt b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/table/MutableTable.kt new file mode 100644 index 00000000..a3678700 --- /dev/null +++ b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/table/MutableTable.kt @@ -0,0 +1,13 @@ +package hep.dataforge.fx.table + +import hep.dataforge.tables.TableFormat +import javafx.collections.FXCollections + +class MutableTable(val tableFormat: TableFormat) : Iterable { + + val rows = FXCollections.observableArrayList(); + + override fun iterator(): Iterator { + return rows.iterator() + } +} \ No newline at end of file diff --git a/dataforge-gui/src/main/kotlin/hep/dataforge/fx/table/MutableValues.kt b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/table/MutableValues.kt new file mode 100644 index 00000000..cdfc648b --- /dev/null +++ b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/table/MutableValues.kt @@ -0,0 +1,43 @@ +package hep.dataforge.fx.table + +import hep.dataforge.values.Value +import hep.dataforge.values.ValueMap +import hep.dataforge.values.Values +import javafx.beans.property.ObjectProperty +import javafx.beans.property.SimpleObjectProperty + +/** + * A mutable counterpart of {@link Values}. Does not inherit Values because Values contract states it is immutable + */ +class MutableValues() { + private val valueMap: MutableMap> = LinkedHashMap(); + + /** + * Construct mutable values from regular one + */ + constructor(values: Values) : this() { + valueMap.putAll(values.asMap().mapValues { SimpleObjectProperty(it.value) }); + } + + /** + * Get a JavaFX property corresponding to given key + */ + fun getProperty(key: String): ObjectProperty { + return valueMap.computeIfAbsent(key) { SimpleObjectProperty(Value.NULL) } + } + + operator fun set(key: String, value: Any) { + getProperty(key).set(Value.of(value)) + } + + operator fun get(key: String): Value { + return getProperty(key).get() + } + + /** + * Convert this MutableValues to regular Values (data is copied so subsequent changes do not affect resulting object) + */ + fun toValues(): Values { + return ValueMap.ofMap(valueMap.mapValues { it.value.get() }) + } +} \ No newline at end of file diff --git a/dataforge-gui/src/main/kotlin/hep/dataforge/fx/table/TableDisplay.kt b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/table/TableDisplay.kt new file mode 100644 index 00000000..478f55cb --- /dev/null +++ b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/table/TableDisplay.kt @@ -0,0 +1,110 @@ +package hep.dataforge.fx.table + +import hep.dataforge.fx.dfIconView +import hep.dataforge.tables.Table +import hep.dataforge.values.Value +import hep.dataforge.values.ValueType +import javafx.beans.property.SimpleObjectProperty +import javafx.scene.input.Clipboard +import javafx.scene.input.ClipboardContent +import org.controlsfx.control.spreadsheet.* +import tornadofx.* + +/** + * Table display fragment + */ +class TableDisplay(title: String? = null) : Fragment(title = title, icon = dfIconView) { + + val tableProperty = SimpleObjectProperty() + var table: Table? by tableProperty + + + private fun buildCell(row: Int, column: Int, value: Value): SpreadsheetCell { + return when (value.type) { + ValueType.NUMBER -> SpreadsheetCellType.DOUBLE.createCell(row, column, 1, 1, value.double) + else -> SpreadsheetCellType.STRING.createCell(row, column, 1, 1, value.string) + } + } + + private val spreadsheet = CustomSpreadSheetView().apply { + isEditable = false +// isShowColumnHeader = false + } + + override val root = borderpane { + top = toolbar { + button("Export as text") { + action(::export) + } + } + center = spreadsheet; + } + + init { + tableProperty.onChange { + runLater { + spreadsheet.grid = buildGrid(it) + } + } + } + + private fun buildGrid(table: Table?): Grid? { + return table?.let { + GridBase(table.size(), table.format.count()).apply { + val format = table.format; + + columnHeaders.setAll(format.names.asList()) +// rows += format.names.asList().observable(); + + (0 until table.size()).forEach { i -> + rows += (0 until format.count()) + .map { j -> buildCell(i, j, table.get(format.names[j], i)) } + .observable() + } + } + } + } + + private fun export() { + table?.let { table -> + chooseFile("Save table data to...", emptyArray(), mode = FileChooserMode.Save).firstOrNull()?.let { + // if(!it.exists()){ +// it.createNewFile() +// } + + it.printWriter().use { writer -> + writer.println(table.format.names.joinToString(separator = "\t")) + table.forEach { values -> + writer.println(table.format.names.map { values[it] }.joinToString(separator = "\t")) + } + writer.flush() + } + } + } + } + + class CustomSpreadSheetView : SpreadsheetView() { + override fun copyClipboard() { + val posList = selectionModel.selectedCells + + val columns = posList.map { it.column }.distinct().sorted() + val rows = posList.map { it.row }.distinct().sorted() + + + //building text + val text = rows.joinToString(separator = "\n") { row -> + columns.joinToString(separator = "\t") { column -> + grid.rows[row][column].text + } + } + + //TODO add HTML binding + + val content = ClipboardContent() + content.putString(text); +// content.put(DataFormat("SpreadsheetView"), list) + Clipboard.getSystemClipboard().setContent(content) + } + //TODO add pasteClipboard + } +} diff --git a/dataforge-gui/src/main/kotlin/hep/dataforge/fx/test/FXOutputManagerTest.kt b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/test/FXOutputManagerTest.kt new file mode 100644 index 00000000..ccc2851b --- /dev/null +++ b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/test/FXOutputManagerTest.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.fx.test + +import hep.dataforge.context.Global +import hep.dataforge.fx.output.FXOutputManager +import hep.dataforge.io.render +import hep.dataforge.meta.Meta +import hep.dataforge.plots.data.DataPlot +import hep.dataforge.plots.output.plotFrame +import hep.dataforge.tables.buildTable + +fun main() { + + Global.output = FXOutputManager() + + Global.output["text1", "nf"].render("affff") + Global.output["text1", "n"].render("affff") + Global.output["text2", "n"].render("affff") + + + Global.plotFrame(stage = "plots", name = "frame") { + DataPlot.plot("data", x = doubleArrayOf(1.0, 2.0, 3.0), y = doubleArrayOf(2.0, 3.0, 4.0)) + } + + val table = buildTable { + row("a" to 1, "b" to 2) + row("a" to 2, "b" to 4) + } + + Global.output.render(table, stage = "tables", name = "table", meta = Meta.empty()) + + System.`in`.read() +} \ No newline at end of file diff --git a/dataforge-gui/src/main/kotlin/hep/dataforge/fx/test/FXOutputPaneTest.kt b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/test/FXOutputPaneTest.kt new file mode 100644 index 00000000..47da145a --- /dev/null +++ b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/test/FXOutputPaneTest.kt @@ -0,0 +1,43 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.fx.test + +import hep.dataforge.context.Global +import hep.dataforge.fx.output.FXTextOutput +import javafx.application.Application +import javafx.scene.Scene +import javafx.stage.Stage +import tornadofx.* + +/** + * + * @author Alexander Nozik + */ +class FXOutputPaneTest : App() { + + override fun start(stage: Stage) { + + val out = FXTextOutput(Global) + out.setMaxLines(5) + + for (i in 0..11) { + out.appendLine("my text number $i") + } + + // onComplete.appendLine("a\tb\tc"); + // onComplete.appendLine("aaaaa\tbbb\tccc"); + + val scene = Scene(out.view.root, 400.0, 400.0) + + stage.title = "FXOutputPaneTest" + stage.scene = scene + stage.show() + } +} + +fun main(args: Array) { + Application.launch(FXOutputPaneTest::class.java, *args) +} diff --git a/dataforge-gui/src/main/kotlin/hep/dataforge/fx/test/FXWebViewTest.kt b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/test/FXWebViewTest.kt new file mode 100644 index 00000000..54fecab1 --- /dev/null +++ b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/test/FXWebViewTest.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.fx.test + +import hep.dataforge.context.Global +import hep.dataforge.fx.output.FXWebOutput +import hep.dataforge.meta.buildMeta +import javafx.application.Application +import javafx.scene.Scene +import javafx.stage.Stage +import tornadofx.* +import java.time.Instant + +/** + * + * @author Alexander Nozik + */ +class FXWebViewTest : App() { + + override fun start(stage: Stage) { + + val out = FXWebOutput(Global) + out.render("This is my text") + out.render( + buildMeta { + "a" to 232 + "b" to "my string" + "node" to { + "c" to listOf(1,2,3) + "d" to Instant.now() + } + } + ) + + val scene = Scene(out.view.root, 400.0, 400.0) + + stage.title = "FXOutputPaneTest" + stage.scene = scene + stage.show() + } +} + +fun main(args: Array) { + Application.launch(FXWebViewTest::class.java, *args) +} diff --git a/dataforge-gui/src/main/kotlin/hep/dataforge/fx/test/MetaEditorTest.kt b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/test/MetaEditorTest.kt new file mode 100644 index 00000000..62f813cf --- /dev/null +++ b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/test/MetaEditorTest.kt @@ -0,0 +1,81 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.fx.test + +import hep.dataforge.description.DescriptorBuilder +import hep.dataforge.fx.meta.ConfigEditor +import hep.dataforge.meta.ConfigChangeListener +import hep.dataforge.meta.Configuration +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.names.Name +import hep.dataforge.values.Value +import hep.dataforge.values.ValueType +import javafx.application.Application +import javafx.scene.Scene +import javafx.stage.Stage +import org.slf4j.LoggerFactory +import tornadofx.* +import java.io.IOException + +/** + * @author Alexander Nozik + */ +class MetaEditorTest : App() { + + private val logger = LoggerFactory.getLogger("test") + + @Throws(IOException::class) + override fun start(stage: Stage) { + + val config = Configuration("test") + .setValue("testValue", "[1,2,3]") + .setValue("anotherTestValue", 15) + .putNode(MetaBuilder("childNode") + .setValue("childValue", true) + .setValue("anotherChildValue", 18) + ).putNode(MetaBuilder("childNode") + .setValue("childValue", true) + .putNode(MetaBuilder("grandChildNode") + .putValue("grandChildValue", "grandChild") + ) + ) + + val descriptor = DescriptorBuilder("test").apply { + info = "Configuration editor test node" + value(name = "testValue", types = listOf(ValueType.STRING), info = "a test value") + value(name = "defaultValue", types = listOf(ValueType.NUMBER), defaultValue = 82.5, info = "A value with default") + node("childNode") { + info = "A child Node" + value("childValue", types = listOf(ValueType.BOOLEAN), info = "A child boolean node") + } + node("descriptedNode") { + info = "A descripted node" + value("descriptedValue", types = listOf(ValueType.BOOLEAN), info = "described value in described node") + } + }.build() + + config.addListener(object : ConfigChangeListener { + override fun notifyValueChanged(name: Name, oldItem: Value?, newItem: Value?) { + logger.info("The value {} changed from {} to {}", name, oldItem, newItem) + } + + override fun notifyNodeChanged(name: Name, oldItem: List, newItem: List) { + logger.info("The node {} changed", name) + } + }) + + val scene = Scene(ConfigEditor(config, descriptor = descriptor).root, 400.0, 400.0) + + stage.title = "Meta editor test" + stage.scene = scene + stage.show() + } +} + +fun main(args: Array) { + Application.launch(MetaEditorTest::class.java, *args) +} \ No newline at end of file diff --git a/dataforge-gui/src/main/kotlin/hep/dataforge/fx/test/MetaViewerTest.kt b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/test/MetaViewerTest.kt new file mode 100644 index 00000000..e9c4d7c5 --- /dev/null +++ b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/test/MetaViewerTest.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.fx.test + +import hep.dataforge.fx.meta.MetaViewer +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.meta.buildMeta +import javafx.scene.Scene +import javafx.stage.Stage +import tornadofx.* + +class MetaViewerTest : App() { + + val meta = buildMeta("test") + .setValue("testValue", "[1,2,3]") + .setValue("anotherTestValue", 15) + .putNode(MetaBuilder("childNode") + .setValue("childValue", true) + .setValue("anotherChildValue", 18) + ).putNode(MetaBuilder("childNode") + .setValue("childValue", true) + .putNode(MetaBuilder("grandChildNode") + .putValue("grandChildValue", "grandChild") + ) + ).build() + + override fun start(stage: Stage) { + val scene = Scene(MetaViewer(meta).root, 400.0, 400.0) + + stage.title = "Meta viewer test" + stage.scene = scene + stage.show() + } +} \ No newline at end of file diff --git a/dataforge-gui/src/main/kotlin/hep/dataforge/fx/values/ColorValueChooser.kt b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/values/ColorValueChooser.kt new file mode 100644 index 00000000..0f453262 --- /dev/null +++ b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/values/ColorValueChooser.kt @@ -0,0 +1,39 @@ +package hep.dataforge.fx.values + +import hep.dataforge.values.Value +import hep.dataforge.values.asValue +import javafx.scene.control.ColorPicker +import javafx.scene.paint.Color +import org.slf4j.LoggerFactory +import tornadofx.* + +/** + * Created by darksnake on 01-May-17. + */ +class ColorValueChooser : ValueChooserBase() { + private fun ColorPicker.setColor(value: Value?) { + if (value != null && value != Value.NULL) { + try { + runLater { + this.value = Color.valueOf(value.string) + } + } catch (ex: Exception) { + LoggerFactory.getLogger(javaClass).warn("Invalid color field value: " + value.string) + } + } + } + + + override fun setDisplayValue(value: Value) { + node.setColor(value) + } + + override fun buildNode(): ColorPicker { + val node = ColorPicker() + node.styleClass.add("split-button") + node.maxWidth = java.lang.Double.MAX_VALUE + node.setColor(value) + node.setOnAction { _ -> value = node.value.toString().asValue() } + return node + } +} diff --git a/dataforge-gui/src/main/kotlin/hep/dataforge/fx/values/ComboBoxValueChooser.kt b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/values/ComboBoxValueChooser.kt new file mode 100644 index 00000000..fd50b8bf --- /dev/null +++ b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/values/ComboBoxValueChooser.kt @@ -0,0 +1,49 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.fx.values + +import hep.dataforge.values.Value +import hep.dataforge.values.ValueFactory +import javafx.collections.FXCollections +import javafx.scene.control.ComboBox +import javafx.util.StringConverter +import java.util.* + +class ComboBoxValueChooser : ValueChooserBase>() { + + // @Override + // protected void displayError(String error) { + // //TODO ControlsFX decorator here + // } + + private fun allowedValues(): Collection { + return descriptor?.allowedValues ?: Collections.emptyList(); + } + + override fun buildNode(): ComboBox { + val node = ComboBox(FXCollections.observableArrayList(allowedValues())) + node.maxWidth = java.lang.Double.MAX_VALUE + node.isEditable = false + node.selectionModel.select(currentValue()) + node.converter = object : StringConverter() { + override fun toString(value: Value?): String { + return value?.string ?: "" + } + + override fun fromString(string: String?): Value { + return ValueFactory.parse(string ?: "") + } + + } + this.valueProperty.bind(node.valueProperty()) + return node + } + + override fun setDisplayValue(value: Value) { + node.selectionModel.select(value) + } + +} diff --git a/dataforge-gui/src/main/kotlin/hep/dataforge/fx/values/TextValueChooser.kt b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/values/TextValueChooser.kt new file mode 100644 index 00000000..87236eae --- /dev/null +++ b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/values/TextValueChooser.kt @@ -0,0 +1,102 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.fx.values + +import hep.dataforge.values.Value +import hep.dataforge.values.ValueFactory +import hep.dataforge.values.ValueType +import javafx.beans.value.ObservableValue +import javafx.scene.control.TextField +import javafx.scene.input.KeyCode +import javafx.scene.input.KeyEvent +import tornadofx.* + +class TextValueChooser : ValueChooserBase() { + + private val displayText: String + get() = currentValue().let { + if (it.isNull) { + "" + } else { + it.string + } + } + + + override fun buildNode(): TextField { + val node = TextField() + val defaultValue = currentValue() + node.text = displayText + node.style = String.format("-fx-text-fill: %s;", textColor(defaultValue)) + + // commit on enter + node.setOnKeyPressed { event: KeyEvent -> + if (event.code == KeyCode.ENTER) { + commit() + } + } + // restoring value on click outside + node.focusedProperty().addListener { _: ObservableValue, oldValue: Boolean, newValue: Boolean -> + if (oldValue && !newValue) { + node.text = displayText + } + } + + // changing text color while editing + node.textProperty().onChange { newValue -> + if(newValue!= null) { + val value = ValueFactory.parse(newValue) + if (!validate(value)) { + node.style = String.format("-fx-text-fill: %s;", "red") + } else { + node.style = String.format("-fx-text-fill: %s;", textColor(value)) + } + } + } + + return node + } + + private fun commit() { + val newValue = ValueFactory.parse(node.text) + if (validate(newValue)) { + value = newValue + } else { + resetValue() + displayError("Value not allowed") + } + + } + + private fun textColor(item: Value): String { + return when (item.type) { + ValueType.BOOLEAN -> if (item.boolean) { + "blue" + } else { + "salmon" + } + ValueType.STRING -> "magenta" + else -> "black" + } + } + + private fun validate(value: Value): Boolean { + return descriptor?.isValueAllowed(value) ?: true + } + + // @Override + // protected void displayError(String error) { + // //TODO ControlsFX decorator here + // } + + override fun setDisplayValue(value: Value) { + node.text = if (value.isNull) { + "" + } else { + value.string + } + } +} diff --git a/dataforge-gui/src/main/kotlin/hep/dataforge/fx/values/ValueCallback.kt b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/values/ValueCallback.kt new file mode 100644 index 00000000..935c7091 --- /dev/null +++ b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/values/ValueCallback.kt @@ -0,0 +1,23 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.fx.values + +import hep.dataforge.values.Value + + +/** + * @param success + * @param value Value after change + * @param message Message on unsuccessful change + */ +class ValueCallbackResponse(val success: Boolean, val value: Value, val message: String) + +/** + * A callback for some visual object trying to change some value + * @author [Alexander Nozik](mailto:altavir@gmail.com) + */ +typealias ValueCallback = (Value) -> ValueCallbackResponse + diff --git a/dataforge-gui/src/main/kotlin/hep/dataforge/fx/values/ValueChooser.kt b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/values/ValueChooser.kt new file mode 100644 index 00000000..09b00351 --- /dev/null +++ b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/values/ValueChooser.kt @@ -0,0 +1,89 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.fx.values + +import hep.dataforge.description.ValueDescriptor +import hep.dataforge.values.Value +import javafx.beans.property.ObjectProperty +import javafx.beans.value.ObservableValue +import javafx.scene.Node +import tornadofx.* + +/** + * A value chooser object. Must have an empty constructor to be invoked by + * reflections. + * + * @author [Alexander Nozik](mailto:altavir@gmail.com) + */ +interface ValueChooser { + + /** + * Get or create a Node that could be later inserted into some parent + * object. + * + * @return + */ + val node: Node + + /** + * The descriptor property for this value. Could be null + * + * @return + */ + val descriptorProperty: ObjectProperty + var descriptor: ValueDescriptor? + + val valueProperty: ObjectProperty + var value: Value? + + + /** + * Set display value but do not notify listeners + * + * @param value + */ + fun setDisplayValue(value: Value) + + + fun setDisabled(disabled: Boolean) { + //TODO replace by property + } + + fun setCallback(callback: ValueCallback) +} + +object ValueChooserFactory { + private fun build(descriptor: ValueDescriptor?): ValueChooser { + if (descriptor == null) { + return TextValueChooser(); + } + //val types = descriptor.type + val chooser: ValueChooser = when { + descriptor.allowedValues.isNotEmpty() -> ComboBoxValueChooser() + descriptor.tags.contains("widget:color") -> ColorValueChooser() + else -> TextValueChooser() + } + chooser.descriptor = descriptor + return chooser + } + + fun build(value: ObservableValue, descriptor: ValueDescriptor? = null, setter: (Value) -> Unit): ValueChooser { + val chooser = build(descriptor) + chooser.setDisplayValue(value.value ?: Value.NULL) + value.onChange { + chooser.setDisplayValue(it ?: Value.NULL) + } + chooser.setCallback { result -> + if (descriptor?.isValueAllowed(result) != false) { + setter(result) + ValueCallbackResponse(true, result, "OK") + } else { + ValueCallbackResponse(false, value.value?: Value.NULL, "Not allowed") + } + } + return chooser + } +} diff --git a/dataforge-gui/src/main/kotlin/hep/dataforge/fx/values/ValueChooserBase.kt b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/values/ValueChooserBase.kt new file mode 100644 index 00000000..db87a3e4 --- /dev/null +++ b/dataforge-gui/src/main/kotlin/hep/dataforge/fx/values/ValueChooserBase.kt @@ -0,0 +1,69 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.fx.values + +import hep.dataforge.description.ValueDescriptor +import hep.dataforge.values.Value +import javafx.beans.property.SimpleObjectProperty +import javafx.scene.Node +import org.slf4j.LoggerFactory +import tornadofx.* + +/** + * ValueChooser boilerplate + * + * @author Alexander Nozik + */ +abstract class ValueChooserBase : ValueChooser { + + override val node by lazy { buildNode() } + final override val valueProperty = SimpleObjectProperty(Value.NULL) + final override val descriptorProperty = SimpleObjectProperty() + + override var descriptor: ValueDescriptor? by descriptorProperty + override var value: Value? by valueProperty + + fun resetValue() { + setDisplayValue(currentValue()) + } + + /** + * Current value or default value + * @return + */ + protected fun currentValue(): Value { + return value ?: descriptor?.default ?: Value.NULL + } + + /** + * True if builder node is successful + * + * @return + */ + protected abstract fun buildNode(): T + + /** + * Display validation error + * + * @param error + */ + protected fun displayError(error: String) { + LoggerFactory.getLogger(javaClass).error(error) + } + + override fun setCallback(callback: ValueCallback) { + valueProperty.onChange { newValue: Value? -> + val response = callback(newValue ?: Value.NULL) + if (response.value != valueProperty.get()) { + setDisplayValue(response.value) + } + + if (!response.success) { + displayError(response.message) + } + } + } +} diff --git a/dataforge-gui/src/main/resources/META-INF/services/hep.dataforge.context.PluginFactory b/dataforge-gui/src/main/resources/META-INF/services/hep.dataforge.context.PluginFactory new file mode 100644 index 00000000..3f1636c3 --- /dev/null +++ b/dataforge-gui/src/main/resources/META-INF/services/hep.dataforge.context.PluginFactory @@ -0,0 +1 @@ +hep.dataforge.fx.FXPlugin$Factory \ No newline at end of file diff --git a/dataforge-gui/src/main/resources/img/df.png b/dataforge-gui/src/main/resources/img/df.png new file mode 100644 index 0000000000000000000000000000000000000000..076e26a2ed849cbaae060d15adb6148a3b56a3c3 GIT binary patch literal 54732 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

s%ww{03OY|N=RqVm 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%0A!(<~w@))CgMB=C}tiVTR$`5(tbM}G`~7pY%g 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 literal 0 HcmV?d00001 diff --git a/dataforge-gui/src/test/kotlin/hep/dataforge/fx/output/FXWebOutputTest.kt b/dataforge-gui/src/test/kotlin/hep/dataforge/fx/output/FXWebOutputTest.kt new file mode 100644 index 00000000..8b850555 --- /dev/null +++ b/dataforge-gui/src/test/kotlin/hep/dataforge/fx/output/FXWebOutputTest.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.fx.output + +import hep.dataforge.context.Global +import hep.dataforge.io.HTMLOutput +import kotlinx.html.body +import kotlinx.html.consumers.onFinalize +import kotlinx.html.dom.append +import kotlinx.html.dom.createHTMLDocument +import kotlinx.html.dom.serialize +import kotlinx.html.head +import kotlinx.html.html +import org.junit.Test + +class FXWebOutputTest { + @Test + fun testRendering() { + + val document = createHTMLDocument().html { + head { } + body { } + } + + val node = document.getElementsByTagName("body").item(0) + + val appender = node.append.onFinalize { from, _ -> + println(from.ownerDocument.serialize(true)) + } + + val out = HTMLOutput(Global,appender) + + out.render("this is my text") + out.render("this is another text") + } +} \ No newline at end of file diff --git a/dataforge-maths/build.gradle b/dataforge-maths/build.gradle new file mode 100644 index 00000000..aaa74a68 --- /dev/null +++ b/dataforge-maths/build.gradle @@ -0,0 +1,5 @@ +description = 'Commons math dependency and some useful tools' +dependencies { + compile 'org.apache.commons:commons-math3:3.+' + compile project(':dataforge-core') +} diff --git a/dataforge-maths/src/jmh/kotlin/hep/dataforge/maths/DSNumberBenchmark.kt b/dataforge-maths/src/jmh/kotlin/hep/dataforge/maths/DSNumberBenchmark.kt new file mode 100644 index 00000000..519e161b --- /dev/null +++ b/dataforge-maths/src/jmh/kotlin/hep/dataforge/maths/DSNumberBenchmark.kt @@ -0,0 +1,33 @@ +package hep.dataforge.maths + +import hep.dataforge.maths.expressions.DSField +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.Fork +import org.openjdk.jmh.annotations.Measurement +import org.openjdk.jmh.annotations.Warmup +import kotlin.math.PI +import kotlin.math.sqrt + + +@Fork(1) +@Warmup(iterations = 1) +@Measurement(iterations = 10) +open class DSNumberBenchmark{ + + @Benchmark + fun benchmarkNumberContext() { + val x = 0 + val context = DSField(1, "amp", "pos", "sigma") + + val gauss = with(context) { + val amp = variable("amp", 1) + val pos = variable("pos", 0) + val sigma = variable("sigma", 1) + amp / (sigma * sqrt(2 * PI)) * exp(-pow(pos - x, 2) / pow(sigma, 2) / 2) + } + + gauss.toDouble() + gauss.deriv("pos") + } +} + diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/GridCalculator.java b/dataforge-maths/src/main/java/hep/dataforge/maths/GridCalculator.java new file mode 100644 index 00000000..5a4c07c7 --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/GridCalculator.java @@ -0,0 +1,71 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.maths; + +import hep.dataforge.values.Values; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + *

GridCalculator class.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class GridCalculator { + + public static double[] getUniformUnivariateGrid(double a, double b, int numpoints) { + assert b > a; + assert numpoints > 1; + double[] res = new double[numpoints]; + + for (int i = 0; i < numpoints; i++) { + res[i] = a + i * (b - a) / (numpoints - 1); + } + return res; + } + + public static double[] getUniformUnivariateGrid(double a, double b, double step) { + return getUniformUnivariateGrid(a, b, (int) ((b - a) / step +1)); + } + + public static List getFromData(Iterable data, String name){ + List grid = new ArrayList<>(); + for (Values point : data) { + grid.add(point.getDouble(name)); + } + return grid; + } + + /** + * Create grid with new node in each center of interval of the old grid + * @param grid + * @return + */ + public static List doubleGrid(List grid){ + Collections.sort(grid); + List doubleGrid = new ArrayList<>(); + for (int i = 0; i < grid.size()-1; i++) { + doubleGrid.add(grid.get(i)); + doubleGrid.add((grid.get(i)+grid.get(i+1))/2); + } + doubleGrid.add(grid.get(grid.size())); + return doubleGrid; + } + +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/Interpolation.java b/dataforge-maths/src/main/java/hep/dataforge/maths/Interpolation.java new file mode 100644 index 00000000..45dcfbeb --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/Interpolation.java @@ -0,0 +1,91 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.maths; + +import org.apache.commons.math3.analysis.UnivariateFunction; +import org.apache.commons.math3.analysis.interpolation.LinearInterpolator; +import org.apache.commons.math3.analysis.interpolation.SplineInterpolator; +import org.apache.commons.math3.analysis.interpolation.UnivariateInterpolator; +import org.apache.commons.math3.util.Precision; + +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * + * @author Alexander Nozik + */ +public class Interpolation { + + public enum InterpolationType { + LINE, + SPLINE + } + + /** + * + * @param values - input values as a map + * @param loValue - value to be assumed for xs lower than lowest value in + * the map. By default is assumed to be lowest value. + * @param upValue - value to be assumed for xs higher than highest value in + * the map. By default is assumed to be highest value. + * @return + */ + public static UnivariateFunction interpolate(Map values, InterpolationType type, Double loValue, Double upValue) { + SortedMap sorted = new TreeMap<>(); + sorted.putAll(values); + + double lo = sorted.firstKey().doubleValue(); + double up = sorted.lastKey().doubleValue(); + double delta = 2 * Precision.EPSILON; + + double lval; + if (loValue == null || Double.isNaN(loValue)) { + lval = sorted.get(sorted.firstKey()).doubleValue(); + } else { + lval = loValue; + } + + double uval; + if (upValue == null || Double.isNaN(upValue)) { + uval = sorted.get(sorted.lastKey()).doubleValue(); + } else { + uval = upValue; + } + + UnivariateInterpolator interpolator; + switch (type) { + case SPLINE: + interpolator = new SplineInterpolator(); + break; + default: + interpolator = new LinearInterpolator(); + } + + double[] xs = new double[sorted.size()]; + double[] ys = new double[sorted.size()]; + + int i = 0; + for (Map.Entry entry : sorted.entrySet()) { + xs[i] = entry.getKey().doubleValue(); + ys[i] = entry.getValue().doubleValue(); + i++; + } + + UnivariateFunction interpolated = interpolator.interpolate(xs, ys); + + return (double x) -> { + if (x < lo + delta) { + return lval; + } else if (x > up + delta) { + return uval; + } else { + return interpolated.value(x); + } + }; + } +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/MathUtils.java b/dataforge-maths/src/main/java/hep/dataforge/maths/MathUtils.java new file mode 100644 index 00000000..11d44ec7 --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/MathUtils.java @@ -0,0 +1,87 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.maths; + +import hep.dataforge.exceptions.NameNotFoundException; +import hep.dataforge.values.Values; +import org.apache.commons.math3.analysis.BivariateFunction; +import org.apache.commons.math3.analysis.UnivariateFunction; + +/** + * + * @author Alexander Nozik + */ +public class MathUtils { + + public static double[] getDoubleArray(Values set, String... names) { + //fast access method for double vectors + if (set instanceof NamedVector) { + return ((NamedVector) set).getArray(names); + } + + if (names.length == 0) { + names = set.namesAsArray(); + } + double[] res = new double[names.length]; + for (String name : names) { + int index = set.getNames().getNumberByName(name); + if (index < 0) { + throw new NameNotFoundException(name); + } + res[index] = set.getDouble(name); + } + return res; + } + + public static String toString(Values set, String... names) { + String res = "["; + if (names.length == 0) { + names = set.getNames().asArray(); + } + boolean flag = true; + for (String name : names) { + if (flag) { + flag = false; + } else { + res += ", "; + } + res += name + ":" + set.getDouble(name); + } + return res + "]"; + } + + /** + * calculate function on grid + * + * @param func + * @param grid + * @return + */ + public static double[] calculateFunction(UnivariateFunction func, double[] grid) { + double[] res = new double[grid.length]; + for (int i = 0; i < res.length; i++) { + res[i] = func.value(grid[i]); + } + return res; + } + + /** + * Calculate bivariate function on grid + * @param func + * @param xGrid + * @param yGrid + * @return + */ + public static double[][] calculateFunction(BivariateFunction func, double[] xGrid, double[] yGrid) { + double[][] res = new double[xGrid.length][yGrid.length]; + for (int i = 0; i < xGrid.length; i++) { + for (int j = 0; j < yGrid.length; j++) { + res[i][j] = func.value(xGrid[i],yGrid[j]); + } + } + return res; + } +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/MatrixOperations.java b/dataforge-maths/src/main/java/hep/dataforge/maths/MatrixOperations.java new file mode 100644 index 00000000..e66a7565 --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/MatrixOperations.java @@ -0,0 +1,151 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.maths; + +import hep.dataforge.context.Global; +import org.apache.commons.math3.linear.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; + +import static java.lang.Math.abs; +import static java.util.Arrays.fill; +import static java.util.Arrays.sort; + +/** + *

MatrixOperations class.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class MatrixOperations { + + /** + * Constant MATRIX_SINGULARITY_THRESHOLD="maths.singularity_threshold" + */ + public static String MATRIX_SINGULARITY_THRESHOLD = "maths.singularity_threshold"; + + /** + *

condCheck.

+ * + * @param matrix a {@link org.apache.commons.math3.linear.RealMatrix} object. + * @return a boolean. + */ + public static boolean condCheck(RealMatrix matrix) { + double det = determinant(matrix); + if (det == 0) { + return false; + } + double det1 = determinant(inverse(matrix)); + return abs(det * det1 - 1) < Global.INSTANCE.getDouble("CONDITIONALITY", 1e-4); + } + + /** + *

determinant.

+ * + * @param matrix a {@link org.apache.commons.math3.linear.RealMatrix} object. + * @return a double. + */ + public static double determinant(RealMatrix matrix) { + LUDecomposition solver = new LUDecomposition(matrix); + return solver.getDeterminant(); + } + + /** + * Обращение положительно определенной квадратной матрицы. Ð’ Ñлучае еÑли + * матрица ÑингулÑрнаÑ, предпринимаетÑÑ Ð¿Ð¾Ð¿Ñ‹Ñ‚ÐºÐ° регулÑризовать ее + * добавлением небольшой величины к диагонали + * + * @param matrix a {@link org.apache.commons.math3.linear.RealMatrix} object. + * @return a {@link org.apache.commons.math3.linear.RealMatrix} object. + */ + public static RealMatrix inverse(RealMatrix matrix) { + Logger logger = LoggerFactory.getLogger(MatrixOperations.class); + assert matrix.getColumnDimension() == matrix.getRowDimension(); + RealMatrix res; + try { + double singularityThreshold = Global.INSTANCE.getDouble(MATRIX_SINGULARITY_THRESHOLD, 1e-11); + DecompositionSolver solver = new LUDecomposition(matrix, singularityThreshold).getSolver(); + res = solver.getInverse(); + } catch (SingularMatrixException ex) { + EigenDecomposition eigen = new EigenDecomposition(matrix); + logger.info("MatrixUtils : Matrix inversion failed. Trying to regulirize matrix by adding a constant to diagonal."); + double[] eigenValues = eigen.getRealEigenvalues(); + + logger.info("MatrixUtils : The eigenvalues are {}", Arrays.toString(eigenValues)); + sort(eigenValues); + double delta = 0; + //Во-первых уÑтранÑем отрицательные ÑобÑтвенные значениÑ, так как предполагаетÑÑ, что матрица положительно определена + + if (eigenValues[0] < 0) { + delta = -eigenValues[0]; + } + /*задаем дельту так, чтобы она была заведомо меньще Ñамого большого ÑобÑтвенного значениÑ. + * Цифра 1/1000 взÑта из минуита. + */ + delta += (eigenValues[eigenValues.length - 1] - delta) / 1000; + logger.info("MatrixUtils : Adding {} to diagonal.", delta); + double[] e = new double[matrix.getColumnDimension()]; + fill(e, delta); + RealMatrix newMatrix = matrix.add(new DiagonalMatrix(e)); + eigen = new EigenDecomposition(newMatrix); + DecompositionSolver solver = eigen.getSolver(); + res = solver.getInverse(); + } + + return res; + } + + /** + *

matrixToSciLab.

+ * + * @param mat a {@link org.apache.commons.math3.linear.RealMatrix} object. + * @return a {@link java.lang.String} object. + */ + public static String matrixToSciLab(RealMatrix mat) { + StringBuilder str = new StringBuilder(); + str.append("["); + for (int i = 0; i < mat.getRowDimension(); i++) { + for (int j = 0; j < mat.getColumnDimension(); j++) { + str.append(mat.getEntry(i, j)); + if (j < mat.getColumnDimension() - 1) { + str.append(","); + } + } + if (i < mat.getColumnDimension() - 1) { + str.append(";"); + } + } + str.append("]"); + return str.toString(); + } + + /** + *

isSquareArray.

+ * + * @param arr an array of double. + * @return a boolean. + */ + public static boolean isSquareArray(double[][] arr) { + for (double[] arr1 : arr) { + if (arr1.length != arr.length) { + return false; + } + } + return true; + } +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/MultivariateUniformDistribution.java b/dataforge-maths/src/main/java/hep/dataforge/maths/MultivariateUniformDistribution.java new file mode 100644 index 00000000..27b769f4 --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/MultivariateUniformDistribution.java @@ -0,0 +1,97 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.maths; + +import hep.dataforge.maths.domains.Domain; +import hep.dataforge.maths.domains.HyperSquareDomain; +import kotlin.Pair; +import org.apache.commons.math3.distribution.AbstractMultivariateRealDistribution; +import org.apache.commons.math3.random.RandomGenerator; + +import java.util.List; + +/** + * A uniform distribution in a + * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class MultivariateUniformDistribution extends AbstractMultivariateRealDistribution { + + /** + * Create a uniform distribution with hyper-square domain + * + * @param rg + * @param loVals + * @param upVals + * @return + */ + public static MultivariateUniformDistribution square(RandomGenerator rg, Double[] loVals, Double[] upVals) { + return new MultivariateUniformDistribution(rg, new HyperSquareDomain(loVals, upVals)); + } + + public static MultivariateUniformDistribution square(RandomGenerator rg, List> borders) { + Double[] loVals = new Double[borders.size()]; + Double[] upVals = new Double[borders.size()]; + for (int i = 0; i < borders.size(); i++) { + loVals[i] = borders.get(i).getFirst(); + upVals[i] = borders.get(i).getSecond(); + } + return new MultivariateUniformDistribution(rg, new HyperSquareDomain(loVals, upVals)); + } + + private Domain domain; + + public MultivariateUniformDistribution(RandomGenerator rg, Domain dom) { + super(rg, dom.getDimension()); + this.domain = dom; + } + + @Override + public double density(double[] doubles) { + if (doubles.length != this.getDimension()) { + throw new IllegalArgumentException(); + } + if (!domain.contains(doubles)) { + return 0; + } + return 1 / domain.volume(); + } + + public double getVolume() { + return domain.volume(); + } + + @Override + public double[] sample() { + double[] res = new double[this.getDimension()]; + + do { + for (int i = 0; i < res.length; i++) { + double loval = domain.getLowerBound(i); + double upval = domain.getUpperBound(i); + if (loval == upval) { + res[i] = loval; + } else { + res[i] = loval + this.random.nextDouble() * (upval - loval); + } + + } + } while (!domain.contains(res)); + + return res; + } +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/NamedMatrix.java b/dataforge-maths/src/main/java/hep/dataforge/maths/NamedMatrix.java new file mode 100644 index 00000000..3dfa2441 --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/NamedMatrix.java @@ -0,0 +1,187 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.maths; + +import hep.dataforge.meta.Meta; +import hep.dataforge.meta.MetaBuilder; +import hep.dataforge.meta.MetaMorph; +import hep.dataforge.names.NameList; +import hep.dataforge.names.NameSetContainer; +import hep.dataforge.values.Values; +import org.apache.commons.math3.exception.DimensionMismatchException; +import org.apache.commons.math3.linear.Array2DRowRealMatrix; +import org.apache.commons.math3.linear.RealMatrix; + +import java.util.HashMap; +import java.util.Map; + +/** + * Square named matrix. + * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class NamedMatrix implements NameSetContainer, MetaMorph { + + private NameList names; + private RealMatrix mat; + + public NamedMatrix() { + names = new NameList(); + mat = new Array2DRowRealMatrix(); + } + + public NamedMatrix(String[] names, RealMatrix mat) { + this.names = new NameList(names); + if (!mat.isSquare()) { + throw new IllegalArgumentException("Only square matrices allowed."); + } + if (mat.getColumnDimension() != names.length) { + throw new DimensionMismatchException(mat.getColumnDimension(), names.length); + } + this.mat = new Array2DRowRealMatrix(mat.getData(), true); + } + + public NamedMatrix(String[] names, double[][] values) { + this.names = new NameList(names); + if (values.length != values[0].length) { + throw new IllegalArgumentException("Only square matrices allowed."); + } + if (values.length != names.length) { + throw new DimensionMismatchException(values.length, names.length); + } + this.mat = new Array2DRowRealMatrix(values, true); + } + + public NamedMatrix(Meta meta){ + Map vectors = new HashMap<>(); + meta.getNodeNames().forEach(name -> { + vectors.put(name, new NamedVector(meta.getMeta(name))); + }); + this.names = new NameList(vectors.keySet()); + this.mat = new Array2DRowRealMatrix(names.size(), names.size()); + for (int i = 0; i < names.size(); i++) { + mat.setRowVector(i, vectors.get(names.get(i)).getVector()); + } + } + + /** + * Create diagonal named matrix from given named double set + * + * @param vector + * @return + */ + public static NamedMatrix diagonal(Values vector) { + double[] vectorValues = MathUtils.getDoubleArray(vector); + double[][] values = new double[vectorValues.length][vectorValues.length]; + for (int i = 0; i < vectorValues.length; i++) { + values[i][i] = vectorValues[i]; + } + return new NamedMatrix(vector.namesAsArray(), values); + } + + public NamedMatrix copy() { + return new NamedMatrix(this.namesAsArray(), getMatrix().copy()); + } + + public double get(int i, int j) { + return mat.getEntry(i, j); + } + + public double get(String name1, String name2) { + return mat.getEntry(this.names.getNumberByName(name1), this.names.getNumberByName(name2)); + } + + public RealMatrix getMatrix() { + return this.mat; + } + + /** + * Return named submatrix with given names. The order of names in submatrix + * is the one provided by arguments. If name list is empty, return this. + * + * @param names a {@link java.lang.String} object. + * @return a {@link hep.dataforge.maths.NamedMatrix} object. + */ + public NamedMatrix subMatrix(String... names) { + if (names.length == 0) { + return this; + } + if (!this.getNames().contains(names)) { + throw new IllegalArgumentException(); + } + int[] numbers = new int[names.length]; + for (int i = 0; i < numbers.length; i++) { + numbers[i] = this.names.getNumberByName(names[i]); + + } + RealMatrix newMat = this.mat.getSubMatrix(numbers, numbers); + return new NamedMatrix(names, newMat); + } + + /** + * {@inheritDoc} + * + * @return + */ + @Override + public NameList getNames() { + return names; + } + + public void setElement(String name1, String name2, double value) { + mat.setEntry(this.names.getNumberByName(name1), this.names.getNumberByName(name2), value); + } + + /** + * update values of this matrix from corresponding values of given named + * matrix. The order of columns does not matter. + * + * @param matrix a {@link hep.dataforge.maths.NamedMatrix} object. + */ + public void setValuesFrom(NamedMatrix matrix) { + for (int i = 0; i < matrix.getNames().size(); i++) { + for (int j = 0; j < matrix.getNames().size(); j++) { + String name1 = matrix.names.get(i); + String name2 = matrix.names.get(j); + if (names.contains(name1) && names.contains(name2)) { + this.setElement(name1, name2, matrix.get(i, j)); + } + } + + } + } + + public NamedVector getRow(String name) { + return new NamedVector(names, getMatrix().getRowVector(names.getNumberByName(name))); + } + + public NamedVector getColumn(String name) { + return new NamedVector(names, getMatrix().getColumnVector(names.getNumberByName(name))); + } + + @Override + public Meta toMeta() { + //Serialisator in fact works for non-square matrices + MetaBuilder res = new MetaBuilder("matrix"); + for (int i = 0; i < mat.getRowDimension(); i++) { + String name = names.get(i); + res.putNode(name, getRow(name).toMeta()); + } + return res; + } + +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/NamedVector.java b/dataforge-maths/src/main/java/hep/dataforge/maths/NamedVector.java new file mode 100644 index 00000000..88c1fc9a --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/NamedVector.java @@ -0,0 +1,184 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.maths; + +import hep.dataforge.exceptions.NameNotFoundException; +import hep.dataforge.exceptions.NamingException; +import hep.dataforge.meta.Meta; +import hep.dataforge.meta.MetaBuilder; +import hep.dataforge.meta.MetaMorph; +import hep.dataforge.names.NameList; +import hep.dataforge.values.Value; +import hep.dataforge.values.ValueFactory; +import hep.dataforge.values.Values; +import org.apache.commons.math3.exception.DimensionMismatchException; +import org.apache.commons.math3.linear.ArrayRealVector; +import org.apache.commons.math3.linear.RealVector; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; + +/** + * A {@link Values} implementation wrapping Commons Math {@link RealVector} + * + * @author Alexander Nozik + */ +public class NamedVector implements Values, MetaMorph { + + private NameList nameList; + private RealVector vector; + + /** + * Serialization constructor + */ + public NamedVector() { + nameList = new NameList(); + vector = new ArrayRealVector(); + } + + public NamedVector(NameList nameList, RealVector vector) { + this.nameList = nameList; + this.vector = vector; + } + + public NamedVector(String[] names, RealVector v) { + if (names.length != v.getDimension()) { + throw new IllegalArgumentException(); + } + vector = new ArrayRealVector(v); + this.nameList = new NameList(names); + } + + public NamedVector(String[] names, double[] d) { + if (names.length != d.length) { + throw new DimensionMismatchException(d.length, names.length); + } + vector = new ArrayRealVector(d); + this.nameList = new NameList(names); + } + + public NamedVector(NameList names, double[] d) { + if (names.size() != d.length) { + throw new DimensionMismatchException(d.length, names.size()); + } + vector = new ArrayRealVector(d); + this.nameList = new NameList(names); + } + + public NamedVector(Values set) { + vector = new ArrayRealVector(MathUtils.getDoubleArray(set)); + this.nameList = new NameList(set.getNames()); + } + + public NamedVector(Meta meta) { + nameList = new NameList(meta.getValueNames()); + double[] values = new double[nameList.size()]; + for (int i = 0; i < nameList.size(); i++) { + values[i] = meta.getDouble(nameList.get(i)); + } + this.vector = new ArrayRealVector(values); + } + + @Override + public Optional optValue(@NotNull String path) { + int n = this.getNumberByName(path); + if (n < 0) { + return Optional.empty(); + } else { + return Optional.of(ValueFactory.of(vector.getEntry(n))); + } + } + + public NamedVector copy() { + return new NamedVector(this.namesAsArray(), vector); + } + + + /** + * {@inheritDoc} + * + * @param name + */ + @Override + public double getDouble(String name) { + int n = this.getNumberByName(name); + if (n < 0) { + throw new NameNotFoundException(name); + } + return vector.getEntry(n); + } + + public int getNumberByName(String name) { + return nameList.getNumberByName(name); + } + + + public NamedVector subVector(String... names) { + if (names.length == 0) { + return this; + } + return new NamedVector(names, getArray(names)); + } + + /** + * {@inheritDoc} + */ + public double[] getArray(String... names) { + if (names.length == 0) { + return vector.toArray(); + } else { + if (!this.getNames().contains(names)) { + throw new NamingException(); + } + double[] res = new double[names.length]; + for (int i = 0; i < names.length; i++) { + res[i] = vector.getEntry(this.getNumberByName(names[i])); + } + return res; + } + } + + public RealVector getVector() { + return vector; + } + + /** + * {@inheritDoc} + */ + @Override + public NameList getNames() { + return nameList; + } + + public void setValue(String name, double val) { + int n = this.getNumberByName(name); + if (n < 0) { + throw new NameNotFoundException(name); + } + + vector.setEntry(n, val); + } + + @NotNull + @Override + public Meta toMeta() { + MetaBuilder builder = new MetaBuilder("vector"); + for (int i = 0; i < getNames().size(); i++) { + builder.setValue(nameList.get(i), vector.getEntry(i)); + } + return builder; + } +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/domains/Domain.java b/dataforge-maths/src/main/java/hep/dataforge/maths/domains/Domain.java new file mode 100644 index 00000000..9bf5c048 --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/domains/Domain.java @@ -0,0 +1,92 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.maths.domains; + +import hep.dataforge.exceptions.NotDefinedException; +import org.apache.commons.math3.linear.ArrayRealVector; +import org.apache.commons.math3.linear.RealVector; + +/** + * n-dimensional volume + * + * @author Alexander Nozik + */ +public interface Domain { + + boolean contains(RealVector point); + + default boolean contains(double[] point) { + return this.contains(new ArrayRealVector(point)); + } + + default boolean contains(Double[] point) { + return this.contains(new ArrayRealVector(point)); + } + + RealVector nearestInDomain(RealVector point); + + /** + * The lower edge for the domain going down from point + * @param num + * @param point + * @return + */ + Double getLowerBound(int num, RealVector point); + + /** + * The upper edge of the domain going up from point + * @param num + * @param point + * @return + */ + Double getUpperBound(int num, RealVector point); + + /** + * Global lower edge + * @param num + * @return + * @throws NotDefinedException + */ + Double getLowerBound(int num) throws NotDefinedException; + + /** + * Global upper edge + * @param num + * @return + * @throws NotDefinedException + */ + Double getUpperBound(int num) throws NotDefinedException; + + /** + * Hyper volume + * @return + */ + double volume(); + + /** + * Number of Hyperspace dimensions + * @return + */ + int getDimension(); + +// /** +// *

isFixed.

+// * +// * @param num a int. +// * @return a boolean. +// */ +// boolean isFixed(int num); +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/domains/HyperSquareDomain.java b/dataforge-maths/src/main/java/hep/dataforge/maths/domains/HyperSquareDomain.java new file mode 100644 index 00000000..233a4d32 --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/domains/HyperSquareDomain.java @@ -0,0 +1,150 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.maths.domains; + +import org.apache.commons.math3.linear.ArrayRealVector; +import org.apache.commons.math3.linear.RealVector; + +/** + *

HyperSquareDomain class.

+ * + * @author Alexander Nozik + */ +public class HyperSquareDomain implements Domain { + + private Double[] lower; + private Double[] upper; + + public HyperSquareDomain(int num) { + this.lower = new Double[num]; + this.upper = new Double[num]; + for (int i = 0; i < num; i++) { + this.lower[i] = Double.NEGATIVE_INFINITY; + this.upper[i] = Double.POSITIVE_INFINITY; + } + } + + public HyperSquareDomain(Double[] lower, Double[] upper) { + if (lower.length != upper.length) { + throw new IllegalArgumentException(); + } + this.lower = lower; + this.upper = upper; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean contains(RealVector point) { + for (int i = 0; i < point.getDimension(); i++) { + if ((point.getEntry(i) < this.lower[i]) || (point.getEntry(i) > this.upper[i])) { + return false; + } + } + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean contains(double[] point) { + return this.contains(new ArrayRealVector(point)); + } + + public void fix(int num, double value) { + this.setDomain(num, value, value); + } + + @Override + public int getDimension() { + return this.lower.length; + } + + @Override + public Double getLowerBound(int num, RealVector point) { + return this.lower[num]; + } + + @Override + public Double getLowerBound(int num) { + return this.lower[num]; + } + + @Override + public Double getUpperBound(int num, RealVector point) { + return this.upper[num]; + } + + @Override + public Double getUpperBound(int num) { + return this.upper[num]; + } + + @Override + public RealVector nearestInDomain(RealVector point) { + RealVector res = point.copy(); + for (int i = 0; i < point.getDimension(); i++) { + if (point.getEntry(i) < this.lower[i]) { + res.setEntry(i, lower[i]); + } + if (point.getEntry(i) > this.upper[i]) { + res.setEntry(i, upper[i]); + } + } + return res; + } + + public void setDomain(int num, Double lower, Double upper) { + if (num >= this.getDimension()) { + throw new IllegalArgumentException(); + } + if (lower > upper) { + throw new IllegalArgumentException("\'lower\' argument should be lower."); + } + this.lower[num] = lower; + this.upper[num] = upper; + } + + public void setLowerBorders(Double[] lower) { + if (lower.length != this.getDimension()) { + throw new IllegalArgumentException(); + } + this.lower = lower; + } + + public void setUpperBorders(Double[] upper) { + if (upper.length != this.getDimension()) { + throw new IllegalArgumentException(); + } + this.upper = upper; + } + + @Override + public double volume() { + double res = 1; + for (int i = 0; i < this.getDimension(); i++) { + if (this.lower[i].isInfinite() || this.upper[i].isInfinite()) { + return Double.POSITIVE_INFINITY; + } + if (upper[i] > lower[i]) { + res *= upper[i] - lower[i]; + } + } + return res; + } +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/domains/RangeDomain.java b/dataforge-maths/src/main/java/hep/dataforge/maths/domains/RangeDomain.java new file mode 100644 index 00000000..3c180fed --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/domains/RangeDomain.java @@ -0,0 +1,36 @@ +package hep.dataforge.maths.domains; + +public class RangeDomain extends UnivariateDomain { + + private final double a; + private final double b; + + public RangeDomain(double a, double b) { + if(a>b){ + throw new IllegalArgumentException("b should be larger than a"); + } + this.a = a; + this.b = b; + } + + + @Override + public boolean contains(double d) { + return d >= a && d <= b; + } + + @Override + public Double getLower() { + return a; + } + + @Override + public Double getUpper() { + return b; + } + + @Override + public double volume() { + return b - a; + } +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/domains/UnconstrainedDomain.java b/dataforge-maths/src/main/java/hep/dataforge/maths/domains/UnconstrainedDomain.java new file mode 100644 index 00000000..a9066bdb --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/domains/UnconstrainedDomain.java @@ -0,0 +1,111 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.maths.domains; + +import hep.dataforge.exceptions.NotDefinedException; +import org.apache.commons.math3.linear.RealVector; + +/** + *

UnconstrainedDomain class.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class UnconstrainedDomain implements Domain { + + private final int dimension; + + /** + *

Constructor for UnconstrainedDomain.

+ * + * @param dimension a int. + */ + public UnconstrainedDomain(int dimension) { + this.dimension = dimension; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean contains(RealVector point) { + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean contains(double[] point) { + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public int getDimension() { + return dimension; + } + + /** + * {@inheritDoc} + */ + @Override + public Double getLowerBound(int num, RealVector point) { + return Double.NEGATIVE_INFINITY; + } + + /** + * {@inheritDoc} + */ + @Override + public Double getLowerBound(int num) throws NotDefinedException { + return Double.NEGATIVE_INFINITY; + } + + /** + * {@inheritDoc} + */ + @Override + public Double getUpperBound(int num, RealVector point) { + return Double.POSITIVE_INFINITY; + } + + /** + * {@inheritDoc} + */ + @Override + public Double getUpperBound(int num) throws NotDefinedException { + return Double.POSITIVE_INFINITY; + } + + /** + * {@inheritDoc} + */ + @Override + public RealVector nearestInDomain(RealVector point) { + return point; + } + + /** + * {@inheritDoc} + */ + @Override + public double volume() { + return Double.POSITIVE_INFINITY; + } +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/domains/UnivariateDomain.java b/dataforge-maths/src/main/java/hep/dataforge/maths/domains/UnivariateDomain.java new file mode 100644 index 00000000..c30fd407 --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/domains/UnivariateDomain.java @@ -0,0 +1,48 @@ +package hep.dataforge.maths.domains; + +import hep.dataforge.exceptions.NotDefinedException; +import org.apache.commons.math3.linear.RealVector; + +public abstract class UnivariateDomain implements Domain { + + public abstract boolean contains(double d); + + @Override + public boolean contains(RealVector point) { + return contains(point.getEntry(0)); + } + + @Override + public RealVector nearestInDomain(RealVector point) { + return null; + } + + public abstract Double getLower(); + + public abstract Double getUpper(); + + @Override + public Double getLowerBound(int num, RealVector point) { + return getLower(); + } + + @Override + public Double getUpperBound(int num, RealVector point) { + return getUpper(); + } + + @Override + public Double getLowerBound(int num) throws NotDefinedException { + return getLower(); + } + + @Override + public Double getUpperBound(int num) throws NotDefinedException { + return getUpper(); + } + + @Override + public int getDimension() { + return 1; + } +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/functions/FunctionCaching.java b/dataforge-maths/src/main/java/hep/dataforge/maths/functions/FunctionCaching.java new file mode 100644 index 00000000..092ab0a6 --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/functions/FunctionCaching.java @@ -0,0 +1,56 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.maths.functions; + +import org.apache.commons.math3.analysis.UnivariateFunction; +import org.apache.commons.math3.analysis.interpolation.SplineInterpolator; +import org.apache.commons.math3.analysis.polynomials.PolynomialSplineFunction; + +import static hep.dataforge.maths.GridCalculator.getUniformUnivariateGrid; + +/** + *

FunctionCaching class.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class FunctionCaching { + + /** + *

cacheUnivariateFunction.

+ * + * @param a a double. + * @param b a double. + * @param numCachePoints a int. + * @param func a {@link UnivariateFunction} object. + * @return a {@link org.apache.commons.math3.analysis.polynomials.PolynomialSplineFunction} object. + */ + public static PolynomialSplineFunction cacheUnivariateFunction(double a, double b, int numCachePoints, UnivariateFunction func){ + assert func != null; + assert a > Double.NEGATIVE_INFINITY; + double[] grid = getUniformUnivariateGrid(a, b, numCachePoints); + double[] vals = new double[grid.length]; + + for (int i = 0; i < vals.length; i++) { + vals[i] = func.value(grid[i]); + + } + SplineInterpolator interpolator = new SplineInterpolator(); + PolynomialSplineFunction interpolated = interpolator.interpolate(grid, vals); + return interpolated; + + } +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/functions/FunctionFactories.java b/dataforge-maths/src/main/java/hep/dataforge/maths/functions/FunctionFactories.java new file mode 100644 index 00000000..89033d33 --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/functions/FunctionFactories.java @@ -0,0 +1,43 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.maths.functions; + +import hep.dataforge.utils.MetaFactory; +import hep.dataforge.values.Value; +import org.apache.commons.math3.analysis.UnivariateFunction; + +import java.util.List; + +/** + * + * @author Alexander Nozik + */ +public class FunctionFactories { + + public static MetaFactory parabola() { + return meta -> { + double a = meta.getDouble("a", 1); + double b = meta.getDouble("b", 0); + double c = meta.getDouble("b", 0); + return x -> a * x * x + b * x + c; + }; + } + + public static MetaFactory polynomial() { + return meta -> { + List coefs = meta.getValue("coef").getList(); + return x -> { + double sum = 0; + double curX = 1; + for (int i = 0; i < coefs.size(); i++) { + sum += coefs.get(i).getDouble()*curX; + curX = curX*x; + } + return sum; + }; + }; + } +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/functions/MultiFactory.java b/dataforge-maths/src/main/java/hep/dataforge/maths/functions/MultiFactory.java new file mode 100644 index 00000000..f4ed83fa --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/functions/MultiFactory.java @@ -0,0 +1,37 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.maths.functions; + +import hep.dataforge.exceptions.NotDefinedException; +import hep.dataforge.meta.Meta; +import hep.dataforge.utils.MetaFactory; + +import java.util.HashMap; +import java.util.Map; + +/** + * A factory combining other factories by type + * + * @author Alexander Nozik + */ +public class MultiFactory { + + private final Map> factoryMap = new HashMap<>(); + + public T build(String key, Meta meta) { + if (factoryMap.containsKey(key)) { + return factoryMap.get(key).build(meta); + } else { + throw new NotDefinedException("Function with type '" + key + "' not defined"); + } + } + + public synchronized MultiFactory addFactory(String type, MetaFactory factory) { + this.factoryMap.put(type, factory); + return this; + } + +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/functions/MultiFunction.java b/dataforge-maths/src/main/java/hep/dataforge/maths/functions/MultiFunction.java new file mode 100644 index 00000000..122b6f58 --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/functions/MultiFunction.java @@ -0,0 +1,46 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.maths.functions; + +import hep.dataforge.exceptions.NotDefinedException; +import org.apache.commons.math3.analysis.MultivariateFunction; + +/** + *

+ * MultiFunction interface.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +public interface MultiFunction extends MultivariateFunction { + + double derivValue(int n, double[] pars) throws NotDefinedException; + + int getDimension();//метод Ð´Ð»Ñ Ð¿Ñ€Ð¾Ð²ÐµÑ€ÐºÐ¸ ÑÐ¾Ð²Ð¿Ð°Ð´ÐµÐ½Ð¸Ñ Ñ€Ð°Ð·Ð¼ÐµÑ€Ð½Ð¾Ñтей + + /** + * ПозволÑет узнать, выдает ли Ñ„ÑƒÐ½ÐºÑ†Ð¸Ñ Ð°Ð½Ð°Ð»Ð¸Ñ‚Ð¸Ñ‡ÐµÑкую производную. ДопуÑкает + * аргумент -1, в Ñтом Ñлучае возвращает true, еÑли заведомо еÑÑ‚ÑŒ + * производные по вÑем параметрам. Возможна ÑитуациÑ, когда providesDeriv + * возвращает false в то времÑ, как derivValue не выкидывает + * NotDefinedException. Ð’ Ñтом Ñлучае Ð¿Ñ€Ð¾Ð¸Ð·Ð²Ð¾Ð´Ð½Ð°Ñ Ð²Ð¾Ð·Ð²Ñ€Ð°Ñ‰Ð°ÐµÑ‚ÑÑ, но + * пользоватьÑÑ ÐµÐ¹ не рекомендуетÑÑ. + * + * @param n a int. + * @return a boolean. + */ + boolean providesDeriv(int n); +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/functions/UniFunction.java b/dataforge-maths/src/main/java/hep/dataforge/maths/functions/UniFunction.java new file mode 100644 index 00000000..723e4c75 --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/functions/UniFunction.java @@ -0,0 +1,32 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.maths.functions; + +import hep.dataforge.exceptions.NotDefinedException; +import org.apache.commons.math3.analysis.UnivariateFunction; + +/** + * A Univariate function that can provide first derivative + * + * @author Alexander Nozik + * @version $Id: $Id + */ +public interface UniFunction extends UnivariateFunction { + + double derivValue(double x) throws NotDefinedException; + + boolean providesDeriv(); +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/functions/UnivariateSplineWrapper.java b/dataforge-maths/src/main/java/hep/dataforge/maths/functions/UnivariateSplineWrapper.java new file mode 100644 index 00000000..27b44b59 --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/functions/UnivariateSplineWrapper.java @@ -0,0 +1,76 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.maths.functions; + +import org.apache.commons.math3.analysis.differentiation.DerivativeStructure; +import org.apache.commons.math3.analysis.differentiation.UnivariateDifferentiableFunction; +import org.apache.commons.math3.analysis.polynomials.PolynomialSplineFunction; +import org.apache.commons.math3.exception.DimensionMismatchException; +import org.apache.commons.math3.exception.OutOfRangeException; + +/** + * A wrapper function for spline including valuew outside the spline region + * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class UnivariateSplineWrapper implements UnivariateDifferentiableFunction { + + private final double outOfRegionValue; + PolynomialSplineFunction source; + + /** + *

Constructor for UnivariateSplineWrapper.

+ * + * @param source a {@link org.apache.commons.math3.analysis.polynomials.PolynomialSplineFunction} object. + * @param outOfRegionValue a double. + */ + public UnivariateSplineWrapper(PolynomialSplineFunction source, double outOfRegionValue) { + this.source = source; + this.outOfRegionValue = outOfRegionValue; + } + + /** + *

Constructor for UnivariateSplineWrapper.

+ * + * @param source a {@link org.apache.commons.math3.analysis.polynomials.PolynomialSplineFunction} object. + */ + public UnivariateSplineWrapper(PolynomialSplineFunction source) { + this.source = source; + this.outOfRegionValue = 0d; + } + + + /** {@inheritDoc} */ + @Override + public DerivativeStructure value(DerivativeStructure t) throws DimensionMismatchException { + try { + return source.value(t); + } catch (OutOfRangeException ex) { + return new DerivativeStructure(t.getFreeParameters(), t.getOrder(), outOfRegionValue); + } + } + + /** {@inheritDoc} */ + @Override + public double value(double x) { + try { + return source.value(x); + } catch (OutOfRangeException ex) { + return outOfRegionValue; + } + } +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/histogram/Bin.java b/dataforge-maths/src/main/java/hep/dataforge/maths/histogram/Bin.java new file mode 100644 index 00000000..68ebac74 --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/histogram/Bin.java @@ -0,0 +1,49 @@ +package hep.dataforge.maths.histogram; + +import hep.dataforge.maths.domains.Domain; +import hep.dataforge.names.NameList; +import hep.dataforge.names.NameSetContainer; +import hep.dataforge.values.Values; +import org.jetbrains.annotations.Nullable; + +/** + * Created by darksnake on 29-Jun-17. + */ +public interface Bin extends Domain, NameSetContainer { + + /** + * Increment counter and return new value + * + * @return + */ + long inc(); + + /** + * The number of counts in bin + * + * @return + */ + long getCount(); + + /** + * Set the counter and return old value + * + * @param c + * @return + */ + long setCount(long c); + + long getBinID(); + + @Nullable + NameList getNames(); + + void setNames(NameList names); + + /** + * Get the description of this bin as a set of named values + * + * @return + */ + Values describe(); +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/histogram/BinFactory.java b/dataforge-maths/src/main/java/hep/dataforge/maths/histogram/BinFactory.java new file mode 100644 index 00000000..99e558d6 --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/histogram/BinFactory.java @@ -0,0 +1,8 @@ +package hep.dataforge.maths.histogram; + +/** + * Creates a new bin with zero count corresponding to given point + */ +public interface BinFactory { + Bin createBin(Double... point); +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/histogram/Histogram.java b/dataforge-maths/src/main/java/hep/dataforge/maths/histogram/Histogram.java new file mode 100644 index 00000000..38d3c6d6 --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/histogram/Histogram.java @@ -0,0 +1,104 @@ +package hep.dataforge.maths.histogram; + +import hep.dataforge.names.NameList; +import hep.dataforge.names.NamesUtils; +import hep.dataforge.tables.ListTable; +import hep.dataforge.tables.Table; +import hep.dataforge.tables.TableFormat; +import hep.dataforge.tables.TableFormatBuilder; + +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import static hep.dataforge.tables.Adapters.X_VALUE_KEY; +import static hep.dataforge.tables.Adapters.Y_VALUE_KEY; + +/** + * A thread safe histogram + * Created by darksnake on 29-Jun-17. + */ +public abstract class Histogram implements BinFactory, Iterable { + + /** + * Lookup a bin containing specific point if it is present + * + * @param point + * @return + */ + public abstract Optional findBin(Double... point); + + /** + * Add a bin to storage + * + * @param bin + * @return + */ + protected abstract Bin addBin(Bin bin); + + /** + * Find or create a bin containing given point and return number of counts in bin after addition + * + * @param point + * @return + */ + public long put(Double... point) { + Bin bin = findBin(point).orElseGet(() -> addBin(createBin(point))); + //PENDING add ability to do some statistical analysis on flight? + return bin.inc(); + } + + public Histogram fill(Stream stream) { + stream.parallel().forEach(this::put); + return this; + } + + public Histogram fill(Iterable iter) { + iter.forEach(this::put); + return this; + } + +// public abstract Bin getBinById(long id); + + public Stream binStream() { + return StreamSupport.stream(spliterator(), false); + } + + /** + * Construct a format for table using given names as axis names. The number of input names should equal to the + * dimension of this histogram or exceed it by one. In later case the last name is count axis name. + * + * @return + */ + protected TableFormat getFormat() { + TableFormatBuilder builder = new TableFormatBuilder(); + for (String axisName : getNames()) { + builder.addNumber(axisName, X_VALUE_KEY); +// builder.addNumber(axisName + ".binEnd"); + } + builder.addNumber("count", Y_VALUE_KEY); + builder.addColumn("id"); + return builder.build(); + } + + + /** + * @return + */ + public Table asTable() { + return new ListTable(getFormat(), binStream().map(Bin::describe).collect(Collectors.toList())); + } + + public abstract int getDimension(); + + /** + * Get axis names excluding count axis + * + * @return + */ + public NameList getNames() { + return NamesUtils.generateNames(getDimension()); + } +} + diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/histogram/NamedHistogram.java b/dataforge-maths/src/main/java/hep/dataforge/maths/histogram/NamedHistogram.java new file mode 100644 index 00000000..cdd53b3d --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/histogram/NamedHistogram.java @@ -0,0 +1,87 @@ +package hep.dataforge.maths.histogram; + +import hep.dataforge.names.NameList; +import hep.dataforge.names.NameSetContainer; +import hep.dataforge.values.Values; +import org.jetbrains.annotations.NotNull; + +import java.util.Iterator; +import java.util.Optional; +import java.util.stream.Stream; + +/** + * Named wrapper for histogram + * Created by darksnake on 30.06.2017. + */ +public class NamedHistogram extends Histogram implements NameSetContainer { + private final Histogram histogram; + private final NameList names; + + public NamedHistogram(Histogram histogram, NameList names) { + this.histogram = histogram; + this.names = names; + } + + public long put(Values point) { + return put(extract(point)); + } + + /** + * Put all value sets + * + * @param iter + */ + public void putAllPoints(Iterable iter) { + iter.forEach(this::put); + } + + public void putAllPoints(Stream stream) { + stream.parallel().forEach(this::put); + } + + /** + * Extract numeric vector from the point + * + * @param set + * @return + */ + private Double[] extract(Values set) { + return names.stream().map(set::getDouble).toArray(Double[]::new); + } + + @Override + public Bin createBin(Double... point) { + return histogram.createBin(point); + } + + @Override + public Optional findBin(Double... point) { + return histogram.findBin(point); + } + + @Override + protected Bin addBin(Bin bin) { + return histogram.addBin(bin); + } + +// @Override +// public Bin getBinById(long id) { +// return histogram.getBinById(id); +// } + + @NotNull + @Override + public Iterator iterator() { + return histogram.iterator(); + } + + @Override + public NameList getNames() { + return names; + } + + @Override + public int getDimension() { + return histogram.getDimension(); + } +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/histogram/SimpleHistogram.java b/dataforge-maths/src/main/java/hep/dataforge/maths/histogram/SimpleHistogram.java new file mode 100644 index 00000000..5a3053ad --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/histogram/SimpleHistogram.java @@ -0,0 +1,66 @@ +package hep.dataforge.maths.histogram; + +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +/** + * A simple histogram with square bins and slow lookup + * Created by darksnake on 29-Jun-17. + */ +public class SimpleHistogram extends Histogram { + + private final UniformBinFactory binFactory; + private final Map binMap = new HashMap<>(); + + public SimpleHistogram(Double[] binStart, Double[] binStep) { + this.binFactory = new UniformBinFactory(binStart, binStep); + } + + public SimpleHistogram(Double binStart, Double binStep) { + this.binFactory = new UniformBinFactory(new Double[]{binStart}, new Double[]{binStep}); + } + + @Override + public SquareBin createBin(Double... point) { + return binFactory.createBin(point); + } + + @Override + public synchronized Optional findBin(Double... point) { + //Simple slow lookup mechanism + return binMap.values().stream().filter(bin -> bin.contains(point)).findFirst(); + } + + @Override + protected synchronized Bin addBin(Bin bin) { + //The call should be thread safe. New bin is added only if it is absent + return binMap.computeIfAbsent(bin.getBinID(), (id) -> bin); + } +// +// @Override +// public Bin getBinById(long id) { +// return binMap.get(id); +// } + + @NotNull + @Override + public Iterator iterator() { + return binMap.values().iterator(); + } + + @Override + public Stream binStream() { + return binMap.values().stream(); + } + + + @Override + public int getDimension() { + return this.binFactory.getDimension(); + } +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/histogram/SquareBin.java b/dataforge-maths/src/main/java/hep/dataforge/maths/histogram/SquareBin.java new file mode 100644 index 00000000..fb75a27a --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/histogram/SquareBin.java @@ -0,0 +1,111 @@ +package hep.dataforge.maths.histogram; + +import hep.dataforge.maths.domains.HyperSquareDomain; +import hep.dataforge.names.NameList; +import hep.dataforge.names.NameSetContainer; +import hep.dataforge.names.NamesUtils; +import hep.dataforge.utils.ArgumentChecker; +import hep.dataforge.values.ValueMap; +import hep.dataforge.values.Values; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Created by darksnake on 29-Jun-17. + */ +public class SquareBin extends HyperSquareDomain implements Bin, NameSetContainer { + + private final long binId; + private AtomicLong counter = new AtomicLong(0); + private NameList names; // optional names for the bin + + /** + * Create a multivariate bin + * + * @param lower + * @param upper + */ + public SquareBin(Double[] lower, Double[] upper) { + super(lower, upper); + this.binId = Arrays.hashCode(lower); + } + + /** + * Create a univariate bin + * + * @param lower + * @param upper + */ + public SquareBin(Double lower, Double upper) { + this(new Double[]{lower}, new Double[]{upper}); + } + + @Override + @NotNull + public synchronized NameList getNames() { + if (names == null) { + names = NamesUtils.generateNames(getDimension()); + } + return names; + } + + public void setNames(NameList names) { + ArgumentChecker.checkEqualDimensions(getDimension(), names.size()); + this.names = names; + } + + /** + * Get the lower bound for 0 axis + * + * @return + */ + public Double getLowerBound() { + return this.getLowerBound(0); + } + + /** + * Get the upper bound for 0 axis + * + * @return + */ + public Double getUpperBound() { + return this.getUpperBound(0); + } + + @Override + public long inc() { + return counter.incrementAndGet(); + } + + @Override + public long getCount() { + return counter.get(); + } + + @Override + public long setCount(long c) { + return counter.getAndSet(c); + } + + @Override + public long getBinID() { + return binId; + } + + @Override + public Values describe() { + ValueMap.Builder builder = new ValueMap.Builder(); + for (int i = 0; i < getDimension(); i++) { + String axisName = getNames().get(i); + Double binStart = getLowerBound(i); + Double binEnd = getUpperBound(i); + builder.putValue(axisName, binStart); + builder.putValue(axisName + ".binEnd", binEnd); + } + builder.putValue("count", getCount()); + builder.putValue("id", getBinID()); + return builder.build(); + } +} \ No newline at end of file diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/histogram/UniformBinFactory.java b/dataforge-maths/src/main/java/hep/dataforge/maths/histogram/UniformBinFactory.java new file mode 100644 index 00000000..507d8027 --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/histogram/UniformBinFactory.java @@ -0,0 +1,40 @@ +package hep.dataforge.maths.histogram; + +import hep.dataforge.utils.ArgumentChecker; + +/** + * Create a uniform binning with given start point and steps + */ +public class UniformBinFactory implements BinFactory { + private final Double[] binStart; + private final Double[] binStep; + + public UniformBinFactory(Double[] binStart, Double[] binStep) { + ArgumentChecker.checkEqualDimensions(binStart.length, binStep.length); + this.binStart = binStart; + this.binStep = binStep; + } + + + @Override + public SquareBin createBin(Double... point) { + ArgumentChecker.checkEqualDimensions(point.length, binStart.length); + Double[] lo = new Double[point.length]; + Double[] up = new Double[point.length]; + + for (int i = 0; i < point.length; i++) { + if (point[i] > binStart[i]) { + lo[i] = binStart[i] + Math.floor((point[i] - binStart[i]) / binStep[i]) * binStep[i]; + up[i] = lo[i] + binStep[i]; + } else { + up[i] = binStart[i] - Math.floor((binStart[i] - point[i]) / binStep[i]) * binStep[i]; + lo[i] = up[i] - binStep[i]; + } + } + return new SquareBin(lo, up); + } + + public int getDimension(){ + return binStart.length; + } +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/histogram/UnivariateHistogram.java b/dataforge-maths/src/main/java/hep/dataforge/maths/histogram/UnivariateHistogram.java new file mode 100644 index 00000000..bd614f11 --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/histogram/UnivariateHistogram.java @@ -0,0 +1,92 @@ +package hep.dataforge.maths.histogram; + +import hep.dataforge.maths.GridCalculator; +import org.apache.commons.math3.exception.DimensionMismatchException; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.stream.DoubleStream; + +/** + * A univariate histogram with fast bin lookup. + * Created by darksnake on 02.07.2017. + */ +public class UnivariateHistogram extends Histogram { + public static UnivariateHistogram buildUniform(double start, double stop, double step) { + return new UnivariateHistogram(GridCalculator.getUniformUnivariateGrid(start, stop, step)); + } + + private final double[] borders; + private TreeMap binMap = new TreeMap<>(); + + public UnivariateHistogram(double[] borders) { + this.borders = borders; + Arrays.sort(borders); + } + + private Double getValue(Double... point) { + if (point.length != 1) { + throw new DimensionMismatchException(point.length, 1); + } else { + return point[0]; + } + } + + @Override + public Bin createBin(Double... point) { + Double value = getValue(point); + int index = Arrays.binarySearch(borders, value); + if (index >= 0) { + if (index == borders.length - 1) { + return new SquareBin(borders[index], Double.POSITIVE_INFINITY); + } else { + return new SquareBin(borders[index], borders[index + 1]); + } + } else if (index == -1) { + return new SquareBin(Double.NEGATIVE_INFINITY, borders[0]); + } else if (index == -borders.length - 1) { + return new SquareBin(borders[borders.length - 1], Double.POSITIVE_INFINITY); + } else { + return new SquareBin(borders[-index - 2], borders[-index - 1]); + } + } + + @Override + public Optional findBin(Double... point) { + Double value = getValue(point); + Map.Entry entry = binMap.floorEntry(value); + if (entry != null && entry.getValue().contains(point)) { + return Optional.of(entry.getValue()); + } else { + return Optional.empty(); + } + + } + + @Override + protected synchronized Bin addBin(Bin bin) { + //The call should be thread safe. New bin is added only if it is absent + return binMap.computeIfAbsent(bin.getLowerBound(0), (id) -> bin); + } + + @Override + public int getDimension() { + return 1; + } + + @NotNull + @Override + public Iterator iterator() { + return binMap.values().iterator(); + } + + public UnivariateHistogram fill(DoubleStream stream) { + stream.forEach(this::put); + return this; + } + +// public UnivariateHistogram fill(LongStream stream) { +// stream.mapToDouble(it -> it).forEach(this::put); +// return this; +// } +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/integration/CMIntegrand.java b/dataforge-maths/src/main/java/hep/dataforge/maths/integration/CMIntegrand.java new file mode 100644 index 00000000..4829e874 --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/integration/CMIntegrand.java @@ -0,0 +1,55 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.maths.integration; + +import org.apache.commons.math3.analysis.UnivariateFunction; + +/** + * An integrand using commons math accuracy notation + * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class CMIntegrand extends UnivariateIntegrand { + + private double absoluteAccuracy = Double.POSITIVE_INFINITY; + private double relativeAccuracy = Double.POSITIVE_INFINITY; + private int iterations = 0; + + public CMIntegrand(Double lower, Double upper, UnivariateFunction function) { + super(lower, upper, function); + } + + public CMIntegrand(double absoluteAccuracy, double relativeAccuracy, int iterations, int numCalls, Double value, UnivariateIntegrand integrand) { + super(integrand, numCalls, value); + this.absoluteAccuracy = absoluteAccuracy; + this.relativeAccuracy = relativeAccuracy; + this.iterations = iterations; + } + + public double getAbsoluteAccuracy() { + return absoluteAccuracy; + } + + public int getIterations() { + return iterations; + } + + public double getRelativeAccuracy() { + return relativeAccuracy; + } + +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/integration/DistributionSampler.java b/dataforge-maths/src/main/java/hep/dataforge/maths/integration/DistributionSampler.java new file mode 100644 index 00000000..28b6ee72 --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/integration/DistributionSampler.java @@ -0,0 +1,67 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.maths.integration; + +import org.apache.commons.math3.distribution.MultivariateNormalDistribution; +import org.apache.commons.math3.distribution.MultivariateRealDistribution; +import org.apache.commons.math3.linear.SingularMatrixException; +import org.apache.commons.math3.random.RandomGenerator; +import org.slf4j.LoggerFactory; + +import static java.util.Arrays.fill; + +/** + *

DistributionSampler class.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class DistributionSampler implements Sampler { + + private MultivariateRealDistribution distr; + + public DistributionSampler(MultivariateRealDistribution distr) { + this.distr = distr; + } + + public DistributionSampler(RandomGenerator rng, double[] means, double[][] covariance) { + assert means.length == covariance.length; + try { + this.distr = new MultivariateNormalDistribution(rng, means, covariance); + } catch (SingularMatrixException ex) { + // ЕÑли ÐºÐ¾Ð²Ð°Ñ€Ð¸Ð°Ñ†Ð¸Ð¾Ð½Ð½Ð°Ñ Ð¼Ð°Ñ‚Ñ€Ð¸Ñ†Ð° Ñлишком плохо определена + double[][] diagonal = new double[means.length][means.length]; + for (int i = 0; i < diagonal.length; i++) { + fill(diagonal[i], 0); + diagonal[i][i] = covariance[i][i]; + } + LoggerFactory.getLogger(getClass()).info("The covariance is singular. Using only diagonal elements."); + this.distr = new MultivariateNormalDistribution(rng, means, diagonal); + } + } + + @Override + public int getDimension() { + return distr.getDimension(); + } + + @Override + public Sample nextSample(Sample previous) { + double[] sample = distr.sample(); + return new Sample(distr.density(sample), sample); + } + +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/integration/GaussRuleIntegrator.java b/dataforge-maths/src/main/java/hep/dataforge/maths/integration/GaussRuleIntegrator.java new file mode 100644 index 00000000..b5336a0b --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/integration/GaussRuleIntegrator.java @@ -0,0 +1,115 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.maths.integration; + +import org.apache.commons.math3.analysis.UnivariateFunction; +import org.apache.commons.math3.analysis.integration.gauss.GaussIntegrator; +import org.apache.commons.math3.analysis.integration.gauss.GaussIntegratorFactory; +import org.apache.commons.math3.util.Pair; + +import java.util.function.Predicate; + +/** + *

GaussRuleIntegrator class.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class GaussRuleIntegrator extends UnivariateIntegrator { + + private static final GaussIntegratorFactory factory = new GaussIntegratorFactory(); + private final int numpoints; + private IntegratorType type = IntegratorType.LEGANDRE; + + /** + *

Constructor for GaussRuleIntegrator.

+ * + * @param nodes a int. + */ + public GaussRuleIntegrator(int nodes) { + this.numpoints = nodes; + } + + /** + *

Constructor for GaussRuleIntegrator.

+ * + * @param nodes a int. + * @param type a {@link hep.dataforge.maths.integration.GaussRuleIntegrator.IntegratorType} object. + */ + public GaussRuleIntegrator(int nodes, IntegratorType type) { + this.numpoints = nodes; + this.type = type; + } + + /** {@inheritDoc} + * @return */ + @Override + public Predicate getDefaultStoppingCondition() { + return (t) -> true; + } + + /** {@inheritDoc} + * @return */ + @Override + protected CMIntegrand init(Double lower, Double upper, UnivariateFunction function) { + return new CMIntegrand(lower, upper, function); + } + + /** {@inheritDoc} + * @return */ + @Override + public CMIntegrand evaluate(CMIntegrand integrand, Predicate condition) { + GaussIntegrator integrator = getIntegrator(integrand.getLower(), integrand.getUpper()); + double res = integrator.integrate(integrand.getFunction()); + return new CMIntegrand(integrand.getAbsoluteAccuracy(), integrand.getRelativeAccuracy(), 1, numpoints, res, integrand); + } + + private GaussIntegrator getIntegrator(double lower, double upper) { + switch (type) { + case LEGANDRE: + return factory.legendre(numpoints, lower, upper); + case LEGANDREHP: + return factory.legendreHighPrecision(numpoints, lower, upper); + case UNIFORM: + return new GaussIntegrator(getUniformRule(lower, upper, numpoints)); + default: + throw new Error(); + } + } + + private Pair getUniformRule(double min, double max, int numPoints) { + assert numPoints > 2; + double[] points = new double[numPoints]; + double[] weights = new double[numPoints]; + + final double step = (max - min) / (numPoints - 1); + points[0] = min; + + for (int i = 1; i < numPoints; i++) { + points[i] = points[i - 1] + step; + weights[i] = step; + + } + return new Pair<>(points, weights); + + } + + public enum IntegratorType { + UNIFORM, + LEGANDRE, + LEGANDREHP + } +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/integration/IncompatibleIntegrandException.java b/dataforge-maths/src/main/java/hep/dataforge/maths/integration/IncompatibleIntegrandException.java new file mode 100644 index 00000000..d4e81766 --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/integration/IncompatibleIntegrandException.java @@ -0,0 +1,28 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.maths.integration; + +import org.apache.commons.math3.exception.MathUnsupportedOperationException; + +/** + *

IncompatibleIntegrandException class.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class IncompatibleIntegrandException extends MathUnsupportedOperationException { + +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/integration/Integrand.java b/dataforge-maths/src/main/java/hep/dataforge/maths/integration/Integrand.java new file mode 100644 index 00000000..1de4698e --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/integration/Integrand.java @@ -0,0 +1,51 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.maths.integration; + +/** + * An Integrand keeps a function to integrate, borders and the history of + * integration. Additionally it keeps any integrator specific transitive data. + * + * The Integrand supposed to be immutable to support functional-style + * programming + * + * @author Alexander Nozik + * @version $Id: $Id + */ +public interface Integrand { + + /** + * The current calculated value. equals Double.NaN if no successful + * iterations were made so far + * + * @return a {@link java.lang.Double} object. + */ + Double getValue(); + + /** + * the number of calls of function + * + * @return a int. + */ + int getNumCalls(); + + /** + * the dimension of function + * + * @return a int. + */ + int getDimension(); +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/integration/Integrator.java b/dataforge-maths/src/main/java/hep/dataforge/maths/integration/Integrator.java new file mode 100644 index 00000000..d5c445e5 --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/integration/Integrator.java @@ -0,0 +1,67 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.maths.integration; + +import java.util.function.Predicate; + +/** + *

+ * Abstract Integrator class.

+ * + * @author Alexander Nozik + * @param + * @version $Id: $Id + */ +public interface Integrator { + + /** + * Integrate with default stopping condition for this integrator + * + * @param integrand a T object. + * @return a T object. + */ + default T evaluate(T integrand) { + return Integrator.this.evaluate(integrand, getDefaultStoppingCondition()); + } + + /** + * Helper method for single integration + * + * @param integrand a T object. + * @return a {@link java.lang.Double} object. + */ + default Double integrate(T integrand) { + return evaluate(integrand).getValue(); + } + + /** + * Integrate with supplied stopping condition + * + * @param integrand a T object. + * @param condition a {@link java.util.function.Predicate} object. + * @return a T object. + */ + T evaluate(T integrand, Predicate condition); + + /** + * Get default stopping condition for this integrator + * + * @return a {@link java.util.function.Predicate} object. + */ + default Predicate getDefaultStoppingCondition() { + return t -> true; + } +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/integration/IntegratorFactory.java b/dataforge-maths/src/main/java/hep/dataforge/maths/integration/IntegratorFactory.java new file mode 100644 index 00000000..7556f6f0 --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/integration/IntegratorFactory.java @@ -0,0 +1,37 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.maths.integration; + +import java.util.HashMap; +import java.util.Map; + +/** + * + * @author Alexander Nozik + */ +public class IntegratorFactory { + private static final Map gaussRuleMap = new HashMap<>(); + + public static GaussRuleIntegrator getGaussRuleIntegrator(int nodes){ + if(gaussRuleMap.containsKey(nodes)){ + return gaussRuleMap.get(nodes); + } else { + GaussRuleIntegrator integrator = new GaussRuleIntegrator(nodes); + gaussRuleMap.put(nodes, integrator); + return integrator; + } + } +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/integration/IterativeUnivariateIntegrator.java b/dataforge-maths/src/main/java/hep/dataforge/maths/integration/IterativeUnivariateIntegrator.java new file mode 100644 index 00000000..ed8f9ee3 --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/integration/IterativeUnivariateIntegrator.java @@ -0,0 +1,91 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.maths.integration; + +import org.apache.commons.math3.analysis.UnivariateFunction; + +import java.util.function.Predicate; + +/** + * Iterative integrator based on any UnivariateIntegrator + * + * @author Alexander Nozik + * @param Integrand type for supplied univariate integrator + * @version $Id: $Id + */ +public class IterativeUnivariateIntegrator extends UnivariateIntegrator { + + private final UnivariateIntegrator integrator; + + /** + *

Constructor for IterativeUnivariateIntegrator.

+ * + * @param integrator a {@link hep.dataforge.maths.integration.UnivariateIntegrator} object. + */ + public IterativeUnivariateIntegrator(UnivariateIntegrator integrator) { + this.integrator = integrator; + } + + /** {@inheritDoc} + * @return */ + @Override + public Predicate getDefaultStoppingCondition() { + return integrator.getDefaultStoppingCondition(); + } + + /** {@inheritDoc} + * @return */ + @Override + protected T init(Double lower, Double upper, UnivariateFunction function) { + return integrator.init(lower, upper, function); + } + +// /** {@inheritDoc} +// * @return */ +// @Override +// public T evaluate(T integrand, Predicate condition) { +// T firstResult = integrator.init(integrand, condition); +// T nextResult = integrator.evaluate(firstResult, condition); +// +// double dif = Math.abs(nextResult.getValue() - firstResult.getValue()); +// double relDif = dif / Math.abs(firstResult.getValue()); +// +// // No improvement. Returning last result +// if(dif == 0){ +// return nextResult; +// } +// +// UnivariateIntegrand res = new UnivariateIntegrand(nextResult, dif, +// relDif, nextResult.getNumCalls(), nextResult.getValue()); +// +// while (!condition.test(res)) { +// firstResult = nextResult; +// nextResult = integrator.evaluate(firstResult, condition); +// dif = Math.abs(nextResult.getValue() - firstResult.getValue()); +// relDif = dif / Math.abs(firstResult.getValue()); +// +// res = new UnivariateIntegrand(nextResult, dif, +// relDif, nextResult.getIterations(), nextResult.getNumCalls(), nextResult.getValue()); +// } +// +// return res; +// } + + @Override + public T evaluate(T integrand, Predicate condition) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/integration/MonteCarloIntegrand.java b/dataforge-maths/src/main/java/hep/dataforge/maths/integration/MonteCarloIntegrand.java new file mode 100644 index 00000000..7377c75d --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/integration/MonteCarloIntegrand.java @@ -0,0 +1,98 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.maths.integration; + +import org.apache.commons.math3.analysis.MultivariateFunction; + +/** + *

+ * MonteCarloIntegrand class.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class MonteCarloIntegrand implements Integrand { + + private final MultivariateFunction function; + private final Sampler sampler; + private final Double value; + private int numCalls; + private double accuracy = Double.POSITIVE_INFINITY; + + public MonteCarloIntegrand(MonteCarloIntegrand integrand, int numCalls, Double value, double accuracy) { + this.numCalls = numCalls; + this.value = value; + this.function = integrand.getFunction(); + this.sampler = integrand.getSampler(); + this.accuracy = accuracy; + } + + public MonteCarloIntegrand(Sampler sampler, int numCalls, Double value, double accuracy, MultivariateFunction function) { + this.numCalls = numCalls; + this.value = value; + this.function = function; + this.sampler = sampler; + this.accuracy = accuracy; + } + + public MonteCarloIntegrand(Sampler sampler, MultivariateFunction function) { + super(); + this.function = function; + this.sampler = sampler; + this.value = Double.NaN; + } + + /** + * {@inheritDoc} + * + * @return + */ + @Override + public int getDimension() { + return getSampler().getDimension(); + } + + public double getFunctionValue(double[] x) { + return function.value(x); + } + +// public Sample getSample() { +// return sampler.nextSample(); +// } + + public MultivariateFunction getFunction() { + return function; + } + + public Sampler getSampler() { + return sampler; + } + + @Override + public Double getValue() { + return value; + } + + @Override + public int getNumCalls() { + return numCalls; + } + + double getRelativeAccuracy() { + return accuracy; + } + +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/integration/MonteCarloIntegrator.java b/dataforge-maths/src/main/java/hep/dataforge/maths/integration/MonteCarloIntegrator.java new file mode 100644 index 00000000..eddf6f7e --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/integration/MonteCarloIntegrator.java @@ -0,0 +1,122 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.maths.integration; + +import org.apache.commons.math3.analysis.MultivariateFunction; + +import java.util.function.Predicate; + +/** + *

+ * MonteCarloIntegrator class.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class MonteCarloIntegrator implements Integrator { + + private static final int DEFAULT_MAX_CALLS = 100000; + private static final double DEFAULT_MIN_RELATIVE_ACCURACY = 1e-3; + + private int sampleSizeStep = 500; + + public MonteCarloIntegrator() { + } + + public MonteCarloIntegrator(int sampleSizeStep) { + this.sampleSizeStep = sampleSizeStep; + } + + /** + * {@inheritDoc} + * + * @return + */ + @Override + public Predicate getDefaultStoppingCondition() { + return (t) -> t.getNumCalls() > DEFAULT_MAX_CALLS || t.getRelativeAccuracy() < DEFAULT_MIN_RELATIVE_ACCURACY; + } + + /** + * Integration with fixed sample size + * + * @param function a + * {@link org.apache.commons.math3.analysis.MultivariateFunction} object. + * @param sampler a {@link hep.dataforge.maths.integration.Sampler} object. + * @param sampleSize a int. + * @return a {@link hep.dataforge.maths.integration.MonteCarloIntegrand} + * object. + */ + public MonteCarloIntegrand evaluate(MultivariateFunction function, Sampler sampler, int sampleSize) { + return evaluate(new MonteCarloIntegrand(sampler, function), sampleSize); + } + + private MonteCarloIntegrand makeStep(MonteCarloIntegrand integrand) { + double res = 0; + + for (int i = 0; i < sampleSizeStep; i++) { + Sample sample = integrand.getSampler().nextSample(null); + res += integrand.getFunctionValue(sample.getArray()) / sample.getWeight(); + } + + double oldValue = integrand.getValue(); + int oldCalls = integrand.getNumCalls(); + double value; + if (Double.isNaN(oldValue)) { + value = res / sampleSizeStep; + } else { + value = (oldValue * oldCalls + res) / (sampleSizeStep + oldCalls); + } + + int evaluations = integrand.getNumCalls() + sampleSizeStep; + + double accuracy = Double.POSITIVE_INFINITY; + + if (!Double.isNaN(oldValue)) { + accuracy = Math.abs(value - oldValue); + } + + return new MonteCarloIntegrand(integrand, evaluations, value, accuracy); + } + + /** + * {@inheritDoc} + * + * @return + */ + @Override + public MonteCarloIntegrand evaluate(MonteCarloIntegrand integrand, Predicate condition) { + MonteCarloIntegrand res = integrand; + while (!condition.test(res)) { + res = makeStep(res); + } + return res; + } + + /** + * Integration with fixed maximum sample size + * + * @param integrand a + * {@link hep.dataforge.maths.integration.MonteCarloIntegrand} object. + * @param sampleSize a int. + * @return a {@link hep.dataforge.maths.integration.MonteCarloIntegrand} + * object. + */ + public MonteCarloIntegrand evaluate(MonteCarloIntegrand integrand, int sampleSize) { + return MonteCarloIntegrator.this.evaluate(integrand, ((t) -> t.getNumCalls() >= sampleSize)); + } + +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/integration/RiemanIntegrator.java b/dataforge-maths/src/main/java/hep/dataforge/maths/integration/RiemanIntegrator.java new file mode 100644 index 00000000..d1a9f30e --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/integration/RiemanIntegrator.java @@ -0,0 +1,90 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.maths.integration; + +import hep.dataforge.maths.GridCalculator; +import org.apache.commons.math3.analysis.UnivariateFunction; + +import java.util.function.Predicate; + +/** + *

+ * GaussRuleIntegrator class.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class RiemanIntegrator extends UnivariateIntegrator { + + private final int numpoints; + + /** + *

+ * Constructor for GaussRuleIntegrator.

+ * + * @param nodes a int. + */ + public RiemanIntegrator(int nodes) { + if (nodes < 5) { + throw new IllegalStateException("The number of integration nodes is to small"); + } + this.numpoints = nodes; + } + + /** + * {@inheritDoc} + * + * @return + */ + @Override + public Predicate getDefaultStoppingCondition() { + return (t) -> true; + } + + /** + * {@inheritDoc} + * + * @return + */ + @Override + protected UnivariateIntegrand init(Double lower, Double upper, UnivariateFunction function) { + return new UnivariateIntegrand(lower, upper, function); + } + + /** + * {@inheritDoc} + * + * @return + */ + @Override + public UnivariateIntegrand evaluate(UnivariateIntegrand integrand, Predicate condition) { + double[] grid = GridCalculator.getUniformUnivariateGrid(integrand.getLower(), integrand.getUpper(), numpoints); + double res = 0; + + UnivariateFunction f = integrand.getFunction(); + + double prevX; + double nextX; + + for (int i = 0; i < grid.length - 1; i++) { + prevX = grid[i]; + nextX = grid[i + 1]; + res += f.value(prevX) * (nextX - prevX); + } + + return new UnivariateIntegrand(integrand, numpoints, res); + } +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/integration/Sample.java b/dataforge-maths/src/main/java/hep/dataforge/maths/integration/Sample.java new file mode 100644 index 00000000..ffa8870a --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/integration/Sample.java @@ -0,0 +1,57 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.maths.integration; + +import org.apache.commons.math3.linear.ArrayRealVector; +import org.apache.commons.math3.linear.RealVector; + +/** + * A single multivariate sample with weight + * + * @author Alexander Nozik + + */ +public class Sample { + + private final double weight; + private final RealVector sample; + + public Sample(double weight, RealVector sample) { + this.weight = weight; + this.sample = sample; + } + + public Sample(double weight, double[] sample) { + this.weight = weight; + this.sample = new ArrayRealVector(sample); + } + + public int getDimension() { + return sample.getDimension(); + } + + public double getWeight() { + return weight; + } + + public RealVector getVector() { + return sample; + } + + public double[] getArray() { + return sample.toArray(); + } +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/integration/Sampler.java b/dataforge-maths/src/main/java/hep/dataforge/maths/integration/Sampler.java new file mode 100644 index 00000000..9c459483 --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/integration/Sampler.java @@ -0,0 +1,51 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.maths.integration; + +import hep.dataforge.maths.MultivariateUniformDistribution; +import kotlin.Pair; +import org.apache.commons.math3.distribution.MultivariateNormalDistribution; +import org.apache.commons.math3.linear.RealMatrix; +import org.apache.commons.math3.linear.RealVector; +import org.apache.commons.math3.random.RandomGenerator; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + *

Abstract Sampler class.

+ * + * @author Alexander Nozik + */ +public interface Sampler { + + static Sampler uniform(RandomGenerator generator, List> borders) { + return new DistributionSampler(MultivariateUniformDistribution.square(generator, borders)); + } + + static Sampler normal(RandomGenerator generator, RealVector means, RealMatrix covariance){ + return new DistributionSampler(new MultivariateNormalDistribution(generator,means.toArray(),covariance.getData())); + } + + + Sample nextSample(@Nullable Sample previousSample); + +// default Stream stream() { +// return Stream.generate(this::nextSample); +// } + + int getDimension(); +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/integration/SplitUnivariateIntegrator.java b/dataforge-maths/src/main/java/hep/dataforge/maths/integration/SplitUnivariateIntegrator.java new file mode 100644 index 00000000..32f15757 --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/integration/SplitUnivariateIntegrator.java @@ -0,0 +1,53 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.maths.integration; + +import org.apache.commons.math3.analysis.UnivariateFunction; + +import java.util.Map; +import java.util.function.Predicate; +import java.util.function.Supplier; + +/** + * Integrator that breaks an interval into subintervals and integrates each + * interval with its own integrator; + * + * @author Alexander Nozik + */ +public class SplitUnivariateIntegrator extends UnivariateIntegrator { + + Map> segments; + Supplier defaultIntegrator; + + @Override + protected UnivariateIntegrand init(Double lower, Double upper, UnivariateFunction function) { + return new UnivariateIntegrand(lower, upper, function); + } + +// @Override +// public UnivariateIntegrand evaluate(UnivariateIntegrand integrand, Predicate condition) { +// Map> integrandMap; +// +// Map res = new HashMap<>(); +// return new UnivariateIntegrand(integrand, +// res.keySet().stream().mapToDouble(it->it.getAbsoluteAccuracy()).sum(), +// 0, +// res.keySet().stream().mapToInt(it->it.getIterations()).sum(), +// 0, +// res.values().stream().mapToDouble(it->it).sum()); +// } + + @Override + public UnivariateIntegrand evaluate(UnivariateIntegrand integrand, Predicate condition) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public Predicate getDefaultStoppingCondition() { + return (t) -> true; + } + +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/integration/UnivariateIntegrand.java b/dataforge-maths/src/main/java/hep/dataforge/maths/integration/UnivariateIntegrand.java new file mode 100644 index 00000000..bf6ecd8a --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/integration/UnivariateIntegrand.java @@ -0,0 +1,95 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.maths.integration; + +import org.apache.commons.math3.analysis.UnivariateFunction; + +/** + *

+ * UnivariateIntegrand class.

+ * + * @author Alexander Nozik + * @version $Id: $Id + */ +public class UnivariateIntegrand implements Integrand { + + private final UnivariateFunction function; + /* + In theory it is possible to make infinite bounds + */ + private Double lower; + private Double upper; + private Double value; + private int numCalls; + + public UnivariateIntegrand(Double lower, Double upper, UnivariateFunction function) { + this.function = function; + if (lower >= upper) { + throw new IllegalArgumentException("Wrong bounds for integrand"); + } + this.lower = lower; + this.upper = upper; + } + + public UnivariateIntegrand(UnivariateIntegrand integrand, int numCalls, Double value) { + //TODO check value + this.value = value; + this.numCalls = numCalls + integrand.numCalls; + this.function = integrand.getFunction(); + if (integrand.getLower() >= integrand.getUpper()) { + throw new IllegalArgumentException("Wrong bounds for integrand"); + } + this.lower = integrand.getLower(); + this.upper = integrand.getUpper(); + } + + /** + * {@inheritDoc} + * + * @return + */ + @Override + public int getDimension() { + return 1; + } + + public double getFunctionValue(double x) { + return function.value(x); + } + + public UnivariateFunction getFunction() { + return function; + } + + public Double getLower() { + return lower; + } + + public Double getUpper() { + return upper; + } + + @Override + public Double getValue() { + return value; + } + + @Override + public int getNumCalls() { + return numCalls; + } + +} diff --git a/dataforge-maths/src/main/java/hep/dataforge/maths/integration/UnivariateIntegrator.java b/dataforge-maths/src/main/java/hep/dataforge/maths/integration/UnivariateIntegrator.java new file mode 100644 index 00000000..d963e3ab --- /dev/null +++ b/dataforge-maths/src/main/java/hep/dataforge/maths/integration/UnivariateIntegrator.java @@ -0,0 +1,62 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.maths.integration; + +import org.apache.commons.math3.analysis.UnivariateFunction; + +import java.util.function.Predicate; + +/** + * General ancestor for univariate integrators + * + * @author Alexander Nozik + * @param + * @version $Id: $Id + */ +public abstract class UnivariateIntegrator implements Integrator { + + /** + * Create initial Integrand for given function and borders. This method is + * required to initialize any + * + * @param lower a {@link Double} object. + * @param upper a {@link Double} object. + * @param function a + * {@link UnivariateFunction} object. + * @return a T object. + */ + protected abstract T init(Double lower, Double upper, UnivariateFunction function); + + public T evaluate(Double lower, Double upper, UnivariateFunction function) { + return evaluate(UnivariateIntegrator.this.init(lower, upper, function)); + } + + public Double integrate(Double lower, Double upper, UnivariateFunction function) { + return evaluate(lower, upper, function).getValue(); + } + + public T evaluate(Predicate condition, Double lower, Double upper, UnivariateFunction function) { + return evaluate(init(lower, upper, function), condition); + } + + public T init(UnivariateIntegrand integrand) { + return evaluate(integrand.getLower(), integrand.getUpper(), integrand.getFunction()); + } + + public T init(UnivariateIntegrand integrand, Predicate condition) { + return evaluate(condition, integrand.getLower(), integrand.getUpper(), integrand.getFunction()); + } +} diff --git a/dataforge-maths/src/main/kotlin/hep/dataforge/maths/chain/Chain.kt b/dataforge-maths/src/main/kotlin/hep/dataforge/maths/chain/Chain.kt new file mode 100644 index 00000000..30adf24a --- /dev/null +++ b/dataforge-maths/src/main/kotlin/hep/dataforge/maths/chain/Chain.kt @@ -0,0 +1,169 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.maths.chain + +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.channels.map +import kotlinx.coroutines.channels.produce +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +/** + * A not-necessary-Markov chain of some type + * @param S - the state of the chain + * @param R - the chain element type + */ +interface Chain : Sequence { + /** + * Last value of the chain + */ + val value: R + + /** + * Generate next value, changing state if needed + */ + suspend fun next(): R + + /** + * Create a copy of current chain state. Consuming resulting chain does not affect initial chain + */ + fun fork(): Chain + + /** + * Chain as a coroutine receive channel + */ + val channel: ReceiveChannel + get() = GlobalScope.produce { while (true) send(next()) } + + override fun iterator(): Iterator { + return object : Iterator { + override fun hasNext(): Boolean = true + + override fun next(): R = runBlocking { this@Chain.next() } + } + } + + /** + * Map the chain result using suspended transformation. Initial chain result can no longer be safely consumed + * since mapped chain consumes tokens. + */ + fun map(func: suspend (R) -> T): Chain { + val parent = this; + return object : Chain { + override val value: T + get() = runBlocking { func.invoke(parent.value) } + + override suspend fun next(): T { + return func(parent.next()) + } + + override fun fork(): Chain { + return parent.fork().map(func) + } + + override val channel: ReceiveChannel + get() { + return parent.channel.map { func.invoke(it) } + } + } + } +} + +private class TransientValue { + val mutex = Mutex() + + var value: R? = null + private set + + suspend fun update(value: R) { + mutex.withLock { this.value = value } + } +} + +//TODO force forks on mapping operations? + +/** + * A simple chain of independent tokens + */ +class SimpleChain(private val gen: suspend () -> R) : Chain { + private val _value = TransientValue() + override val value: R + get() = _value.value ?: runBlocking { next() } + + override suspend fun next(): R { + _value.update(gen()) + return value + } + + override fun fork(): Chain = this +} + +/** + * A stateless Markov chain + */ +class MarkovChain(private val seed: () -> R, private val gen: suspend (R) -> R) : Chain { + + constructor(seed: R, gen: suspend (R) -> R) : this({ seed }, gen) + + private val _value = TransientValue() + override val value: R + get() = _value.value ?: runBlocking { next() } + + override suspend fun next(): R { + _value.update(gen(_value.value ?: seed())) + return value + } + + override fun fork(): Chain { + return MarkovChain(value, gen) + } +} + +/** + * A chain with possibly mutable state. The state must not be changed outside the chain. Two chins should never share the state + */ +class StatefulChain(val state: S, private val seed: S.() -> R, private val gen: suspend S.(R) -> R) : Chain { + constructor(state: S, seed: R, gen: suspend S.(R) -> R) : this(state, { seed }, gen) + + private val _value = TransientValue() + override val value: R + get() = _value.value ?: runBlocking { next() } + + override suspend fun next(): R { + _value.update(gen(state,_value.value ?: seed(state))) + return value + } + + override fun fork(): Chain { + throw RuntimeException("Fork not supported for stateful chain") + } +} + +/** + * A chain that repeats the same value + */ +class ConstantChain(override val value: T) : Chain { + override suspend fun next(): T { + return value + } + + override fun fork(): Chain { + return this + } +} \ No newline at end of file diff --git a/dataforge-maths/src/main/kotlin/hep/dataforge/maths/expressions/DSNumber.kt b/dataforge-maths/src/main/kotlin/hep/dataforge/maths/expressions/DSNumber.kt new file mode 100644 index 00000000..a877aa5e --- /dev/null +++ b/dataforge-maths/src/main/kotlin/hep/dataforge/maths/expressions/DSNumber.kt @@ -0,0 +1,127 @@ +package hep.dataforge.maths.expressions + +import hep.dataforge.names.NameList +import org.apache.commons.math3.analysis.differentiation.DerivativeStructure + +class DSNumber(val ds: DerivativeStructure, nc: DSField) : FieldCompat(nc) { + override val self: DSNumber = this + + fun deriv(parName: String): Double { + return deriv(mapOf(parName to 1)) + } + + fun deriv(orders: Map): Double { + return ds.getPartialDerivative(*nc.names.map { orders[it] ?: 0 }.toIntArray()) + } + + override fun toByte(): Byte = ds.value.toByte() + + override fun toChar(): Char = ds.value.toChar() + + override fun toDouble(): Double = ds.value + + override fun toFloat(): Float = ds.value.toFloat() + + override fun toInt(): Int = ds.value.toInt() + + override fun toLong(): Long = ds.value.toLong() + + override fun toShort(): Short = ds.value.toShort() + + /** + * Return new DSNumber, obtained by applying given function to underlying ds + */ + inline fun eval(func: (DerivativeStructure) -> DerivativeStructure): DSNumber { + return DSNumber(func(ds), nc) + } +} + +class DSField(val order: Int, val names: NameList) : VariableField, ExtendedField { + + override val one: DSNumber = transform(1.0) + + override val zero: DSNumber = transform( 0.0) + + constructor(order: Int, vararg names: String) : this(order, NameList(*names)) + + override fun transform(n: Number): DSNumber { + return if (n is DSNumber) { + if (n.nc.names == this.names) { + n + } else { + //TODO add conversion + throw RuntimeException("Names mismatch in derivative structure") + } + } else { + DSNumber(DerivativeStructure(names.size(), order, n.toDouble()), this) + } + } + + override fun variable(name: String, value: Number): DSNumber { + if (!names.contains(name)) { + //TODO add conversions probably + throw RuntimeException("Name $name is not a part of the number context") + } + return DSNumber(DerivativeStructure(names.size(), order, names.getNumberByName(name), value.toDouble()), this) + } + + override fun add(a: Number,b: Number): DSNumber { + return if (b is DSNumber) { + transform(a).eval { it.add(b.ds) } + } else { + transform(a).eval { it.add(b.toDouble()) } + } + } + + override fun subtract(a: Number,b: Number): DSNumber { + return if (b is DSNumber) { + transform(a).eval { it.subtract(b.ds) } + } else { + transform(a).eval { it.subtract(b.toDouble()) } + } + + } + + override fun divide(a: Number,b: Number): DSNumber { + return if (b is DSNumber) { + transform(a).eval { it.divide(b.ds) } + } else { + transform(a).eval { it.divide(b.toDouble()) } + } + + } + + override fun negate(a: Number): DSNumber { + return (a as? DSNumber)?.eval { it.negate() } ?: transform(-a.toDouble()) + } + + override fun multiply(a: Number,b: Number): DSNumber { + return when (b) { + is DSNumber -> transform(a).eval { it.multiply(b.ds) } + is Int -> transform(a).eval { it.multiply(b) } + else -> transform(a).eval { it.multiply(b.toDouble()) } + } + + } + + + override fun sin(n: Number): DSNumber { + return transform(n).eval { it.sin() } + } + + override fun cos(n: Number): DSNumber { + return transform(n).eval { it.cos() } + } + + override fun exp(n: Number): DSNumber { + return transform(n).eval { it.exp() } + } + + override fun pow(n: Number, p: Number): DSNumber { + return when (p) { + is Int -> transform(n).eval { it.pow(p) } + is DSNumber -> transform(n).eval { it.pow(p.ds) } + else -> transform(n).eval { it.pow(p.toDouble()) } + } + } +} \ No newline at end of file diff --git a/dataforge-maths/src/main/kotlin/hep/dataforge/maths/expressions/Expression.kt b/dataforge-maths/src/main/kotlin/hep/dataforge/maths/expressions/Expression.kt new file mode 100644 index 00000000..a6ef6eaa --- /dev/null +++ b/dataforge-maths/src/main/kotlin/hep/dataforge/maths/expressions/Expression.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.maths.expressions + +import hep.dataforge.names.NameList +import hep.dataforge.values.Values + +/** + * The expression that could be evaluated in runtime + * + * @author Alexander Nozik + */ +interface Expression { + + /** + * Evaluate expression using given set of parameters. + * The provided set of parameters could be broader then requested set. Also expression could provide defaults for + * some values, in this case exception is not thrown even if one of such parameters is missing. + * @return + */ + operator fun invoke(parameters: Values): T +} + +class BasicExpression(val function: (Values) -> T) : Expression { + override fun invoke(parameters: Values): T { + return function.invoke(parameters) + } +} + +/** + * @param names A set of names for parameters of this expression + */ +class ExpressionField(val names: NameList, private val field: Field) : ExtendedField> { + + override val one: Expression = BasicExpression { field.one } + + override val zero: Expression = BasicExpression { field.one } + + override fun transform(n: T): Expression { + return if (n is Expression<*>) { + n as Expression + } else { + BasicExpression { field.transform(n) } + } + } + + override fun add(a: T, b: T): Expression { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun subtract(a: T, b: T): Expression { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun divide(a: T, b: T): Expression { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun multiply(a: T, b: T): Expression { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun negate(a: T): Expression { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun sin(n: T): Expression { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun cos(n: T): Expression { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun exp(n: T): Expression { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun pow(n: T, p: T): Expression { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + +} \ No newline at end of file diff --git a/dataforge-maths/src/main/kotlin/hep/dataforge/maths/expressions/Field.kt b/dataforge-maths/src/main/kotlin/hep/dataforge/maths/expressions/Field.kt new file mode 100644 index 00000000..bfc2a8f4 --- /dev/null +++ b/dataforge-maths/src/main/kotlin/hep/dataforge/maths/expressions/Field.kt @@ -0,0 +1,147 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.maths.expressions + +/** + * A context for mathematical operations on specific type of numbers. The input could be defined as any number, + * but the output is always typed as given type. + * @param T - input type for operation arguments + * @param N - the type of operation result. In general supposed to be subtype of T + */ +interface Field { + val one: N + val zero: N + + fun add(a: T, b: T): N + + /** + * Arithmetical sum of arguments. + * Looks like N plus(a:Number, b: Number) in java + */ + operator fun T.plus(b: T): N { + return add(this, b) + } + + fun subtract(a: T, b: T): N + + /** + * Second argument subtracted from the first + */ + operator fun T.minus(b: T): N { + return subtract(this, b) + } + + fun divide(a: T, b: T): N + + /** + * Division + */ + operator fun T.div(b: T): N { + return divide(this, b) + } + + fun multiply(a: T, b: T): N + + /** + * Multiplication + */ + operator fun T.times(b: T): N { + return multiply(this, b) + } + + fun negate(a: T): N + + /** + * Negation + */ + operator fun T.unaryMinus(): N { + return negate(this) + } + + /** + * Transform from input type to output type. + * Throws an exception if transformation is not available. + */ + fun transform(n: T): N + +} + +/** + * Additional operations that could be performed on numbers in context + */ +interface ExtendedField : Field { + fun sin(n: T): N + fun cos(n: T): N + fun exp(n: T): N + fun pow(n: T, p: T): N + +// fun reminder(a: T, b: T): N + //TODO etc +} + +/** + * Additional tools to work with expressions + */ +interface VariableField : Field { + /** + * define a variable and its value + */ + fun variable(name: String, value: Number): N +} + + +/** + * Backward compatibility class for connoms-math/commons-numbers field/ + */ +abstract class FieldCompat>(val nc: C) : Number() { + abstract val self: T + operator fun plus(n: T): N { + return with(nc) { self.plus(n) } + } + + operator fun minus(n: T): N { + return with(nc) { self.minus(n) } + } + + operator fun times(n: T): N { + return with(nc) { self.times(n) } + } + + operator fun div(n: T): N { + return with(nc) { self.div(n) } + } + + operator fun unaryMinus(): N { + return with(nc) { self.unaryMinus() } + } + + //A temporary fix for https://youtrack.jetbrains.com/issue/KT-22972 + abstract override fun toByte(): Byte + + abstract override fun toChar(): Char + + abstract override fun toDouble(): Double + + abstract override fun toFloat(): Float + + abstract override fun toInt(): Int + + abstract override fun toLong(): Long + + abstract override fun toShort(): Short + +} \ No newline at end of file diff --git a/dataforge-maths/src/main/kotlin/hep/dataforge/maths/extensions/MathExtensions.kt b/dataforge-maths/src/main/kotlin/hep/dataforge/maths/extensions/MathExtensions.kt new file mode 100644 index 00000000..4288b0f9 --- /dev/null +++ b/dataforge-maths/src/main/kotlin/hep/dataforge/maths/extensions/MathExtensions.kt @@ -0,0 +1,249 @@ +package hep.dataforge.maths.extensions + +import org.apache.commons.math3.analysis.UnivariateFunction +import org.apache.commons.math3.linear.* + +/** + * A number of useful extensions for existing common-maths classes + * + * Created by darksnake on 24-Apr-17. + */ + +//vector extensions + +operator fun RealVector.plus(other: RealVector): RealVector { + return this.add(other) +} + +operator fun RealVector.plus(num: Number): RealVector { + return this.mapAdd(num.toDouble()) +} + +operator fun RealVector.minus(other: RealVector): RealVector { + return this.subtract(other) +} + +operator fun RealVector.minus(num: Number): RealVector { + return this.mapSubtract(num.toDouble()) +} + +operator fun RealVector.unaryMinus(): RealVector { + return this.mapMultiply(-1.0) +} + +/** + * scalar product + * @param this + * @param other + * @return + */ +operator fun RealVector.times(other: RealVector): Number { + return this.dotProduct(other) +} + +operator fun RealVector.times(num: Number): RealVector { + return this.mapMultiply(num.toDouble()) +} + +operator fun RealVector.times(matrix: RealMatrix): RealVector { + return matrix.preMultiply(this) +} + +operator fun RealVector.div(num: Number): RealVector { + return this.mapDivide(num.toDouble()) +} + + +operator fun RealVector.get(index: Int): Double { + return this.getEntry(index); +} + +operator fun RealVector.set(index: Int, value: Number) { + return this.setEntry(index, value.toDouble()); +} + +fun DoubleArray.toVector(): RealVector { + return ArrayRealVector(this); +} + +//matrix extensions + +/** + * Return new map and apply given transformation to each of its elements. Closure takes 3 arguments: row number, + * column number and actual value of matrix cell. + * @param self + * @param func + * @return + */ +fun RealMatrix.map(func: (Int, Int, Double) -> Double): RealMatrix { + val res = this.copy(); + res.walkInColumnOrder(object : DefaultRealMatrixChangingVisitor() { + override fun visit(row: Int, column: Int, value: Double): Double { + return func(row, column, value); + } + }) + return res; +} + +operator fun RealMatrix.plus(other: RealMatrix): RealMatrix { + return this.add(other) +} + +/** + * A diagonal matrix with the equal numbers + */ +fun identityMatrix(dim: Int, num: Number): RealMatrix { + return DiagonalMatrix(DoubleArray(dim) { num.toDouble() }); +} + +/** + * Identity matrix + */ +fun identityMatrix(dim: Int): RealMatrix { + return DiagonalMatrix(DoubleArray(dim) { 1.0 }); +} + +/** + * Add identity matrix x num to this matrix + * @param self + * @param num + * @return + */ +operator fun RealMatrix.plus(num: Number): RealMatrix { + return this.add(identityMatrix(this.rowDimension, num)) +} + +operator fun RealMatrix.minus(num: Number): RealMatrix { + return this.subtract(identityMatrix(this.rowDimension, num)) +} + +operator fun RealMatrix.minus(other: RealMatrix): RealMatrix { + return this.subtract(other) +} + +operator fun RealMatrix.unaryMinus(): RealMatrix { + return this.map { _, _, v -> -v }; +} + +operator fun RealMatrix.times(num: Number): RealMatrix { + return this.scalarMultiply(num.toDouble()) +} + +operator fun RealMatrix.times(vector: RealVector): RealVector { + return this.operate(vector); +} + +operator fun RealMatrix.div(num: Number): RealMatrix { + return this.scalarMultiply(1.0 / num.toDouble()) +} + +operator fun RealMatrix.get(i1: Int, i2: Int): Double { + return this.getEntry(i1, i2); +} + +operator fun RealMatrix.set(i1: Int, i2: Int, value: Number) { + this.setEntry(i1, i2, value.toDouble()); +} + +//function extensions + +operator fun UnivariateFunction.plus(function: (Double) -> Double): UnivariateFunction { + return UnivariateFunction { x -> this.value(x) + function(x) } +} + +operator fun UnivariateFunction.plus(num: Number): UnivariateFunction { + return UnivariateFunction { x -> this.value(x) + num.toDouble() } +} + +operator fun UnivariateFunction.minus(function: (Double) -> Double): UnivariateFunction { + return UnivariateFunction { x -> this.value(x) - function(x) } +} + +operator fun UnivariateFunction.minus(num: Number): UnivariateFunction { + return UnivariateFunction { x -> this.value(x) - num.toDouble() } +} + +operator fun UnivariateFunction.times(function: (Double) -> Double): UnivariateFunction { + return UnivariateFunction { x -> this.value(x) * function(x) } +} + +operator fun UnivariateFunction.times(num: Number): UnivariateFunction { + return UnivariateFunction { x -> this.value(x) * num.toDouble() } +} + +operator fun UnivariateFunction.div(function: (Double) -> Double): UnivariateFunction { + return UnivariateFunction { x -> this.value(x) / function(x) } +} + +operator fun UnivariateFunction.div(num: Number): UnivariateFunction { + return UnivariateFunction { x -> this.value(x) / num.toDouble() } +} + +operator fun UnivariateFunction.unaryMinus(): UnivariateFunction { + return UnivariateFunction { x -> -this.value(x) } +} + +operator fun UnivariateFunction.invoke(num: Number): Double { + return this.value(num.toDouble()); +} + +operator fun UnivariateFunction.invoke(vector: RealVector): RealVector { + return vector.map(this); +} + +operator fun UnivariateFunction.invoke(array: DoubleArray): DoubleArray { + return array.toVector().map(this).toArray(); +} + + +//number extensions + +operator fun Number.plus(other: RealVector): RealVector { + return other + this +} + +operator fun Number.minus(other: RealVector): RealVector { + return (-other) + this +} + +operator fun Number.times(other: RealVector): RealVector { + return other * this; +} + +operator fun Number.plus(other: RealMatrix): RealMatrix { + return other + this +} + +operator fun Number.minus(other: RealMatrix): RealMatrix { + return (-other) + this +} + +operator fun Number.times(other: RealMatrix): RealMatrix { + return other * this; +} + +operator fun Number.plus(other: UnivariateFunction): UnivariateFunction { + return other + this +} + +operator fun Number.minus(other: UnivariateFunction): UnivariateFunction { + return -other + this +} + +operator fun Number.times(other: UnivariateFunction): UnivariateFunction { + return other * this +} + +//TODO differentiable functions algebra + +//fun UnivariateDifferentiableFunction plus(final Number this, UnivariateDifferentiableFunction other) { +// return other + this +//} +// +//fun UnivariateDifferentiableFunction minus(final Number this, UnivariateDifferentiableFunction other) { +// return (-other) + this +//} +// +//fun UnivariateDifferentiableFunction multiply(final Number this, UnivariateDifferentiableFunction other) { +// return other * this; +//} \ No newline at end of file diff --git a/dataforge-maths/src/main/kotlin/hep/dataforge/maths/extensions/RealFieldExtensions.kt b/dataforge-maths/src/main/kotlin/hep/dataforge/maths/extensions/RealFieldExtensions.kt new file mode 100644 index 00000000..778ead5c --- /dev/null +++ b/dataforge-maths/src/main/kotlin/hep/dataforge/maths/extensions/RealFieldExtensions.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.maths.extensions + +import org.apache.commons.math3.RealFieldElement + + +operator fun > T.plus(arg: T): T = this.add(arg) + +operator fun > T.minus(arg: T): T = this.subtract(arg) + +operator fun > T.div(arg: T): T = this.divide(arg) + +operator fun > T.times(arg: T): T = this.multiply(arg) + +operator fun > T.plus(arg: Number): T = this.add(arg.toDouble()) + +operator fun > T.minus(arg: Number): T = this.subtract(arg.toDouble()) + +operator fun > T.div(arg: Number): T = this.divide(arg.toDouble()) + +operator fun > T.times(arg: Number): T = this.multiply(arg.toDouble()) + +operator fun > T.unaryMinus(): T = this.negate() + + +fun > abs(arg: T): T = arg.abs() + +fun > ceil(arg: T): T = arg.ceil() + +fun > floor(arg: T): T = arg.floor() + +fun > rint(arg: T): T = arg.rint() + +fun > sin(arg: T): T = arg.sin() + +fun > sqrt(arg: T): T = arg.sqrt() + +fun > exp(arg: T): T = arg.exp() + +//TODO add everything else \ No newline at end of file diff --git a/dataforge-maths/src/main/kotlin/hep/dataforge/maths/functions/FunctionLibrary.kt b/dataforge-maths/src/main/kotlin/hep/dataforge/maths/functions/FunctionLibrary.kt new file mode 100644 index 00000000..8059dc7b --- /dev/null +++ b/dataforge-maths/src/main/kotlin/hep/dataforge/maths/functions/FunctionLibrary.kt @@ -0,0 +1,124 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.maths.functions + +import hep.dataforge.context.* +import hep.dataforge.meta.Meta +import org.apache.commons.math3.analysis.BivariateFunction +import org.apache.commons.math3.analysis.MultivariateFunction +import org.apache.commons.math3.analysis.UnivariateFunction + +/** + * Mathematical plugin. Stores function library and other useful things. + * + * @author Alexander Nozik + */ +@PluginDef(name = "functions", group = "hep.dataforge", info = "A library of pre-compiled functions") +class FunctionLibrary : BasicPlugin() { + + private val univariateFactory = MultiFactory() + private val bivariateFactory = MultiFactory() + private val multivariateFactory = MultiFactory() + + fun buildUnivariateFunction(key: String, meta: Meta): UnivariateFunction { + return univariateFactory.build(key, meta) + } + + fun addUnivariateFactory(type: String, factory: (Meta) -> UnivariateFunction) { + this.univariateFactory.addFactory(type, factory) + } + + fun addUnivariate(type: String, function: (Double) -> Double) { + this.univariateFactory.addFactory(type) { UnivariateFunction(function) } + } + + fun buildBivariateFunction(key: String, meta: Meta): BivariateFunction { + return bivariateFactory.build(key, meta) + } + + fun buildBivariateFunction(key: String): BivariateFunction { + return bivariateFactory.build(key, Meta.empty()) + } + + fun addBivariateFactory(key: String, factory: (Meta) -> BivariateFunction) { + this.bivariateFactory.addFactory(key, factory) + } + + fun addBivariate(key: String, function: BivariateFunction) { + this.bivariateFactory.addFactory(key) { function } + } + + fun addBivariate(key: String, function: (Double, Double) -> Double) { + this.bivariateFactory.addFactory(key) { BivariateFunction(function) } + } + + +// override fun respond(message: Envelope): Envelope { +// val action = message.meta.getString("action", "getValue"); +// if (action == "getValue") { +// val builder = EnvelopeBuilder().setDataType("hep.dataforge.function.response") +// message.meta.getMetaList("request").forEach { request -> +// val functionKey = request.getString("key") +// val functionMeta = message.meta.getMetaOrEmpty("meta") +// val arguments = request.getValue("argument").list.map { it.double } +// val requestID = request.getValue("id", -1) +// +// +// val result = when (arguments.size) { +// 0 -> throw RuntimeException("No arguments found") +// 1 -> { +// val univariateFunction = univariateFactory.build(functionKey, functionMeta) +// univariateFunction.value(arguments[0]) +// } +// 2 -> { +// val bivariateFunction = bivariateFactory.build(functionKey, functionMeta) +// bivariateFunction.value(arguments[0], arguments[1]) +// } +// else -> { +// val multivariateFunction = multivariateFactory.build(functionKey, functionMeta) +// multivariateFunction.value(arguments.toDoubleArray()) +// } +// } +// buildMeta("response", "result" to result, "id" to requestID) +// } +// return builder.build() +// } else { +// throw RuntimeException("Unknown action $action") +// } +// } + + class Factory : PluginFactory() { + + override val type: Class = FunctionLibrary::class.java + + override fun build(meta: Meta): Plugin { + return FunctionLibrary() + } + } + + companion object { + fun buildFrom(context: Context): FunctionLibrary { + return context.plugins.load(FunctionLibrary::class.java) + } + } + +} diff --git a/dataforge-maths/src/main/resources/META-INF/services/hep.dataforge.context.PluginFactory b/dataforge-maths/src/main/resources/META-INF/services/hep.dataforge.context.PluginFactory new file mode 100644 index 00000000..c70f0494 --- /dev/null +++ b/dataforge-maths/src/main/resources/META-INF/services/hep.dataforge.context.PluginFactory @@ -0,0 +1 @@ +hep.dataforge.maths.functions.FunctionLibrary$Factory \ No newline at end of file diff --git a/dataforge-maths/src/test/java/hep/dataforge/maths/NamedMatrixTest.groovy b/dataforge-maths/src/test/java/hep/dataforge/maths/NamedMatrixTest.groovy new file mode 100644 index 00000000..efdea687 --- /dev/null +++ b/dataforge-maths/src/test/java/hep/dataforge/maths/NamedMatrixTest.groovy @@ -0,0 +1,21 @@ +package hep.dataforge.maths + + +import org.apache.commons.math3.linear.Array2DRowRealMatrix +import spock.lang.Specification + +/** + * Created by darksnake on 14.06.2017. + */ +class NamedMatrixTest extends Specification { + def "MetaMorph"() { + given: + def matrix = new NamedMatrix(["a", "b"] as String[], new Array2DRowRealMatrix([[1d, 2d], [-2d, 3d]] as double[][])) + when: + def meta = matrix.toMeta(); + def newMatrix = MetaMorph.morph(NamedMatrix.class, meta); + then: + newMatrix.get(0, 1) == 2 + newMatrix.get(1, 1) == 3 + } +} diff --git a/dataforge-maths/src/test/java/hep/dataforge/maths/NamedVectorTest.groovy b/dataforge-maths/src/test/java/hep/dataforge/maths/NamedVectorTest.groovy new file mode 100644 index 00000000..ef08a58f --- /dev/null +++ b/dataforge-maths/src/test/java/hep/dataforge/maths/NamedVectorTest.groovy @@ -0,0 +1,21 @@ +package hep.dataforge.maths + + +import org.apache.commons.math3.linear.ArrayRealVector +import spock.lang.Specification + +/** + * Created by darksnake on 14.06.2017. + */ +class NamedVectorTest extends Specification { + def "MetaMorphing"() { + given: + def vector = new NamedVector(Names.of(["a", "b", "c"]), new ArrayRealVector([1d, 2d, 3d] as double[])); + when: + def meta = vector.toMeta(); + def newVector = MetaMorph.morph(NamedVector,meta); + then: + newVector.getDouble("b") == 2 + } + +} diff --git a/dataforge-maths/src/test/java/hep/dataforge/maths/expressions/DSFieldTest.kt b/dataforge-maths/src/test/java/hep/dataforge/maths/expressions/DSFieldTest.kt new file mode 100644 index 00000000..c114b237 --- /dev/null +++ b/dataforge-maths/src/test/java/hep/dataforge/maths/expressions/DSFieldTest.kt @@ -0,0 +1,36 @@ +package hep.dataforge.maths.expressions + +import org.junit.Assert.assertEquals +import org.junit.Test +import kotlin.math.PI +import kotlin.math.sqrt + + +class DSFieldTest { + + @Test + fun testNormal() { + val x = 0 + val context = DSField(1, "amp", "pos", "sigma") + + val gauss: DSNumber = with(context) { + val amp = variable("amp", 1) + val pos = variable("pos", 0) + val sigma = variable("sigma", 1) + amp / (sigma * sqrt(2 * PI)) * exp(-pow(pos - x, 2) / pow(sigma, 2) / 2) + } + + //println(gauss) + assertEquals(1.0 / sqrt(2.0 * PI), gauss.toDouble(), 0.001) + assertEquals(0.0, gauss.deriv("pos"), 0.001) + } + +// @Test +// fun performanceTest(){ +// (1..100000000).forEach{ +// testNormal() +// } +// } + + +} \ No newline at end of file diff --git a/dataforge-maths/src/test/java/hep/dataforge/maths/histogram/HistogramTest.groovy b/dataforge-maths/src/test/java/hep/dataforge/maths/histogram/HistogramTest.groovy new file mode 100644 index 00000000..028024be --- /dev/null +++ b/dataforge-maths/src/test/java/hep/dataforge/maths/histogram/HistogramTest.groovy @@ -0,0 +1,34 @@ +package hep.dataforge.maths.histogram + +import org.apache.commons.math3.random.JDKRandomGenerator +import spock.lang.Specification + +import java.util.stream.DoubleStream + +import static spock.util.matcher.HamcrestMatchers.closeTo +import static spock.util.matcher.HamcrestSupport.expect + +/** + * Created by darksnake on 02.07.2017. + */ +class HistogramTest extends Specification { + + def testUnivariate() { + given: + def histogram = new UnivariateHistogram(-5, 5, 0.1) + def generator = new JDKRandomGenerator(2234); + + when: + histogram.fill(DoubleStream.generate { generator.nextGaussian() }.limit(200000)) + double average = histogram.binStream() + .filter{!it.getLowerBound(0).infinite && !it.getUpperBound(0).infinite} + .mapToDouble{ + //println it.describe() + return (it.getLowerBound(0) + it.getUpperBound(0)) / 2d * it.count + } + .average() + .getAsDouble() + then: + expect average, closeTo(0,3) + } +} diff --git a/dataforge-maths/src/test/java/hep/dataforge/maths/integration/MonteCarloIntegratorTest.groovy b/dataforge-maths/src/test/java/hep/dataforge/maths/integration/MonteCarloIntegratorTest.groovy new file mode 100644 index 00000000..fdbdfd6a --- /dev/null +++ b/dataforge-maths/src/test/java/hep/dataforge/maths/integration/MonteCarloIntegratorTest.groovy @@ -0,0 +1,47 @@ +package hep.dataforge.maths.integration + +import kotlin.Pair +import org.apache.commons.math3.analysis.MultivariateFunction +import org.apache.commons.math3.linear.Array2DRowRealMatrix +import org.apache.commons.math3.linear.ArrayRealVector +import org.apache.commons.math3.random.JDKRandomGenerator +import spock.lang.Specification + +import static spock.util.matcher.HamcrestMatchers.closeTo + +/** + * Created by darksnake on 06.07.2017. + */ +class MonteCarloIntegratorTest extends Specification { + + //triangular function + MultivariateFunction function = { pars -> + def x = pars[0]; + def y = pars[1]; + if (x >= -1 && y <= 1 && y >= x) { + return 1d + } else { + return 0d + } + } + def integrator = new MonteCarloIntegrator(); + def generator = new JDKRandomGenerator(12345) + + + def testUniform(){ + Sampler sampler = Sampler.uniform(generator, [new Pair<>(-1d,1d), new Pair<>(-1d,1d)]); + def integrand = new MonteCarloIntegrand(sampler,function); + def res = integrator.integrate(integrand) + expect: + res closeTo(2d,0.05d); + } + + def testNormal(){ + Sampler sampler = Sampler.normal(generator, new ArrayRealVector([0d,0d] as double[]), new + Array2DRowRealMatrix([[1d,0d],[0d,1d]] as double[][])); + def integrand = new MonteCarloIntegrand(sampler,function); + def res = integrator.integrate(integrand) + expect: + res closeTo(2d,0.05d); + } +} diff --git a/dataforge-maths/src/test/java/hep/dataforge/maths/integration/RiemanIntegratorTest.java b/dataforge-maths/src/test/java/hep/dataforge/maths/integration/RiemanIntegratorTest.java new file mode 100644 index 00000000..e5a7a2be --- /dev/null +++ b/dataforge-maths/src/test/java/hep/dataforge/maths/integration/RiemanIntegratorTest.java @@ -0,0 +1,43 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.maths.integration; + +import org.apache.commons.math3.analysis.UnivariateFunction; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * + * @author Alexander Nozik + */ +public class RiemanIntegratorTest { + + static UnivariateFunction gausss = (e) -> 1d/Math.sqrt(2*Math.PI)*Math.exp(-(e*e)/2); + + public RiemanIntegratorTest() { + } + + /** + * Test of evaluate method, of class RiemanIntegrator. + */ + @Test + public void testIntegration() { + System.out.println("integration test with simple Rieman integrator"); + assertEquals(1d, new RiemanIntegrator(400).integrate(-5d, 5d, gausss), 1e-2); + } + +} diff --git a/dataforge-plots/build.gradle b/dataforge-plots/build.gradle new file mode 100644 index 00000000..5f7bea40 --- /dev/null +++ b/dataforge-plots/build.gradle @@ -0,0 +1,13 @@ +//allprojects { +// apply plugin: 'org.openjfx.javafxplugin' +// +// javafx { +// modules = ['javafx.controls'] +// } +//} + +description = 'dataforge-plots' + +dependencies { + compile project(':dataforge-core') +} diff --git a/dataforge-plots/plots-jfc/build.gradle b/dataforge-plots/plots-jfc/build.gradle new file mode 100644 index 00000000..2ff8e777 --- /dev/null +++ b/dataforge-plots/plots-jfc/build.gradle @@ -0,0 +1,16 @@ +//apply plugin: 'org.openjfx.javafxplugin' +// +//javafx { +// modules = [ 'javafx.controls' ] +//} + + +description = 'jFreeChart plugin' + +dependencies { + compile 'org.jfree:jfreesvg:3.3' + // https://mvnrepository.com/artifact/org.jfree/jfreechart-fx + compile group: 'org.jfree', name: 'jfreechart-fx', version: '1.0.1' + + compile project(":dataforge-plots") +} \ No newline at end of file diff --git a/dataforge-plots/plots-jfc/src/main/kotlin/hep/dataforge/plots/jfreechart/FXPlotUtils.kt b/dataforge-plots/plots-jfc/src/main/kotlin/hep/dataforge/plots/jfreechart/FXPlotUtils.kt new file mode 100644 index 00000000..144041bd --- /dev/null +++ b/dataforge-plots/plots-jfc/src/main/kotlin/hep/dataforge/plots/jfreechart/FXPlotUtils.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.plots.jfreechart + +import hep.dataforge.io.envelopes.DefaultEnvelopeType +import hep.dataforge.io.envelopes.DefaultEnvelopeWriter +import hep.dataforge.io.envelopes.xmlMetaType +import hep.dataforge.meta.Meta +import hep.dataforge.plots.PlotFrame +import javafx.scene.control.MenuItem +import javafx.stage.FileChooser +import javafx.stage.Window +import java.awt.Color +import java.io.FileOutputStream +import java.io.IOException + +object FXPlotUtils { + fun getAWTColor(meta: Meta, def: Color?): Color? { + return when { + meta.hasValue("color") -> { + val fxColor = javafx.scene.paint.Color.valueOf(meta.getString("color")) + Color(fxColor.red.toFloat(), fxColor.green.toFloat(), fxColor.blue.toFloat()) + } + else -> def + } + } + + fun awtColorToString(color: Color): String { + val fxColor = javafx.scene.paint.Color.rgb( + color.red, + color.green, + color.blue, + color.transparency.toDouble() + ) + return String.format("#%02X%02X%02X", + (fxColor.red * 255).toInt(), + (fxColor.green * 255).toInt(), + (fxColor.blue * 255).toInt()) + } + + + /** + * + * @param window + * @param frame + * @return + */ + fun getDFPlotExportMenuItem(window: Window?, frame: PlotFrame): MenuItem { + val dfpExport = MenuItem("DF...") + dfpExport.setOnAction { _ -> + val chooser = FileChooser() + chooser.extensionFilters.setAll(FileChooser.ExtensionFilter("DataForge envelope", "*.df")) + chooser.title = "Select file to save plot into" + val file = chooser.showSaveDialog(window) + if (file != null) { + try { + DefaultEnvelopeWriter(DefaultEnvelopeType.INSTANCE, xmlMetaType) + .write(FileOutputStream(file), PlotFrame.Wrapper().wrap(frame)) + } catch (ex: IOException) { + throw RuntimeException("Failed to save plot to file", ex) + } + + } + } + return dfpExport + } + +} \ No newline at end of file diff --git a/dataforge-plots/plots-jfc/src/main/kotlin/hep/dataforge/plots/jfreechart/JFCDataWrapper.kt b/dataforge-plots/plots-jfc/src/main/kotlin/hep/dataforge/plots/jfreechart/JFCDataWrapper.kt new file mode 100644 index 00000000..c8bdcf98 --- /dev/null +++ b/dataforge-plots/plots-jfc/src/main/kotlin/hep/dataforge/plots/jfreechart/JFCDataWrapper.kt @@ -0,0 +1,89 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.plots.jfreechart + +import hep.dataforge.names.Name +import hep.dataforge.plots.Plot +import hep.dataforge.tables.Adapters +import hep.dataforge.tables.ValuesAdapter +import hep.dataforge.values.Values +import hep.dataforge.values.nullableDouble +import org.jfree.data.xy.AbstractIntervalXYDataset + +/** + * Wrapper for plot. Multiple xs are not allowed + * + * @author Alexander Nozik + */ +internal class JFCDataWrapper(val index: Int, private var plot: Plot) : AbstractIntervalXYDataset() { + + private val adapter: ValuesAdapter + get() = plot.adapter + + private inner class JFCValuesWrapper(val values: Values) { + val x: Number? by lazy { Adapters.getXValue(adapter, values).nullableDouble } + + val y: Number? by lazy { Adapters.getYValue(adapter, values).nullableDouble } + + val startX: Number? by lazy { Adapters.getLowerBound(adapter, Adapters.X_AXIS, values) } + val endX: Number? by lazy { Adapters.getUpperBound(adapter, Adapters.X_AXIS, values) } + + val startY: Number? by lazy { Adapters.getLowerBound(adapter, Adapters.Y_AXIS, values) } + val endY: Number? by lazy { Adapters.getUpperBound(adapter, Adapters.Y_AXIS, values) } + + } + + private var cache: List? = null + + + fun setPlot(plot: Plot) { + synchronized(this) { + this.plot = plot + cache = null + } + } + + private val data: List + get() { + synchronized(this) { + + if (cache == null) { + cache = plot.data.map { JFCValuesWrapper(it) } + } + return cache!! + } + } + + private operator fun get(i: Int): JFCValuesWrapper { + return data[i] + } + + override fun getSeriesKey(i: Int): Comparable<*> { + return if (seriesCount == 1) { + plot.name + } else { + Name.joinString(plot.name, Adapters.getTitle(adapter, Adapters.Y_AXIS)) + } + } + + override fun getX(i: Int, i1: Int): Number? = this[i1].x + + override fun getY(i: Int, i1: Int): Number? = this[i1].y + + override fun getStartX(i: Int, i1: Int): Number? = this[i1].startX + + override fun getEndX(i: Int, i1: Int): Number? = this[i1].endX + + override fun getStartY(i: Int, i1: Int): Number? = this[i1].startY + + override fun getEndY(i: Int, i1: Int): Number? = this[i1].endY + + override fun getSeriesCount(): Int = 1 + + override fun getItemCount(i: Int): Int { + return data.size + } +} diff --git a/dataforge-plots/plots-jfc/src/main/kotlin/hep/dataforge/plots/jfreechart/JFreeChartFrame.kt b/dataforge-plots/plots-jfc/src/main/kotlin/hep/dataforge/plots/jfreechart/JFreeChartFrame.kt new file mode 100644 index 00000000..7f183d56 --- /dev/null +++ b/dataforge-plots/plots-jfc/src/main/kotlin/hep/dataforge/plots/jfreechart/JFreeChartFrame.kt @@ -0,0 +1,361 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.plots.jfreechart + +import hep.dataforge.exceptions.NameNotFoundException +import hep.dataforge.meta.Laminate +import hep.dataforge.meta.Meta +import hep.dataforge.names.Name +import hep.dataforge.nullable +import hep.dataforge.orElse +import hep.dataforge.plots.* +import hep.dataforge.values.Value +import hep.dataforge.values.ValueFactory +import javafx.application.Platform +import javafx.scene.Node +import javafx.scene.control.ContextMenu +import javafx.scene.control.Menu +import org.jfree.chart.JFreeChart +import org.jfree.chart.axis.DateAxis +import org.jfree.chart.axis.LogarithmicAxis +import org.jfree.chart.axis.NumberAxis +import org.jfree.chart.axis.ValueAxis +import org.jfree.chart.encoders.SunPNGEncoderAdapter +import org.jfree.chart.fx.ChartViewer +import org.jfree.chart.plot.XYPlot +import org.jfree.chart.renderer.xy.XYErrorRenderer +import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer +import org.jfree.chart.renderer.xy.XYSplineRenderer +import org.jfree.chart.renderer.xy.XYStepRenderer +import org.jfree.chart.title.LegendTitle +import org.jfree.data.Range +import org.jfree.data.general.DatasetChangeEvent +import org.slf4j.LoggerFactory +import java.awt.BasicStroke +import java.awt.Color +import java.awt.Shape +import java.io.IOException +import java.io.ObjectStreamException +import java.io.OutputStream +import java.io.Serializable +import java.util.* +import java.util.stream.Collectors +import kotlin.math.abs + +/** + * @author Alexander Nozik + */ +class JFreeChartFrame : XYPlotFrame(), FXPlotFrame, Serializable { + + private val xyPlot: XYPlot = XYPlot(null, NumberAxis(), NumberAxis(), XYLineAndShapeRenderer()) + val chart: JFreeChart = JFreeChart(xyPlot) + + /** + * Index mapping names to datasets + */ + @Transient + private val index = HashMap() + /** + * Caches for color and shape + */ + @Transient + private val colorCache = HashMap() + @Transient + private val shapeCache = HashMap() + +// init { +// //pre-configure axis using default values +// configure(Meta.empty()) +// } + + private fun runLater(runnable: () -> Unit) { + try { + Platform.runLater(runnable) + } catch (ex: IllegalStateException) { + //if toolkit is not initialized + runnable.invoke() + } + } + + override val fxNode: Node + get() { + val viewer = ChartViewer(chart, true) + addExportPlotAction(viewer.contextMenu, this) + return viewer + } + + + private fun addExportPlotAction(menu: ContextMenu, frame: JFreeChartFrame) { + val parent = menu.items.stream() + .filter { it -> it is Menu && it.getText() == "Export As" } + .map { Menu::class.java.cast(it) } + .findFirst() + .orElseGet { + val sub = Menu("Export As") + menu.items.add(sub) + sub + } + + + val dfpExport = FXPlotUtils.getDFPlotExportMenuItem(menu.ownerWindow, frame) + + parent.items.add(dfpExport) + } + + + private fun getNumberAxis(meta: Meta): ValueAxis { + val axis = NumberAxis() + axis.autoRangeIncludesZero = meta.getBoolean("includeZero", false) + axis.autoRangeStickyZero = meta.getBoolean("stickyZero", false) + return axis + } + + private fun getDateAxis(meta: Meta): DateAxis { + val axis = DateAxis() + axis.timeZone = TimeZone.getTimeZone(meta.getString("timeZone", "UTC")) + return axis + } + + private fun getLogAxis(meta: Meta): ValueAxis { + //FIXME autorange with negative values + val logAxis = LogarithmicAxis("") + // logAxis.setMinorTickCount(10); + logAxis.expTickLabelsFlag = true + logAxis.isMinorTickMarksVisible = true + if (meta.hasMeta("range")) { + logAxis.range = getRange(meta.getMeta("range")) + } else { + logAxis.isAutoRange = meta.getBoolean("autoRange", true) + } + logAxis.allowNegativesFlag = false + logAxis.autoRangeNextLogFlag = true + logAxis.strictValuesFlag = false // Omit negatives but do not throw exception + return logAxis + } + + private fun getRange(meta: Meta): Range { + return Range(meta.getDouble("lower", java.lang.Double.NEGATIVE_INFINITY), meta.getDouble("upper", java.lang.Double.POSITIVE_INFINITY)) + } + + private fun getAxis(axisMeta: Meta): ValueAxis { + return when (axisMeta.getString("type", "number").toLowerCase()) { + "log" -> getLogAxis(axisMeta) + "time" -> getDateAxis(axisMeta) + else -> getNumberAxis(axisMeta) + } + } + + override fun updateAxis(axisName: String, axisMeta: Meta, plotMeta: Meta) { + val axis = getAxis(axisMeta) + + val crosshair = axisMeta.getString("crosshair") { plotMeta.getString("crosshair", "none") } + + + val from = axisMeta.getDouble("range.from", java.lang.Double.NEGATIVE_INFINITY) + + if (java.lang.Double.isFinite(from)) { + axis.lowerBound = from + } + + val to = axisMeta.getDouble("range.to", java.lang.Double.NEGATIVE_INFINITY) + + if (java.lang.Double.isFinite(to)) { + axis.upperBound = to + } + // if (Double.isFinite(from) && Double.isFinite(to)) { + // axis.setRange(from,to); + // } else { + // axis.setAutoRange(true); + // } + + when (axisName) { + "x" -> { + xyPlot.domainAxis = axis + when (crosshair) { + "free" -> { + xyPlot.isDomainCrosshairVisible = true + xyPlot.isDomainCrosshairLockedOnData = false + } + "data" -> { + xyPlot.isDomainCrosshairVisible = true + xyPlot.isDomainCrosshairLockedOnData = true + } + "none" -> xyPlot.isDomainCrosshairVisible = false + } + } + "y" -> { + xyPlot.rangeAxis = axis + when (crosshair) { + "free" -> { + xyPlot.isRangeCrosshairVisible = true + xyPlot.isRangeCrosshairLockedOnData = false + } + "data" -> { + xyPlot.isRangeCrosshairVisible = true + xyPlot.isRangeCrosshairLockedOnData = true + } + "none" -> xyPlot.isRangeCrosshairVisible = false + } + } + else -> throw NameNotFoundException(axisName, "No such axis in this plot") + } + + if (axisMeta.hasValue("title")) { + var label = axisMeta.getString("title") + if (axisMeta.hasValue("units")) { + label += " (" + axisMeta.getString("units") + ")" + } + axis.label = label + } + } + + @Synchronized + override fun updateLegend(legendMeta: Meta) { + runLater { + if (legendMeta.getBoolean("show", true)) { + if (chart.legend == null) { + chart.addLegend(LegendTitle(xyPlot)) + } + } else { + chart.removeLegend() + } + this.xyPlot.legendItems + } + } + + @Synchronized + override fun updateFrame(annotation: Meta) { + runLater { this.chart.setTitle(annotation.getString("title", "")) } + } + + @Synchronized + override fun updatePlotData(name: Name, plot: Plottable?) { + if (plot == null) { + index[name]?.index?.let { + runLater { + xyPlot.setDataset(it, null) + } + } + index.remove(name) + } else if (plot is Plot) { + //ignore groups + index[name]?.let { wrapper -> + //TODO move data calculation off the UI thread somehow + wrapper.setPlot(plot) + runLater { + this.xyPlot.datasetChanged(DatasetChangeEvent(this.xyPlot, wrapper)) + } + }.orElse { + val wrapper = JFCDataWrapper(abs(name.hashCode()), plot) + index[name] = wrapper + runLater { + this.xyPlot.setDataset(wrapper.index, wrapper) + } + metaChanged(this.plots, name, plot) + } + } + } + + private fun createRenderer(name: Name, config: Laminate): XYLineAndShapeRenderer { + val render: XYLineAndShapeRenderer = if (config.getBoolean("showErrors", true)) { + XYErrorRenderer() + } else { + when (config.getString("connectionType", "DEFAULT").toUpperCase()) { + "STEP" -> XYStepRenderer() + "SPLINE" -> XYSplineRenderer() + else -> XYLineAndShapeRenderer() + } + } + val showLines = config.getBoolean("showLine", false) + val showSymbols = config.getBoolean("showSymbol", true) + render.defaultShapesVisible = showSymbols + render.defaultLinesVisible = showLines + + //Build Legend map to avoid serialization issues + val thickness = PlotUtils.getThickness(config) + if (thickness > 0) { + render.setSeriesStroke(0, BasicStroke(thickness.toFloat())) + } + + val color = FXPlotUtils.getAWTColor(config, colorCache[name]) + if (color != null) { + render.setSeriesPaint(0, color) + } + + val shape = shapeCache[name] + if (shape != null) { + render.setSeriesShape(0, shape) + } + + val visible = config + .collectValue( + "visible", + Collectors.reducing(ValueFactory.of(true)) { v1: Value, v2: Value -> ValueFactory.of(v1.boolean && v2.boolean) } + ) + .boolean + + render.setSeriesVisible(0, visible) + render.setLegendItemLabelGenerator { _, _ -> + config.optString("title").nullable ?: name.unescaped + + } + return render + } + + @Synchronized + override fun updatePlotConfig(name: Name, config: Laminate) { + index[name]?.index?.let { + val render = createRenderer(name, config) + runLater { + xyPlot.setRenderer(it, render) + + // update cache to default colors + val paint = render.lookupSeriesPaint(0) + if (paint is Color) { + colorCache[name] = paint + } + shapeCache[name] = render.lookupSeriesShape(0) + } + } + + } + + /** + * Take a snapshot of plot frame and save it in a given OutputStream + * + * @param stream + * @param config + */ + @Synchronized + override fun asImage(stream: OutputStream, config: Meta) { + Thread { + try { + SunPNGEncoderAdapter().encode(chart.createBufferedImage(config.getInt("width", 800), config.getInt("height", 600)), stream) + } catch (ex: IOException) { + LoggerFactory.getLogger(javaClass).error("IO error during image encoding", ex) + } + }.start() + } + + override fun getActualColor(name: Name): Optional { + return Optional.ofNullable(colorCache[name]).map { color -> ValueFactory.of(FXPlotUtils.awtColorToString(color)) } + } + + @Throws(ObjectStreamException::class) + private fun writeReplace(): Any { + return PlotFrame.PlotFrameEnvelope(PlotFrame.wrapper.wrap(this)) + } +} diff --git a/dataforge-plots/plots-jfc/src/main/kotlin/hep/dataforge/plots/jfreechart/JFreeChartPlugin.kt b/dataforge-plots/plots-jfc/src/main/kotlin/hep/dataforge/plots/jfreechart/JFreeChartPlugin.kt new file mode 100644 index 00000000..ab15e1fd --- /dev/null +++ b/dataforge-plots/plots-jfc/src/main/kotlin/hep/dataforge/plots/jfreechart/JFreeChartPlugin.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.plots.jfreechart + +import hep.dataforge.context.BasicPlugin +import hep.dataforge.context.Plugin +import hep.dataforge.context.PluginDef +import hep.dataforge.context.PluginFactory +import hep.dataforge.meta.Meta +import hep.dataforge.plots.PlotFactory +import hep.dataforge.plots.PlotFrame + +@PluginDef(group = "hep.dataforge", name = "plots.jfreechart", dependsOn = ["hep.dataforge:fx"], info = "JFreeChart plot frame factory") +class JFreeChartPlugin : BasicPlugin(), PlotFactory { + + override fun build(meta: Meta): PlotFrame = JFreeChartFrame().apply { configure(meta) } + + + class Factory : PluginFactory() { + override val type: Class = JFreeChartPlugin::class.java + + override fun build(meta: Meta): Plugin { + return JFreeChartPlugin() + } + } +} + +@Deprecated("To be replaced by outputs") +fun chart(transform: JFreeChartFrame.() -> Unit = {}): JFreeChartFrame { + return JFreeChartFrame().apply(transform) +} \ No newline at end of file diff --git a/dataforge-plots/plots-jfc/src/main/resources/META-INF/services/hep.dataforge.context.PluginFactory b/dataforge-plots/plots-jfc/src/main/resources/META-INF/services/hep.dataforge.context.PluginFactory new file mode 100644 index 00000000..02c5ad72 --- /dev/null +++ b/dataforge-plots/plots-jfc/src/main/resources/META-INF/services/hep.dataforge.context.PluginFactory @@ -0,0 +1 @@ +hep.dataforge.plots.jfreechart.JFreeChartPlugin$Factory \ No newline at end of file diff --git a/dataforge-plots/plots-viewer/build.gradle b/dataforge-plots/plots-viewer/build.gradle new file mode 100644 index 00000000..591aba26 --- /dev/null +++ b/dataforge-plots/plots-viewer/build.gradle @@ -0,0 +1,32 @@ +plugins { + id 'com.github.johnrengelman.shadow' version '2.0.1' + id 'application' +} + +apply plugin: "kotlin" + +description = 'dataforge-plots-viewer' + +if (!hasProperty('mainClass')) { + ext.mainClass = 'hep.dataforge.plots.viewer.ViewerApp' +} +mainClassName = mainClass + + +compileKotlin { + kotlinOptions { + jvmTarget = "1.8" + javaParameters = true + } +} + +kotlin { + experimental { + coroutines "enable" + } +} + +dependencies { + compile project(':dataforge-plots:plots-jfc') + compile project(':dataforge-gui') +} diff --git a/dataforge-plots/plots-viewer/src/main/kotlin/hep/dataforge/plots/viewer/PlotView.kt b/dataforge-plots/plots-viewer/src/main/kotlin/hep/dataforge/plots/viewer/PlotView.kt new file mode 100644 index 00000000..cf4fd69a --- /dev/null +++ b/dataforge-plots/plots-viewer/src/main/kotlin/hep/dataforge/plots/viewer/PlotView.kt @@ -0,0 +1,74 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.plots.viewer + +import hep.dataforge.fx.plots.PlotContainer +import hep.dataforge.io.envelopes.EnvelopeType +import hep.dataforge.plots.FXPlotFrame +import hep.dataforge.plots.PlotFrame +import javafx.scene.Parent +import javafx.scene.control.Button +import javafx.scene.control.Tab +import javafx.scene.control.TabPane +import javafx.scene.layout.BorderPane +import javafx.stage.FileChooser +import org.slf4j.LoggerFactory +import tornadofx.* +import java.io.File +import java.io.IOException +import java.util.* + +/** + * Controller for ViewerApp + + * @author Alexander Nozik + */ +class PlotView : View("DataForge plot viewer") { + override val root: Parent by fxml("/fxml/PlotViewer.fxml"); + private val loadButton: Button by fxid(); + private val tabs: TabPane by fxid(); + + private val plotMap = HashMap() + + init { + loadButton.setOnAction { + val chooser = FileChooser() + chooser.title = "Select plot file to load" + chooser.extensionFilters.setAll(FileChooser.ExtensionFilter("DataForge plot", "*.df", "*.dfp")) + val list = chooser.showOpenMultipleDialog(loadButton.scene.window) + list.forEach { f -> + try { + loadPlot(f) + } catch (ex: IOException) { + LoggerFactory.getLogger(javaClass).error("Failed to load dfp file", ex) + } + } + } + } + + @Throws(IOException::class) + fun loadPlot(file: File) { + val pane: BorderPane = plotMap.getOrElse(file) { + val pane = BorderPane() + val tab = Tab(file.name, pane) + tab.setOnClosed { plotMap.remove(file) } + tabs.tabs.add(tab) + pane + } + + EnvelopeType.infer(file.toPath())?.let { type -> + try { + val envelope = type.reader.read(file.toPath()) + val frame = PlotFrame.Wrapper().unWrap(envelope) + pane.center = PlotContainer(frame as FXPlotFrame).root; + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + + } +} diff --git a/dataforge-plots/plots-viewer/src/main/kotlin/hep/dataforge/plots/viewer/ViewerApp.kt b/dataforge-plots/plots-viewer/src/main/kotlin/hep/dataforge/plots/viewer/ViewerApp.kt new file mode 100644 index 00000000..4e13b576 --- /dev/null +++ b/dataforge-plots/plots-viewer/src/main/kotlin/hep/dataforge/plots/viewer/ViewerApp.kt @@ -0,0 +1,31 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.plots.viewer + +import tornadofx.* + +/** + + * @author Alexander Nozik + */ +class ViewerApp : App(PlotView::class) { + +// override fun start(stage: Stage) { +// +// val loader = FXMLLoader(javaClass.getResource("/fxml/ViewerApp.fxml")) +// val root = loader.load() +// val controller = loader.getController() +// val scene = Scene(root) +// +// stage.title = "DataForge plot viewer" +// stage.scene = scene +// stage.show() +// +// for (fileName in this.parameters.unnamed) { +// controller.loadPlot(File(fileName)) +// } +// } +} diff --git a/dataforge-plots/plots-viewer/src/main/resources/fxml/PlotViewer.fxml b/dataforge-plots/plots-viewer/src/main/resources/fxml/PlotViewer.fxml new file mode 100644 index 00000000..a400e523 --- /dev/null +++ b/dataforge-plots/plots-viewer/src/main/resources/fxml/PlotViewer.fxml @@ -0,0 +1,23 @@ + + + + + + + + +
+ + + + + +
+ + + +
{ + return async { ListTable(format, toList()) } +} + +interface IndexedTableLoader : TableLoader, IndexedLoader { + suspend fun get(any: Any): Values? = get(Value.of(any)) + + /** + * Notify loader that it should update index for this loader + */ + fun updateIndex() +} + +/** + * Select a range from this table loade + */ +suspend fun IndexedTableLoader.select(from: Value, to: Value): Table { + return ListTable(format, keys.subSet(from, true, to, true).map { get(it)!! }) +} + +fun IndexedTableLoader.select(query: Meta): Deferred
{ + TODO("To be implemented") +} + + +interface MutableTableLoader : TableLoader, AppendableLoader + +/** + * Create a new table loader with given name and format + */ +fun MutableStorage.createTable(name: String, format: TableFormat): MutableTableLoader { + val meta = buildMeta { + "name" to name + TableLoaderType.TABLE_FORMAT_KEY to format.toMeta() + Envelope.ENVELOPE_DATA_TYPE_KEY to TableLoaderType.BINARY_DATA_TYPE + } + return create(meta) as MutableTableLoader +} \ No newline at end of file diff --git a/dataforge-storage/src/main/resources/META-INF/services/hep.dataforge.context.PluginFactory b/dataforge-storage/src/main/resources/META-INF/services/hep.dataforge.context.PluginFactory new file mode 100644 index 00000000..213e40ee --- /dev/null +++ b/dataforge-storage/src/main/resources/META-INF/services/hep.dataforge.context.PluginFactory @@ -0,0 +1 @@ +hep.dataforge.storage.StorageManager$Factory \ No newline at end of file diff --git a/dataforge-storage/src/main/resources/META-INF/services/hep.dataforge.storage.StorageElementType b/dataforge-storage/src/main/resources/META-INF/services/hep.dataforge.storage.StorageElementType new file mode 100644 index 00000000..497a1fd3 --- /dev/null +++ b/dataforge-storage/src/main/resources/META-INF/services/hep.dataforge.storage.StorageElementType @@ -0,0 +1,2 @@ +hep.dataforge.storage.files.FileStorage$Directory +hep.dataforge.storage.files.TableLoaderType \ No newline at end of file diff --git a/dataforge-storage/src/test/kotlin/hep.dataforge.storage/TableLoaderTest.kt b/dataforge-storage/src/test/kotlin/hep.dataforge.storage/TableLoaderTest.kt new file mode 100644 index 00000000..f9167f06 --- /dev/null +++ b/dataforge-storage/src/test/kotlin/hep.dataforge.storage/TableLoaderTest.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.storage + +import hep.dataforge.context.Global +import hep.dataforge.storage.files.TableLoaderType +import hep.dataforge.tables.MetaTableFormat +import hep.dataforge.values.ValueMap +import kotlinx.coroutines.runBlocking +import org.junit.AfterClass +import org.junit.Assert.assertEquals +import org.junit.BeforeClass +import org.junit.Test +import java.nio.file.Files +import java.nio.file.Path +import java.time.Duration +import java.time.Instant + + +class TableLoaderTest { + val tableLoaderType = TableLoaderType() + + companion object { + lateinit var dir: Path + + @BeforeClass + @JvmStatic + fun setup() { + dir = Files.createTempDirectory(Global.tmpDir, "storage-test") + } + + @AfterClass + @JvmStatic + fun tearDown() { + dir.toFile().deleteRecursively() + } + } + + fun benchmark(action: () -> Unit): Duration { + val start = Instant.now() + action.invoke() + return Duration.between(start, Instant.now()) + } + + @Test + fun testReadWrite() { + val path = dir.resolve("read-write.df") + val format = MetaTableFormat.forNames("a", "b", "c") + val loader = tableLoaderType.create(Global, path, format) + val writer = loader.mutable() + + runBlocking { + (1..10).forEach { + writer.append(it, it + 1, it * 2) + } + } + + assertEquals(3, runBlocking { loader.get(1)?.getInt("b")}) + writer.close() + loader.close() + } + + @Test + fun testPerformance() { + val n = 10000 + + val path = dir.resolve("performance.df") + val format = MetaTableFormat.forNames("a", "b", "c") + val loader = tableLoaderType.create(Global, path, format) + val writer = loader.mutable() + val data = (1..n).map { ValueMap.of(format.namesAsArray(), it, it + 1, it * 2) } + val writeTime = benchmark { + writer.appendAll(data) + } + println("Write of $n elements completed in $writeTime. The average time per element is ${writeTime.toMillis().toDouble() / n} milliseconds") + writer.close() + loader.close() + + val reader = tableLoaderType.read(Global, path) + + var lastValue: Int + val readTime = benchmark { + reader.forEachIndexed { index, it -> + lastValue = it["a"].int + if (lastValue != index + 1) { + throw error("Data read broken on element $index") + } + } + } + + assertEquals(n - 1, reader.keys.last().int) + println("Read of $n elements completed in $readTime. The average time per element is ${readTime.toMillis().toDouble() / n} milliseconds") + assert(writeTime < Duration.ofMillis((0.05*n).toLong())) + assert(readTime < Duration.ofMillis((0.01*n).toLong())) + } +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..cc4fdc293d0e50b0ad9b65c16e7ddd1db2f6025b GIT binary patch literal 58702 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

@up^y4&I8N>~wvL%f(S2W%NN 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 D613UA literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..6ce793f2 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.0-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 00000000..2fe81a7d --- /dev/null +++ b/gradlew @@ -0,0 +1,183 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# 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 +# +# https://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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..9618d8d9 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,100 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/grind/build.gradle b/grind/build.gradle new file mode 100644 index 00000000..c9b06aea --- /dev/null +++ b/grind/build.gradle @@ -0,0 +1,13 @@ +apply plugin: 'groovy' + +description = 'The GRIND (GRoovy INteractive Dataforge) environment' + +compileGroovy.dependsOn(compileKotlin) +compileGroovy.classpath += files(compileKotlin.destinationDir) + +dependencies { + compile project(":dataforge-core") + compile 'org.codehaus.groovy:groovy-all:2.5+' + + testCompile project(":dataforge-gui") +} \ No newline at end of file diff --git a/grind/grind-terminal/build.gradle b/grind/grind-terminal/build.gradle new file mode 100644 index 00000000..9ffcc3d5 --- /dev/null +++ b/grind/grind-terminal/build.gradle @@ -0,0 +1,53 @@ +buildscript { + repositories { jcenter() } + dependencies { + classpath 'com.github.jengelman.gradle.plugins:shadow:2+' + } +} + +apply plugin: 'groovy' +apply plugin: "application" +apply plugin: 'com.github.johnrengelman.shadow' +//apply plugin: 'org.openjfx.javafxplugin' +// +//javafx { +// modules = [ 'javafx.controls' ] +//} + + +if (!hasProperty('mainClass')) { + ext.mainClass = 'hep.dataforge.grind.terminal.RunGrindShell' +} +mainClassName = mainClass + +description = 'The grind plugin for dataforge framework' + +dependencies { + compile project(':grind') + compile project(':dataforge-plots:plots-jfc') + compile project(':dataforge-gui') + compile group: 'org.jline', name: 'jline', version: '3.5.1' +// compile group: 'net.java.dev.jna', name: 'jna', version: '4.4.0' + compile group: 'org.fusesource.jansi', name: 'jansi', version: '1.16' +} + +task shell(dependsOn: classes, type: JavaExec) { + main mainClass +// jvmArgs ['-Djansi.passthrough=true'] + standardInput = System.in + standardOutput = System.out + classpath = sourceSets.main.runtimeClasspath + description "Start a Grind shell with default context in terminal" + group "dataforge" +} + +task dumbShell(dependsOn: classes, type: JavaExec) { + main 'hep.dataforge.grind.terminal.RunDumbGrindShell' +// jvmArgs ['-Djansi.passthrough=true'] + + standardInput = System.in + standardOutput = System.out + classpath = sourceSets.main.runtimeClasspath + description "Start a Grind shell with default context in dumb terminal" + group "dataforge" +} \ No newline at end of file diff --git a/grind/grind-terminal/src/main/groovy/hep/dataforge/grind/demo/WorkspaceDemo.groovy b/grind/grind-terminal/src/main/groovy/hep/dataforge/grind/demo/WorkspaceDemo.groovy new file mode 100644 index 00000000..427ef9a4 --- /dev/null +++ b/grind/grind-terminal/src/main/groovy/hep/dataforge/grind/demo/WorkspaceDemo.groovy @@ -0,0 +1,84 @@ +package hep.dataforge.grind.demo + +import hep.dataforge.context.Global +import hep.dataforge.grind.GrindShell +import hep.dataforge.grind.workspace.WorkspaceSpec +import hep.dataforge.plots.PlotFrame +import hep.dataforge.plots.data.DataPlot +import hep.dataforge.plots.output.PlotOutputKt +import hep.dataforge.tables.ColumnTable +import hep.dataforge.tables.Table +import hep.dataforge.values.ValueType +import javafx.application.Platform + +import static hep.dataforge.grind.workspace.DefaultTaskLib.custom +import static hep.dataforge.grind.workspace.DefaultTaskLib.pipe + +def workspace = new WorkspaceSpec(Global.instance()).with { + + context { + name = "TEST" + properties{ + power = 2d + } + } + + data { + item("xs") { + meta(axis: "x") + (1..100).asList() //generate xs + } + node("ys") { + Random rnd = new Random() + + meta(showLine: true) + item("y1") { + meta(axis: "y") + (1..100).collect { it**2 } + } + item("y2") { + meta(axis: "y") + (1..100).collect { it**2 + rnd.nextDouble() } + } + item("y3") { + meta(thickness: 4, color: "magenta", showSymbol: false, showErrors: false) + (1..100).collect { (it + rnd.nextDouble() / 2)**2 } + } + } + } + + task custom("table") { + def xs = input.optData("xs").get() + def ys = input.getNode("ys") + ys.dataStream().forEach { + //yield result + yield it.name, combine(xs, it, Table.class, it.meta) { x, y -> + new ColumnTable() + .addColumn("x", ValueType.NUMBER, (x as List).stream()) + .addColumn("y", ValueType.NUMBER, (y as List).stream()) + } + } + } + + task pipe("dif", dependsOn: "table") { + result {input -> + def power = meta.getDouble("power", context.getDouble("power")) + return (input as ColumnTable).buildColumn("y", ValueType.NUMBER) { + it["y"] - it["x"]**power + } + } + } + +}.build() + +new GrindShell().eval { + //loading plot feature + PlotFrame frame = PlotOutputKt.getPlotFrame(context, "demo") + + frame.configure("yAxis.type": "log") + + workspace.run("dif").dataStream().forEach { + frame.add new DataPlot(it.name, it.meta).fillData(it.get() as Table) + } + Platform.implicitExit = true +} diff --git a/grind/grind-terminal/src/main/groovy/hep/dataforge/grind/helpers/PlotHelper.groovy b/grind/grind-terminal/src/main/groovy/hep/dataforge/grind/helpers/PlotHelper.groovy new file mode 100644 index 00000000..29694295 --- /dev/null +++ b/grind/grind-terminal/src/main/groovy/hep/dataforge/grind/helpers/PlotHelper.groovy @@ -0,0 +1,150 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.grind.helpers + +import hep.dataforge.context.Context +import hep.dataforge.context.Global +import hep.dataforge.io.output.TextOutput +import hep.dataforge.meta.Meta +import hep.dataforge.plots.PlotFrame +import hep.dataforge.plots.data.DataPlot +import hep.dataforge.plots.data.XYFunctionPlot +import hep.dataforge.plots.data.XYPlot +import hep.dataforge.plots.output.PlotOutputKt +import hep.dataforge.tables.Adapters +import hep.dataforge.tables.ValuesAdapter +import javafx.scene.paint.Color +import kotlin.jvm.functions.Function1 +import org.jetbrains.annotations.NotNull + +/** + * Created by darksnake on 30-Aug-16. + */ +class PlotHelper extends AbstractHelper { + static final String DEFAULT_FRAME = "default"; + + PlotHelper(Context context = Global.INSTANCE) { + super(context) + } + + private PlotFrame getPlotFrame(String name) { + return PlotOutputKt.getPlotFrame(context, name) + } + + def configure(String frame, Closure config) { + getPlotFrame(frame).configure(config); + } + + def configure(Closure config) { + getPlotFrame(DEFAULT_FRAME).configure(config); + } + + @MethodDescription("Apply meta to frame with given name") + def configure(String frame, Map values, Closure config) { + getPlotFrame(frame).configure(values, config); + } + + @MethodDescription("Apply meta to default frame") + def configure(Map values, Closure config) { + getPlotFrame(DEFAULT_FRAME).configure(values, config); + } + + /** + * Plot function and return resulting frame to be configured if necessary + * @param parameters + * @param function + */ + @MethodDescription("Plot a function defined by a closure.") + + XYPlot plotFunction(double from = 0d, double to = 1d, int numPoints = 100, String name = "data", String frame = DEFAULT_FRAME, Closure function) { + Function1 func = new Function1() { + @Override + Double invoke(Double x) { + return function.call(x) as Double + } + } + XYFunctionPlot res = XYFunctionPlot.Companion.plot(name, from, to, numPoints, func); + getPlotFrame(frame).add(res) + return res; + } + + XYPlot plotFunction(Map parameters, Closure function) { + double from = (parameters.from ?: 0d) as Double + double to = (parameters.to ?: 1d) as Double + int numPoints = (parameters.to ?: 200) as Integer + String name = (parameters.name ?: "data") as String + String frame = (parameters.name ?: "frame") as String + Function1 func = new Function1() { + @Override + Double invoke(Double x) { + return function.call(x) as Double + } + } + XYFunctionPlot res = XYFunctionPlot.Companion.plot(name, from, to, numPoints, func) + getPlotFrame(frame).add(res) + return res; + } + + private XYPlot buildDataPlot(Map map) { + DataPlot plot = new DataPlot(map.getOrDefault("name", "data") as String, map.getOrDefault("meta", Meta.empty()) as Meta) + if (map["adapter"]) { + plot.setAdapter(map["adapter"] as ValuesAdapter) + } else { + plot.setAdapter(Adapters.DEFAULT_XY_ADAPTER) + } + if (map["data"]) { + def data = map.data + if (data instanceof Map) { + data.forEach { k, v -> + plot.append(k as Number, v as Number) + } + } else if (data instanceof Iterable) { + plot.fillData(data) + } else { + throw new RuntimeException("Unrecognized data type: ${data.class}") + } + } else if (map["x"] && map["y"]) { + def x = map["x"] as List + def y = map["y"] as List + [x, y].transpose().each { List it -> + plot.append(it[0] as Number, it[1] as Number) + } + } + return plot; + } + + @MethodDescription("Plot data using supplied parameters") + XYPlot plot(Map parameters) { + def res = buildDataPlot(parameters) + getPlotFrame(parameters.getOrDefault("frame", DEFAULT_FRAME) as String).add(res) + return res + } + + @Override + Context getContext() { + return context; + } + + @Override + protected void renderDescription(@NotNull TextOutput output, @NotNull Meta meta) { + output.renderText("This is ") + output.renderText("plots", Color.BLUE) + output.renderText(" helper") + } + + +} diff --git a/grind/grind-terminal/src/main/groovy/hep/dataforge/grind/helpers/PlotHelperFactory.groovy b/grind/grind-terminal/src/main/groovy/hep/dataforge/grind/helpers/PlotHelperFactory.groovy new file mode 100644 index 00000000..9587de1d --- /dev/null +++ b/grind/grind-terminal/src/main/groovy/hep/dataforge/grind/helpers/PlotHelperFactory.groovy @@ -0,0 +1,16 @@ +package hep.dataforge.grind.helpers + +import hep.dataforge.context.Context +import hep.dataforge.meta.Meta + +class PlotHelperFactory implements GrindHelperFactory { + @Override + GrindHelper build(Context context, Meta meta) { + return new PlotHelper(context); + } + + @Override + String getName() { + return "plots" + } +} diff --git a/grind/grind-terminal/src/main/groovy/hep/dataforge/grind/terminal/GrindTerminal.groovy b/grind/grind-terminal/src/main/groovy/hep/dataforge/grind/terminal/GrindTerminal.groovy new file mode 100644 index 00000000..9e2bc6f1 --- /dev/null +++ b/grind/grind-terminal/src/main/groovy/hep/dataforge/grind/terminal/GrindTerminal.groovy @@ -0,0 +1,311 @@ +package hep.dataforge.grind.terminal + +import hep.dataforge.context.Context +import hep.dataforge.context.Global +import hep.dataforge.data.Data +import hep.dataforge.data.DataNode +import hep.dataforge.grind.GrindShell +import hep.dataforge.io.IOUtils +import hep.dataforge.io.output.ANSIStreamOutput +import hep.dataforge.io.output.Output +import hep.dataforge.meta.Meta +import hep.dataforge.meta.SimpleConfigurable +import hep.dataforge.workspace.FileBasedWorkspace +import org.jline.builtins.Completers +import org.jline.reader.* +import org.jline.terminal.Terminal +import org.jline.terminal.TerminalBuilder +import org.jline.terminal.impl.DumbTerminal +import org.jline.utils.AttributedString +import org.jline.utils.AttributedStringBuilder +import org.jline.utils.AttributedStyle + +import java.nio.file.Files +import java.nio.file.Path +import java.time.Duration +import java.util.stream.Stream + +/** + * A REPL Groovy shell with embedded DataForge features + * Created by darksnake on 29-Aug-16. + */ + +class GrindTerminal extends SimpleConfigurable { + private static final AttributedStyle RES = AttributedStyle.BOLD.foreground(AttributedStyle.YELLOW); + private static final AttributedStyle PROMPT = AttributedStyle.BOLD.foreground(AttributedStyle.CYAN); + private static final AttributedStyle DEFAULT = AttributedStyle.DEFAULT; + + private final GrindShell shell; + private final Terminal terminal; + + final Output renderer; + + /** + * Build default jline console based on operating system. Do not use for preview inside IDE + * @return + */ + static GrindTerminal system(Context context = Global.INSTANCE) { + context.logger.debug("Starting grind terminal using system shell") + return new GrindTerminal(context, + TerminalBuilder.builder() + .name("df") + .system(true) + .encoding("UTF-8") + .build() + ) + } + + static GrindTerminal dumb(Context context = Global.INSTANCE) { + context.logger.debug("Starting grind terminal using dumb shell") + return new GrindTerminal(context); + } + + GrindTerminal(Context context, Terminal terminal = null) { + //define terminal if it is not defined + if (terminal == null) { + terminal = new DumbTerminal(System.in, System.out); + terminal.echo(false); + } + this.terminal = terminal + context.logger.debug("Using ${terminal.class} terminal") + + //builder shell context + if (Global.INSTANCE == context) { + context = Global.INSTANCE.getContext("GRIND") + } + + //create the shell + shell = new GrindShell(context) + + renderer = new ANSIStreamOutput(context, terminal.output()) + + //bind helper commands + + shell.bind("show", this.&show); + + //shell.bind("describe", this.&describe); + + shell.bind("run", this.&run); + + //binding.setProperty("man", help); + shell.bind("help", this.&help); + + //binding workspace builder from default location + File wsFile = new File("workspace.groovy"); + if (wsFile.exists()) { + try { + context.logger.info("Found 'workspace.groovy' in default location. Using it to builder workspace.") + shell.bind("ws", FileBasedWorkspace.build(context, wsFile.toPath())); + context.logger.info("Workspace builder bound to 'ws'") + } catch (Exception ex) { + context.logger.error("Failed to builder workspace from 'workspace.groovy'", ex) + } + } + } + + private Completers.TreeCompleter.Node completerNode(Object obj) { + List objs = new ArrayList() + if (obj != null) { + obj.class.declaredFields.findAll { !it.synthetic } + .collect { obj.properties.get(it.name) }.findAll { it != null } + .each { + def node = completerNode(it) + if (node != null) { + objs.add(node) + } + } + obj.class.declaredMethods.findAll { !it.synthetic }.each { objs.add(it.name) } + } + if (objs.size() > 0) { + return Completers.TreeCompleter.node(objs as Object[]) + } else { + return null + } + } + + private Completer setupCompleter() { + new Completers.TreeCompleter(shell.getBinding().list().values().collect { + completerNode(it) + }.findAll { it != null }) + } + + /** + * Apply some closure to each of sub-results using shell configuration + * @param res + * @return + */ + def unwrap(Object res, Closure cl = { it }) { + if (getConfig().getBoolean("evalClosures", false) && res instanceof Closure) { + res = (res as Closure).call() + } else if (getConfig().getBoolean("evalData", true) && res instanceof Data) { + res = (res as Data).get(); + } else if (res instanceof DataNode) { + def node = res as DataNode + node.nodeGoal().run()// start computation of the whole node + node.dataStream().forEach { unwrap(it, cl) }; + } + + if (getConfig().getBoolean("unwrap", true)) { + if (res instanceof Collection) { + (res as Collection).forEach { unwrap(it, cl) } + } else if (res instanceof Stream) { + (res as Stream).forEach { unwrap(it, cl) } + } + } + cl.call(res); + } + + def show(Object obj) { + renderer.render(obj, Meta.empty()) + return null; + } + + def run(Object obj) { + Path scriptPath; + if (obj instanceof File) { + scriptPath = (obj as File).toPath(); + } else if (obj instanceof Path) { + scriptPath = obj as Path + } else { + scriptPath = shell.context.getOutput().getFile(obj as String).absolutePath; + } + + Files.newBufferedReader(scriptPath).withCloseable { + shell.eval(it) + } + } + + def help() { + this.help(null) + } + + def help(Object obj) { + switch (obj) { + case null: + case "": + println("This is DataForge Grind terminal shell") + println("Any Groovy statement is allowed") + println("Current list of shell bindings:") + shell.binding.list().each { k, v -> + println("\t$k") + } + + println("In order to display state of object and show help type `help `"); + break; + case "show": + println("Show given object in its visual representation") + break; + case "describe": + println("Show meta description for the object") + break; + case "run": + println("Run given script") + break; + + default: + describe(obj); + } + } + + Terminal getTerminal() { + return terminal; + } + + GrindShell getShell() { + return shell + } + + def println(String str) { + def writer = getTerminal().writer() + writer.println(str) + writer.flush() + } + + def print(String str) { + getTerminal().writer().with { + print(str) + } + } + + private def eval(String expression) { + def start = System.currentTimeMillis() + def res = unwrap(shell.eval(expression)) + def now = System.currentTimeMillis() + if (getConfig().getBoolean("benchmark", true)) { + Duration duration = Duration.ofMillis(now - start); + shell.context.logger.debug("Expression $expression evaluated in $duration") + } + return res; + } + + /** + * Start the terminal + * @return + */ + def launch() { + LineReader reader = LineReaderBuilder.builder() + .terminal(getTerminal()) + //.completer(setupCompleter()) + .appName("DataForge Grind terminal") + .build(); + PrintWriter writer = getTerminal().writer(); + +// +// def appender = TerminalLogLayout.buildAppender(context.logger.loggerContext, terminal); +// context.logger.addAppender(appender) + + def promptLine = new AttributedString("[${shell.context.getName()}] --> ", PROMPT).toAnsi(getTerminal()); + try { + while (true) { + String expression = reader.readLine(promptLine); + if ("exit" == expression || expression == null) { + shell.getContext().logger.debug("Exit command received") + break; + } + try { + def res = eval(expression); + if (res != null) { + String str = res.toString(); + +// //abbreviating the result +// //TODO improve string abbreviation +// if (str.size() > 50) { +// str = str[0..50] + "..." +// } + + def resStr = new AttributedStringBuilder() + .style(RES) + .append("\tres = ") + .style(DEFAULT) + .append(str); + println(resStr.toAnsi(getTerminal())) + } + } catch (Exception ex) { + writer.print(IOUtils.ANSI_RED); + ex.printStackTrace(writer); + writer.print(IOUtils.ANSI_RESET); + } + } + } catch (UserInterruptException ignored) { + writer.println("Interrupted by user") + } catch (EndOfFileException ignored) { + writer.println("Terminated by user") + } finally { + shell.getContext().logger.info("Closing terminal") + getTerminal().close() + shell.getContext().logger.debug("Terminal closed") + } + + } + + /** + * Start using provided closure as initializing script + * @param closure + */ + def launch(@DelegatesTo(GrindShell) Closure closure) { + this.shell.with(closure) + launch() + } + + +} diff --git a/grind/grind-terminal/src/main/groovy/hep/dataforge/grind/terminal/RunDumbGrindShell.groovy b/grind/grind-terminal/src/main/groovy/hep/dataforge/grind/terminal/RunDumbGrindShell.groovy new file mode 100644 index 00000000..1e80e39a --- /dev/null +++ b/grind/grind-terminal/src/main/groovy/hep/dataforge/grind/terminal/RunDumbGrindShell.groovy @@ -0,0 +1,16 @@ +package hep.dataforge.grind.terminal + +import hep.dataforge.context.Global + +/** + * Created by darksnake on 05-Nov-16. + */ +println "DataForge grind shell" +try { + GrindTerminal.dumb().launch() +} catch (Exception ex) { + ex.printStackTrace(); +} finally { + Global.instance().close(); +} +println "grind shell closed" \ No newline at end of file diff --git a/grind/grind-terminal/src/main/groovy/hep/dataforge/grind/terminal/RunGrindShell.groovy b/grind/grind-terminal/src/main/groovy/hep/dataforge/grind/terminal/RunGrindShell.groovy new file mode 100644 index 00000000..c995cb17 --- /dev/null +++ b/grind/grind-terminal/src/main/groovy/hep/dataforge/grind/terminal/RunGrindShell.groovy @@ -0,0 +1,17 @@ +package hep.dataforge.grind.terminal + +import hep.dataforge.context.Global + +/** + * Created by darksnake on 27-Oct-16. + */ + +println "DataForge grind shell" +try { + GrindTerminal.system().launch() +} catch (Exception ex) { + ex.printStackTrace(); +} finally { + Global.terminate(); +} +println "grind shell closed" \ No newline at end of file diff --git a/grind/grind-terminal/src/main/groovy/hep/dataforge/grind/terminal/TerminalLogLayout.groovy b/grind/grind-terminal/src/main/groovy/hep/dataforge/grind/terminal/TerminalLogLayout.groovy new file mode 100644 index 00000000..44bfb107 --- /dev/null +++ b/grind/grind-terminal/src/main/groovy/hep/dataforge/grind/terminal/TerminalLogLayout.groovy @@ -0,0 +1,81 @@ +package hep.dataforge.grind.terminal + +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.LoggerContext +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.Appender +import ch.qos.logback.core.LayoutBase +import ch.qos.logback.core.OutputStreamAppender +import ch.qos.logback.core.encoder.LayoutWrappingEncoder +import groovy.transform.CompileStatic +import org.jline.terminal.Terminal +import org.jline.utils.AttributedStringBuilder +import org.jline.utils.AttributedStyle + +import java.time.Instant +import java.time.format.DateTimeFormatter + +/** + * Logback log formatter for terminals + * Created by darksnake on 06-Nov-16. + */ +@CompileStatic +class TerminalLogLayout extends LayoutBase { + private static final AttributedStyle TIME = AttributedStyle.DEFAULT; + private static final AttributedStyle THREAD = AttributedStyle.DEFAULT.foreground(AttributedStyle.CYAN); + private static final AttributedStyle DEBUG = AttributedStyle.DEFAULT; + private static final AttributedStyle INFO = AttributedStyle.BOLD; + private static final AttributedStyle WARN = AttributedStyle.BOLD.foreground(AttributedStyle.RED); + private static + final AttributedStyle ERR = AttributedStyle.BOLD.foreground(AttributedStyle.BLACK).background(AttributedStyle.RED); + + public static Appender buildAppender(LoggerContext context, Terminal terminal) { + TerminalLogLayout layout = new TerminalLogLayout(terminal); + layout.setContext(context); + layout.start(); + LayoutWrappingEncoder encoder = new LayoutWrappingEncoder<>(); + encoder.setContext(context); + encoder.layout = new TerminalLogLayout(terminal); + encoder.start() + OutputStreamAppender appender = new OutputStreamAppender<>(); + appender.setContext(context); + appender.setOutputStream(terminal.output()) + appender.encoder = encoder; + appender.name = "terminal" + appender.start(); + return appender; + } + + final Terminal terminal; + boolean showThread = false; + DateTimeFormatter timeFormatter = DateTimeFormatter.ISO_TIME; + + TerminalLogLayout(Terminal terminal) { + this.terminal = terminal + } + + @Override + String doLayout(ILoggingEvent event) { + AttributedStringBuilder builder = new AttributedStringBuilder().with { + append(timeFormatter.format(Instant.ofEpochMilli(event.timeStamp)), TIME); + append(" ") + if (showThread) { + append("[${event.threadName}] ", THREAD); + } + switch (event.level) { + case Level.ERROR: + append(event.level.toString(), ERR); + break; + case Level.WARN: + append(event.level.toString(), WARN); + break; + case Level.DEBUG: + append(event.level.toString(), DEBUG); + break; + } + append(" ") + append(event.formattedMessage); + }; + return builder.toAnsi(terminal); + } +} diff --git a/grind/grind-terminal/src/main/resources/META-INF/services/hep.dataforge.grind.helpers.GrindHelperFactory b/grind/grind-terminal/src/main/resources/META-INF/services/hep.dataforge.grind.helpers.GrindHelperFactory new file mode 100644 index 00000000..0f839e48 --- /dev/null +++ b/grind/grind-terminal/src/main/resources/META-INF/services/hep.dataforge.grind.helpers.GrindHelperFactory @@ -0,0 +1 @@ +hep.dataforge.grind.helpers.PlotHelperFactory \ No newline at end of file diff --git a/grind/groovymath/README.md b/grind/groovymath/README.md new file mode 100644 index 00000000..d9535691 --- /dev/null +++ b/grind/groovymath/README.md @@ -0,0 +1,19 @@ +# Groovy maths # +This project contains some useful tools to work with mathematical objects using Groovy. Some tools are just thin dynamic layer over [commons-maths library](https://commons.apache.org/proper/commons-math/) and some are completely new. The project is primarily intended for [Beaker](http://beakernotebook.com/) but also will be later used in [GrindStone DataForge module](http://www.inr.ru/~nozik/dataforge/modules.html). + + +## Groovy table ## +Few groovy objects which allow to easily work with columns and rows. The work with columns should be [Origin](http://www.originlab.com/)-like. + +### Column ### +Since the most data manipulation is done with column, the column is the main dynamic structure. One can perform arithmetical operations on columns like `+`, `-` or `*`. The outcome of operation depends on right hand argument. If it is value, it is applied to each value in column, if it is column, then element-by-element operation is performed. More complex operations could be performed by `Column::transform {value, index ->... }` method. + +The values in column could be closures without parameters`{->...}`. **NOT TESTED** + +In addition to data, column has a name and general type. Both optional. + +### Table ### +Table is basically a list of columns, which could be accessed by index as well as name. Also table allows to access rows one by one or all together. Row information is not stored in table and accessed on-demand. + +### Row ### +Row is immutable list of values (latter it will also contain value names). Rows could be added to table and extracted from table but could not be modified at this moment. Later it will probably be good to replace hard copied rows by soft references to appropriate column values. \ No newline at end of file diff --git a/grind/groovymath/build.gradle b/grind/groovymath/build.gradle new file mode 100644 index 00000000..46b05a76 --- /dev/null +++ b/grind/groovymath/build.gradle @@ -0,0 +1,6 @@ +apply plugin: 'groovy' + +dependencies { + compile project(':grind') + compile project(':dataforge-maths') +} diff --git a/grind/groovymath/src/main/groovy/hep/dataforge/maths/GM.groovy b/grind/groovymath/src/main/groovy/hep/dataforge/maths/GM.groovy new file mode 100644 index 00000000..82457d26 --- /dev/null +++ b/grind/groovymath/src/main/groovy/hep/dataforge/maths/GM.groovy @@ -0,0 +1,47 @@ +package hep.dataforge.maths + +import org.apache.commons.math3.analysis.UnivariateFunction +import org.apache.commons.math3.linear.* + +/** + * A common class for static constructors and converters + * Created by darksnake on 25-Nov-16. + */ +class GM { + /** + * Build identity matrix with given dimension multiplied by given value + * @param dim + * @param val + * @return + */ + static RealMatrix identityMatrix(int dim, double val) { + List diag = new ArrayList(); + for (int i = 0; i < dim; i++) { + diag.add(val); + } + return new DiagonalMatrix(diag as double[]); + } + + static RealMatrix matrix(double[][] values) { + return new Array2DRowRealMatrix(values); + } + + static RealMatrix matrix(List> values) { + double[][] dvals = values as double[][]; + return new Array2DRowRealMatrix(dvals); + } + + static RealVector vector(double[] values) { + return new ArrayRealVector(values); + } + + static RealVector vector(Collection values) { + return new ArrayRealVector(values as double[]); + } + + static UnivariateFunction function(Closure cl) { + return { x -> + cl.call(x).toDouble() + } + } +} diff --git a/grind/groovymath/src/main/groovy/hep/dataforge/maths/HistogramBuilder.groovy b/grind/groovymath/src/main/groovy/hep/dataforge/maths/HistogramBuilder.groovy new file mode 100644 index 00000000..2e754887 --- /dev/null +++ b/grind/groovymath/src/main/groovy/hep/dataforge/maths/HistogramBuilder.groovy @@ -0,0 +1,45 @@ +package hep.dataforge.maths + +import hep.dataforge.maths.tables.GTable + +import java.util.concurrent.atomic.AtomicInteger +import java.util.stream.Stream + +/** + * Created by darksnake on 13-Nov-16. + */ +class HistogramBuilder { + private double[] binBorders; + private Stream dataStream; + + GTable build(){ + List bins = []; + def min = binBorders[0] + def max = binBorders[1]; + for(d in binBorders){ + bins << new Bin() + } + } + + + private class Bin{ + double center; + double min; + double max; + + Bin(double min, double max) { + this.min = min + this.max = max + center = (min + max)/2 + } + private AtomicInteger counter = new AtomicInteger(); + + def inc(){ + return counter.incrementAndGet(); + } + + def getCount(){ + return counter.intValue(); + } + } +} diff --git a/grind/groovymath/src/main/groovy/hep/dataforge/maths/extensions/NumberExtension.groovy b/grind/groovymath/src/main/groovy/hep/dataforge/maths/extensions/NumberExtension.groovy new file mode 100644 index 00000000..6e28a876 --- /dev/null +++ b/grind/groovymath/src/main/groovy/hep/dataforge/maths/extensions/NumberExtension.groovy @@ -0,0 +1,81 @@ +package hep.dataforge.maths.extensions + +import org.apache.commons.math3.analysis.UnivariateFunction +import org.apache.commons.math3.analysis.differentiation.UnivariateDifferentiableFunction +import org.apache.commons.math3.linear.RealMatrix +import org.apache.commons.math3.linear.RealVector + +/** + * Created by darksnake on 06-Nov-16. + */ +class NumberExtension { + static RealVector plus(final Number self, RealVector other) { + return other + self + } + + static RealVector minus(final Number self, RealVector other) { + return (-other) + self + } + + static RealVector multiply(final Number self, RealVector other) { + return other * self; + } + + static RealMatrix plus(final Number self, RealMatrix other) { + return other + self + } + + static RealMatrix minus(final Number self, RealMatrix other) { + return (-other) + self + } + + static RealMatrix multiply(final Number self, RealMatrix other) { + return other * self; + } + + static UnivariateFunction plus(final Number self, UnivariateFunction other) { + return other + self + } + + static UnivariateFunction minus(final Number self, UnivariateFunction other) { + return (-other) + self + } + + static UnivariateFunction multiply(final Number self, UnivariateFunction other) { + return other * self; + } + + static UnivariateDifferentiableFunction plus(final Number self, UnivariateDifferentiableFunction other) { + return other + self + } + + static UnivariateDifferentiableFunction minus(final Number self, UnivariateDifferentiableFunction other) { + return (-other) + self + } + + static UnivariateDifferentiableFunction multiply(final Number self, UnivariateDifferentiableFunction other) { + return other * self; + } + + /** + * Fix for bugged power method in DefaultGroovyMethods + * @param self + * @param exponent + * @return + */ + static Number power(Number self, Number exponent) { + return Math.pow(self.doubleValue(),exponent.doubleValue()); +// double base, exp, answer; +// base = self.doubleValue(); +// exp = exponent.doubleValue(); +// +// answer = Math.pow(base, exp); +// if ((double) ((int) answer) == answer) { +// return (int) answer; +// } else if ((double) ((long) answer) == answer) { +// return (long) answer; +// } else { +// return answer; +// } + } +} diff --git a/grind/groovymath/src/main/groovy/hep/dataforge/maths/extensions/RealMatrixExtension.groovy b/grind/groovymath/src/main/groovy/hep/dataforge/maths/extensions/RealMatrixExtension.groovy new file mode 100644 index 00000000..f31b90b9 --- /dev/null +++ b/grind/groovymath/src/main/groovy/hep/dataforge/maths/extensions/RealMatrixExtension.groovy @@ -0,0 +1,71 @@ +package hep.dataforge.maths.extensions + +import hep.dataforge.maths.GM +import org.apache.commons.math3.linear.DefaultRealMatrixChangingVisitor +import org.apache.commons.math3.linear.RealMatrix +import org.apache.commons.math3.linear.RealVector + +/** + * Created by darksnake on 01-Jul-16. + */ +class RealMatrixExtension { + + //TODO add number to matrix conversion + + /** + * Return new map and apply given transformation to each of its elements. Closure takes 3 arguments: row number, + * column number and actual value of matrix cell. + * @param self + * @param func + * @return + */ + static RealMatrix map(final RealMatrix self, Closure func) { + RealMatrix res = self.copy(); + res.walkInColumnOrder(new DefaultRealMatrixChangingVisitor() { + @Override + double visit(int row, int column, double value) { + func.call(row, column, value); + } + }) + } + + static RealMatrix plus(final RealMatrix self, RealMatrix other) { + return self.add(other) + } + + /** + * Add identity matrix x num to this matrix + * @param self + * @param num + * @return + */ + static RealMatrix plus(final RealMatrix self, Number num) { + return self.add(GM.identityMatrix(self.rowDimension, num)) + } + + static RealMatrix minus(final RealMatrix self, Number num) { + return self.subtract(GM.identityMatrix(self.rowDimension, num)) + } + + static RealMatrix minus(final RealMatrix self, RealMatrix other) { + return self.subtract(other) + } + + static RealMatrix negative(final RealMatrix self) { + return self.map { row, col, val -> -val } + } + + static RealMatrix multiply(final RealMatrix self, Number num) { + return self.scalarMultiply(num) + } + + static RealMatrix multiply(final RealMatrix self, RealVector vector) { + return self.operate(vector); + } + + static RealMatrix div(final RealMatrix self, Number num) { + return self.scalarMultiply(1d/num) + } + + //TODO add get and setAt +} diff --git a/grind/groovymath/src/main/groovy/hep/dataforge/maths/extensions/RealVectorExtension.groovy b/grind/groovymath/src/main/groovy/hep/dataforge/maths/extensions/RealVectorExtension.groovy new file mode 100644 index 00000000..69e29737 --- /dev/null +++ b/grind/groovymath/src/main/groovy/hep/dataforge/maths/extensions/RealVectorExtension.groovy @@ -0,0 +1,89 @@ +package hep.dataforge.maths.extensions + +import hep.dataforge.maths.GM +import org.apache.commons.math3.linear.RealMatrix +import org.apache.commons.math3.linear.RealVector + +/** + * Extension module for Commons Math linear + * Created by darksnake on 01-Jul-16. + */ +class RealVectorExtension { + + static { + //TODO add class cast from double[] to vector + } + + static RealVector plus(final RealVector self, RealVector other) { + return self.add(other) + } + + static RealVector plus(final RealVector self, Number num) { + return self.mapAdd(num) + } + + static RealVector minus(final RealVector self, RealVector other) { + return self.subtract(other) + } + + static RealVector minus(final RealVector self, Number num) { + return self.mapSubtract(num) + } + + static RealVector negative(final RealVector self) { + return self.mapMultiply(-1d) + } + + /** + * scalar product + * @param self + * @param other + * @return + */ + static Number multiply(final RealVector self, RealVector other) { + return self.dotProduct(other) + } + + static RealVector multiply(final RealVector self, Number num) { + return self.mapMultiply(num) + } + + static RealVector multiply(final RealVector self, RealMatrix matrix) { + return matrix.preMultiply(self) + } + + static RealVector div(final RealVector self, Number num) { + return self.mapDivide(num) + } + + static RealVector power(final RealVector self, Number num) { + return self.map { it**num } + } + + static RealVector leftShift(final RealVector self, Object obj) { + return self.append(obj) + } + + static Number getAt(final RealVector self, int index) { + return self.getEntry(index); + } + + static Void putAt(final RealVector self, int index, Number value) { + return self.setEntry(index, value); + } + + static RealVector transform(final RealVector self, Closure transformation) { + return GM.vector(self.toArray().collect(transformation)) + } + + static Object asType(final RealVector self, Class type) { + if (type.isAssignableFrom(List.class) || type == double[]) { + return self.toArray() + } + } + + static Object asVector(final List self) { + return GM.vector(self) + } + +} diff --git a/grind/groovymath/src/main/groovy/hep/dataforge/maths/extensions/UnivariateDifferentiableFunctionExtension.groovy b/grind/groovymath/src/main/groovy/hep/dataforge/maths/extensions/UnivariateDifferentiableFunctionExtension.groovy new file mode 100644 index 00000000..9a1ecc56 --- /dev/null +++ b/grind/groovymath/src/main/groovy/hep/dataforge/maths/extensions/UnivariateDifferentiableFunctionExtension.groovy @@ -0,0 +1,65 @@ +package hep.dataforge.maths.extensions + +import org.apache.commons.math3.analysis.differentiation.DerivativeStructure +import org.apache.commons.math3.analysis.differentiation.UnivariateDifferentiableFunction + +/** + * A static extension for commons-math UnivariateDifferentiableFunction. + * To use complicated functions one should use {@code import static DerivativeStructure* } + * Created by darksnake on 06-Nov-15. + */ +class UnivariateDifferentiableFunctionExtension { + static UnivariateDifferentiableFunction plus( + final UnivariateDifferentiableFunction self, UnivariateDifferentiableFunction function) { + return { DerivativeStructure d -> self.value(d).add(function.value(d)) } + } + + static UnivariateDifferentiableFunction plus(final UnivariateDifferentiableFunction self, Number num) { + return { DerivativeStructure d -> self.value(d).add(num.doubleValue()) } + } + + static UnivariateDifferentiableFunction minus( + final UnivariateDifferentiableFunction self, UnivariateDifferentiableFunction function) { + return { DerivativeStructure d -> self.value(d).subtract(function.value(d)) } + } + + static UnivariateDifferentiableFunction minus(final UnivariateDifferentiableFunction self, Number num) { + return { DerivativeStructure d -> self.value(d).subtract(num.d) } + } + + static UnivariateDifferentiableFunction multiply( + final UnivariateDifferentiableFunction self, UnivariateDifferentiableFunction function) { + return { DerivativeStructure d -> self.value(d).multiply(function.value(d)) } + } + + static UnivariateDifferentiableFunction multiply(final UnivariateDifferentiableFunction self, Number num) { + return { DerivativeStructure d -> self.value(d).multiply(num.doubleValue()) } + } + + static UnivariateDifferentiableFunction div( + final UnivariateDifferentiableFunction self, UnivariateDifferentiableFunction function) { + return { DerivativeStructure d -> self.value(d).divide(function.value(d)) } + } + + static UnivariateDifferentiableFunction div(final UnivariateDifferentiableFunction self, Number num) { + return { DerivativeStructure d -> self.value(d).divide(num.doubleValue()) } + } + + static UnivariateDifferentiableFunction power( + final UnivariateDifferentiableFunction self, UnivariateDifferentiableFunction function) { + return { DerivativeStructure d -> self.value(d).pow(function.value(d)) } + } + + static UnivariateDifferentiableFunction power(final UnivariateDifferentiableFunction self, Number num) { + return { DerivativeStructure d -> self.value(d).pow(num.doubleValue()) } + } + + static UnivariateDifferentiableFunction negative(final UnivariateDifferentiableFunction self) { + return { DerivativeStructure d -> self.value(d).negate() } + } + + static DerivativeStructure call(final UnivariateDifferentiableFunction self, DerivativeStructure d) { + return self.value(d) + } + +} diff --git a/grind/groovymath/src/main/groovy/hep/dataforge/maths/extensions/UnivariateFunctionExtension.groovy b/grind/groovymath/src/main/groovy/hep/dataforge/maths/extensions/UnivariateFunctionExtension.groovy new file mode 100644 index 00000000..a3c2a60f --- /dev/null +++ b/grind/groovymath/src/main/groovy/hep/dataforge/maths/extensions/UnivariateFunctionExtension.groovy @@ -0,0 +1,97 @@ +package hep.dataforge.maths.extensions + +import org.apache.commons.math3.analysis.UnivariateFunction +import org.apache.commons.math3.linear.RealVector + +/** + * A static extension for commons-math UnivariateFunctions + * Created by darksnake on 06-Nov-15. + */ +class UnivariateFunctionExtension { + + static Double value(final UnivariateFunction self, Number x) { + return self.value(x as Double) + } + + static Double value(final UnivariateFunction self, int x) { + //FIXME do some magic to force groovy to work with integers as doubles + return self.value(x as Double) + } + + static Double call(final UnivariateFunction self, Number x) { + return value(self, x) + } + + static UnivariateFunction plus(final UnivariateFunction self, UnivariateFunction function) { + return { double x -> + return self.value(x) + function.value(x) + } + } + + static UnivariateFunction plus(final UnivariateFunction self, Number num) { + return { double x -> + return self.value(x) + num + } + } + + static UnivariateFunction minus(final UnivariateFunction self, UnivariateFunction function) { + return { double x -> + return self.value(x) - function.value(x) + } + } + + static UnivariateFunction minus(final UnivariateFunction self, Number num) { + return { double x -> + return self.value(x as double) - num + } + } + + static UnivariateFunction multiply(final UnivariateFunction self, UnivariateFunction function) { + return { double x -> + return self.value(x) * function.value(x) + } + } + + static UnivariateFunction multiply(final UnivariateFunction self, Number num) { + return { double x -> + return self.value(x) * num + } + } + + static UnivariateFunction div(final UnivariateFunction self, UnivariateFunction function) { + return { double x -> + return self.value(x) / function.value(x) + } + } + + static UnivariateFunction div(final UnivariateFunction self, Number num) { + return { double x -> + return self.value(x) / num + } + } + + static UnivariateFunction power(final UnivariateFunction self, UnivariateFunction function) { + return { double x -> + return (self.value(x)**(function.value(x))).doubleValue() + } + } + + static UnivariateFunction power(final UnivariateFunction self, Number num) { + return { double x -> + return (self.value(x)**(num)).getDouble() + } + } + + static UnivariateFunction negative(final UnivariateFunction self) { + return { double x -> + return -self.value(x) + } + } + + static RealVector value(final UnivariateFunction self, RealVector vector) { + return vector.map(self); + } + + +} + diff --git a/grind/groovymath/src/main/groovy/hep/dataforge/maths/tables/GColumn.groovy b/grind/groovymath/src/main/groovy/hep/dataforge/maths/tables/GColumn.groovy new file mode 100644 index 00000000..bb18dea4 --- /dev/null +++ b/grind/groovymath/src/main/groovy/hep/dataforge/maths/tables/GColumn.groovy @@ -0,0 +1,190 @@ +package hep.dataforge.maths.tables + +import java.time.Instant + +/** + * GColumn for dynamic groovy table. Column can grow, but cannot be diminished. + * + * Created by darksnake on 26-Oct-15. + */ +class GColumn implements Iterable { + /** + * A default name for unnamed column + */ + public static final String DEFAULT_COLUMN_NAME = ""; + + /** + * The values of the column + */ + List values; + + /** + * The name of the column + */ + String name; + + /** + * The type of the column. Currently not used. + */ + String type; + + /** + * An empty anonymous column + */ + GColumn() { + } + + /** + * A copy constructor + * @param column + */ + GColumn(GColumn column) { + this.values = column.values.clone(); + this.name = column.name; + this.type = column.type; + } + + /** + * A general constructor + * @param name + * @param type + * @param values + */ + GColumn(String name = DEFAULT_COLUMN_NAME, String type = ValueType.ANY, List values) { + this.values = values.clone() + this.name = name + this.type = type + } + + /** + * Create new GColumn, each of its values is obtained by provided transformation. Transformation gets value and index as parameters + * @param transformation + * @return + */ + GColumn transform(String newName = DEFAULT_COLUMN_NAME, String newType = type, Closure transformation) { + return new GColumn(name: newName, type: newType, values: values.withIndex().collect(transformation)); + } + + /** + * Sum of columns + * @param other + * @return + */ + GColumn plus(GColumn other) { + return transform { value, index -> value + other[index] } + } + + /** + * Add a value to each column element + * @param obj + * @return + */ + GColumn plus(Object obj) { + return transform { value, index -> value + obj } + } + + /** + * Difference of columns + * @param other + * @return + */ + GColumn minus(GColumn other) { + return transform { value, index -> value - other[index] } + } + + /** + * Subtract value from each column element + * @param obj + * @return + */ + GColumn minus(Object obj) { + return transform { value, index -> value - obj } + } + + /** + * Element by element multiplication of columns + * @param other + * @return + */ + GColumn multiply(GColumn other) { + return transform { value, index -> value * other[index] } + } + + /** + * Multiply all elements by given value + * @param obj + * @return + */ + GColumn multiply(Object obj) { + return transform { value, index -> value * obj } + } + + /** + * Negate column + * @param obj + * @return + */ + GColumn negative() { + return transform { value, index -> -value } + } + + /** + * Add a value + * @param obj + * @return + */ + GColumn add(Object obj) { + values += obj; + return this + } + +// /** +// * remove value +// * @param obj +// * @return +// */ +// GColumn remove(Object obj) { +// values -= obj; +// return this +// } + + GColumn leftShift(Object obj) { + add(obj); + } + + def getAt(int index) { + if (index >= values.size()) { + return nullValue(); + } else { + return values[index]; + } + } + + def putAt(int index, Object obj) { + this.values.putAt(index, obj); + } + + /** + * The number of values in the column + * @return + */ + int size() { + return values.size(); + } + + @Override + Iterator iterator() { + return values.iterator(); + } + + def nullValue() { + switch (type) { + case ValueType.NUMBER: + return Double.NaN; + case ValueType.TIME: + return Instant.MIN; + default: + return ""; + } + } +} diff --git a/grind/groovymath/src/main/groovy/hep/dataforge/maths/tables/GRow.groovy b/grind/groovymath/src/main/groovy/hep/dataforge/maths/tables/GRow.groovy new file mode 100644 index 00000000..5de78aff --- /dev/null +++ b/grind/groovymath/src/main/groovy/hep/dataforge/maths/tables/GRow.groovy @@ -0,0 +1,13 @@ +package hep.dataforge.maths.tables + +/** + * + * Created by darksnake on 13-Nov-16. + */ +interface GRow extends Iterable { + Object getAt(String key); + + Object getAt(int index); + + Map asMap(); +} \ No newline at end of file diff --git a/grind/groovymath/src/main/groovy/hep/dataforge/maths/tables/GTable.groovy b/grind/groovymath/src/main/groovy/hep/dataforge/maths/tables/GTable.groovy new file mode 100644 index 00000000..f5429761 --- /dev/null +++ b/grind/groovymath/src/main/groovy/hep/dataforge/maths/tables/GTable.groovy @@ -0,0 +1,175 @@ +package hep.dataforge.maths.tables + +/** + * A dynamic Groovy table + * Created by darksnake on 26-Oct-15. + */ +class GTable implements Iterable { + List columns = new ArrayList<>(); + + /** + * empty constructor + */ + GTable() { + } + + /** + * From a list of columns + * @param columns + */ + GTable(List columns) { + this.columns = columns + } + + GColumn getAt(int index) { + return columns[index]; + } + + GColumn getAt(String name) { + return columns.find { it.name == name } + } + + /** + * A double index access. First index is a column, second is a value in column + * @param index1 + * @param index2 + * @return + */ + def getAt(index1, index2) { + return getAt(index1).getAt(index2); + } + + /** + * Put or replace column with given index. The method uses copy-constructor of the GColumn class. + * @param index + * @param column + * @return + */ + def putAt(int index, Object column) { + columns[index] = (new GColumn(column)); + } + + /** + * Add new column with the given name. + * The method uses copy-constructor of the GColumn class and changes the name of the column. + * @param name + * @param column + * @return + */ + int addColumn(String name, Object column) { + //Using list constructor or copy constructor + GColumn col = new GColumn(column); + //replacing name + col.name = name; + ///adding to column list + columns << col + return columns.size() - 1; + } + + + def putAt(String name, Object column) { + addColumn(name, column) + } + + def synchronized addRow(GRow row) { + //filling blank spaces + fillNulls(); + + for (e in row.asMap()) { + getAt(e.key).add(e.value); + } + } + + def addRow(List row) { + addRow(new MapRow(getColumnNames(), row)) + } + + /** + * add a GColumn + * @param column + */ + def leftShift(GColumn column) { + columns += column; + } + + /** + * Add a GRow + * @param row + * @return + */ + def leftShift(MapRow row) { + addRow(row) + } + + GRow row(int index) { + return new TableRow(this, index); + } + + /** + * List of all rows. Missing values are automatically replaced by apropriate nulls + * @return + */ + List getRows() { + return [0..maxColumnLength()].collect { row(it) }; + } + + /** + * Iterator for better performance and less memory impact work with rows (does not store all rows in separate structure simultaneously) + * @return + */ + Iterator getRowIterator() { + return new Iterator() { + int index = 0; + + @Override + boolean hasNext() { + return index < maxColumnLength() - 1; + } + + @Override + MapRow next() { + index++; + return row(index); + } + } + } + + /** + * The length of the longest column + * @return + */ + protected int maxColumnLength() { + columns.parallelStream().mapToInt { it.size() }.max(); + } + + /** + * Fill all existing columns with nulls to the maximum column length + */ + protected fillNulls() { + int maxSize = maxColumnLength(); + columns.each { + if (it.size() < maxSize) { + for (int i = it.size(); i < maxSize; i++) { + it[i] = it.nullValue(); + } + } + } + } + + List> getValues() { + getRows().collect { it.asList() } + } + + List getColumnNames() { + columns.withIndex().collect { item, index -> item.name ?: index; } + } + + List getTypes() { + columns.collect { it.type } + } + + @Override + Iterator iterator() { + return columns.iterator(); + } +} diff --git a/grind/groovymath/src/main/groovy/hep/dataforge/maths/tables/MapRow.groovy b/grind/groovymath/src/main/groovy/hep/dataforge/maths/tables/MapRow.groovy new file mode 100644 index 00000000..6adfc49f --- /dev/null +++ b/grind/groovymath/src/main/groovy/hep/dataforge/maths/tables/MapRow.groovy @@ -0,0 +1,44 @@ +package hep.dataforge.maths.tables + +import org.apache.commons.math3.exception.DimensionMismatchException + +/** + * An unmodifiable row of values + * Created by darksnake on 26-Oct-15. + */ +class MapRow implements GRow { + LinkedHashMap map = new LinkedHashMap<>(); + + MapRow(Map map) { + //TODO clone here + this.map.putAll(map); + } + + MapRow(List keys, List values) { + if (keys.size() != values.size()) { + throw new DimensionMismatchException(values.size(), keys.size()); + } + + for (int i = 0; i < keys.size(); i++) { + map.put(keys[i], values[i]); + } + } + + + Object getAt(String key) { + map.getAt(key); + } + + Object getAt(int index) { + return asList().get(index); + } + + @Override + Iterator iterator() { + return map.values().iterator(); + } + + Map asMap() { + return map.asImmutable(); + } +} diff --git a/grind/groovymath/src/main/groovy/hep/dataforge/maths/tables/TableRow.groovy b/grind/groovymath/src/main/groovy/hep/dataforge/maths/tables/TableRow.groovy new file mode 100644 index 00000000..2d06767b --- /dev/null +++ b/grind/groovymath/src/main/groovy/hep/dataforge/maths/tables/TableRow.groovy @@ -0,0 +1,51 @@ +package hep.dataforge.maths.tables + +/** + * The row representing fixed number row in the table. This row is changed whenever underlying table is changed. + * Created by darksnake on 13-Nov-16. + */ +class TableRow implements GRow { + private final GTable table; + private final int rowNum; + + TableRow(GTable table, int rowNum) { + this.table = table + this.rowNum = rowNum + } + + @Override + Object getAt(String key) { + return table[key, rowNum]; + } + + @Override + Object getAt(int index) { + return table[index, rowNum]; + } + + @Override + Map asMap() { + def res = new LinkedHashMap(); + for (column in table) { + res.put(column.name, column[rowNum]) + } + return res; + } + + @Override + Iterator iterator() { + return new Iterator() { + private Iterator columnIterator = table.iterator(); + + @Override + boolean hasNext() { + return columnIterator.hasNext(); + } + + @Override + Object next() { + return (++columnIterator)[rowNum]; + } + } + } +} diff --git a/grind/groovymath/src/main/groovy/hep/dataforge/maths/tables/ValueType.groovy b/grind/groovymath/src/main/groovy/hep/dataforge/maths/tables/ValueType.groovy new file mode 100644 index 00000000..f9a47c62 --- /dev/null +++ b/grind/groovymath/src/main/groovy/hep/dataforge/maths/tables/ValueType.groovy @@ -0,0 +1,12 @@ +package hep.dataforge.maths.tables + +/** + * Created by darksnake on 26-Oct-15. + */ +enum ValueType { + NUMBER, + STRING, + TIME, + BOOLEAN, + ANY +} \ No newline at end of file diff --git a/grind/groovymath/src/main/resources/META-INF/services/org.codehaus.groovy.runtime.ExtensionModule b/grind/groovymath/src/main/resources/META-INF/services/org.codehaus.groovy.runtime.ExtensionModule new file mode 100644 index 00000000..b1125321 --- /dev/null +++ b/grind/groovymath/src/main/resources/META-INF/services/org.codehaus.groovy.runtime.ExtensionModule @@ -0,0 +1,8 @@ +moduleName = groovymaths +moduleVersion = 1.0 +extensionClasses = \ + hep.dataforge.maths.extensions.UnivariateFunctionExtension,\ + hep.dataforge.maths.extensions.UnivariateDifferentiableFunctionExtension,\ + hep.dataforge.maths.extensions.RealVectorExtension,\ + hep.dataforge.maths.extensions.RealMatrixExtension,\ + hep.dataforge.maths.extensions.NumberExtension \ No newline at end of file diff --git a/grind/groovymath/src/test/groovy/hep/dataforge/maths/GMTest.groovy b/grind/groovymath/src/test/groovy/hep/dataforge/maths/GMTest.groovy new file mode 100644 index 00000000..af86000a --- /dev/null +++ b/grind/groovymath/src/test/groovy/hep/dataforge/maths/GMTest.groovy @@ -0,0 +1,25 @@ +package hep.dataforge.maths + +import org.apache.commons.math3.analysis.UnivariateFunction +import spock.lang.Specification + +/** + * Created by darksnake on 25-Nov-16. + */ +class GMTest extends Specification { + def "Function"() { + when: + def f = 1 + GM.function { x -> x**2 } + { x -> 2 * x }; + then: + f(1) == 4 + } + + def "test closure function"() { + given: + UnivariateFunction func = { x -> x**2 }; + when: + def newFunc = 1 + func ; + then: + newFunc(2) == 5; + } +} diff --git a/grind/groovymath/src/test/groovy/hep/dataforge/maths/extensions/FunctionExensionTest.groovy b/grind/groovymath/src/test/groovy/hep/dataforge/maths/extensions/FunctionExensionTest.groovy new file mode 100644 index 00000000..bfc4d378 --- /dev/null +++ b/grind/groovymath/src/test/groovy/hep/dataforge/maths/extensions/FunctionExensionTest.groovy @@ -0,0 +1,27 @@ +package hep.dataforge.maths.extensions + +import org.apache.commons.math3.analysis.UnivariateFunction +import spock.lang.Specification + +/** + * Created by darksnake on 06-Nov-15. + */ +class FunctionExensionTest extends Specification { + def "testing function plus construction"() { + when: + UnivariateFunction f = { x -> x*x }; + UnivariateFunction g = { x -> 2*x }; + def h = f - g + 1 + then: + h(2) == 1 + } + + def "testing combination construction"() { + when: + UnivariateFunction f = { x -> x }; + UnivariateFunction g = { x -> 2*x }; + def h = f*g + then: + h(3) == 18 + } +} diff --git a/grind/groovymath/src/test/groovy/hep/dataforge/maths/extensions/NumberExtensionTest.groovy b/grind/groovymath/src/test/groovy/hep/dataforge/maths/extensions/NumberExtensionTest.groovy new file mode 100644 index 00000000..c8454c22 --- /dev/null +++ b/grind/groovymath/src/test/groovy/hep/dataforge/maths/extensions/NumberExtensionTest.groovy @@ -0,0 +1,21 @@ +package hep.dataforge.maths.extensions + +import spock.lang.Specification + +/** + * Created by darksnake on 06-Nov-16. + */ +class NumberExtensionTest extends Specification { + def "test"(){ + given: + + def vec1 = 4d + [2,2].asVector(); + + when: + def res = 0.5*vec1; + + then: + + res as double[] == [3,3] + } +} diff --git a/grind/groovymath/src/test/groovy/hep/dataforge/maths/extensions/RealMatrixExtensionTest.groovy b/grind/groovymath/src/test/groovy/hep/dataforge/maths/extensions/RealMatrixExtensionTest.groovy new file mode 100644 index 00000000..9e926fe0 --- /dev/null +++ b/grind/groovymath/src/test/groovy/hep/dataforge/maths/extensions/RealMatrixExtensionTest.groovy @@ -0,0 +1,22 @@ +package hep.dataforge.maths.extensions + +import hep.dataforge.maths.GM +import org.apache.commons.math3.linear.RealMatrix +import org.apache.commons.math3.linear.RealVector +import spock.lang.Specification + +/** + * Created by darksnake on 01-Jul-16. + */ +class RealMatrixExtensionTest extends Specification { + def "matrix extension"(){ + given: + RealMatrix mat = GM.matrix([[0, -0.5], [-0.5, 0]]); + RealVector vec = GM.vector([1,1]); + when: + def res = vec*(mat + 1)*vec; + then: + res == 1; + + } +} diff --git a/grind/groovymath/src/test/groovy/hep/dataforge/maths/extensions/RealVectorExtensionTest.groovy b/grind/groovymath/src/test/groovy/hep/dataforge/maths/extensions/RealVectorExtensionTest.groovy new file mode 100644 index 00000000..5a844d81 --- /dev/null +++ b/grind/groovymath/src/test/groovy/hep/dataforge/maths/extensions/RealVectorExtensionTest.groovy @@ -0,0 +1,24 @@ +package hep.dataforge.maths.extensions + +import hep.dataforge.maths.GM +import org.apache.commons.math3.linear.RealVector +import spock.lang.Specification + +/** + * Created by darksnake on 06-Nov-16. + */ +class RealVectorExtensionTest extends Specification { + def "Map"() { + given: + RealVector vec1 = GM.vector([1, 2]); + RealVector vec2 = GM.vector([1.5, 1]); + + when: + def res = (vec1 + vec2*2).transform{ + it**2d + } + + then: + [16,16] == res.toArray() + } +} diff --git a/grind/groovymath/src/test/groovy/hep/dataforge/maths/tables/GColumnTest.groovy b/grind/groovymath/src/test/groovy/hep/dataforge/maths/tables/GColumnTest.groovy new file mode 100644 index 00000000..1343533c --- /dev/null +++ b/grind/groovymath/src/test/groovy/hep/dataforge/maths/tables/GColumnTest.groovy @@ -0,0 +1,25 @@ +package hep.dataforge.maths.tables + +/** + * Created by darksnake on 26-Oct-15. + */ +class GColumnTest extends spock.lang.Specification { + def "test transform"() { + given: + GColumn a = new GColumn([1, 2, 3]) + when: + def aTrans = a.transform {value, index -> "value" + value} + then: + aTrans[1] == "value2" + } + + def "test plus"() { + given: + GColumn a = new GColumn([1, 2, 3]) + GColumn b = new GColumn([2, 2, 2]) + when: + def c = a+b + then: + c[2] == 5 + } +} diff --git a/grind/groovymath/src/test/groovy/hep/dataforge/maths/tables/GTableTest.groovy b/grind/groovymath/src/test/groovy/hep/dataforge/maths/tables/GTableTest.groovy new file mode 100644 index 00000000..812b230b --- /dev/null +++ b/grind/groovymath/src/test/groovy/hep/dataforge/maths/tables/GTableTest.groovy @@ -0,0 +1,34 @@ +package hep.dataforge.maths.tables + +import spock.lang.Specification + +/** + * Created by darksnake on 27-Oct-15. + */ +class GTableTest extends Specification { + + def "test table row reading"() { + given: + GTable table = new GTable(); + table["a-column"] = [1, 2, 3] + table["b-column"] = ["some", "text", "here"] + table[2] = [0, 0, 0, 0] + + when: + GRow third = table.row(2) + + then: + third[1] == "here" + } + + def "test table double index reading"() { + when: + GTable table = new GTable(); + table["a-column"] = [1, 2, 3] + table["b-column"] = ["some", "text", "here"] + table[2] = [0, 0, 0, 0] + + then: + table["a-column"][1] == 2 + } +} diff --git a/grind/src/main/groovy/hep/dataforge/grind/Grind.groovy b/grind/src/main/groovy/hep/dataforge/grind/Grind.groovy new file mode 100644 index 00000000..b614b4b4 --- /dev/null +++ b/grind/src/main/groovy/hep/dataforge/grind/Grind.groovy @@ -0,0 +1,172 @@ +package hep.dataforge.grind + +import groovy.transform.CompileStatic +import hep.dataforge.context.Global +import hep.dataforge.grind.extensions.ExtensionInitializer +import hep.dataforge.grind.workspace.WorkspaceSpec +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.workspace.Workspace +import org.codehaus.groovy.control.CompilerConfiguration +import org.slf4j.LoggerFactory + +/** + * Created by darksnake on 04-Aug-16. + */ +@CompileStatic +class Grind { + + static { + LoggerFactory.getLogger("GRIND").debug("Initializing static GRIND extensions") + ExtensionInitializer.initAll() + } + + /** + * Build a fully defined node with given node name, root node values and delegating closure + * @param nodeName + * @param values + * @param cl + * @return + */ + static MetaBuilder buildMeta(String nodeName, Map values = [:], @DelegatesTo(GrindMetaBuilder) Closure cl = null) { + MetaBuilder builder + if (cl != null) { + def metaSpec = new GrindMetaBuilder() + def metaExec = cl.rehydrate(metaSpec, null, null); + metaExec.resolveStrategy = Closure.DELEGATE_ONLY; + builder = metaSpec.invokeMethod(nodeName, metaExec) as MetaBuilder + } else { + builder = new MetaBuilder(); + } + + if (!nodeName.isEmpty()) { + builder.rename(nodeName); + } + + builder.update(values) + + return builder + } + + /** + * Build anonymous meta node using {@code GrindMetaBuilder} and root node values + * @param values + * @param cl + * @return + */ + static MetaBuilder buildMeta(Map values, @DelegatesTo(GrindMetaBuilder) Closure cl = null) { + return buildMeta("", values, cl); + } + + /** + * Build anonymous meta node using {@code GrindMetaBuilder} + * @param cl + * @return + */ + static MetaBuilder buildMeta(@DelegatesTo(GrindMetaBuilder) Closure cl) { + return buildMeta("", [:], cl); + } + + static MetaBuilder buildMeta(String nodeName, @DelegatesTo(GrindMetaBuilder) Closure cl) { + return buildMeta(nodeName, [:], cl); + } + + /** + * Parse meta from a string + * @param input + * @return + */ + static MetaBuilder parseMeta(String input) { + if (input.contains("(") || input.contains("{")) { + def compilerConfiguration = new CompilerConfiguration() + compilerConfiguration.scriptBaseClass = DelegatingScript.class.name + def shell = new GroovyShell(Grind.class.classLoader, compilerConfiguration) + DelegatingScript script = shell.parse(input) as DelegatingScript; + GrindMetaBuilder builder = new GrindMetaBuilder(); + script.setDelegate(builder) + return script.run() as MetaBuilder + } else { + return new MetaBuilder(input); + } + } + +// static Workspace buildWorkspace(File file, Class spec) { +// return new GrindWorkspaceBuilder().read(file).withSpec(spec).builder(); +// } + +// /** +// * A universal grind meta builder. Using reflections to determine arguments. +// * @param args +// * @return +// */ +// static MetaBuilder buildMeta(Object... args) { +// if (args.size() == 0) { +// return new MetaBuilder(""); +// } else if (args.size() == 1 && args[0] instanceof String) { +// return parseMeta(args[0] as String); +// } else { +// String nodeName = args[0] instanceof String ? args[0] : ""; +// Map values; +// if (args[0] instanceof Map) { +// values = args[0] as Map; +// } else if (args.size() > 1 && args[1] instanceof Map) { +// values = args[1] as Map; +// } else { +// values = [:]; +// } +// Closure closure; +// if (args[0] instanceof Closure) { +// closure = args[0] as Closure; +// } else if (args.size() > 1 && args[1] instanceof Closure) { +// closure = args[1] as Closure; +// } else if (args.size() > 2 && args[2] instanceof Closure) { +// closure = args[2] as Closure; +// } else { +// closure = {}; +// } +// +// return buildMeta(values, nodeName, closure); +// } +// } + +// static Workspace buildWorkspace(File file) { +// return FileBasedWorkspace.Companion.build(file.toPath()); +// } +// +// static Workspace buildWorkspace(String file) { +// return FileBasedWorkspace.Companion.build(Paths.get(file)); +// } + + static Workspace buildWorkspace(@DelegatesTo(value = WorkspaceSpec, strategy = Closure.DELEGATE_ONLY) Closure cl) { + WorkspaceSpec spec = new WorkspaceSpec(Global.INSTANCE); + def script = cl.rehydrate(spec, null, null); + script.setResolveStrategy(Closure.DELEGATE_ONLY) + script.call() + return spec.builder.build(); + } + +// /** +// * Build MetaMorph using convenient meta builder +// * @param type +// * @param args +// * @return +// */ +// static T morph(Class type, +// Map values = [:], +// String nodeName = "", +// @DelegatesTo(GrindMetaBuilder) Closure cl = null) { +// MetaMorph.Companion.morph(type, buildMeta(nodeName, values, cl)) +// } + +// /** +// * Build a simple pipe action +// * @param cl +// * @return +// */ +// static Action pipe(Map params = Collections.emptyMap(), Closure cl) { +// return GrindPipe.build(params, cl) +// } +// +// static Action join(Map params = Collections.emptyMap(), Closure cl) { +// return GrindPipe.build(params, cl) +// } +} diff --git a/grind/src/main/groovy/hep/dataforge/grind/GrindMetaBuilder.groovy b/grind/src/main/groovy/hep/dataforge/grind/GrindMetaBuilder.groovy new file mode 100644 index 00000000..0c2af807 --- /dev/null +++ b/grind/src/main/groovy/hep/dataforge/grind/GrindMetaBuilder.groovy @@ -0,0 +1,89 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.grind + +import groovy.transform.CompileStatic +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.values.NamedValue + +/** + * A builder to create annotations + * @author Alexander Nozik + */ +@CompileStatic +class GrindMetaBuilder extends BuilderSupport { + @Override + MetaBuilder createNode(Object name) { + return createNode(name, [:]); + } + + @Override + MetaBuilder createNode(Object name, Map attributes) { + return createNode(name, attributes, null); + } + + private static boolean isCollectionOrArray(Object object) { + return object instanceof Collection || object.getClass().isArray() + } + + @Override + MetaBuilder createNode(Object name, Map attributes, Object value) { + MetaBuilder res = new MetaBuilder(name.toString()); + attributes.each { k, v -> + if (isCollectionOrArray(v)) { + v.each { + res.putValue(k.toString(), it); + } + } else { + res.putValue(k.toString(), v); + } + } + if (value != null && value instanceof MetaBuilder) { + res.putNode((MetaBuilder) value); + } + return res; + } + + @Override + MetaBuilder createNode(Object name, Object value) { + MetaBuilder res = new MetaBuilder(name.toString()); + if (value != null && value instanceof MetaBuilder) { + res.putNode((MetaBuilder) value); + } + return res; + } + + @Override + void setParent(Object parent, Object child) { + ((MetaBuilder) parent).attachNode((MetaBuilder) child); + } + + void put(Meta meta) { + (this.current as MetaBuilder).putNode(meta); + } + + void put(NamedValue value) { + (this.current as MetaBuilder).putValue(value.name, value.anonymous); + } + + void put(Map map) { + map.each { k, v -> + (this.current as MetaBuilder).putValue(k, v); + } + } +} + diff --git a/grind/src/main/groovy/hep/dataforge/grind/GrindShell.groovy b/grind/src/main/groovy/hep/dataforge/grind/GrindShell.groovy new file mode 100644 index 00000000..710034f5 --- /dev/null +++ b/grind/src/main/groovy/hep/dataforge/grind/GrindShell.groovy @@ -0,0 +1,164 @@ +package hep.dataforge.grind + +import groovy.transform.CompileStatic +import hep.dataforge.context.Context +import hep.dataforge.context.ContextAware +import hep.dataforge.context.Global +import hep.dataforge.description.NodeDef +import hep.dataforge.description.NodeDefs +import hep.dataforge.grind.helpers.GrindHelperFactory +import hep.dataforge.meta.Meta +import org.codehaus.groovy.control.CompilerConfiguration +import org.codehaus.groovy.control.customizers.ImportCustomizer +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +/** + * Created by darksnake on 15-Dec-16. + */ + +@CompileStatic +@NodeDefs([ + @NodeDef(key = "import", info = "Import customization"), + @NodeDef(key = "import.one", multiple = true, info = "A single import. Can contain alas. If field is present, then using static import") +]) +class GrindShell implements ContextAware { + + private Context context; + private ShellBinding binding = new ShellBinding(); + private final GroovyShell shell; + + GrindShell(Context context = Global.INSTANCE, Meta meta = Meta.empty()) { + this.context = context + ImportCustomizer importCustomizer = new ImportCustomizer(); + + //adding package import + importCustomizer.addStarImports(meta.getStringArray("import.package") { new String[0] }) + //adding static imports + importCustomizer.addStaticStars( + meta.getStringArray("import.utils") { + ["java.lang.Math", "hep.dataforge.grind.Grind"] as String[] + } + ) + //adding regular imports + importCustomizer.addImports(meta.getStringArray("import.classes") { new String[0] }); + //add import with aliases + meta.getMetaList("import.one").each { + if (it.hasValue("field")) { + importCustomizer.addStaticImport(it.getString("alias", (String) null), it.getString("name"), it.getString("field")); + } else { + importCustomizer.addImport(it.getString("alias", (String) null), it.getString("name")); + } + } + + CompilerConfiguration configuration = new CompilerConfiguration(); + configuration.addCompilationCustomizers(importCustomizer); + + //define important properties + binding.setInternal("context", context) + + //Load all available helpers + context.serviceStream(GrindHelperFactory).forEach { + def helper = it.build(context, meta.getMetaOrEmpty(it.name)) + if (binding.internals.containsKey(it.name)) { + context.logger.warn("The helper with the name ${it.name} already loaded into shell. Overriding.") + } + binding.setInternal(it.name, helper); + } + shell = new GroovyShell(context.classLoader, binding, configuration); + } + + Logger getLogger(){ + return LoggerFactory.getLogger(context.name + ".grind") + } + + def bind(String key, Object value) { + binding.setInternal(key, value) + } + + ShellBinding getBinding(){ + return binding; + } + + @Override + Context getContext() { + return context; + } + + /** + * remembering last answer + * @param res + * @return + */ + private def postProcess(Object res) { + if (res != null) { + bind("res", res) + }; + //TODO remember n last answers + return res; + } + + /** + * Evaluate string expression + * @param expression + * @return + */ + synchronized def eval(String expression) { + return postProcess(shell.evaluate(expression)) + } + + + synchronized def eval(Reader reader){ + return postProcess(shell.evaluate(reader)) + } + + /** + * Evaluate a closure using shell bindings + * @param cl + * @return + */ + synchronized def eval(Closure cl) { + Closure script = cl.rehydrate(binding, null, null); + script.resolveStrategy = Closure.DELEGATE_ONLY; + return postProcess(script.call()) + } + + /** + * A shell binding with pre-defined immutable internal properties + */ + class ShellBinding extends Binding { + private Map internals = new HashMap<>(); + + void setInternal(String key, Object obj) { + this.internals.put(key, obj); + } + + @Override + void setVariable(String propertyName, Object newValue) { + if (internals.containsKey(propertyName)) { + getContext().getLogger().error("The variable name ${propertyName} is occupied by internal object. It won't be accessible from the shell.") + } + super.setVariable(propertyName, newValue) + } + + @Override + Object getVariable(String propertyName) { + if (internals.containsKey(propertyName)) { + return internals.get(propertyName); + } else { + return super.getVariable(propertyName) + } + } + + /** + * Get immutable map of internals + * @return + */ + Map list(){ + return internals.asImmutable(); + } + + + } +} + diff --git a/grind/src/main/groovy/hep/dataforge/grind/extensions/CoreExtension.groovy b/grind/src/main/groovy/hep/dataforge/grind/extensions/CoreExtension.groovy new file mode 100644 index 00000000..fda81334 --- /dev/null +++ b/grind/src/main/groovy/hep/dataforge/grind/extensions/CoreExtension.groovy @@ -0,0 +1,358 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.grind.extensions + +import groovy.transform.CompileStatic +import hep.dataforge.data.Data +import hep.dataforge.data.DataNode +import hep.dataforge.grind.Grind +import hep.dataforge.grind.GrindMetaBuilder +import hep.dataforge.meta.* +import hep.dataforge.tables.Table +import hep.dataforge.values.* +import hep.dataforge.workspace.Workspace + +import java.time.Instant + +/** + * Created by darksnake on 20-Aug-16. + */ +@CompileStatic +class CoreExtension { + + //value extensions + + static Value plus(final Value self, Object obj) { + return plus(self, ValueFactory.of(obj)) + } + + static Value plus(final Value self, Value other) { + switch (self.getType()) { + case ValueType.NUMBER: + return ValueFactory.of(self.getNumber() + other.getNumber()); + case ValueType.STRING: + return ValueFactory.of(self.getString() + other.getString()); + case ValueType.TIME: + //TODO implement + throw new RuntimeException("Time plus operator is not yet supported") + case ValueType.BOOLEAN: + //TODO implement + throw new RuntimeException("Boolean plus operator is not yet supported") + case ValueType.NULL: + return other; + } + } + + static Value minus(final Value self, Object obj) { + return minus(self, ValueFactory.of(obj)) + } + + static Value minus(final Value self, Value other) { + switch (self.getType()) { + case ValueType.NUMBER: + return ValueFactory.of(self.getNumber() - other.getNumber()); + case ValueType.STRING: + return ValueFactory.of(self.getString() - other.getString()); + case ValueType.TIME: + //TODO implement + throw new RuntimeException("Time plus operator is not yet supported") + case ValueType.BOOLEAN: + //TODO implement + throw new RuntimeException("Boolean plus operator is not yet supported") + case ValueType.NULL: + return negative(other); + } + } + + + static Value negative(final Value self) { + switch (self.getType()) { + case ValueType.NUMBER: + //TODO fix non-dobule values + return ValueFactory.of(-self.getDouble()); + case ValueType.STRING: + throw new RuntimeException("Can't negate String value") + case ValueType.TIME: + throw new RuntimeException("Can't negate time value") + case ValueType.BOOLEAN: + return ValueFactory.of(!self.getBoolean()); + case ValueType.NULL: + return self; + } + } + + static Value multiply(final Value self, Object obj) { + return multiply(self, ValueFactory.of(obj)) + } + + static Value multiply(final Value self, Value other) { + switch (self.getType()) { + case ValueType.NUMBER: + return ValueFactory.of(self.getNumber() * other.getNumber()); + case ValueType.STRING: + return ValueFactory.of(self.getString() * other.getInt()); + case ValueType.TIME: + //TODO implement + throw new RuntimeException("Time multiply operator is not yet supported") + case ValueType.BOOLEAN: + //TODO implement + throw new RuntimeException("Boolean multiply operator is not yet supported") + case ValueType.NULL: + return ValueFactory.NULL; + } + } + + static Object asType(final Value self, Class type) { + switch (type) { + case double: + return self.getDouble(); + case int: + return self.getInt(); + case short: + return self.getNumber().shortValue(); + case long: + return self.getNumber().longValue(); + case Number: + return self.getNumber(); + case String: + return self.getString(); + case boolean: + return self.getBoolean(); + case Instant: + return self.getTime(); + case Date: + return Date.from(self.getTime()); + default: + throw new RuntimeException("Unknown value cast type: ${type}"); + } + } + +// /** +// * Unwrap value and return its content in its native form. Possible loss of precision for numbers +// * @param self +// * @return +// */ +// static Object unbox(final Value self) { +// switch (self.getType()) { +// case ValueType.NUMBER: +// return self.doubleValue(); +// case ValueType.STRING: +// return self.getString(); +// case ValueType.TIME: +// return self.getTime(); +// case ValueType.BOOLEAN: +// return self.booleanValue(); +// case ValueType.NULL: +// return null; +// } +// } + + /** + * Represent DataPoint as a map of typed objects according to value type + * @param self + * @return + */ + static Map unbox(final Values self) { + self.getNames().collectEntries { + [it: self.getValue(it).getValue()] + } + } + + /** + * Groovy extension to access DataPoint fields + * @param self + * @param field + * @return + */ + static Object getAt(final Values self, String field) { + return self.getValue(field).getValue(); + } + + static Value getProperty(final Values self, String name) { + return self.getValue(name) + } + + //meta extensions + + static MetaBuilder plus(final Meta self, MetaBuilder other) { + return new JoinRule().merge(self, other); + } + + static MetaBuilder plus(final Meta self, NamedValue other) { + return new MetaBuilder(self).putValue(other.getName(), other); + } + + /** + * Put a mixed map of nodes and values into new meta based on existing one + * @param self + * @param map + * @return + */ + static MetaBuilder plus(final Meta self, Map map) { + MetaBuilder res = new MetaBuilder(self); + map.forEach { String key, value -> + if (value instanceof Meta) { + res.putNode(key, value); + } else { + res.putValue(key, value) + } + } + return res; + } + + static MetaBuilder leftShift(final MetaBuilder self, MetaBuilder other) { + return new MetaBuilder(self).putNode(other); + } + + static MetaBuilder leftShift(final MetaBuilder self, NamedValue other) { + return new MetaBuilder(self).putValue(other.getName(), other); + } + + static MetaBuilder leftShift(final MetaBuilder self, Map map) { + map.forEach { String key, value -> + if (value instanceof Meta) { + self.putNode(key, value); + } else { + self.putValue(key, value) + } + } + return self; + } + + /** + * Update existing builder using closure + * @param self + * @param cl + * @return + */ + static MetaBuilder update(final MetaBuilder self, @DelegatesTo(GrindMetaBuilder) Closure cl) { + return self.update(Grind.buildMeta(cl)) + } + + /** + * Update existing builder using map of values and closure + * @param self + * @param cl + * @return + */ + static MetaBuilder update(final MetaBuilder self, Map values, @DelegatesTo(GrindMetaBuilder) Closure cl) { + return self.update(Grind.buildMeta(values, cl)) + } + + /** + * Create a new builder and update it from closure (existing one not changed) + * @param self + * @param cl + * @return + */ + static MetaBuilder transform(final Meta self, @DelegatesTo(GrindMetaBuilder) Closure cl) { + return new MetaBuilder(self).update(Grind.buildMeta(self.getName(), cl)) + } + + /** + * Create a new builder and update it from closure and value map (existing one not changed) + * @param self + * @param cl + * @return + */ + static MetaBuilder transform(final Meta self, Map values, @DelegatesTo(GrindMetaBuilder) Closure cl) { + return new MetaBuilder(self).update(Grind.buildMeta(self.getName(), values, cl)) + } + + /** + * Create a new builder and update it from map (existing one not changed) + * @param self + * @param cl + * @return + */ + static MetaBuilder transform(final Meta self, Map values) { + return new MetaBuilder(self).update(values) + } + + static Object getAt(final Meta self, String name) { + return self.getValue(name).getValue(); + } + + static void setAt(final MetaBuilder self, String name, Object value) { + self.setValue(name, value) + } + + /** + * Compile new builder using self as a template + * @param self + * @param dataSource + * @return + */ + static MetaBuilder compile(final Meta self, Meta dataSource) { + return Template.compileTemplate(self, dataSource); + } + + static MetaBuilder compile(final Meta self, Map dataMap) { + return Template.compileTemplate(self, dataMap); + } + + /** + * Use map as a value provider and given meta as meta provider + * @param template + * @param map + * @param cl + * @return + */ + static MetaBuilder compile(final Meta self, Map map, @DelegatesTo(GrindMetaBuilder) Closure cl) { + Template tmp = new Template(self); + return tmp.compile(new MapValueProvider(map), Grind.buildMeta(cl)); + } + + static Configurable configure(final Configurable self, Closure configuration) { + self.configure(Grind.buildMeta("config", configuration)); + } + + static Configurable configure(final Configurable self, Map values, Closure configuration) { + self.configure(Grind.buildMeta("config", values, configuration)); + } + + static Configurable configure(final Configurable self, Map values) { + self.configure(Grind.buildMeta("config", values)); + } + + static Configurable setAt(final Configurable self, String key, Object value) { + self.configureValue(key, value); + } + + //table extension + static Values getAt(final Table self, int index) { + return self.getRow(index); + } + + static Object getAt(final Table self, String name, int index) { + return self.get(name, index).getValue(); + } + + //workspace extension + static DataNode run(final Workspace wsp, String command) { + if (command.contains("(") || command.contains("{")) { + Meta meta = Grind.parseMeta(command); + return wsp.runTask(meta); + } else { + return wsp.runTask(command,command) + } + } + + static Data getAt(final DataNode self, String key){ + return self.optData(key) + } +} diff --git a/grind/src/main/groovy/hep/dataforge/grind/extensions/ExtensionInitializer.groovy b/grind/src/main/groovy/hep/dataforge/grind/extensions/ExtensionInitializer.groovy new file mode 100644 index 00000000..17bd81fd --- /dev/null +++ b/grind/src/main/groovy/hep/dataforge/grind/extensions/ExtensionInitializer.groovy @@ -0,0 +1,62 @@ +package hep.dataforge.grind.extensions + +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MutableMetaNode +import hep.dataforge.tables.Table +import hep.dataforge.workspace.Workspace + +/** + * A set of dynamic initializers for groovy features. Must be called explicitly at the start of the program. + */ +class ExtensionInitializer { + + /** + * Add property access to meta nodes + * @return + */ + static def initMeta(){ + Meta.metaClass.getProperty = {String name -> + delegate.getMetaOrEmpty(name) + } + + MutableMetaNode.metaClass.setProperty = { String name, Object value -> + if (value instanceof Meta) { + delegate.setNode(name, (Meta) value) + } else if (value instanceof Collection) { + delegate.setNode(name, (Collection) value) + } else if (value.getClass().isArray()) { + delegate.setNode(name, (Meta[]) value) + } else { + throw new RuntimeException("Can't convert ${value.getClass()} to Meta") + } + } + } + + /** + * Add property access to column tables + * @return + */ + static def initTable(){ + Table.metaClass.getProperty = { String propName -> + def meta = Table.metaClass.getMetaProperty(propName) + if (meta) { + meta.getProperty(delegate) + } else { + return (delegate as Table).getColumn(propName) + } + } + } + + static def initWorkspace(){ + Workspace.metaClass.methodMissing = {String name, Object args -> + String str = args.getClass().isArray() ? ((Object[]) args).join(" ") : args.toString() + return (delegate as Workspace).runTask(name, str) + } + } + + static def initAll(){ + initMeta() + initTable() + initWorkspace() + } +} diff --git a/grind/src/main/groovy/hep/dataforge/grind/helpers/AbstractHelper.groovy b/grind/src/main/groovy/hep/dataforge/grind/helpers/AbstractHelper.groovy new file mode 100644 index 00000000..eb215acc --- /dev/null +++ b/grind/src/main/groovy/hep/dataforge/grind/helpers/AbstractHelper.groovy @@ -0,0 +1,71 @@ +package hep.dataforge.grind.helpers + +import groovy.transform.CompileStatic +import hep.dataforge.context.Context +import hep.dataforge.io.output.Output +import hep.dataforge.io.output.SelfRendered +import hep.dataforge.io.output.TextOutput +import hep.dataforge.meta.Meta +import org.jetbrains.annotations.NotNull +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import java.awt.* +import java.lang.reflect.Method + +@CompileStatic +abstract class AbstractHelper implements GrindHelper, SelfRendered { + private final Context context; + + AbstractHelper(Context context) { + this.context = context + } + + @Override + Context getContext() { + return context + } + + /** + * get the list of all methods that need describing + * @return + */ + protected Collection listDescribedMethods() { + return getClass().getDeclaredMethods() + .findAll { it.isAnnotationPresent(MethodDescription) } + } + + protected abstract void renderDescription(@NotNull TextOutput output, @NotNull Meta meta) + + @Override + void render(@NotNull Output output, @NotNull Meta meta) { + if (output instanceof TextOutput) { + TextOutput textOutput = output + renderDescription(textOutput, meta) + listDescribedMethods().each { + textOutput.renderText(it.name, Color.MAGENTA) + + if (it.parameters) { + textOutput.renderText(" (") + for (int i = 0; i < it.parameters.length; i++) { + def par = it.parameters[i] + textOutput.renderText(par.type.simpleName, Color.BLUE) + if (i != it.parameters.length - 1) { + textOutput.renderText(", ") + } + } + textOutput.renderText(")") + + } + + textOutput.renderText(": ") + textOutput.renderText(it.getAnnotation(MethodDescription).value()) + textOutput.newLine(meta) + } + } + } + + Logger getLogger() { + return LoggerFactory.getLogger(getClass()) + } +} diff --git a/grind/src/main/groovy/hep/dataforge/grind/helpers/GrindHelper.groovy b/grind/src/main/groovy/hep/dataforge/grind/helpers/GrindHelper.groovy new file mode 100644 index 00000000..c6afb878 --- /dev/null +++ b/grind/src/main/groovy/hep/dataforge/grind/helpers/GrindHelper.groovy @@ -0,0 +1,11 @@ +package hep.dataforge.grind.helpers + +import hep.dataforge.context.ContextAware +import hep.dataforge.description.Described + +/** + * A helper that is injected into the shell. + */ +interface GrindHelper extends ContextAware, Described { + +} diff --git a/grind/src/main/groovy/hep/dataforge/grind/helpers/GrindHelperFactory.groovy b/grind/src/main/groovy/hep/dataforge/grind/helpers/GrindHelperFactory.groovy new file mode 100644 index 00000000..fec73b03 --- /dev/null +++ b/grind/src/main/groovy/hep/dataforge/grind/helpers/GrindHelperFactory.groovy @@ -0,0 +1,8 @@ +package hep.dataforge.grind.helpers + +import hep.dataforge.Named +import hep.dataforge.utils.ContextMetaFactory + +interface GrindHelperFactory extends ContextMetaFactory, Named { + +} \ No newline at end of file diff --git a/grind/src/main/groovy/hep/dataforge/grind/helpers/MethodDescription.groovy b/grind/src/main/groovy/hep/dataforge/grind/helpers/MethodDescription.groovy new file mode 100644 index 00000000..e3affb55 --- /dev/null +++ b/grind/src/main/groovy/hep/dataforge/grind/helpers/MethodDescription.groovy @@ -0,0 +1,12 @@ +package hep.dataforge.grind.helpers + +import java.lang.annotation.ElementType +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy +import java.lang.annotation.Target + +@Retention(RetentionPolicy.RUNTIME) +@Target(value = ElementType.METHOD) +@interface MethodDescription { + String value(); +} \ No newline at end of file diff --git a/grind/src/main/groovy/hep/dataforge/grind/helpers/WorkspaceHelper.groovy b/grind/src/main/groovy/hep/dataforge/grind/helpers/WorkspaceHelper.groovy new file mode 100644 index 00000000..69c186bd --- /dev/null +++ b/grind/src/main/groovy/hep/dataforge/grind/helpers/WorkspaceHelper.groovy @@ -0,0 +1,33 @@ +package hep.dataforge.grind.helpers + +import groovy.transform.CompileStatic +import hep.dataforge.context.Context +import hep.dataforge.grind.workspace.WorkspaceSpec +import hep.dataforge.io.output.TextOutput +import hep.dataforge.meta.Meta +import org.jetbrains.annotations.NotNull + +import java.lang.reflect.Method + +@CompileStatic +class WorkspaceHelper extends AbstractHelper { + @Delegate private WorkspaceSpec builder; + + WorkspaceHelper(Context context) { + super(context) + builder = new WorkspaceSpec(context); + } + + @Override + protected Collection listDescribedMethods() { + return builder.getClass().getDeclaredMethods() + .findAll { it.isAnnotationPresent(MethodDescription) } + } + + @Override + protected void renderDescription(@NotNull TextOutput output, @NotNull Meta meta) { + output.renderText("The helper for workspace operations") + } + + +} diff --git a/grind/src/main/groovy/hep/dataforge/grind/helpers/WorkspaceHelperFactory.groovy b/grind/src/main/groovy/hep/dataforge/grind/helpers/WorkspaceHelperFactory.groovy new file mode 100644 index 00000000..1c2c6dd2 --- /dev/null +++ b/grind/src/main/groovy/hep/dataforge/grind/helpers/WorkspaceHelperFactory.groovy @@ -0,0 +1,16 @@ +package hep.dataforge.grind.helpers + +import hep.dataforge.context.Context +import hep.dataforge.meta.Meta + +class WorkspaceHelperFactory implements GrindHelperFactory { + @Override + String getName() { + return "spaces" + } + + @Override + GrindHelper build(Context context, Meta meta) { + return new WorkspaceHelper(context) + } +} diff --git a/grind/src/main/groovy/hep/dataforge/grind/misc/ReWrapper.groovy b/grind/src/main/groovy/hep/dataforge/grind/misc/ReWrapper.groovy new file mode 100644 index 00000000..fe2b2129 --- /dev/null +++ b/grind/src/main/groovy/hep/dataforge/grind/misc/ReWrapper.groovy @@ -0,0 +1,90 @@ +package hep.dataforge.grind.misc + +import hep.dataforge.data.binary.Binary +import hep.dataforge.io.envelopes.Envelope +import hep.dataforge.io.envelopes.EnvelopeType +import hep.dataforge.io.envelopes.SimpleEnvelope +import hep.dataforge.meta.Meta +import org.slf4j.LoggerFactory + +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.StandardOpenOption +import java.util.stream.Collectors + +class ReWrapper { + + def logger = LoggerFactory.getLogger("reWrapper"); + + def reWrap(Meta meta) { + Path path = Paths.get(new URI(meta.getString("path"))); + if (Files.isDirectory(path)) { + String mask = meta.getString("mask", "*"); + String regex = mask.replace(".", "\\.").replace("?", ".?").replace("*", ".+"); + Files.find(path, Integer.MAX_VALUE, { name, attr -> name.toString().matches(regex) }) + } else { + reWrapFile(path, meta); + } + } + + def reWrapFile(Path path, Meta meta) { + EnvelopeType readType = inferType(path); + if (readType) { + Map readProperties = resolveProperties(meta.getMetaOrEmpty("input.properties")); + logger.info("Reading envelope from file ${path}") + + //reading file content + Envelope envelope = Files.newInputStream(path, StandardOpenOption.READ).withCloseable { + //TODO ensure binary is not lazy? + readType.getReader(readProperties).read(it); + } + + Envelope newEnvelope = new SimpleEnvelope(transformMeta(envelope.getMeta(), meta), transformData(envelope.data, meta)) + + Map writeProperties = resolveProperties(meta.getMetaOrEmpty("output.properties")) + EnvelopeType writeType = getOutputType(envelope, meta); + Path newPath = outputPath(path, meta) + + Files.newOutputStream(newPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING).withCloseable { + writeType.getWriter(writeProperties).write(it, newEnvelope); + } + + logger.info("Finished writing rewrapped envelope to file ${newPath}") + + } + } + + private Map resolveProperties(Meta meta) { + return meta.getNodeNames().collect(Collectors.toList()).collectEntries { String it -> [it: meta.getString(it)] } + } + + /** + * Return type of the file envelope and null if file is not an envelope + * @param path + * @return + */ + EnvelopeType inferType(Path path) { + return EnvelopeType.infer(path).orElse(null); + } + + EnvelopeType getOutputType(Envelope input, Meta meta) { + return EnvelopeType.resolve(meta.getString("target", "default")) + } + + Path outputPath(Path input, Meta meta) { + Path resultPath = input; + if (meta.hasValue("prefix")) { + resultPath = resultPath.resolveSibling(meta.getString("prefix") + resultPath.getFileName()) + } + return resultPath; + } + + Meta transformMeta(Meta input, Meta config) { + return input; + } + + Binary transformData(Binary input, Meta config) { + return input + } +} diff --git a/grind/src/main/groovy/hep/dataforge/grind/workspace/ContextSpec.groovy b/grind/src/main/groovy/hep/dataforge/grind/workspace/ContextSpec.groovy new file mode 100644 index 00000000..ef79a08e --- /dev/null +++ b/grind/src/main/groovy/hep/dataforge/grind/workspace/ContextSpec.groovy @@ -0,0 +1,53 @@ +package hep.dataforge.grind.workspace + +import groovy.transform.CompileStatic +import hep.dataforge.context.Context +import hep.dataforge.context.ContextBuilder +import hep.dataforge.grind.Grind +import hep.dataforge.meta.Meta + +/** + * A specification to builder context via grind workspace definition + */ +@CompileStatic +class ContextSpec { + private final Context parent; + + String name = "workspace" + Map properties = new HashMap() + Map pluginMap = new HashMap<>() + + ContextSpec(Context parent) { + this.parent = parent + } + + Context build() { + //using current context as a parent for workspace context + Context res = new ContextBuilder(name, parent).build() + properties.each { key, value -> res.setValue(key.toString(), value) } + pluginMap.forEach { String key, Meta meta -> + res.getPlugins().load(key, meta) + } + return res + } + + def properties(Closure cl) { + def spec = [:]//new PropertySetSpec(); + def code = cl.rehydrate(spec, this, this) + code.resolveStrategy = Closure.DELEGATE_ONLY + code() + properties.putAll(spec) + } + + def plugin(String key) { + pluginMap.put(key, Meta.empty()) + } + + def plugin(String key, Closure cl) { + pluginMap.put(key, Grind.buildMeta(cl)) + } + + def rootDir(String path) { + properties.put("rootDir", path) + } +} diff --git a/grind/src/main/groovy/hep/dataforge/grind/workspace/DataNodeSpec.groovy b/grind/src/main/groovy/hep/dataforge/grind/workspace/DataNodeSpec.groovy new file mode 100644 index 00000000..fac336a3 --- /dev/null +++ b/grind/src/main/groovy/hep/dataforge/grind/workspace/DataNodeSpec.groovy @@ -0,0 +1,174 @@ +package hep.dataforge.grind.workspace + +import groovy.transform.CompileStatic +import hep.dataforge.Named +import hep.dataforge.context.Context +import hep.dataforge.data.* +import hep.dataforge.goals.GeneratorGoal +import hep.dataforge.goals.Goal +import hep.dataforge.goals.StaticGoal +import hep.dataforge.grind.Grind +import hep.dataforge.grind.GrindMetaBuilder +import hep.dataforge.meta.Meta + +/** + * A specification to build data node. Not thread safe + */ +@CompileStatic +class DataNodeSpec { +// +// /** +// * Put a static resource as data +// * @param place +// * @param path +// * @return +// */ +// def resource(String place, String path) { +// URI uri = URI.create(path) +// builder.data(place, Data.buildStatic(uri)) +// } +// + + static DataNode buildNode(Context context, + @DelegatesTo(value = DataNodeSpec, strategy = Closure.DELEGATE_ONLY) Closure cl) { + def spec = new DataNodeSpec(context, "data", Object.class) + def code = cl.rehydrate(spec, null, null) + code.resolveStrategy = Closure.DELEGATE_ONLY + code() + return spec.build() + } + + private final Context context; + private final String name; + private final Class type; + private Meta meta = Meta.empty() + private DataNodeBuilder tree; + + DataNodeSpec(Context context, String name, Class type = Object.class) { + this.context = context + this.name = name + this.type = type + tree = DataTree.edit(Object) + tree.setName(name) + } + + void meta(Map values = [:], @DelegatesTo(GrindMetaBuilder) Closure cl = null) { + this.meta = Grind.buildMeta("meta", values, cl); + } + + void load(Meta meta) { + if (!tree.isEmpty()) { + throw new RuntimeException("Trying to load data into non-empty tree. Load should be called first.") + } + //def newRoot = node("", DataLoader.SMART.build(context, meta)) + DataNode node = new SmartDataLoader().build(context, meta) + tree = node.edit() + } + + void load(Map values = [:], String nodeName = "", @DelegatesTo(GrindMetaBuilder) Closure cl = null) { + load(Grind.buildMeta(nodeName, values, cl)) + } + + + void file(String place, String path, @DelegatesTo(GrindMetaBuilder) Closure fileMeta = null) { + item(place, DataUtils.INSTANCE.readFile(context.getFile(path), Grind.buildMeta(fileMeta))) + } + + void item(NamedData data) { + tree.add(data) + } + + void item(String name, Data data) { + tree.putData(name, data, false) + } + + void item(String name, @DelegatesTo(ItemSpec) Closure cl) { + ItemSpec spec = new ItemSpec(name) + def code = cl.rehydrate(spec, this, this) + code.resolveStrategy = Closure.DELEGATE_FIRST + def res = code.call() + if (res) { + spec.setValue(res) + } + item(spec.build()) + } + + void item(String name, Object obj) { + item(name, Data.buildStatic(obj)) + } + + void node(DataNode node) { + tree.add(node) + } + + void node(String name, DataNode node) { + tree.putNode(name, node) + } + + void node(String name, Class type = Object.class, @DelegatesTo(DataNodeSpec) Closure cl) { + DataNodeSpec spec = new DataNodeSpec(context, name, type) + def code = cl.rehydrate(spec, this, this) + code.resolveStrategy = Closure.DELEGATE_FIRST + code.call() + tree.add(spec.build()); + } + + /** + * Load static data from map + * @param items + * @return + */ + void items(Map items) { + items.each { key, value -> + item(key, Data.buildStatic(value)) + } + } + + /** + * Load static data from collection of Named objects + */ + void items(Collection something) { + something.each { + item(it.name, Data.buildStatic(it)) + } + } + + private DataNode build() { + tree.setMeta(meta) + return tree.build(); + } + + + static class ItemSpec { + private final String name; + Class type = Object; + private Meta meta = Meta.empty() + private Goal goal = null; + + ItemSpec(String name) { + this.name = name + } + + void meta(Map values = [:], @DelegatesTo(GrindMetaBuilder) Closure cl = null) { + this.meta = Grind.buildMeta("meta", values, cl); + } + + void setValue(Object obj) { + this.goal = new StaticGoal(obj); + this.type = obj.class; + } + + void setValue(Class type, Closure cl) { + this.type = type + this.goal = new GeneratorGoal({ cl.call() }) + } + + void setValue(Closure cl) { + setValue(Object.class, cl) + } + + private NamedData build() { + return new NamedData(name, type, goal, meta); + } + } +} diff --git a/grind/src/main/groovy/hep/dataforge/grind/workspace/DefaultTaskLib.groovy b/grind/src/main/groovy/hep/dataforge/grind/workspace/DefaultTaskLib.groovy new file mode 100644 index 00000000..14a68c37 --- /dev/null +++ b/grind/src/main/groovy/hep/dataforge/grind/workspace/DefaultTaskLib.groovy @@ -0,0 +1,197 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.grind.workspace + +import hep.dataforge.actions.Action +import hep.dataforge.actions.ActionEnv +import hep.dataforge.data.Data +import hep.dataforge.data.DataNode +import hep.dataforge.data.DataNodeBuilder +import hep.dataforge.data.DataSet +import hep.dataforge.meta.Meta +import hep.dataforge.workspace.tasks.AbstractTask +import hep.dataforge.workspace.tasks.Task +import hep.dataforge.workspace.tasks.TaskModel + +/** + * A collection of static methods to create tasks for WorkspaceSpec + */ +class DefaultTaskLib { + +// /** +// * Create a task using +// * @param parameters +// * @param taskName +// * @param cl +// * @return +// */ +// static Task template(Map parameters = [:], +// String taskName, +// @DelegatesTo(GrindMetaBuilder) Closure cl) { +// Meta meta = Grind.buildMeta(parameters, taskName, cl); +// Context context = parameters.getOrDefault("context", Global.instance()); +// +// return StreamSupport.stream(ServiceLoader.load(TaskTemplate).spliterator(), false) +// .filter { it.name == meta.getName() } +// .map { it.build(context, meta) } +// .findFirst().orElseThrow { new NameNotFoundException("Task template with name $taskName not found") } +// } + + /** + * Create a task using {@ling TaskSpec} + * @param taskName + * @param cl + * @return + */ + static Task build(String taskName, + @DelegatesTo(value = GrindTaskBuilder, strategy = Closure.DELEGATE_ONLY) Closure closure) { + def taskSpec = new GrindTaskBuilder(taskName); + def code = closure.rehydrate(taskSpec, null, null) + code.resolveStrategy = Closure.DELEGATE_ONLY + code.call() + return taskSpec.build(); + } + + /** + * A task with single join action delegated to {@link hep.dataforge.workspace.tasks.KTaskBuilder#pipe} + * @param params + * @param name + * @param action + * @return + */ + static Task pipe(String name, Map params = [:], + @DelegatesTo(value = ActionEnv, strategy = Closure.DELEGATE_ONLY) Closure action) { + def builder = new GrindTaskBuilder(name) + builder.model(params) + builder.pipe { env -> + def innerAction = action.rehydrate(env, null, null) + innerAction.resolveStrategy = Closure.DELEGATE_ONLY; + return { input -> innerAction.call(input) } + } + return builder.build() + } + + /** + * A task with single join action delegated to {@link hep.dataforge.workspace.tasks.KTaskBuilder#join} + * @param params + * @param name + * @param action + * @return + */ + static Task join(String name, Map params = [:], + @DelegatesTo(value = ActionEnv, strategy = Closure.DELEGATE_FIRST) Closure action) { + def builder = new GrindTaskBuilder(name) + builder.model(params) + builder.join { env -> + def innerAction = action.rehydrate(env, null, null) + innerAction.resolveStrategy = Closure.DELEGATE_ONLY; + return { input -> innerAction.call(input) } + } + return builder.build() + } + + /** + * Create a task from single action using custom dependency builder + * @param action + * @return + */ + static Task action(Action action, Map params = [:]) { + def builder = new GrindTaskBuilder(action.name) + builder.model(params) + builder.action(action) + return builder.build() + } + + /** + * Create a single action task using action class reference and custom dependency builder + * @param action + * @param dependencyBuilder + * @return + */ + static Task action(Class actionClass, Map params = [:]) { + Action ac = actionClass.newInstance() + return action(ac,params) + } + + static class CustomTaskSpec { + final TaskModel model + final DataNode input + final DataNodeBuilder result = DataSet.Companion.edit(); + + CustomTaskSpec(TaskModel model, DataNode input) { + this.model = model + this.input = input + } + + void yield(String name, Data data) { + result.putData(name, data) + } + + void yield(DataNode node) { + result.putNode(node) + } + + } + + static Task custom(Map parameters = [data: "*"], String name, + @DelegatesTo(value = CustomTaskSpec, strategy = Closure.DELEGATE_FIRST) Closure cl) { + return new AbstractTask() { + + @Override + Class getType() { + return Object + } + + @Override + protected DataNode run(TaskModel model, DataNode dataNode) { + CustomTaskSpec spec = new CustomTaskSpec(model, dataNode); + Closure code = cl.rehydrate(spec, null, null) + code.resolveStrategy = Closure.DELEGATE_ONLY + code.call() + return spec.result.build(); + } + + @Override + protected void buildModel(TaskModel.Builder model, Meta meta) { + dependencyBuilder(parameters).accept(model, meta) + } + + @Override + String getName() { + return name + } + } + } + + /** + * Execute external process task + * @param parameters + * @param name the name of the task + * @return + */ + static Task exec(String name, Map params = [:], + @DelegatesTo(value = ExecSpec, strategy = Closure.DELEGATE_ONLY) Closure cl) { + ExecSpec spec = new ExecSpec(); + spec.actionName = name; + Closure script = cl.rehydrate(spec, null, null) + script.setResolveStrategy(Closure.DELEGATE_ONLY) + script.call() + + Action execAction = spec.build(); + return action(execAction, params) + } +} diff --git a/grind/src/main/groovy/hep/dataforge/grind/workspace/ExecSpec.groovy b/grind/src/main/groovy/hep/dataforge/grind/workspace/ExecSpec.groovy new file mode 100644 index 00000000..161baac3 --- /dev/null +++ b/grind/src/main/groovy/hep/dataforge/grind/workspace/ExecSpec.groovy @@ -0,0 +1,362 @@ +package hep.dataforge.grind.workspace + +import groovy.transform.TupleConstructor +import hep.dataforge.actions.Action +import hep.dataforge.actions.OneToOneAction +import hep.dataforge.context.Context +import hep.dataforge.description.NodeDef +import hep.dataforge.io.IOUtils +import hep.dataforge.meta.Laminate +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaUtils +import org.slf4j.Logger + +import java.nio.ByteBuffer +import java.util.concurrent.TimeUnit + +/** + * A specification for system exec task + * + */ +class ExecSpec { + + /** + * A task input handler. By default ignores input object. + */ + private Closure handleInput = Closure.IDENTITY; + + /** + * Handle task output. By default returns the output as text. + */ + private Closure handleOutput = Closure.IDENTITY; + + /** + * Build command line + */ + private Closure cliTransform = Closure.IDENTITY; + + String actionName = "exec"; + + void input(@DelegatesTo(value = InputTransformer, strategy = Closure.DELEGATE_ONLY) Closure handleInput) { + this.handleInput = handleInput + } + + void output(@DelegatesTo(value = OutputTransformer, strategy = Closure.DELEGATE_ONLY) Closure handleOutput) { + this.handleOutput = handleOutput + } + + void cli(@DelegatesTo(value = CLITransformer, strategy = Closure.DELEGATE_ONLY) Closure cliTransform) { + this.cliTransform = cliTransform + } + + void name(String name) { + this.actionName = name; + } + + Action build() { + return new GrindExecAction(actionName); + } + + @TupleConstructor + private class InputTransformer { + final String name; + final Object input; + final Laminate meta + + private ByteArrayOutputStream stream; + + InputTransformer(String name, Object input, Laminate meta) { + this.name = name + this.input = input + this.meta = meta + } + + ByteArrayOutputStream getStream() { + if (stream == null) { + stream = new ByteArrayOutputStream(); + } + return stream + } + + def print(Object obj) { + getStream().print(obj) + } + + def println(Object obj) { + getStream().println(obj) + } + + def printf(String format, Object... args) { + getStream().printf(format, args) + } + } + + @TupleConstructor + private class OutputTransformer { + + /** + * The name of the data + */ + final String name; + + /** + * Context for task execution + */ + final Context context; + + /** + * task configuration + */ + final Laminate meta; + + final String out; + final String err; + + OutputTransformer(Context context, String name, Laminate meta, String out, String err) { + this.name = name + this.context = context +// this.process = process + this.meta = meta + this.out = out; + this.err = err; + } + + private OutputStream outputStream; + + /** + * Create task output (not result) + * @return + */ + OutputStream getStream() { + if (stream == null) { + outputStream = context.getOutput().out(actionName, name) + } + return stream + } + + /** + * Print something to default task output + * @param obj + * @return + */ + def print(Object obj) { + getStream().print(obj) + } + + def println(Object obj) { + getStream().println(obj) + } + + def printf(String format, Object... args) { + getStream().printf(format, args) + } + +// /** +// * Render a markedup object into default task output +// * @param markedup +// * @return +// */ +// def render(Markedup markedup) { +// new SimpleMarkupRenderer(getStream()).render(markedup.markup()) +// } + } + + @TupleConstructor + private class CLITransformer { + final Context context + final String name + final Meta meta + + + String executable = "" + List cli = []; + + CLITransformer(Context context, String name, Meta meta) { + this.context = context + this.name = name + this.meta = meta + } + + /** + * Apply inside parameters only if OS is windows + * @param cl + * @return + */ + def windows(@DelegatesTo(CLITransformer) Closure cl) { + if (System.properties['os.name'].toLowerCase().contains('windows')) { + this.with(cl) + } + } + + /** + * Apply inside parameters only if OS is linux + * @param cl + * @return + */ + def linux(@DelegatesTo(CLITransformer) Closure cl) { + if (System.properties['os.name'].toLowerCase().contains('linux')) { + this.with(cl) + } + } + + def executable(String exec) { + this.executable = executable + } + + def append(String... commands) { + cli.addAll(commands) + } + + def argument(String key = "", Object obj) { + String value; + if (obj instanceof File) { + value = obj.absoluteFile.toString(); + } else if (obj instanceof URL) { + value = new File(obj.toURI()).absoluteFile.toString(); + } else { + value = obj.toString() + } + + if (key) { + cli.add(key) + } + + cli.add(value); + } + + /** + * Create + * @return + */ + private List transform() { + return [] + + meta.getString("exec", executable) + + cli + + Arrays.asList(meta.getStringArray("command", new String[0])) + } + } + +// @ValueDef(name = "inheritIO", type = ValueType.BOOLEAN, def = "true", info = "Define if process should inherit IO from DataForge process") + + @NodeDef(key = "env", info = "Environment variables as a key-value pairs") +// @NodeDef(name = "parameter", info = "The definition for command parameter") + private class GrindExecAction extends OneToOneAction { + + GrindExecAction(String name) { + super(name, Object, Object) + } + + @Override + protected Object execute(Context context, String name, Object input, Laminate meta) { + Logger logger = getLogger(context, meta); + + try { + + StringBuilder out = new StringBuilder(); + StringBuilder err = new StringBuilder() + + ProcessBuilder builder = buildProcess(context, name, input, meta); + logger.info("Starting process with command \"" + String.join(" ", builder.command()) + "\""); + + Process process = builder.start(); + process.consumeProcessOutput(out, err) + + //sending input into process + ByteBuffer bytes = transformInput(name, input, meta); + if (bytes != null && bytes.limit() > 0) { + logger.debug("The action input is transformed into byte array with length of " + bytes.limit()); + process.getOutputStream().write(bytes.array()); + } + + //consume process output + logger.debug("Handling process output"); + + if (process.isAlive()) { + logger.debug("Starting listener for process end"); + try { + if (meta.hasValue("timeout")) { + if (!process.waitFor(meta.getInt("timeout"), TimeUnit.MILLISECONDS)) { + process.destroyForcibly(); + } + } else { + logger.info("Process finished with exit value " + process.waitFor()); + } + } catch (Exception ex) { + logger.debug("Process failed to complete", ex); + } + } else { + logger.info("Process finished with exit value " + process.exitValue()); + } + + return transformOutput(context, name, meta, out.toString(), err.toString()); + } catch (IOException e) { + throw new RuntimeException("Process execution failed with error", e); + } + } + + ProcessBuilder buildProcess(Context context, String name, Object input, Laminate meta) { + //setting up the process + ProcessBuilder builder = new ProcessBuilder(getCommand(context, name, meta)); + + //updating environment variables + if (meta.hasMeta("env")) { + MetaUtils.nodeStream(meta.getMeta("env")).forEach { envNode -> + builder.environment().put(envNode.getValue().getString("name", envNode.getKey()), envNode.getValue().getString("value")); + } + } + + // Setting working directory + if (meta.hasValue("workDir")) { + builder.directory(context.getOutput().getFile(meta.getString("workDir"))); + } + +// if (meta.getBoolean("inheritIO", true)) { +// builder.inheritIO(); +// } + return builder; + } + + + ByteBuffer transformInput(String name, Object input, Laminate meta) { + def inputTransformer = new InputTransformer(name, input, meta); + def handler = handleInput.rehydrate(inputTransformer, null, null); + handler.setResolveStrategy(Closure.DELEGATE_ONLY); + def res = handler.call(); + + //If stream output is initiated, use it, otherwise, convert results + if (inputTransformer.stream != null) { + return ByteBuffer.wrap(inputTransformer.stream.toByteArray()); + } else if (res instanceof ByteBuffer) { + return res; + } else if (res != null) { + return ByteBuffer.wrap(res.toString().getBytes(IOUtils.UTF8_CHARSET)) + } else { + return null + } + } + + /** + * Transform action output. By default use text output + * @param context + * @param name + * @param meta + * @param out + * @param err + * @return + */ + Object transformOutput(Context context, String name, Laminate meta, String out, String err) { + def outputTransformer = new OutputTransformer(context, name, meta, out, err); + def handler = handleOutput.rehydrate(outputTransformer, null, null); + handler.setResolveStrategy(Closure.DELEGATE_ONLY); + return handler.call() ?: out; + } + + List getCommand(Context context, String name, Meta meta) { + def transformer = new CLITransformer(context, name, meta); + def handler = cliTransform.rehydrate(transformer, null, null); + handler.setResolveStrategy(Closure.DELEGATE_ONLY); + handler.call() + return transformer.transform().findAll { !it.isEmpty() } + } + } +} diff --git a/grind/src/main/groovy/hep/dataforge/grind/workspace/GrindWorkspace.groovy b/grind/src/main/groovy/hep/dataforge/grind/workspace/GrindWorkspace.groovy new file mode 100644 index 00000000..0497f620 --- /dev/null +++ b/grind/src/main/groovy/hep/dataforge/grind/workspace/GrindWorkspace.groovy @@ -0,0 +1,68 @@ +package hep.dataforge.grind.workspace + +import groovy.transform.CompileStatic +import hep.dataforge.context.Context +import hep.dataforge.data.DataNode +import hep.dataforge.meta.Meta +import hep.dataforge.workspace.Workspace +import hep.dataforge.workspace.tasks.Task +import org.jetbrains.annotations.NotNull + +/** + * Workspace wrapper that implements methodMissing for tasks and propertyMissing for targets + */ +@CompileStatic +class GrindWorkspace implements Workspace { + + private Workspace workspace + + GrindWorkspace(Workspace workspace) { + this.workspace = workspace + } + + + + @Override + DataNode getData() { + return workspace.getData() + } + + @Override + Collection> getTasks() { + return workspace.tasks + } + + @Override + Collection getTargets() { + return workspace.targets + } + + @Override + Task optTask(@NotNull String taskName) { + return workspace.optTask(taskName) + } + + @Override + Meta optTarget(@NotNull String name) { + return workspace.optTarget(name) + } + + @Override + void clean() { + workspace.clean() + } + + @Override + Context getContext() { + return workspace.context + } + + def methodMissing(String name, Object args) { + String str = args.getClass().isArray() ? ((Object[]) args).join(" ") : args.toString() + return runTask(name, str) + } + + def propertyMissing(String name) { + return getTarget(name) + } +} diff --git a/grind/src/main/groovy/hep/dataforge/grind/workspace/GroovyWorkspaceParser.groovy b/grind/src/main/groovy/hep/dataforge/grind/workspace/GroovyWorkspaceParser.groovy new file mode 100644 index 00000000..54cb1224 --- /dev/null +++ b/grind/src/main/groovy/hep/dataforge/grind/workspace/GroovyWorkspaceParser.groovy @@ -0,0 +1,50 @@ +package hep.dataforge.grind.workspace + +import groovy.transform.CompileStatic +import hep.dataforge.context.Context +import hep.dataforge.context.Global +import hep.dataforge.workspace.Workspace +import hep.dataforge.workspace.WorkspaceParser +import org.codehaus.groovy.control.CompilerConfiguration +import org.codehaus.groovy.control.customizers.ImportCustomizer + +import java.nio.file.Files +import java.nio.file.Path + +@CompileStatic +class GroovyWorkspaceParser implements WorkspaceParser { + + @Override + List listExtensions() { + return [".groovy", ".grind"]; + } + + + + @Override + Workspace.Builder parse(Context parentContext = Global.INSTANCE, Reader reader) { +// String scriptText = new String(reader.Files.readAllBytes(path), IOUtils.UTF8_CHARSET); + + def compilerConfiguration = new CompilerConfiguration() + compilerConfiguration.scriptBaseClass = DelegatingScript.class.name; + ImportCustomizer importCustomizer = new ImportCustomizer(); + importCustomizer.addStaticStars( + "java.lang.Math", + "hep.dataforge.grind.Grind", + "hep.dataforge.grind.workspace.DefaultTaskLib" + ) + compilerConfiguration.addCompilationCustomizers(importCustomizer) + + def shell = new GroovyShell(this.class.classLoader, new Binding(), compilerConfiguration) + DelegatingScript script = shell.parse(reader) as DelegatingScript; + WorkspaceSpec spec = new WorkspaceSpec(parentContext) + script.setDelegate(spec); + script.run() + return spec.getBuilder(); + } + + + Workspace.Builder parse(Context context, Path path){ + return parse(context, Files.newBufferedReader(path)) + } +} diff --git a/grind/src/main/groovy/hep/dataforge/grind/workspace/WorkspaceSpec.groovy b/grind/src/main/groovy/hep/dataforge/grind/workspace/WorkspaceSpec.groovy new file mode 100644 index 00000000..c89d10d9 --- /dev/null +++ b/grind/src/main/groovy/hep/dataforge/grind/workspace/WorkspaceSpec.groovy @@ -0,0 +1,115 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package hep.dataforge.grind.workspace + +import groovy.transform.CompileStatic +import hep.dataforge.context.Context +import hep.dataforge.grind.Grind +import hep.dataforge.grind.GrindMetaBuilder +import hep.dataforge.grind.helpers.MethodDescription +import hep.dataforge.meta.Meta +import hep.dataforge.workspace.BasicWorkspace +import hep.dataforge.workspace.Workspace +import hep.dataforge.workspace.tasks.Task + +/** + * A DSL helper to build workspace + * @author Alexander Nozik + */ +@CompileStatic +class WorkspaceSpec { + private Workspace.Builder builder; +// private final Context context; + + /** + * Create a new specification for a workspace + * @param context - the context for specification it is by default used as a parent for resulting workspace + */ + WorkspaceSpec(Context context) { + this.builder = new BasicWorkspace.Builder(context); +// this.context = context + } + + /** + * builder context for the workspace using closure + */ + def context(@DelegatesTo(value = ContextSpec, strategy = Closure.DELEGATE_FIRST) Closure cl) { + def contextSpec = new ContextSpec(builder.context) + def code = cl.rehydrate(contextSpec, this, this) + code.resolveStrategy = Closure.DELEGATE_FIRST + code() + builder.setContext(contextSpec.build()) + } + + Workspace.Builder getBuilder() { + return builder + } + + /** + * Set workspace data + * @param cl + * @return + */ + @MethodDescription("Load data via closure") + void data(@DelegatesTo(value = DataNodeSpec, strategy = Closure.DELEGATE_FIRST) Closure cl) { + builder.data("", DataNodeSpec.buildNode(builder.context, cl)) + } + + /** + * Load a task into the workspace. One can use task libraries like {@link DefaultTaskLib} to define task builders + * @param task + * @return + */ + @MethodDescription("Register a task") + def task(Task task) { + builder.task(task) + } + + /** + * Load existing task by class + * @param taskClass + * @return + */ + @MethodDescription("Define a task by its class") + def task(Class taskClass) { + builder.task(taskClass.getDeclaredConstructor().newInstance()) + } + + /** + * Load meta target using grind meta builder + * @param closure + * @return + */ + @MethodDescription("Create a list of targets") + def targets(Closure closure) { + MetaSpec spec = new MetaSpec() + def code = closure.rehydrate(spec, this, this) + code.resolveStrategy = Closure.DELEGATE_FIRST + code() + } + + private class MetaSpec { + def methodMissing(String methodName, Closure cl) { + WorkspaceSpec.this.builder.target(Grind.buildMeta(methodName, cl)) + } + } + + @MethodDescription("Create new meta using Grind builder") + def target(String name, @DelegatesTo(GrindMetaBuilder) Closure closure = null) { + this.builder.target(Grind.buildMeta(name, [:], closure)) + } + + def target(String name, Map parameters, @DelegatesTo(GrindMetaBuilder) Closure closure = null) { + this.builder.target(Grind.buildMeta(name, parameters, closure)) + } + + @MethodDescription("Assign target as meta") + def target(Meta meta) { + this.builder.target(meta) + } +} + diff --git a/grind/src/main/kotlin/hep/dataforge/grind/workspace/GrindTaskBuilder.kt b/grind/src/main/kotlin/hep/dataforge/grind/workspace/GrindTaskBuilder.kt new file mode 100644 index 00000000..c7c167e1 --- /dev/null +++ b/grind/src/main/kotlin/hep/dataforge/grind/workspace/GrindTaskBuilder.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 hep.dataforge.grind.workspace + +import groovy.lang.Closure +import hep.dataforge.actions.Action +import hep.dataforge.actions.ActionEnv +import hep.dataforge.meta.Meta +import hep.dataforge.workspace.tasks.KTaskBuilder +import hep.dataforge.workspace.tasks.Task +import hep.dataforge.workspace.tasks.TaskModel + +/** + * A simplified wrapper class on top of KTaskBuilder to allow access from groovy + */ +class GrindTaskBuilder(name: String) { + private val builder = KTaskBuilder(name) + + + fun model(modelTransform: (TaskModel.Builder, Meta) -> Unit) { + builder.model(modelTransform) + } + + fun model(params: Map) { + builder.model { meta -> + params["data"]?.let { + if (it is List<*>) { + it.forEach { this.data(it as String) } + } else { + data(it as String) + } + } + params["dependsOn"]?.let { + dependsOn(it as String, meta) + } + if (params["data"] == null && params["dependsOn"] == null) { + data("*") + } + } + } + + fun pipe(action: (ActionEnv) -> Closure) { + builder.pipe { action(this).call(it) } + } + + fun join(action: (ActionEnv) -> Closure) { + builder.join { action(this).call(it) } + } + + fun action(action: Action) { + builder.action(action) + } + + fun build(): Task { + return builder.build() + } +} \ No newline at end of file diff --git a/grind/src/main/resources/META-INF/services/hep.dataforge.grind.helpers.GrindHelperFactory b/grind/src/main/resources/META-INF/services/hep.dataforge.grind.helpers.GrindHelperFactory new file mode 100644 index 00000000..761a60f6 --- /dev/null +++ b/grind/src/main/resources/META-INF/services/hep.dataforge.grind.helpers.GrindHelperFactory @@ -0,0 +1 @@ +hep.dataforge.grind.helpers.WorkspaceHelperFactory \ No newline at end of file diff --git a/grind/src/main/resources/META-INF/services/hep.dataforge.workspace.WorkspaceParser b/grind/src/main/resources/META-INF/services/hep.dataforge.workspace.WorkspaceParser new file mode 100644 index 00000000..23d98d1f --- /dev/null +++ b/grind/src/main/resources/META-INF/services/hep.dataforge.workspace.WorkspaceParser @@ -0,0 +1 @@ +hep.dataforge.grind.workspace.GroovyWorkspaceParser \ No newline at end of file diff --git a/grind/src/main/resources/META-INF/services/org.codehaus.groovy.runtime.ExtensionModule b/grind/src/main/resources/META-INF/services/org.codehaus.groovy.runtime.ExtensionModule new file mode 100644 index 00000000..4788af4f --- /dev/null +++ b/grind/src/main/resources/META-INF/services/org.codehaus.groovy.runtime.ExtensionModule @@ -0,0 +1,3 @@ +moduleName=grind +moduleVersion=1.0 +extensionClasses = hep.dataforge.grind.extensions.CoreExtension \ No newline at end of file diff --git a/grind/src/test/groovy/hep/dataforge/grind/GrindMetaBuilderSpec.groovy b/grind/src/test/groovy/hep/dataforge/grind/GrindMetaBuilderSpec.groovy new file mode 100644 index 00000000..5c2edf02 --- /dev/null +++ b/grind/src/test/groovy/hep/dataforge/grind/GrindMetaBuilderSpec.groovy @@ -0,0 +1,63 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 hep.dataforge.grind + +import hep.dataforge.io.XMLMetaWriter +import hep.dataforge.meta.Meta +import spock.lang.Specification + +/** + * + * @author Alexander Nozik + */ +class GrindMetaBuilderSpec extends Specification { + + def "Check meta building"() { + when: + Meta root = new GrindMetaBuilder().root(someValue: "some text here") { + childNode(childNodeValue: ["some", "other", "text", "here"], something: 18) + childNode(childNodeValue: "some givverish text here", something: 398) + otherChildNode { + grandChildNode(a: 22, text: "fslfjsldfj") + } + for (int i = 0; i < 10; i++) { + numberedNode(number: i) + } + } + + then: + println new XMLMetaWriter().writeString(root) + root.getInt("otherChildNode.grandChildNode.a") == 22 + } + + def "Check simple meta"() { + when: + Meta m = Grind.parseMeta("myMeta"); + then: + m.name == "myMeta" + } + + def "Check unary operations"() { + when: + Meta root = new GrindMetaBuilder().root(someValue: "some text here") { + put a: 22 + } + then: + root.getInt("a") == 22 + } + +} + diff --git a/grind/src/test/groovy/hep/dataforge/grind/GrindWorkspaceBuilderTest.groovy b/grind/src/test/groovy/hep/dataforge/grind/GrindWorkspaceBuilderTest.groovy new file mode 100644 index 00000000..a247aa20 --- /dev/null +++ b/grind/src/test/groovy/hep/dataforge/grind/GrindWorkspaceBuilderTest.groovy @@ -0,0 +1,32 @@ +package hep.dataforge.grind + +import hep.dataforge.data.DataNode +import hep.dataforge.grind.workspace.GroovyWorkspaceParser +import spock.lang.Specification + +/** + * Created by darksnake on 04-Aug-16. + */ +class GrindWorkspaceBuilderTest extends Specification { + + + def "Run Task"() { + given: + def workspace = new GroovyWorkspaceParser().parse(getClass().getResourceAsStream('/workspace/workspace.groovy').newReader()).build() + when: + DataNode res = workspace.run("testTask") + res.dataStream().forEach { println("${it.name}: ${it.get()}") } + then: + res.get("a").int == 4; + } + + def "Run Task with meta"() { + given: + def workspace = new GroovyWorkspaceParser().parse(getClass().getResourceAsStream('/workspace/workspace.groovy').newReader()).build() + when: + DataNode res = workspace.run("testTask{childNode(metaValue: 18); otherChildNode(val: false)}") + res.dataStream().forEach { println("${it.name}: ${it.get()}") } + then: + res.get("meta.childNode.metaValue").int == 18 + } +} diff --git a/grind/src/test/groovy/hep/dataforge/grind/TestTask.groovy b/grind/src/test/groovy/hep/dataforge/grind/TestTask.groovy new file mode 100644 index 00000000..c9ee5e75 --- /dev/null +++ b/grind/src/test/groovy/hep/dataforge/grind/TestTask.groovy @@ -0,0 +1,40 @@ +package hep.dataforge.grind + +import hep.dataforge.data.DataNodeBuilder +import hep.dataforge.data.DataSet +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaUtils +import hep.dataforge.workspace.tasks.MultiStageTask +import hep.dataforge.workspace.tasks.TaskModel + +/** + * Created by darksnake on 04-Aug-16. + */ +class TestTask extends MultiStageTask { + TestTask() { + super(Object) + } + + @Override + String getName() { + return "testTask" + } + + @Override + protected MultiStageTask.MultiStageTaskState transform(TaskModel model, MultiStageTask.MultiStageTaskState state) { + DataNodeBuilder b = DataSet.edit() + model.context.getProperties().forEach { key, value -> + b.putStatic(key, value); + } + MetaUtils.valueStream(model.getMeta()).forEach { pair -> + b.putStatic("meta." + pair.first, pair.second) + } + + state.finish(b.build()) + } + + @Override + protected void buildModel(TaskModel.Builder model, Meta meta) { + + } +} diff --git a/grind/src/test/groovy/hep/dataforge/grind/WorkspaceSpecTest.groovy b/grind/src/test/groovy/hep/dataforge/grind/WorkspaceSpecTest.groovy new file mode 100644 index 00000000..b8d88744 --- /dev/null +++ b/grind/src/test/groovy/hep/dataforge/grind/WorkspaceSpecTest.groovy @@ -0,0 +1,80 @@ +package hep.dataforge.grind + +import hep.dataforge.meta.Meta +import hep.dataforge.workspace.Workspace +import spock.lang.Specification + +/** + * Created by darksnake on 04-Aug-16. + */ +class WorkspaceSpecTest extends Specification { + + def "Test meta builder delegation"() { + given: + def closure = { + myMeta(myPar: "val", myOtherPar: 28) { + childNode(childValue: true) + otherChildNode { + grandChildNode(grandChildValue: 88.6) + } + } + } + when: + def metaSpec = new GrindMetaBuilder() + def metaExec = closure.rehydrate(metaSpec, this, this); + metaExec.resolveStrategy = Closure.DELEGATE_ONLY; + def res = metaExec() + then: +// println res.getString("otherChildNode.grandChildNode.grandChildValue") + res.getBoolean("childNode.childValue"); + } + + def "Test meta from string"() { + given: + String metaStr = """ + myMeta(myPar: "val", myOtherPar: 28) { + childNode(childValue: true) + otherChildNode { + grandChildNode(grandChildValue: 88.6) + } + } + """ + when: + Meta meta = Grind.parseMeta(metaStr); + then: + meta.getName() == "myMeta" + meta.getDouble("otherChildNode.grandChildNode.grandChildValue") == 88.6 + } + + def "Test task builder"() { + when: + Workspace wsp = Grind.buildWorkspace { + data { + (1..10).each { + item("data_$it", "value_$it") + } + } + task hep.dataforge.grind.workspace.DefaultTaskLib.pipe("test") { String input -> + if (input.endsWith("5")) { + return input + "_hurray!" + } else { + return input + } + } + + task hep.dataforge.grind.workspace.DefaultTaskLib.pipe("preJoin") { String input -> + input[6] + } + + task hep.dataforge.grind.workspace.DefaultTaskLib.join("testJoin", [dependsOn: "preJoin"]) { Map input -> + input.sort().values().sum() + } + + } + def result = wsp.runTask("test", "test").get("data_5") + def joinResult = wsp.runTask("testJoin", "testJoin").getData().get() + then: + result.endsWith("hurray!") + joinResult == "1123456789" + } +} diff --git a/grind/src/test/groovy/hep/dataforge/grind/extensions/CoreExtensionTest.groovy b/grind/src/test/groovy/hep/dataforge/grind/extensions/CoreExtensionTest.groovy new file mode 100644 index 00000000..2cfd5556 --- /dev/null +++ b/grind/src/test/groovy/hep/dataforge/grind/extensions/CoreExtensionTest.groovy @@ -0,0 +1,29 @@ +package hep.dataforge.grind.extensions + +import hep.dataforge.grind.Grind +import hep.dataforge.meta.Meta +import spock.lang.Specification + +class CoreExtensionTest extends Specification { + + def "Property read"() { + when: + Meta meta = Grind.buildMeta(a: 22, b: "asdfg") { + child(c: 22.8, d: "hocus-pocus") + } + then: + meta.child["c"] == 22.8 + meta.getValue("child.d").string == "hocus-pocus" + } + + def "Property write"() { + given: + Meta meta = Grind.buildMeta(a: 22, b: "asdfg") + when: + meta.child = Grind.buildMeta(b: 33) + then: + meta["child.b"] == 33 + meta.child["b"] == 33 + } + +} diff --git a/grind/src/test/groovy/hep/dataforge/grind/extensions/ValueExtensionTest.groovy b/grind/src/test/groovy/hep/dataforge/grind/extensions/ValueExtensionTest.groovy new file mode 100644 index 00000000..2ce2cea5 --- /dev/null +++ b/grind/src/test/groovy/hep/dataforge/grind/extensions/ValueExtensionTest.groovy @@ -0,0 +1,32 @@ +package hep.dataforge.grind.extensions + +import spock.lang.Specification + +/** + * Created by darksnake on 05-Aug-16. + */ +class ValueExtensionTest extends Specification { + +// def "Type conversion"() { +// when: +// Value val = Value.of(22.5); +// expect: +// val as Double == 22.5 +// val as String == "22.5" +// } +// +// def "Equality"() { +// when: +// Value val = Value.of(22.5); +// expect: +// val == 22.5 +// } +// +// def "value plus operation"(){ +// when: +// Value a = Value.of("123"); +// def b = 4; +// then: +// a + b == Value.of(127) +// } +} diff --git a/grind/src/test/groovy/hep/dataforge/grind/workspace/ExecTest.groovy b/grind/src/test/groovy/hep/dataforge/grind/workspace/ExecTest.groovy new file mode 100644 index 00000000..27a8d4d5 --- /dev/null +++ b/grind/src/test/groovy/hep/dataforge/grind/workspace/ExecTest.groovy @@ -0,0 +1,77 @@ +package hep.dataforge.grind.workspace + +import hep.dataforge.context.Global +import hep.dataforge.data.DataSet +import hep.dataforge.grind.Grind +import hep.dataforge.meta.Meta +import spock.lang.Specification +import spock.lang.Timeout + +class ExecTest extends Specification { + + @Timeout(3) + def "get Java version"() { + given: + def exec = new ExecSpec() + exec.with{ + cli { + append "java" + append "-version" + } + output { + println "Out: " + out + println "Err: " + err + return err.split()[0] + } + } + def action = exec.build() + when: + def res = action.simpleRun("test") + then: + res == "java" + } + + @Timeout(5) + def "run python script"(){ + given: + def exec = new ExecSpec() + exec.with{ + cli { + append "python" + argument context.getClassLoader().getResource('workspace/test.py') + append "-d 1" + append "-r ${meta["result"]}" + } + } + when: + def meta = Grind.buildMeta(result: "OK") + def res = exec.build().simpleRun("test", meta); + println "Result: $res" + then: + res.trim().endsWith "OK" + } + + @Timeout(5) + def "parallel test"(){ + given: + def exec = new ExecSpec() + exec.with{ + cli { + append "python" + argument context.getClassLoader().getResource('workspace/test.py') + append "-d 1" + append "-r $name: ${meta["result"]}" + } + } + when: + def builder = DataSet.edit(Object) + (1..8).each { + builder.putData("test$it","test$it",Grind.buildMeta(result: "OK$it")) + } + def res = exec.build().run(Global.INSTANCE, builder.build(), Meta.empty()) + then: + res.computeAll() + res.getSize() == 8 + res["test4"].get().trim().endsWith("OK4") + } +} diff --git a/grind/src/test/groovy/hep/dataforge/grind/workspace/WorkspaceTest.groovy b/grind/src/test/groovy/hep/dataforge/grind/workspace/WorkspaceTest.groovy new file mode 100644 index 00000000..e6c74cda --- /dev/null +++ b/grind/src/test/groovy/hep/dataforge/grind/workspace/WorkspaceTest.groovy @@ -0,0 +1,62 @@ +package hep.dataforge.grind.workspace + +import hep.dataforge.context.Global +import org.junit.Test + +import static org.junit.Assert.assertEquals + +class WorkspaceTest { + + def workspace = new WorkspaceSpec(Global.INSTANCE).with { + + context { + name = "TEST" + } + + data { + item("xs") { + meta(axis: "x") + (1..100).asList() //generate xs + } + node("ys") { + Random rnd = new Random() + item("y1") { + meta(axis: "y") + (1..100).collect { it**2 } + } + item("y2") { + meta(axis: "y") + (1..100).collect { it**2 + rnd.nextDouble() } + } + item("y3") { + meta(axis: "y") + (1..100).collect { (it + rnd.nextDouble() / 2)**2 } + } + } + } + +// task custom("dif") { +// def xs = data.optData("xs").get() +// def ys = data.getNode("ys") +// ys.dataStream().forEach { +// yield it.name, combine(it, xs, Table.class, it.meta) { x, y -> +// ListTable.Builder +// } +// } +// } + +// task pipe("plot", data: "*") { +// //PlotManager pm = context.getFeature(PlotManager)//loading plot feature +// def helper = new PlotHelper(context) +// helper.plot((1..100), input as List, name as String) +// } + + it.builder.build() + } + + + @Test + void testData() { + assertEquals(3, workspace.data.getNode("ys").getSize()) + } +} diff --git a/grind/src/test/resources/workspace/test.py b/grind/src/test/resources/workspace/test.py new file mode 100644 index 00000000..7c0d715b --- /dev/null +++ b/grind/src/test/resources/workspace/test.py @@ -0,0 +1,11 @@ +import argparse +import time + +parser = argparse.ArgumentParser(description='external exec tester') +parser.add_argument('-d','--delay', type=int, help='A delay before result is returned') +parser.add_argument('-r','--result', type=str, help='The resulting string') + +args = parser.parse_args() + +time.sleep(args.delay) +print( "python says: " + args.result) diff --git a/grind/src/test/resources/workspace/workspace.groovy b/grind/src/test/resources/workspace/workspace.groovy new file mode 100644 index 00000000..036f565d --- /dev/null +++ b/grind/src/test/resources/workspace/workspace.groovy @@ -0,0 +1,12 @@ + +// define context +context{ + name = "TEST" + plugin "fx" + properties{ + a = 4 + b = false + } +} + +task(hep.dataforge.grind.TestTask) \ No newline at end of file diff --git a/numass-control/build.gradle b/numass-control/build.gradle new file mode 100644 index 00000000..3c5ec191 --- /dev/null +++ b/numass-control/build.gradle @@ -0,0 +1,78 @@ +allprojects { + apply plugin: "kotlin" + +// apply plugin: 'org.openjfx.javafxplugin' +// +// javafx { +// modules = [ 'javafx.controls' ] +// } + + compileKotlin { + kotlinOptions { + jvmTarget = "1.8" + javaParameters = true + } + } + + kotlin { + experimental { + coroutines "enable" + } + } +} + +dependencies { + compile project(':dataforge-plots:plots-jfc') + compile project(':dataforge-control') + compile project(':dataforge-gui') + + // https://mvnrepository.com/artifact/commons-cli/commons-cli + compile group: 'commons-cli', name: 'commons-cli', version: '1.4' + +} + +task installAll(type: Copy) { + group "numass" + + description "Install all control projects into the same directory" + + def parser = new XmlParser() + + + + subprojects { sub -> + if (sub.plugins.findPlugin("application")) { + dependsOn sub.getTasksByName("installDist", false).first() + String distDir = "${sub.buildDir}/install/${sub.name}" + from distDir + } + } + into "$buildDir/install/numass-control" + + doLast { + def configRoot = new Node(null, "config"); + subprojects { sub -> + //add device configuration file + sub.fileTree(dir: 'src/main/resources/config', includes: ['**/*.xml']).each { cfgFile -> + println "Found config file ${cfgFile}" + parser.parse(cfgFile).children().each { + configRoot.append(it as Node) + } + } + } + if (!configRoot.children().isEmpty()) { + File outFile = file("$buildDir/install/numass-control/bin/numass-control.xml") + outFile.getParentFile().mkdirs(); + outFile.createNewFile(); + new XmlNodePrinter(outFile.newPrintWriter()).print(configRoot) + } + } +} + +task distAll(dependsOn: installAll, type: Zip) { + group "numass" + + description "Make a distribution of all control projects" + + from "$buildDir/install/numass-control" +} diff --git a/numass-control/control-room/build.gradle b/numass-control/control-room/build.gradle new file mode 100644 index 00000000..1cb0d9d7 --- /dev/null +++ b/numass-control/control-room/build.gradle @@ -0,0 +1,80 @@ +plugins { + id "application" + id 'com.github.johnrengelman.shadow' version '2.0.1' +} + + +if (!hasProperty('mainClass')) { + ext.mainClass = 'inr.numass.control.ServerApp'//"inr.numass.viewer.test.TestApp" +} + +mainClassName = mainClass + +version = "0.3.0" + +description = "The control room application for numass slow control" + +compileKotlin.kotlinOptions.jvmTarget = "1.8" + +configurations { + devices.extendsFrom(compile) +} + +dependencies { + //DataForge dependencies + compile project(':numass-control') + //compile project(':numass-server') + + // optional device classpath + devices project(':numass-control:cryotemp') + devices project(':numass-control:msp') + devices project(':numass-control:vac') +} + +shadowJar { + mergeServiceFiles() +} + + +task debugWithDevice(dependsOn: classes, type: JavaExec) { + main mainClass + args = ["--config.resource=/config/control.xml"] + classpath = sourceSets.main.runtimeClasspath + configurations.devices + description "Start application in debug mode" + group "debug" +} + +task runWithDevice(dependsOn: classes, type: JavaExec) { + main mainClass + args = ["--config.resource=/config/control-real.xml"] + classpath = sourceSets.main.runtimeClasspath + configurations.devices + description "Start application in debug mode" + group "debug" +} + +task startScriptWithDevices(type: CreateStartScripts) { + applicationName = "control-room-devices" + mainClassName = mainClass + outputDir = new File(project.buildDir, 'scripts') + classpath = jar.outputs.files + project.configurations.devices +} + +distributions { + devices { + contents { + into("lib") { + from jar + from configurations.devices + } + into("bin") { + from startScriptWithDevices + } + } + } +} + + + + + + diff --git a/numass-control/control-room/build/resources/main/config/control-real.xml b/numass-control/control-room/build/resources/main/config/control-real.xml new file mode 100644 index 00000000..06d25124 --- /dev/null +++ b/numass-control/control-room/build/resources/main/config/control-real.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + diff --git a/numass-control/control-room/build/resources/main/config/control.xml b/numass-control/control-room/build/resources/main/config/control.xml new file mode 100644 index 00000000..03f3c4dd --- /dev/null +++ b/numass-control/control-room/build/resources/main/config/control.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/numass-control/control-room/build/scripts/control-room b/numass-control/control-room/build/scripts/control-room new file mode 100644 index 00000000..fa34b102 --- /dev/null +++ b/numass-control/control-room/build/scripts/control-room @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## control-room start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/.." >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="control-room" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and CONTROL_ROOM_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/lib/control-room-0.3.0.jar:$APP_HOME/lib/numass-control-1.0.0.jar:$APP_HOME/lib/numass-server-1.0.0.jar:$APP_HOME/lib/numass-client-1.0.0.jar:$APP_HOME/lib/plots-jfc-0.4.0 - SNAPSHOT.jar:$APP_HOME/lib/dataforge-control-0.4.0 - SNAPSHOT.jar:$APP_HOME/lib/dataforge-gui-0.4.0 - SNAPSHOT.jar:$APP_HOME/lib/numass-core-1.0.0.jar:$APP_HOME/lib/storage-server-0.4.0 - SNAPSHOT.jar:$APP_HOME/lib/dataforge-messages-0.4.0 - SNAPSHOT.jar:$APP_HOME/lib/dataforge-plots-0.4.0 - SNAPSHOT.jar:$APP_HOME/lib/dataforge-storage-0.4.0 - SNAPSHOT.jar:$APP_HOME/lib/dataforge-json-0.4.0 - SNAPSHOT.jar:$APP_HOME/lib/dataforge-core-0.4.0 - SNAPSHOT.jar:$APP_HOME/lib/tornadofx-controlsfx-0.1.jar:$APP_HOME/lib/tornadofx-1.7.15.jar:$APP_HOME/lib/kotlin-stdlib-jdk8-1.2.50.jar:$APP_HOME/lib/kotlin-reflect-1.2.50.jar:$APP_HOME/lib/kotlin-stdlib-jdk7-1.2.50.jar:$APP_HOME/lib/kotlinx-coroutines-jdk8-0.22.jar:$APP_HOME/lib/kotlinx-coroutines-core-0.22.jar:$APP_HOME/lib/kotlin-stdlib-1.2.50.jar:$APP_HOME/lib/ratpack-core-1.4.6.jar:$APP_HOME/lib/commons-daemon-1.1.0.jar:$APP_HOME/lib/kotlin-stdlib-common-1.2.50.jar:$APP_HOME/lib/annotations-15.0.jar:$APP_HOME/lib/commons-cli-1.4.jar:$APP_HOME/lib/zt-zip-1.13.jar:$APP_HOME/lib/jfreesvg-3.3.jar:$APP_HOME/lib/jfreechart-fx-1.0.1.jar:$APP_HOME/lib/jssc-2.8.0.jar:$APP_HOME/lib/controlsfx-8.40.14.jar:$APP_HOME/lib/richtextfx-0.9.0.jar:$APP_HOME/lib/netty-codec-http-4.1.6.Final.jar:$APP_HOME/lib/netty-handler-4.1.6.Final.jar:$APP_HOME/lib/netty-transport-native-epoll-4.1.6.Final-linux-x86_64.jar:$APP_HOME/lib/guava-19.0.jar:$APP_HOME/lib/logback-classic-1.2.3.jar:$APP_HOME/lib/jcl-over-slf4j-1.7.25.jar:$APP_HOME/lib/slf4j-api-1.7.25.jar:$APP_HOME/lib/reactive-streams-1.0.0.final.jar:$APP_HOME/lib/caffeine-2.3.1.jar:$APP_HOME/lib/javassist-3.19.0-GA.jar:$APP_HOME/lib/jackson-datatype-guava-2.7.5.jar:$APP_HOME/lib/jackson-datatype-jdk8-2.7.5.jar:$APP_HOME/lib/jackson-datatype-jsr310-2.7.5.jar:$APP_HOME/lib/jackson-databind-2.7.5.jar:$APP_HOME/lib/jackson-dataformat-yaml-2.7.5.jar:$APP_HOME/lib/snakeyaml-1.15.jar:$APP_HOME/lib/protobuf-java-3.5.0.jar:$APP_HOME/lib/sftp-fs-1.1.3.jar:$APP_HOME/lib/freemarker-2.3.26-incubating.jar:$APP_HOME/lib/jfreechart-1.5.0.jar:$APP_HOME/lib/fxgraphics2d-1.6.jar:$APP_HOME/lib/cache-api-1.0.0.jar:$APP_HOME/lib/commons-io-2.5.jar:$APP_HOME/lib/javax.json-1.1.2.jar:$APP_HOME/lib/undofx-2.0.0.jar:$APP_HOME/lib/flowless-0.6.jar:$APP_HOME/lib/reactfx-2.0-M5.jar:$APP_HOME/lib/wellbehavedfx-0.3.3.jar:$APP_HOME/lib/netty-codec-4.1.6.Final.jar:$APP_HOME/lib/netty-transport-4.1.6.Final.jar:$APP_HOME/lib/netty-buffer-4.1.6.Final.jar:$APP_HOME/lib/netty-resolver-4.1.6.Final.jar:$APP_HOME/lib/netty-common-4.1.6.Final.jar:$APP_HOME/lib/jackson-annotations-2.7.0.jar:$APP_HOME/lib/jackson-core-2.7.5.jar:$APP_HOME/lib/fs-core-1.2.jar:$APP_HOME/lib/jsch-0.1.54.jar:$APP_HOME/lib/logback-core-1.2.3.jar:$APP_HOME/lib/json-simple-3.0.2.jar:$APP_HOME/lib/javax.json-api-1.1.2.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $CONTROL_ROOM_OPTS -classpath "\"$CLASSPATH\"" inr.numass.control.ServerApp "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/numass-control/control-room/build/scripts/control-room-devices b/numass-control/control-room/build/scripts/control-room-devices new file mode 100644 index 00000000..e2391a77 --- /dev/null +++ b/numass-control/control-room/build/scripts/control-room-devices @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## control-room-devices start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/.." >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="control-room-devices" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and CONTROL_ROOM_DEVICES_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/lib/control-room-0.3.0.jar:$APP_HOME/lib/cryotemp-0.2.0.jar:$APP_HOME/lib/msp-0.4.0.jar:$APP_HOME/lib/vac-0.5.0.jar:$APP_HOME/lib/numass-control-1.0.0.jar:$APP_HOME/lib/numass-server-1.0.0.jar:$APP_HOME/lib/numass-client-1.0.0.jar:$APP_HOME/lib/plots-jfc-0.4.0 - SNAPSHOT.jar:$APP_HOME/lib/dataforge-control-0.4.0 - SNAPSHOT.jar:$APP_HOME/lib/dataforge-gui-0.4.0 - SNAPSHOT.jar:$APP_HOME/lib/numass-core-1.0.0.jar:$APP_HOME/lib/storage-server-0.4.0 - SNAPSHOT.jar:$APP_HOME/lib/dataforge-messages-0.4.0 - SNAPSHOT.jar:$APP_HOME/lib/dataforge-plots-0.4.0 - SNAPSHOT.jar:$APP_HOME/lib/dataforge-storage-0.4.0 - SNAPSHOT.jar:$APP_HOME/lib/dataforge-json-0.4.0 - SNAPSHOT.jar:$APP_HOME/lib/dataforge-core-0.4.0 - SNAPSHOT.jar:$APP_HOME/lib/tornadofx-controlsfx-0.1.jar:$APP_HOME/lib/tornadofx-1.7.15.jar:$APP_HOME/lib/kotlin-stdlib-jdk8-1.2.50.jar:$APP_HOME/lib/kotlin-reflect-1.2.50.jar:$APP_HOME/lib/kotlin-stdlib-jdk7-1.2.50.jar:$APP_HOME/lib/kotlinx-coroutines-jdk8-0.22.jar:$APP_HOME/lib/kotlinx-coroutines-core-0.22.jar:$APP_HOME/lib/kotlin-stdlib-1.2.50.jar:$APP_HOME/lib/ratpack-core-1.4.6.jar:$APP_HOME/lib/commons-daemon-1.1.0.jar:$APP_HOME/lib/kotlin-stdlib-common-1.2.50.jar:$APP_HOME/lib/annotations-15.0.jar:$APP_HOME/lib/commons-cli-1.4.jar:$APP_HOME/lib/zt-zip-1.13.jar:$APP_HOME/lib/jfreesvg-3.3.jar:$APP_HOME/lib/jfreechart-fx-1.0.1.jar:$APP_HOME/lib/jssc-2.8.0.jar:$APP_HOME/lib/controlsfx-8.40.14.jar:$APP_HOME/lib/richtextfx-0.9.0.jar:$APP_HOME/lib/netty-codec-http-4.1.6.Final.jar:$APP_HOME/lib/netty-handler-4.1.6.Final.jar:$APP_HOME/lib/netty-transport-native-epoll-4.1.6.Final-linux-x86_64.jar:$APP_HOME/lib/guava-19.0.jar:$APP_HOME/lib/logback-classic-1.2.3.jar:$APP_HOME/lib/jcl-over-slf4j-1.7.25.jar:$APP_HOME/lib/slf4j-api-1.7.25.jar:$APP_HOME/lib/reactive-streams-1.0.0.final.jar:$APP_HOME/lib/caffeine-2.3.1.jar:$APP_HOME/lib/javassist-3.19.0-GA.jar:$APP_HOME/lib/jackson-datatype-guava-2.7.5.jar:$APP_HOME/lib/jackson-datatype-jdk8-2.7.5.jar:$APP_HOME/lib/jackson-datatype-jsr310-2.7.5.jar:$APP_HOME/lib/jackson-databind-2.7.5.jar:$APP_HOME/lib/jackson-dataformat-yaml-2.7.5.jar:$APP_HOME/lib/snakeyaml-1.15.jar:$APP_HOME/lib/protobuf-java-3.5.0.jar:$APP_HOME/lib/sftp-fs-1.1.3.jar:$APP_HOME/lib/freemarker-2.3.26-incubating.jar:$APP_HOME/lib/jfreechart-1.5.0.jar:$APP_HOME/lib/fxgraphics2d-1.6.jar:$APP_HOME/lib/cache-api-1.0.0.jar:$APP_HOME/lib/commons-io-2.5.jar:$APP_HOME/lib/javax.json-1.1.2.jar:$APP_HOME/lib/undofx-2.0.0.jar:$APP_HOME/lib/flowless-0.6.jar:$APP_HOME/lib/reactfx-2.0-M5.jar:$APP_HOME/lib/wellbehavedfx-0.3.3.jar:$APP_HOME/lib/netty-codec-4.1.6.Final.jar:$APP_HOME/lib/netty-transport-4.1.6.Final.jar:$APP_HOME/lib/netty-buffer-4.1.6.Final.jar:$APP_HOME/lib/netty-resolver-4.1.6.Final.jar:$APP_HOME/lib/netty-common-4.1.6.Final.jar:$APP_HOME/lib/jackson-annotations-2.7.0.jar:$APP_HOME/lib/jackson-core-2.7.5.jar:$APP_HOME/lib/fs-core-1.2.jar:$APP_HOME/lib/jsch-0.1.54.jar:$APP_HOME/lib/logback-core-1.2.3.jar:$APP_HOME/lib/json-simple-3.0.2.jar:$APP_HOME/lib/javax.json-api-1.1.2.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $CONTROL_ROOM_DEVICES_OPTS -classpath "\"$CLASSPATH\"" inr.numass.control.ServerApp "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/numass-control/control-room/build/scripts/control-room-devices.bat b/numass-control/control-room/build/scripts/control-room-devices.bat new file mode 100644 index 00000000..70db665c --- /dev/null +++ b/numass-control/control-room/build/scripts/control-room-devices.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem control-room-devices startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME%.. + +@rem Add default JVM options here. You can also use JAVA_OPTS and CONTROL_ROOM_DEVICES_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\lib\control-room-0.3.0.jar;%APP_HOME%\lib\cryotemp-0.2.0.jar;%APP_HOME%\lib\msp-0.4.0.jar;%APP_HOME%\lib\vac-0.5.0.jar;%APP_HOME%\lib\numass-control-1.0.0.jar;%APP_HOME%\lib\numass-server-1.0.0.jar;%APP_HOME%\lib\numass-client-1.0.0.jar;%APP_HOME%\lib\plots-jfc-0.4.0 - SNAPSHOT.jar;%APP_HOME%\lib\dataforge-control-0.4.0 - SNAPSHOT.jar;%APP_HOME%\lib\dataforge-gui-0.4.0 - SNAPSHOT.jar;%APP_HOME%\lib\numass-core-1.0.0.jar;%APP_HOME%\lib\storage-server-0.4.0 - SNAPSHOT.jar;%APP_HOME%\lib\dataforge-messages-0.4.0 - SNAPSHOT.jar;%APP_HOME%\lib\dataforge-plots-0.4.0 - SNAPSHOT.jar;%APP_HOME%\lib\dataforge-storage-0.4.0 - SNAPSHOT.jar;%APP_HOME%\lib\dataforge-json-0.4.0 - SNAPSHOT.jar;%APP_HOME%\lib\dataforge-core-0.4.0 - SNAPSHOT.jar;%APP_HOME%\lib\tornadofx-controlsfx-0.1.jar;%APP_HOME%\lib\tornadofx-1.7.15.jar;%APP_HOME%\lib\kotlin-stdlib-jdk8-1.2.50.jar;%APP_HOME%\lib\kotlin-reflect-1.2.50.jar;%APP_HOME%\lib\kotlin-stdlib-jdk7-1.2.50.jar;%APP_HOME%\lib\kotlinx-coroutines-jdk8-0.22.jar;%APP_HOME%\lib\kotlinx-coroutines-core-0.22.jar;%APP_HOME%\lib\kotlin-stdlib-1.2.50.jar;%APP_HOME%\lib\ratpack-core-1.4.6.jar;%APP_HOME%\lib\commons-daemon-1.1.0.jar;%APP_HOME%\lib\kotlin-stdlib-common-1.2.50.jar;%APP_HOME%\lib\annotations-15.0.jar;%APP_HOME%\lib\commons-cli-1.4.jar;%APP_HOME%\lib\zt-zip-1.13.jar;%APP_HOME%\lib\jfreesvg-3.3.jar;%APP_HOME%\lib\jfreechart-fx-1.0.1.jar;%APP_HOME%\lib\jssc-2.8.0.jar;%APP_HOME%\lib\controlsfx-8.40.14.jar;%APP_HOME%\lib\richtextfx-0.9.0.jar;%APP_HOME%\lib\netty-codec-http-4.1.6.Final.jar;%APP_HOME%\lib\netty-handler-4.1.6.Final.jar;%APP_HOME%\lib\netty-transport-native-epoll-4.1.6.Final-linux-x86_64.jar;%APP_HOME%\lib\guava-19.0.jar;%APP_HOME%\lib\logback-classic-1.2.3.jar;%APP_HOME%\lib\jcl-over-slf4j-1.7.25.jar;%APP_HOME%\lib\slf4j-api-1.7.25.jar;%APP_HOME%\lib\reactive-streams-1.0.0.final.jar;%APP_HOME%\lib\caffeine-2.3.1.jar;%APP_HOME%\lib\javassist-3.19.0-GA.jar;%APP_HOME%\lib\jackson-datatype-guava-2.7.5.jar;%APP_HOME%\lib\jackson-datatype-jdk8-2.7.5.jar;%APP_HOME%\lib\jackson-datatype-jsr310-2.7.5.jar;%APP_HOME%\lib\jackson-databind-2.7.5.jar;%APP_HOME%\lib\jackson-dataformat-yaml-2.7.5.jar;%APP_HOME%\lib\snakeyaml-1.15.jar;%APP_HOME%\lib\protobuf-java-3.5.0.jar;%APP_HOME%\lib\sftp-fs-1.1.3.jar;%APP_HOME%\lib\freemarker-2.3.26-incubating.jar;%APP_HOME%\lib\jfreechart-1.5.0.jar;%APP_HOME%\lib\fxgraphics2d-1.6.jar;%APP_HOME%\lib\cache-api-1.0.0.jar;%APP_HOME%\lib\commons-io-2.5.jar;%APP_HOME%\lib\javax.json-1.1.2.jar;%APP_HOME%\lib\undofx-2.0.0.jar;%APP_HOME%\lib\flowless-0.6.jar;%APP_HOME%\lib\reactfx-2.0-M5.jar;%APP_HOME%\lib\wellbehavedfx-0.3.3.jar;%APP_HOME%\lib\netty-codec-4.1.6.Final.jar;%APP_HOME%\lib\netty-transport-4.1.6.Final.jar;%APP_HOME%\lib\netty-buffer-4.1.6.Final.jar;%APP_HOME%\lib\netty-resolver-4.1.6.Final.jar;%APP_HOME%\lib\netty-common-4.1.6.Final.jar;%APP_HOME%\lib\jackson-annotations-2.7.0.jar;%APP_HOME%\lib\jackson-core-2.7.5.jar;%APP_HOME%\lib\fs-core-1.2.jar;%APP_HOME%\lib\jsch-0.1.54.jar;%APP_HOME%\lib\logback-core-1.2.3.jar;%APP_HOME%\lib\json-simple-3.0.2.jar;%APP_HOME%\lib\javax.json-api-1.1.2.jar + +@rem Execute control-room-devices +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %CONTROL_ROOM_DEVICES_OPTS% -classpath "%CLASSPATH%" inr.numass.control.ServerApp %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable CONTROL_ROOM_DEVICES_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%CONTROL_ROOM_DEVICES_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/numass-control/control-room/build/scripts/control-room.bat b/numass-control/control-room/build/scripts/control-room.bat new file mode 100644 index 00000000..39578098 --- /dev/null +++ b/numass-control/control-room/build/scripts/control-room.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem control-room startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME%.. + +@rem Add default JVM options here. You can also use JAVA_OPTS and CONTROL_ROOM_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\lib\control-room-0.3.0.jar;%APP_HOME%\lib\numass-control-1.0.0.jar;%APP_HOME%\lib\numass-server-1.0.0.jar;%APP_HOME%\lib\numass-client-1.0.0.jar;%APP_HOME%\lib\plots-jfc-0.4.0 - SNAPSHOT.jar;%APP_HOME%\lib\dataforge-control-0.4.0 - SNAPSHOT.jar;%APP_HOME%\lib\dataforge-gui-0.4.0 - SNAPSHOT.jar;%APP_HOME%\lib\numass-core-1.0.0.jar;%APP_HOME%\lib\storage-server-0.4.0 - SNAPSHOT.jar;%APP_HOME%\lib\dataforge-messages-0.4.0 - SNAPSHOT.jar;%APP_HOME%\lib\dataforge-plots-0.4.0 - SNAPSHOT.jar;%APP_HOME%\lib\dataforge-storage-0.4.0 - SNAPSHOT.jar;%APP_HOME%\lib\dataforge-json-0.4.0 - SNAPSHOT.jar;%APP_HOME%\lib\dataforge-core-0.4.0 - SNAPSHOT.jar;%APP_HOME%\lib\tornadofx-controlsfx-0.1.jar;%APP_HOME%\lib\tornadofx-1.7.15.jar;%APP_HOME%\lib\kotlin-stdlib-jdk8-1.2.50.jar;%APP_HOME%\lib\kotlin-reflect-1.2.50.jar;%APP_HOME%\lib\kotlin-stdlib-jdk7-1.2.50.jar;%APP_HOME%\lib\kotlinx-coroutines-jdk8-0.22.jar;%APP_HOME%\lib\kotlinx-coroutines-core-0.22.jar;%APP_HOME%\lib\kotlin-stdlib-1.2.50.jar;%APP_HOME%\lib\ratpack-core-1.4.6.jar;%APP_HOME%\lib\commons-daemon-1.1.0.jar;%APP_HOME%\lib\kotlin-stdlib-common-1.2.50.jar;%APP_HOME%\lib\annotations-15.0.jar;%APP_HOME%\lib\commons-cli-1.4.jar;%APP_HOME%\lib\zt-zip-1.13.jar;%APP_HOME%\lib\jfreesvg-3.3.jar;%APP_HOME%\lib\jfreechart-fx-1.0.1.jar;%APP_HOME%\lib\jssc-2.8.0.jar;%APP_HOME%\lib\controlsfx-8.40.14.jar;%APP_HOME%\lib\richtextfx-0.9.0.jar;%APP_HOME%\lib\netty-codec-http-4.1.6.Final.jar;%APP_HOME%\lib\netty-handler-4.1.6.Final.jar;%APP_HOME%\lib\netty-transport-native-epoll-4.1.6.Final-linux-x86_64.jar;%APP_HOME%\lib\guava-19.0.jar;%APP_HOME%\lib\logback-classic-1.2.3.jar;%APP_HOME%\lib\jcl-over-slf4j-1.7.25.jar;%APP_HOME%\lib\slf4j-api-1.7.25.jar;%APP_HOME%\lib\reactive-streams-1.0.0.final.jar;%APP_HOME%\lib\caffeine-2.3.1.jar;%APP_HOME%\lib\javassist-3.19.0-GA.jar;%APP_HOME%\lib\jackson-datatype-guava-2.7.5.jar;%APP_HOME%\lib\jackson-datatype-jdk8-2.7.5.jar;%APP_HOME%\lib\jackson-datatype-jsr310-2.7.5.jar;%APP_HOME%\lib\jackson-databind-2.7.5.jar;%APP_HOME%\lib\jackson-dataformat-yaml-2.7.5.jar;%APP_HOME%\lib\snakeyaml-1.15.jar;%APP_HOME%\lib\protobuf-java-3.5.0.jar;%APP_HOME%\lib\sftp-fs-1.1.3.jar;%APP_HOME%\lib\freemarker-2.3.26-incubating.jar;%APP_HOME%\lib\jfreechart-1.5.0.jar;%APP_HOME%\lib\fxgraphics2d-1.6.jar;%APP_HOME%\lib\cache-api-1.0.0.jar;%APP_HOME%\lib\commons-io-2.5.jar;%APP_HOME%\lib\javax.json-1.1.2.jar;%APP_HOME%\lib\undofx-2.0.0.jar;%APP_HOME%\lib\flowless-0.6.jar;%APP_HOME%\lib\reactfx-2.0-M5.jar;%APP_HOME%\lib\wellbehavedfx-0.3.3.jar;%APP_HOME%\lib\netty-codec-4.1.6.Final.jar;%APP_HOME%\lib\netty-transport-4.1.6.Final.jar;%APP_HOME%\lib\netty-buffer-4.1.6.Final.jar;%APP_HOME%\lib\netty-resolver-4.1.6.Final.jar;%APP_HOME%\lib\netty-common-4.1.6.Final.jar;%APP_HOME%\lib\jackson-annotations-2.7.0.jar;%APP_HOME%\lib\jackson-core-2.7.5.jar;%APP_HOME%\lib\fs-core-1.2.jar;%APP_HOME%\lib\jsch-0.1.54.jar;%APP_HOME%\lib\logback-core-1.2.3.jar;%APP_HOME%\lib\json-simple-3.0.2.jar;%APP_HOME%\lib\javax.json-api-1.1.2.jar + +@rem Execute control-room +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %CONTROL_ROOM_OPTS% -classpath "%CLASSPATH%" inr.numass.control.ServerApp %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable CONTROL_ROOM_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%CONTROL_ROOM_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/numass-control/control-room/out/production/resources/config/control-real.xml b/numass-control/control-room/out/production/resources/config/control-real.xml new file mode 100644 index 00000000..06d25124 --- /dev/null +++ b/numass-control/control-room/out/production/resources/config/control-real.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + diff --git a/numass-control/control-room/out/production/resources/config/control.xml b/numass-control/control-room/out/production/resources/config/control.xml new file mode 100644 index 00000000..03f3c4dd --- /dev/null +++ b/numass-control/control-room/out/production/resources/config/control.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/numass-control/control-room/src/main/kotlin/inr/numass/control/BoardController.kt b/numass-control/control-room/src/main/kotlin/inr/numass/control/BoardController.kt new file mode 100644 index 00000000..77f245ce --- /dev/null +++ b/numass-control/control-room/src/main/kotlin/inr/numass/control/BoardController.kt @@ -0,0 +1,77 @@ +package inr.numass.control + +import hep.dataforge.context.Context +import hep.dataforge.context.Global +import hep.dataforge.control.DeviceManager +import hep.dataforge.control.connections.Roles +import hep.dataforge.control.devices.Device +import hep.dataforge.meta.Meta +import hep.dataforge.server.ServerManager +import hep.dataforge.storage.api.Storage +import hep.dataforge.storage.commons.StorageConnection +import hep.dataforge.storage.commons.StorageManager +import hep.dataforge.useMeta +import hep.dataforge.useMetaList +import inr.numass.client.ClientUtils +import javafx.beans.property.SimpleObjectProperty +import javafx.collections.FXCollections +import javafx.collections.ObservableList +import tornadofx.* + +/** + * Created by darksnake on 12-May-17. + */ +class BoardController() : Controller(), AutoCloseable { + + val contextProperty = SimpleObjectProperty(Global) + var context: Context by contextProperty + private set + + val storageProperty = SimpleObjectProperty(null) + + val serverManagerProperty = objectBinding(contextProperty) { + context.opt(ServerManager::class.java) + } + + val devices: ObservableList = FXCollections.observableArrayList(); + + + fun configure(meta: Meta) { + Context.build("NUMASS", Global, meta.getMeta("context", meta)).apply { + val numassRun = meta.optMeta("numass").map { ClientUtils.getRunName(it) }.orElse("") + + meta.useMeta("storage") { + pluginManager.load(StorageManager::class.java,it) + } + + val rootStorage = pluginManager.load(StorageManager::class.java).defaultStorage + + val storage = if (!numassRun.isEmpty()) { + logger.info("Run information found. Selecting run {}", numassRun) + rootStorage.buildShelf(numassRun, Meta.empty()); + } else { + rootStorage + } + val connection = StorageConnection(storage) + + val deviceManager = pluginManager.load(DeviceManager::class.java) + + meta.useMetaList("device") { + it.forEach { + deviceManager.buildDevice(it) + } + } + deviceManager.devices.forEach { it.connect(connection, Roles.STORAGE_ROLE) } + }.also { + runLater { + context = it + devices.setAll(context.get(DeviceManager::class.java).devices.toList()); + } + } + } + + override fun close() { + context.close() + //Global.terminate() + } +} \ No newline at end of file diff --git a/numass-control/control-room/src/main/kotlin/inr/numass/control/BoardView.kt b/numass-control/control-room/src/main/kotlin/inr/numass/control/BoardView.kt new file mode 100644 index 00000000..97768fa6 --- /dev/null +++ b/numass-control/control-room/src/main/kotlin/inr/numass/control/BoardView.kt @@ -0,0 +1,103 @@ +package inr.numass.control + +import hep.dataforge.fx.dfIcon +import hep.dataforge.storage.filestorage.FileStorage +import javafx.geometry.Orientation +import javafx.geometry.Pos +import javafx.scene.image.ImageView +import javafx.scene.layout.Priority +import tornadofx.* + +/** + * Created by darksnake on 11-May-17. + */ +class BoardView : View("Numass control board", ImageView(dfIcon)) { + private val controller: BoardController by inject(); + + override val root = borderpane { + prefHeight = 200.0 + prefWidth = 200.0 + center { + vbox { + //Server pane + titledpane(title = "Server", collapsible = false) { + vgrow = Priority.ALWAYS; + hbox { + alignment = Pos.CENTER_LEFT + prefHeight = 40.0 + togglebutton("Start") { + isSelected = false + disableProperty().bind(controller.serverManagerProperty.booleanBinding { it == null }) + action { + if (isSelected) { + text = "Stop" + controller.serverManagerProperty.value?.startServer() + } else { + text = "Start" + controller.serverManagerProperty.value?.stopServer() + } + } + } + text("Started: ") { + paddingHorizontal = 5 + } + indicator { + bind(controller.serverManagerProperty.select { it?.isStartedProperty ?: false.toProperty() }) + } + separator(Orientation.VERTICAL) + text("Address: ") + hyperlink { + textProperty().bind(controller.serverManagerProperty.stringBinding { + it?.link ?: "" + }) + action { + app.hostServices.showDocument(controller.serverManagerProperty.value?.link); + } + } + } + } + titledpane(title = "Storage", collapsible = true) { + vgrow = Priority.ALWAYS; + hbox { + alignment = Pos.CENTER_LEFT + prefHeight = 40.0 + label(controller.storageProperty.stringBinding { storage -> + if (storage == null) { + "Storage not initialized" + } else { + if (storage is FileStorage) { + "Path: " + storage.dataDir; + } else { + "Name: " + storage.fullName + } + } + }) + } + } + separator(Orientation.HORIZONTAL) + scrollpane(fitToWidth = true, fitToHeight = true) { + vgrow = Priority.ALWAYS; + vbox { + prefHeight = 40.0 + controller.devices.onChange { change -> + children.setAll(change.list.map { + titledpane( + title = "Device: " + it.name, + collapsible = true, + node = it.getDisplay().getBoardView() + ) + }) + } +// bindChildren(controller.devices) { device -> +// titledpane( +// title = "Device: " + device.name, +// collapsible = true, +// node = device.getDisplay().getBoardView() +// ) +// } + } + } + } + } + } +} diff --git a/numass-control/control-room/src/main/kotlin/inr/numass/control/ServerApp.kt b/numass-control/control-room/src/main/kotlin/inr/numass/control/ServerApp.kt new file mode 100644 index 00000000..1a8c179c --- /dev/null +++ b/numass-control/control-room/src/main/kotlin/inr/numass/control/ServerApp.kt @@ -0,0 +1,25 @@ +package inr.numass.control + +import javafx.stage.Stage +import tornadofx.* + +/** + * Created by darksnake on 19-May-17. + */ +class ServerApp : App(BoardView::class) { + private val controller: BoardController by inject(); + + + override fun start(stage: Stage) { + getConfig(this@ServerApp)?.let { + controller.configure(it) + } + + super.start(stage) + } + + override fun stop() { + controller.close() + super.stop() + } +} \ No newline at end of file diff --git a/numass-control/control-room/src/main/resources/config/control-real.xml b/numass-control/control-room/src/main/resources/config/control-real.xml new file mode 100644 index 00000000..06d25124 --- /dev/null +++ b/numass-control/control-room/src/main/resources/config/control-real.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + diff --git a/numass-control/control-room/src/main/resources/config/control.xml b/numass-control/control-room/src/main/resources/config/control.xml new file mode 100644 index 00000000..03f3c4dd --- /dev/null +++ b/numass-control/control-room/src/main/resources/config/control.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/numass-control/cryotemp/build.gradle b/numass-control/cryotemp/build.gradle new file mode 100644 index 00000000..5eeae11c --- /dev/null +++ b/numass-control/cryotemp/build.gradle @@ -0,0 +1,32 @@ +apply plugin: 'application' +apply plugin: 'kotlin' + +if (!hasProperty('mainClass')) { + ext.mainClass = 'inr.numass.control.cryotemp.PKT8App' +} +mainClassName = mainClass + +version = "0.2.0"; + +//mainClassName = "inr.numass.readvac.Main" + +dependencies { + compile project(':numass-control') +} + +task testDevice(dependsOn: classes, type: JavaExec) { + main mainClass + args = ["--config.resource=config-debug/devices.xml"] + classpath = sourceSets.main.runtimeClasspath + description = "Start application in debug mode with default virtual port" + group = "application" +} + + +//task testRun(dependsOn: classes, type: JavaExec) { +// main mainClass +// args = ["--config=D:/temp/test/numass-devices.xml", "--device=thermo-1"] +// classpath = sourceSets.main.runtimeClasspath +// description "Start application using real device" +// group "debug" +//} \ No newline at end of file diff --git a/numass-control/cryotemp/src/main/kotlin/inr/numass/control/cryotemp/PKT8App.kt b/numass-control/cryotemp/src/main/kotlin/inr/numass/control/cryotemp/PKT8App.kt new file mode 100644 index 00000000..c135f44e --- /dev/null +++ b/numass-control/cryotemp/src/main/kotlin/inr/numass/control/cryotemp/PKT8App.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.control.cryotemp + +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaUtils +import inr.numass.control.NumassControlApplication +import javafx.stage.Stage + +/** + * @author darksnake + */ +class PKT8App : NumassControlApplication() { + + override val deviceFactory = PKT8DeviceFactory() + + override fun setupStage(stage: Stage, device: PKT8Device) { + stage.title = "Numass temperature view " + device.name + stage.minHeight = 400.0 + stage.minWidth = 400.0 + } + + override fun getDeviceMeta(config: Meta): Meta { + return MetaUtils.findNode(config,"device"){it.getString("type") == "PKT8"} + .orElseThrow{RuntimeException("Temperature measurement configuration not found")} + } +} diff --git a/numass-control/cryotemp/src/main/kotlin/inr/numass/control/cryotemp/PKT8Channel.kt b/numass-control/cryotemp/src/main/kotlin/inr/numass/control/cryotemp/PKT8Channel.kt new file mode 100644 index 00000000..d6f5558f --- /dev/null +++ b/numass-control/cryotemp/src/main/kotlin/inr/numass/control/cryotemp/PKT8Channel.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.control.cryotemp + +import hep.dataforge.Named +import hep.dataforge.meta.* + + +internal fun createChannel(name: String): PKT8Channel = + PKT8Channel(MetaBuilder("channel").putValue("name", name)) { d -> d } + +internal fun createChannel(meta: Meta): PKT8Channel { + val transformationType = meta.getString("transformationType", "default") + if (meta.hasValue("coefs")) { + when (transformationType) { + "default", "hyperbolic" -> { + val coefs = meta.getValue("coefs").list + val r0 = meta.getDouble("r0", 1000.0) + return PKT8Channel(meta) { r -> + coefs.indices.sumByDouble { coefs[it].double * Math.pow(r0 / r, it.toDouble()) } + } + } + else -> throw RuntimeException("Unknown transformation type") + } + } else { + //identity transformation + return PKT8Channel(meta) { d -> d } + } +} + +/** + * Created by darksnake on 28-Sep-16. + */ +class PKT8Channel(override val meta: Meta, private val func: (Double) -> Double) : Named, Metoid { + + override val name: String by meta.stringValue() + + fun description(): String { + return meta.getString("description", "") + } + + /** + * @param r negative if temperature transformation not defined + * @return + */ + fun getTemperature(r: Double): Double { + return func(r) + } + + fun evaluate(r: Double): Meta { + return buildMeta { + "channel" to name + "raw" to r + "temperature" to getTemperature(r) + } + } + +} \ No newline at end of file diff --git a/numass-control/cryotemp/src/main/kotlin/inr/numass/control/cryotemp/PKT8Device.kt b/numass-control/cryotemp/src/main/kotlin/inr/numass/control/cryotemp/PKT8Device.kt new file mode 100644 index 00000000..cfcfb9a9 --- /dev/null +++ b/numass-control/cryotemp/src/main/kotlin/inr/numass/control/cryotemp/PKT8Device.kt @@ -0,0 +1,330 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.control.cryotemp + +import hep.dataforge.connections.RoleDef +import hep.dataforge.connections.RoleDefs +import hep.dataforge.context.Context +import hep.dataforge.control.collectors.RegularPointCollector +import hep.dataforge.control.connections.Roles +import hep.dataforge.control.devices.PortSensor +import hep.dataforge.control.devices.Sensor +import hep.dataforge.control.devices.notifyError +import hep.dataforge.control.ports.GenericPortController +import hep.dataforge.control.ports.Port +import hep.dataforge.control.ports.PortFactory +import hep.dataforge.description.ValueDef +import hep.dataforge.exceptions.ControlException +import hep.dataforge.meta.Meta +import hep.dataforge.states.StateDef +import hep.dataforge.states.valueState +import hep.dataforge.storage.StorageConnection +import hep.dataforge.storage.tables.TableLoader +import hep.dataforge.storage.tables.createTable +import hep.dataforge.tables.TableFormat +import hep.dataforge.tables.TableFormatBuilder +import hep.dataforge.utils.DateTimeUtils +import inr.numass.control.DeviceView +import inr.numass.control.StorageHelper +import java.time.Duration +import java.time.Instant +import java.util.* + + +/** + * A device controller for Dubna PKT 8 cryogenic thermometry device + * + * @author Alexander Nozik + */ +@RoleDefs( + RoleDef(name = Roles.STORAGE_ROLE), + RoleDef(name = Roles.VIEW_ROLE) +) +@ValueDef(key = "port", def = "virtual", info = "The name of the port for this PKT8") +@StateDef(value = ValueDef(key = "storing", info = "Define if this device is currently writes to storage"), writable = true) +@DeviceView(PKT8Display::class) +class PKT8Device(context: Context, meta: Meta) : PortSensor(context, meta) { + /** + * The key is the letter (a,b,c,d...) as in measurements + */ + val channels = LinkedHashMap() + + val storing = valueState("storing") + private var storageHelper: StorageHelper? = null + + /** + * Cached values + */ + //private var format: TableFormat? = null + + + // Building data format + private val tableFormat: TableFormat by lazy { + val tableFormatBuilder = TableFormatBuilder() + .addTime("timestamp") + + for (channel in this.channels.values) { + tableFormatBuilder.addNumber(channel.name) + } + tableFormatBuilder.build() + } + + val sps: String by valueState(SPS).stringDelegate + + val pga: String by valueState(PGA).stringDelegate + + val abuf: String by valueState(ABUF).stringDelegate + + private val duration = Duration.parse(meta.getString("averagingDuration", "PT30S")) + + private fun buildLoader(connection: StorageConnection): TableLoader { + val storage = connection.storage + val suffix = DateTimeUtils.fileSuffix() + return storage.createTable("cryotemp_$suffix", tableFormat) + } + + @Throws(ControlException::class) + override fun init() { + + //read channel configuration + if (meta.hasMeta("channel")) { + for (node in meta.getMetaList("channel")) { + val designation = node.getString("designation", "default") + this.channels[designation] = createChannel(node) + } + } else { + //set default channel configuration + for (designation in CHANNEL_DESIGNATIONS) { + this.channels[designation] = createChannel(designation) + } + logger.warn("No channels defined in configuration") + } + + super.init() + + //update parameters from meta + meta.optValue("pga").ifPresent { + logger.info("Setting dynamic range to " + it.int) + val response = sendAndWait("g" + it.int).trim { it <= ' ' } + if (response.contains("=")) { + updateState(PGA, Integer.parseInt(response.substring(4))) + } else { + logger.error("Setting pga failed with message: $response") + } + } + + + setSPS(meta.getInt("sps", 0)) + setBUF(meta.getInt("abuf", 100)) + + // setting up the collector + storageHelper = StorageHelper(this) { connection: StorageConnection -> this.buildLoader(connection) } + } + + @Throws(ControlException::class) + override fun shutdown() { + storageHelper?.close() + super.shutdown() + } + + override fun buildConnection(meta: Meta): GenericPortController { + val portName = meta.getString("name", "virtual") + + val port: Port = if (portName == "virtual") { + logger.info("Starting {} using virtual debug port", name) + PKT8VirtualPort("PKT8", meta) + } else { + logger.info("Connecting to port {}", portName) + PortFactory.build(meta) + } + return GenericPortController(context, port, "\n") + } + + private fun setBUF(buf: Int) { + logger.info("Setting averaging buffer size to $buf") + var response: String + try { + response = sendAndWait("b$buf").trim { it <= ' ' } + } catch (ex: Exception) { + response = ex.message ?: "" + } + + if (response.contains("=")) { + updateState(ABUF, Integer.parseInt(response.substring(14))) + // getLogger().info("successfully set buffer size to {}", this.abuf); + } else { + logger.error("Setting averaging buffer failed with message: $response") + } + } + + @Throws(ControlException::class) + fun changeParameters(sps: Int, abuf: Int) { + stopMeasurement() + //setting sps + setSPS(sps) + //setting buffer + setBUF(abuf) + } + + /** + * '0' : 2,5 SPS '1' : 5 SPS '2' : 10 SPS '3' : 25 SPS '4' : 50 SPS '5' : + * 100 SPS '6' : 500 SPS '7' : 1 kSPS '8' : 3,75 kSPS + * + * @param sps + * @return + */ + private fun spsToStr(sps: Int): String { + return when (sps) { + 0 -> "2.5 SPS" + 1 -> "5 SPS" + 2 -> "10 SPS" + 3 -> "25 SPS" + 4 -> "50 SPS" + 5 -> "100 SPS" + 6 -> "500 SPS" + 7 -> "1 kSPS" + 8 -> "3.75 kSPS" + else -> "unknown value" + } + } + + /** + * '0' : ± 5 Ð’ '1' : ± 2,5 Ð’ '2' : ± 1,25 Ð’ '3' : ± 0,625 Ð’ '4' : ± 312.5 мВ + * '5' : ± 156,25 мВ '6' : ± 78,125 мВ + * + * @param pga + * @return + */ + private fun pgaToStr(pga: Int): String { + return when (pga) { + 0 -> "± 5 V" + 1 -> "± 2,5 V" + 2 -> "± 1,25 V" + 3 -> "± 0,625 V" + 4 -> "± 312.5 mV" + 5 -> "± 156.25 mV" + 6 -> "± 78.125 mV" + else -> "unknown value" + } + } + + private val collector = RegularPointCollector(duration) { + notifyResult(it) + storageHelper?.push(it) + } + + private fun notifyChannelResult(designation: String, rawValue: Double) { + updateState("raw.$designation", rawValue) + + val channel = channels[designation] + + val temperature = channel?.let { + val temp = it.getTemperature(rawValue) + updateState("temp.$designation", temp) + collector.put(it.name, temp) + temp + } + forEachConnection(PKT8ValueListener::class.java) { + it.report(PKT8Reading(channel?.name ?: designation, rawValue, temperature)) + } + } + + override fun startMeasurement(oldMeta: Meta?, newMeta: Meta) { + if (oldMeta != null) { + stopMeasurement() + } + +// if (!oldMeta.isEmpty) { +// logger.warn("Trying to start measurement which is already started") +// } + + + logger.info("Starting measurement") + + connection.also { + it.onPhrase("[Ss]topped\\s*", this) { + notifyMeasurementState(Sensor.MeasurementState.STOPPED) + } + + //add weak measurement listener + it.onPhrase("[a-f].*", this) { + val trimmed = it.trim() + val designation = trimmed.substring(0, 1) + val rawValue = java.lang.Double.parseDouble(trimmed.substring(1)) / 100 + + notifyChannelResult(designation, rawValue) + } + + //send start signal + it.send("s") + notifyMeasurementState(Sensor.MeasurementState.IN_PROGRESS) + } + + } + + override fun stopMeasurement() { + try { + logger.info("Stopping measurement") + val response = sendAndWait("p").trim() + // Должно быть именно Ñ Ð±Ð¾Ð»ÑŒÑˆÐ¾Ð¹ буквы!!! + if ("Stopped" == response || "stopped" == response) { + notifyMeasurementState(Sensor.MeasurementState.STOPPED) + } + } catch (ex: Exception) { + notifyError("Failed to stop measurement", ex) + } finally { + connection.removeErrorListener(this) + connection.removePhraseListener(this) + collector.stop() + logger.debug("Collector stopped") + } + } + + private fun setSPS(sps: Int) { + logger.info("Setting sampling rate to " + spsToStr(sps)) + val response: String = try { + sendAndWait("v$sps").trim { it <= ' ' } + } catch (ex: Exception) { + ex.message ?: "" + } + + if (response.contains("=")) { + updateState(SPS, Integer.parseInt(response.substring(4))) + } else { + logger.error("Setting sps failed with message: $response") + } + } + + companion object { + const val PKT8_DEVICE_TYPE = "numass.pkt8" + + const val PGA = "pga" + const val SPS = "sps" + const val ABUF = "abuf" + private val CHANNEL_DESIGNATIONS = arrayOf("a", "b", "c", "d", "e", "f", "g", "h") + } +} + +data class PKT8Reading(val channel: String, val rawValue: Double, val temperature: Double?) { + + val rawString: String = String.format("%.2f", rawValue) + + val temperatureString: String = String.format("%.2f", temperature) +} + +interface PKT8ValueListener { + fun report(reading: PKT8Reading, time: Instant = Instant.now()) +} \ No newline at end of file diff --git a/numass-control/cryotemp/src/main/kotlin/inr/numass/control/cryotemp/PKT8DeviceFactory.kt b/numass-control/cryotemp/src/main/kotlin/inr/numass/control/cryotemp/PKT8DeviceFactory.kt new file mode 100644 index 00000000..7aba8e99 --- /dev/null +++ b/numass-control/cryotemp/src/main/kotlin/inr/numass/control/cryotemp/PKT8DeviceFactory.kt @@ -0,0 +1,16 @@ +package inr.numass.control.cryotemp + +import hep.dataforge.context.Context +import hep.dataforge.control.devices.DeviceFactory +import hep.dataforge.meta.Meta + +/** + * Created by darksnake on 09-May-17. + */ +class PKT8DeviceFactory : DeviceFactory { + override val type: String = PKT8Device.PKT8_DEVICE_TYPE + + override fun build(context: Context, meta: Meta): PKT8Device { + return PKT8Device(context, meta) + } +} diff --git a/numass-control/cryotemp/src/main/kotlin/inr/numass/control/cryotemp/PKT8Display.kt b/numass-control/cryotemp/src/main/kotlin/inr/numass/control/cryotemp/PKT8Display.kt new file mode 100644 index 00000000..89f5b492 --- /dev/null +++ b/numass-control/cryotemp/src/main/kotlin/inr/numass/control/cryotemp/PKT8Display.kt @@ -0,0 +1,216 @@ +package inr.numass.control.cryotemp + +import hep.dataforge.fx.asBooleanProperty +import hep.dataforge.fx.bindWindow +import hep.dataforge.fx.dfIconView +import hep.dataforge.fx.fragments.LogFragment +import hep.dataforge.fx.plots.PlotContainer +import hep.dataforge.meta.Meta +import hep.dataforge.plots.Plot +import hep.dataforge.plots.PlotUtils +import hep.dataforge.plots.data.TimePlot +import hep.dataforge.plots.jfreechart.JFreeChartFrame +import inr.numass.control.DeviceDisplayFX +import javafx.beans.binding.ListBinding +import javafx.beans.property.SimpleObjectProperty +import javafx.collections.FXCollections +import javafx.collections.MapChangeListener +import javafx.collections.ObservableList +import javafx.geometry.Orientation +import javafx.scene.Parent +import javafx.scene.control.ToggleButton +import javafx.scene.layout.Priority +import javafx.scene.layout.VBox +import javafx.scene.text.Font +import tornadofx.* +import java.time.Instant + +/** + * Created by darksnake on 30-May-17. + */ +class PKT8Display : DeviceDisplayFX(), PKT8ValueListener { + + override fun buildView(device: PKT8Device) = CryoView() + + internal val table = FXCollections.observableHashMap() + val lastUpdateProperty = SimpleObjectProperty("NEVER") + + + override fun getBoardView(): Parent { + return VBox().apply { + this += super.getBoardView() + } + } + +// override fun onMeasurementFailed(measurement: Measurement<*>, exception: Throwable) { +// +// } +// +// override fun onMeasurementResult(measurement: Measurement<*>, result: Any, time: Instant) { +// if (result is PKT8Result) { +// Platform.runLater { +// lastUpdateProperty.set(time.toString()) +// table[result.channel] = result; +// } +// } +// } +// + + override fun report(reading: PKT8Reading, time: Instant) { + runLater { + lastUpdateProperty.set(time.toString()) + table[reading.channel] = reading; + } + } + + inner class CryoView : Fragment("PKT values", dfIconView) { + private var plotButton: ToggleButton by singleAssign() + private var logButton: ToggleButton by singleAssign() + +// private val logWindow = FragmentWindow(LogFragment().apply { +// addLogHandler(device.logger) +// }) + + // need those to have strong references to listeners + private val plotView = CryoPlotView(); +// private val plotWindow = FragmentWindow(FXFragment.buildFromNode(plotView.title) { plotView.root }) + + override val root = borderpane { + top { + toolbar { + togglebutton("Measure") { + isSelected = false + selectedProperty().bindBidirectional(device.measuring.asBooleanProperty()) + } + togglebutton("Store") { + isSelected = false + selectedProperty().bindBidirectional(device.storing.asBooleanProperty()) + } + separator(Orientation.VERTICAL) + pane { + hgrow = Priority.ALWAYS + } + separator(Orientation.VERTICAL) + + plotButton = togglebutton("Plot") { + isSelected = false + plotView.bindWindow(this, selectedProperty()) + } + + logButton = togglebutton("Log") { + isSelected = false + LogFragment().apply { + addLogHandler(device.logger) + bindWindow(this@togglebutton, selectedProperty()) + } + } + } + } + center { + tableview { + items = object : ListBinding() { + init { + bind(table) + } + + override fun computeValue(): ObservableList { + return FXCollections.observableArrayList(table.values).apply { + sortBy { it.channel } + } + } + } + readonlyColumn("Sensor", PKT8Reading::channel); + readonlyColumn("Resistance", PKT8Reading::rawValue).cellFormat { + text = String.format("%.2f", it) + } + readonlyColumn("Temperature", PKT8Reading::temperature).cellFormat { + text = String.format("%.2f", it) + } + } + } + bottom { + toolbar { + label("Last update: ") + label(lastUpdateProperty) { + font = Font.font("System Bold") + } + } + } + } + } + + inner class CryoPlotView : Fragment("PKT8 temperature plot", dfIconView) { + private val plotFrameMeta: Meta = device.meta.getMetaOrEmpty("plotConfig") + + private val plotFrame by lazy { + JFreeChartFrame().apply { + configure(plotFrameMeta) + plots.setType() + PlotUtils.setXAxis(this, "timestamp", "", "time") + } + } + + var rawDataButton: ToggleButton by singleAssign() + + override val root: Parent = borderpane { + prefWidth = 800.0 + prefHeight = 600.0 + center = PlotContainer(plotFrame).root + top { + toolbar { + rawDataButton = togglebutton("Raw data") { + isSelected = false + action { + clearPlot() + } + } + button("Reset") { + action { + clearPlot() + } + } + } + } + } + + init { + if (device.meta.hasMeta("plotConfig")) { + with(plotFrame.plots) { + //configure(device.meta().getMeta("plotConfig")) + TimePlot.setMaxItems(this, 1000) + TimePlot.setPrefItems(this, 400) + } + } + table.addListener(MapChangeListener { change -> + if (change.wasAdded()) { + change.valueAdded.apply { + getPlot(channel)?.apply { + if (rawDataButton.isSelected) { + TimePlot.put(this, rawValue) + } else { + if (temperature != null) { + TimePlot.put(this, temperature) + } + } + } + } + } + }) + } + + private fun getPlot(channelName: String): Plot? { + return plotFrame[channelName] as? Plot ?: device.channels.values.find { it.name == channelName }?.let { + TimePlot(it.name).apply { + configure(it.meta) + plotFrame.add(this) + } + } + + } + + private fun clearPlot() { + plotFrame.clear() + } + } +} + diff --git a/numass-control/cryotemp/src/main/kotlin/inr/numass/control/cryotemp/PKT8VirtualPort.kt b/numass-control/cryotemp/src/main/kotlin/inr/numass/control/cryotemp/PKT8VirtualPort.kt new file mode 100644 index 00000000..21922fe1 --- /dev/null +++ b/numass-control/cryotemp/src/main/kotlin/inr/numass/control/cryotemp/PKT8VirtualPort.kt @@ -0,0 +1,70 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package inr.numass.control.cryotemp + +import hep.dataforge.control.ports.VirtualPort +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaUtils +import hep.dataforge.meta.Metoid +import hep.dataforge.values.asValue +import java.time.Duration +import java.util.* +import java.util.function.Supplier + + +/** + * @author Alexander Nozik + */ +class PKT8VirtualPort(override val name: String, meta: Meta) : VirtualPort(meta), Metoid { + private val generator = Random() + + @Synchronized override fun evaluateRequest(request: String) { + when (request) { + "s" -> { + val letters = arrayOf("a", "b", "c", "d", "e", "f", "g", "h") + for (letter in letters) { + val channelMeta = MetaUtils.findNodeByValue(meta, "channel", "letter", letter.asValue()).orElse(Meta.empty()) + + val average: Double + val sigma: Double + if (channelMeta != null) { + average = channelMeta.getDouble("av", 1200.0) + sigma = channelMeta.getDouble("sigma", 50.0) + } else { + average = 1200.0 + sigma = 50.0 + } + + this.planRegularResponse( + Supplier { + val res = average + generator.nextGaussian() * sigma + //TODO convert double value to formatted string + String.format("%s000%d", letter, (res * 100).toInt()) + }, + Duration.ZERO, Duration.ofMillis(500), letter, "measurement" + ) + } + return + } + "p" -> { + cancelByTag("measurement") + planResponse("Stopped", Duration.ofMillis(50)) + } + } + } + + override fun toMeta(): Meta { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + + @Throws(Exception::class) + override fun close() { + cancelByTag("measurement") + super.close() + } + +} diff --git a/numass-control/cryotemp/src/main/resources/META-INF/services/hep.dataforge.control.devices.DeviceFactory b/numass-control/cryotemp/src/main/resources/META-INF/services/hep.dataforge.control.devices.DeviceFactory new file mode 100644 index 00000000..965ef7d7 --- /dev/null +++ b/numass-control/cryotemp/src/main/resources/META-INF/services/hep.dataforge.control.devices.DeviceFactory @@ -0,0 +1 @@ +inr.numass.control.cryotemp.PKT8DeviceFactory \ No newline at end of file diff --git a/numass-control/cryotemp/src/main/resources/config-debug/devices.xml b/numass-control/cryotemp/src/main/resources/config-debug/devices.xml new file mode 100644 index 00000000..d3e2e250 --- /dev/null +++ b/numass-control/cryotemp/src/main/resources/config-debug/devices.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/numass-control/cryotemp/src/main/resources/config/devices.xml b/numass-control/cryotemp/src/main/resources/config/devices.xml new file mode 100644 index 00000000..0db4e9ca --- /dev/null +++ b/numass-control/cryotemp/src/main/resources/config/devices.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/numass-control/cryotemp/src/main/resources/fxml/PKT8Indicator.fxml b/numass-control/cryotemp/src/main/resources/fxml/PKT8Indicator.fxml new file mode 100644 index 00000000..618215d4 --- /dev/null +++ b/numass-control/cryotemp/src/main/resources/fxml/PKT8Indicator.fxml @@ -0,0 +1,42 @@ + + + + + + +
+ + + + + + + + + + +
+ + + + + + + + + + + + + + + +
diff --git a/numass-control/cryotemp/src/main/resources/fxml/PKT8Plot.fxml b/numass-control/cryotemp/src/main/resources/fxml/PKT8Plot.fxml new file mode 100644 index 00000000..61b541a1 --- /dev/null +++ b/numass-control/cryotemp/src/main/resources/fxml/PKT8Plot.fxml @@ -0,0 +1,36 @@ + + + + + + + + + +
+ +
+ + + + + + + + +
diff --git a/numass-control/dante/build.gradle b/numass-control/dante/build.gradle new file mode 100644 index 00000000..5e61a61e --- /dev/null +++ b/numass-control/dante/build.gradle @@ -0,0 +1,22 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + +version = "0.1.0" + +dependencies { + compile project(':numass-control') + compile project(':numass-core') +} \ No newline at end of file diff --git a/numass-control/dante/src/main/kotlin/inr/numass/control/dante/DanteClient.kt b/numass-control/dante/src/main/kotlin/inr/numass/control/dante/DanteClient.kt new file mode 100644 index 00000000..feb048e5 --- /dev/null +++ b/numass-control/dante/src/main/kotlin/inr/numass/control/dante/DanteClient.kt @@ -0,0 +1,581 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 inr.numass.control.dante + +import hep.dataforge.context.Context +import hep.dataforge.context.ContextAware +import hep.dataforge.context.launch +import hep.dataforge.meta.Meta +import hep.dataforge.meta.buildMeta +import hep.dataforge.orElse +import inr.numass.control.dante.DanteClient.Companion.CommandType.* +import inr.numass.control.dante.DanteClient.Companion.Register.* +import inr.numass.data.NumassProto +import inr.numass.data.ProtoNumassPoint +import inr.numass.data.api.NumassPoint +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import java.io.DataInputStream +import java.io.OutputStream +import java.lang.Math.pow +import java.net.Socket +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.time.Duration +import java.time.Instant +import java.util.* +import java.util.concurrent.atomic.AtomicLong +import kotlin.collections.HashMap +import kotlin.math.ceil + +internal val Byte.positive + get() = toInt() and 0xFF + +internal val Int.positive + get() = toLong() and 0xFFFFFFFF + +internal val Int.byte: Byte + get() { + if (this >= 256) { + throw RuntimeException("Number less than 256 is expected") + } else { + return toByte() + } + } + +internal val Long.int: Int + get() { + if (this >= 0xFFFFFFFF) { + throw RuntimeException("Number less than ${Int.MAX_VALUE * 2L} is expected") + } else { + return toInt() + } + } + +internal val ByteArray.hex + get() = this.joinToString(separator = "") { it.positive.toString(16).padStart(2, '0') } + + +//TODO implement using Device +class DanteClient(override val context: Context,val ip: String, chainLength: Int) : AutoCloseable, ContextAware { + private val RESET_COMMAND = byteArrayOf(0xDD.toByte(), 0x55, 0xDD.toByte(), 0xEE.toByte()) + + private val packetNumber = AtomicLong(0) + + private val parentJob: Job = SupervisorJob() + private val pool = newFixedThreadPoolContext(8, "Dante") + parentJob + + private val connections: MutableMap> = HashMap() + + private val sendChannel = Channel(capacity = Channel.UNLIMITED) + private lateinit var output: OutputStream + private lateinit var outputJob: Job + + /** + * Synchronous reading and writing of registers + */ + private val comChannel = Channel(capacity = Channel.UNLIMITED) + + /** + * Data packets channel + */ + private val dataChannel = Channel(capacity = Channel.UNLIMITED) + //private val statisticsChannel = Channel(capacity = Channel.UNLIMITED) + + /** + * @param num number + * @param name the name of the board + */ + private data class BoardState(val num: Int, var meta: Meta? = null) + + private val boards = (0 until chainLength).map { BoardState(it) } + + + fun open() { + //start supervisor job + parentJob.start() + (0..3).forEach { + openPort(it) + } + } + + override fun close() { + //TODO send termination signal + connections.values.forEach { + it.first.close() + it.second.cancel() + } + parentJob.cancel() + } + + /** + * Disconnect and reconnect without clearing configuration + */ + fun reconnect() { + close() + runBlocking { + clearData() + } + open() + } + + /** + * Reset everything + */ + fun reset() { + launch { + sendChannel.send(RESET_COMMAND) + } + } + + private fun openPort(port: Int) { + //closing existing connection + connections[port]?.let { + it.first.close() + it.second.cancel() + } + + val socket = Socket(ip, 8000 + port) + + logger.info("Opened socket {}", "${socket.inetAddress}:${socket.port}") + + //Create command queue on port 0 + if (port == 0) { + //outputJob.cancel() + output = socket.getOutputStream() + outputJob = launch(pool) { + while (true) { + val command = sendChannel.receive() + output.write(command) + logger.trace("Sent {}", command.hex) + } + } + } + + + val job = launch(pool) { + val stream = socket.getInputStream() + while (true) { + if (stream.read() == PACKET_HEADER_START_BYTES[0] && stream.read() == PACKET_HEADER_START_BYTES[1]) { + // second check is not executed unless first one is true + val header = ByteArray(6) + stream.read(header) + val command = CommandType.values().find { it.byte == header[0] } + ?: throw RuntimeException("Unknown command code: ${header[0]}") + val board = header[1] + val packet = header[2] + val length = (header[3].positive * 0x100 + header[4].positive * 0x010 + header[5].positive) * 4 + if (length > 8) { + logger.trace("Received long message with header $header") + } + val payload = ByteArray(length) + DataInputStream(stream).readFully(payload) + handle(DanteMessage(command, board.positive, packet.positive, payload)) + } + } + //TODO handle errors and reconnect + } + + connections[port] = Pair(socket, job) + } + + private suspend fun send(command: CommandType, board: Int, packet: Int, register: Int, data: ByteArray = ByteArray(0), length: Int = (data.size / 4)) { + logger.debug("Sending {}[{}, {}] of size {}*4 to {}", command.name, board, packet, length, register) + sendChannel.send(wrapCommand(command, board, packet, register, length, data)) + } + + private suspend fun handle(response: DanteMessage) { + logger.debug("Received {}", response.toString()) + when (response.command) { + READ, WRITE -> comChannel.send(response) + SINGLE_SPECTRUM_MODE, MAP_MODE, LIST_MODE, WAVEFORM_MODE -> dataChannel.send(response) + } + } + + /** + * Generate next packet number + */ + private fun nextPacket(): Int { + return (packetNumber.incrementAndGet() % 256).toInt() + } + + private fun List.asBuffer(): ByteBuffer { + val buffer = ByteBuffer.allocate(this.size * 4) + buffer.order(ByteOrder.BIG_ENDIAN) + this.forEach { buffer.putInt(it.int) } //safe transform to 4-byte integer + return buffer + } + + private val ByteBuffer.bits: BitSet + get() = BitSet.valueOf(this) + + /** + * Write single or multiple registers + * + */ + private suspend fun writeRegister(board: Int, register: Int, data: List, mask: Long? = null) { + //representing data as byte buffer + val buffer = if (mask != null) { + val oldData = withTimeout(5000) { readRegister(board, register, data.size) } + //(oldData & not_mask) | (newData & mask); + (0 until data.size).map { (oldData[it] and mask.inv()).or(data[it] and mask) }.asBuffer(); + } else { + data.asBuffer() + } + + send(WRITE, board, nextPacket(), register, buffer.array()) + } + + private suspend fun writeRegister(board: Int, register: Int, data: Long, mask: Long? = null) { + writeRegister(board, register, listOf(data), mask) + } + + private suspend fun readRegister(board: Int, register: Int, length: Int = 1): List { + val packet = nextPacket() + send(READ, board, packet, register, length = length) + var message: DanteMessage? = null + //skip other responses + while (message == null || !(message.command == READ && message.packet == packet)) { + message = comChannel.receive() + } + + return sequence { + val intBuffer = ByteBuffer.wrap(message!!.payload).asIntBuffer() + while (intBuffer.hasRemaining()) { + yield(intBuffer.get()) + } + }.map { it.positive }.toList() + } + + suspend fun getFirmwareVersion(): Long { + return readRegister(0, Register.FIRMWARE_VERSION.code)[0] + } + + /** + * Write a single DPP parameter + * @param register number of parameter register + * @param value value of the parameter as unsigned integer + */ + suspend fun writeDPP(board: Int, register: Int, value: Long) { + //sending register number and value in the same command + writeRegister(board, DPP_REGISTER_1.code, listOf(register.toLong(), value)) + writeRegister(board, DPP_CONFIG_COMMIT_OFFSET.code, 0x00000001, 0x00000001) + } + + /** + * Configure single board using provided meta + */ + suspend fun configureBoard(board: Int, meta: Meta) { + val gain = meta.getDouble("gain") + val det_thresh = meta.getInt("detection_threshold") + val pileup_thr = meta.getInt("pileup_threshold") + val en_fil_peak_time = meta.getInt("energy_filter.peaking_time") + val en_fil_flattop = meta.getInt("energy_filter.flat_top") + val fast_peak_time = meta.getInt("fast_filter.peaking_time") + val fast_flattop = meta.getInt("fast_filter.flat_top") + val recovery_time = meta.getValue("recovery_time").long + val zero_peak_rate = meta.getInt("zero_peak_rate") + val inverted_input = meta.getInt("inverted_input", 0) + + assert(en_fil_peak_time in 1..511) + assert(gain in 0.01..(en_fil_peak_time * 2 - 0.01)) + assert(det_thresh in 0..4096) + assert(pileup_thr in 0..4096) + assert(en_fil_flattop in 1..15) + assert(fast_peak_time in 1..31) + assert(fast_flattop in 1..31) + assert(recovery_time in (0.0..pow(2.0, 24.0) - 1)) + assert(zero_peak_rate in 0..500) + assert(inverted_input in listOf(0, 1)) + assert((en_fil_peak_time + en_fil_flattop) * 2 < 1023) + + logger.info("Starting {} board configuration", board) + + writeDPP(board, 128, inverted_input * (1L shl 24)) + writeDPP(board, 162, recovery_time) + writeDPP(board, 181, 0) + writeDPP(board, 185, 0) + writeDPP(board, 175, 1) + writeDPP(board, 160, (pileup_thr / gain).toLong() + (1 shl 31)) + writeDPP(board, 160, (pileup_thr / gain * 2).toLong() and (1 shl 31).inv()) //set bit 2 to zero + writeDPP(board, 152, (det_thresh / gain * fast_peak_time).toLong()) + + if (fast_flattop == 1) { + writeDPP(board, 154, 0) + } else { + writeDPP(board, 154, ceil(fast_flattop.toDouble() / 2).toLong()) // TODO check this + } + + if (zero_peak_rate == 0) { + writeDPP(board, 142, 0) + } else { + writeDPP(board, 142, (1.0 / zero_peak_rate / 10 * 1e6).toLong() + (1 shl 31)) + } + + writeDPP(board, 180, (fast_flattop + 1).toLong()) + + if ((2 * fast_peak_time + fast_flattop) > 4 * en_fil_flattop) { + writeDPP(board, 140, ((2 * fast_peak_time + fast_flattop) * 4 + 1).toLong()) + } else { + writeDPP(board, 140, en_fil_flattop.toLong()) + } + + writeDPP(board, 141, (fast_peak_time + fast_flattop + 4).toLong()) + writeDPP(board, 156, fast_peak_time + 0 * (1L shl 28)) + writeDPP(board, 150, fast_flattop + 0 * (1L shl 28)) + writeDPP(board, 149, en_fil_peak_time + 1 * (1L shl 28)) + writeDPP(board, 150, en_fil_flattop + 1 * (1L shl 28)) + writeDPP(board, 153, en_fil_peak_time * 2 + en_fil_flattop * 2 + 1 * (1L shl 28)) + writeDPP(board, 184, (gain * (1 shl 24) / en_fil_peak_time).toLong() + 1 * (1L shl 28)) + writeDPP(board, 148, 0b11) + writeDPP(board, 128, 0b100 + inverted_input * (1L shl 24)) + writeDPP(board, 128, 1 + inverted_input * (1L shl 24)) + + + logger.info("Finished {} board configuration", board) + boards.find { it.num == board }?.meta = meta + } + + /** + * Configure all boards + */ + suspend fun configureAll(meta: Meta) { + boards.forEach { + configureBoard(it.num, meta) + } + logger.info("Finished configuration of all actibe boards") + } + + /** + * Clear unused data + */ + private suspend fun clearData() { + while (!dataChannel.isEmpty) { + logger.warn("Dumping residual data packet {}", dataChannel.receive().toString()) + } + } + + private suspend fun clearCommunications() { + while (!dataChannel.isEmpty) { + logger.debug("Dumping residual communication packet {}", dataChannel.receive().toString()) + } + } + + /** + * Handle statistics asynchronously + */ + fun handleStatistics(channel: Int, message: ByteBuffer) { + logger.info("Received statistics packet from board {}", channel) + //TODO + } + + /** + * Gather data in list mode + * @param length measurement time in milliseconds + * @param statisticsPackets number of statistics packets per measurement + */ + suspend fun readPoint(length: Int, statisticsInterval: Int = 1000): NumassPoint { + clearData() + + logger.info("Starting list point acquisition {} ms", length) + boards.forEach { + writeRegister(it.num, ACQUISITION_SETTINGS.code, AcquisitionMode.LIST_MODE.long, 0x00000007) + writeRegister(it.num, ACQUISITION_TIME.code, length.toLong()) + writeRegister(it.num, TIME_PER_MAP_POINT.code, statisticsInterval.toLong(), 0x00FFFFFF) + writeRegister(it.num, MAP_POINTS.code, (length.toDouble() / statisticsInterval).toLong(), 0x00FFFFFF) + + if (readRegister(it.num, ACQUISITION_SETTINGS.code)[0] != AcquisitionMode.LIST_MODE.long) { + throw RuntimeException("Set list mode failed") + } + } + + writeRegister(0, ACQUISITION_STATUS.code, 0x00000001, 0x00000001) + + val start = Instant.now() + val builder = NumassProto.Point.newBuilder() + + //collecting packages + + while (Duration.between(start, Instant.now()) < Duration.ofMillis(length + 2000L)) { + try { + //Waiting for a packet for a second. If it is not there, returning and checking time condition + val packet = withTimeout(1000) { dataChannel.receive() } + if (packet.command != LIST_MODE) { + logger.warn("Unexpected packet type: {}", packet.command.name) + continue + } + val channel = packet.board + // get or create new channel block + val channelBuilder = builder.channelsBuilderList.find { it.id.toInt() == channel } + .orElse { + builder.addChannelsBuilder().setId(channel.toLong()) + .also { + //initializing single block + it.addBlocksBuilder().also { + it.time = (start.epochSecond * 1e9 + start.nano).toLong() + it.binSize = 8 // tick in nanos + it.length = (length * 1e6).toLong() //block length in nanos + } + } + } + val blockBuilder = channelBuilder.getBlocksBuilder(0) + val eventsBuilder = blockBuilder.eventsBuilder + + val buffer = ByteBuffer.wrap(packet.payload) + while (buffer.hasRemaining()) { + val firstWord = buffer.getInt() + val secondWord = buffer.getInt() + if (firstWord == STATISTIC_HEADER && secondWord == STATISTIC_HEADER) { + if (buffer.remaining() < 128) { + logger.error("Can't read statistics from list message, 128 bytes expected, but {} found", buffer.remaining()) + break + } else { + val statistics = ByteArray(32 * 4) + buffer.get(statistics) + //TODO use statistics for meta + handleStatistics(channel, ByteBuffer.wrap(statistics)) + } + } else if (firstWord == 0) { + //TODO handle zeros + logger.info("Received zero packet from board {}", channel) + } else { + val time: Long = secondWord.positive shl 14 + firstWord ushr 18 + val amp: Short = (firstWord and 0x0000FFFF).toShort() + eventsBuilder.addTimes(time * 8) + eventsBuilder.addAmplitudes(amp.toLong()) + } + } + } catch (ex: Exception) { + if (ex !is CancellationException) { + logger.error("Exception raised during packet gathering", ex) + } + } + } + //Stopping acquisition just in case + writeRegister(0, ACQUISITION_STATUS.code, 0x00000001, 0x00000000) + + val meta = buildMeta { + boards.first().meta?.let { + putNode("dpp", it) + } + } + val proto = builder.build() + + return ProtoNumassPoint(meta) { proto } + } + + companion object { + const val STATISTIC_HEADER: Int = 0xC0000000.toInt() + + val PACKET_HEADER_START_BYTES = arrayOf(0xAA, 0xEE) + + enum class Register(val code: Int) { + FIRMWARE_VERSION(0), + DPP_REGISTER_1(1), + DPP_REGISTER_2(2), + DPP_CONFIG_COMMIT_OFFSET(3), + ACQUISITION_STATUS(4), + ACQUISITION_TIME(5), + ELAPSED_TIME(6), + ACQUISITION_SETTINGS(7), + WAVEFORM_LENGTH(8), + MAP_POINTS(9), + TIME_PER_MAP_POINT(10), + ETH_CONFIGURATION_DATA(11), + ETHERNET_COMMIT(13), + CALIB_DONE_SIGNALS(14) + } + + enum class CommandType(val byte: Byte) { + READ(0xD0.toByte()), + WRITE(0xD1.toByte()), + SINGLE_SPECTRUM_MODE(0xD2.toByte()), + MAP_MODE(0xD6.toByte()), + LIST_MODE(0xD4.toByte()), + WAVEFORM_MODE(0xD3.toByte()), + } + + enum class AcquisitionMode(val byte: Byte) { + SINGLE_SPECTRUM_MODE(2), + MAP_MODE(6), + LIST_MODE(4), + WAVEFORM_MODE(3); + + val long = byte.toLong() + } + + + /** + * Build command header + */ + fun buildHeader(command: CommandType, board: Byte, packet: Byte, start: Byte, length: Byte): ByteArray { + assert(command in listOf(CommandType.READ, CommandType.WRITE)) + assert(board in 0..255) + assert(packet in 0..255) + assert(length in 0..255) + val header = ByteArray(8) + header[0] = PACKET_HEADER_START_BYTES[0].toByte() + header[1] = PACKET_HEADER_START_BYTES[1].toByte() + header[2] = command.byte + header[3] = board + header[4] = packet + header[5] = start + header[6] = length + return header + } + + /** + * Escape the sequence using DANTE convention + */ + private fun ByteArray.escape(): ByteArray { + return sequence { + this@escape.forEach { + yield(it) + if (it == 0xdd.toByte()) { + yield(it) + } + } + }.toList().toByteArray() + } + + + /** + * Create DANTE command and stuff it. + * @param length size of data array/4 + */ + fun wrapCommand(command: CommandType, board: Int, packet: Int, start: Int, length: Int, data: ByteArray): ByteArray { + if (command == CommandType.READ) { + assert(data.isEmpty()) + } else { + assert(data.size % 4 == 0) + assert(length == data.size / 4) + } + + val header = buildHeader(command, board.byte, packet.byte, start.byte, length.byte) + + val res = (header + data).escape() + return byteArrayOf(0xdd.toByte(), 0xaa.toByte()) + res + byteArrayOf(0xdd.toByte(), 0x55.toByte()) + } + + data class DanteMessage(val command: CommandType, val board: Int, val packet: Int, val payload: ByteArray) { + override fun toString(): String { + return "${command.name}[$board, $packet] of size ${payload.size}" + } + } + } + +} \ No newline at end of file diff --git a/numass-control/dante/src/main/kotlin/inr/numass/control/dante/DanteTest.kt b/numass-control/dante/src/main/kotlin/inr/numass/control/dante/DanteTest.kt new file mode 100644 index 00000000..5b9869ca --- /dev/null +++ b/numass-control/dante/src/main/kotlin/inr/numass/control/dante/DanteTest.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 inr.numass.control.dante + +import hep.dataforge.configure +import hep.dataforge.context.Context +import hep.dataforge.context.Global +import hep.dataforge.meta.KMetaBuilder +import hep.dataforge.meta.buildMeta +import hep.dataforge.nullable +import hep.dataforge.plots.data.DataPlot +import hep.dataforge.plots.output.plotFrame +import hep.dataforge.tables.Adapters +import inr.numass.data.analyzers.NumassAnalyzer +import inr.numass.data.analyzers.SimpleAnalyzer +import inr.numass.data.analyzers.withBinning +import inr.numass.data.api.NumassBlock +import kotlinx.coroutines.runBlocking + + +fun main() { + val client = DanteClient(Global,"192.168.111.120", 8) + client.open() + val meta = buildMeta { + "gain" to 1.0 + "detection_threshold" to 150 + "pileup_threshold" to 1 + "energy_filter" to { + "peaking_time" to 63 + "flat_top" to 2 + } + "fast_filter" to { + "peaking_time" to 4 + "flat_top" to 1 + } + "recovery_time" to 100 + "zero_peak_rate" to 0 + } + runBlocking { + println("Firmware version: ${client.getFirmwareVersion()}") + +// client.reset() +// delay(500) + + client.configureAll(meta) + + val point = client.readPoint(10 * 1000) + + println("***META***") + println(point.meta) + println("***BLOCKS***") + point.blocks.forEach { + println("channel: ${it.channel}") + println("\tlength: ${it.length}") + println("\tevents: ${it.events.count()}") + it.plotAmplitudeSpectrum(plotName = it.channel.toString()) + } + } +} + +fun NumassBlock.plotAmplitudeSpectrum(plotName: String = "spectrum", frameName: String = "", context: Context = Global, metaAction: KMetaBuilder.() -> Unit = {}) { + val meta = buildMeta("meta", metaAction) + val binning = meta.getInt("binning", 20) + val lo = meta.optNumber("window.lo").nullable?.toInt() + val up = meta.optNumber("window.up").nullable?.toInt() + val data = SimpleAnalyzer().getAmplitudeSpectrum(this, meta.getMetaOrEmpty("spectrum")).withBinning(binning, lo, up) + context.plotFrame(plotName) { + val valueAxis = if (meta.getBoolean("normalize", false)) { + NumassAnalyzer.COUNT_RATE_KEY + } else { + NumassAnalyzer.COUNT_KEY + } + plots.configure { + "connectionType" to "step" + "thickness" to 2 + "showLine" to true + "showSymbol" to false + "showErrors" to false + }.setType() + + val plot = DataPlot.plot( + plotName, + data, + Adapters.buildXYAdapter(NumassAnalyzer.CHANNEL_KEY, valueAxis) + ) + plot.configure(meta) + +plot + } + + +} \ No newline at end of file diff --git a/numass-control/gun/build.gradle b/numass-control/gun/build.gradle new file mode 100644 index 00000000..70362097 --- /dev/null +++ b/numass-control/gun/build.gradle @@ -0,0 +1,24 @@ +// +//plugins { +// id 'application' +// id 'org.openjfx.javafxplugin' version '0.0.5' +//} +// +//javafx { +// modules = [ 'javafx.controls' ] +//} + +plugins { + id 'application' +} + +version = "0.1.0" + +if (!hasProperty('mainClass')) { + ext.mainClass = 'inr.numass.control.gun.EGunApplication' +} +mainClassName = mainClass + +dependencies { + compile project(':numass-control') +} \ No newline at end of file diff --git a/numass-control/gun/src/main/kotlin/inr/numass/control/gun/EGun.kt b/numass-control/gun/src/main/kotlin/inr/numass/control/gun/EGun.kt new file mode 100644 index 00000000..c5eae992 --- /dev/null +++ b/numass-control/gun/src/main/kotlin/inr/numass/control/gun/EGun.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 inr.numass.control.gun + +import hep.dataforge.context.Context +import hep.dataforge.context.ContextAware +import hep.dataforge.control.devices.AbstractDevice +import hep.dataforge.control.devices.Device +import hep.dataforge.control.devices.DeviceHub +import hep.dataforge.meta.Meta +import hep.dataforge.meta.Metoid +import hep.dataforge.names.Name +import hep.dataforge.optional +import inr.numass.control.DeviceView +import java.util.* + +@DeviceView(GunDisplay::class) +class EGun(context: Context, meta: Meta) : AbstractDevice(context, meta), DeviceHub, ContextAware, Metoid { + val sources: List by lazy { + meta.getMetaList("source").map {IT6800Device(context, it) } + } + + override val deviceNames: List by lazy{ sources.map { Name.of(it.name) }} + + override fun optDevice(name: Name): Optional { + return sources.find { it.name == name.toString() }.optional + } + + override fun init() { + super.init() + sources.forEach { it.init() } + } + + override fun shutdown() { + sources.forEach { it.shutdown() } + super.shutdown() + } +} \ No newline at end of file diff --git a/numass-control/gun/src/main/kotlin/inr/numass/control/gun/EGunApplication.kt b/numass-control/gun/src/main/kotlin/inr/numass/control/gun/EGunApplication.kt new file mode 100644 index 00000000..96fc2a23 --- /dev/null +++ b/numass-control/gun/src/main/kotlin/inr/numass/control/gun/EGunApplication.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 inr.numass.control.gun + +import hep.dataforge.context.Context +import hep.dataforge.control.devices.Device +import hep.dataforge.control.devices.DeviceFactory +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaUtils +import inr.numass.control.NumassControlApplication +import javafx.stage.Stage + +class EGunApplication: NumassControlApplication() { + override val deviceFactory: DeviceFactory = object :DeviceFactory{ + override val type: String = "numass.lambda" + + override fun build(context: Context, meta: Meta): Device { + return EGun(context, meta) + } + } + + override fun setupStage(stage: Stage, device: EGun) { + stage.title = "Numass gun control" + } + + override fun getDeviceMeta(config: Meta): Meta { + return MetaUtils.findNode(config,"device"){it.getString("type") == "numass.gun"}.orElseThrow{RuntimeException("Gun configuration not found")} + } +} \ No newline at end of file diff --git a/numass-control/gun/src/main/kotlin/inr/numass/control/gun/EGunView.kt b/numass-control/gun/src/main/kotlin/inr/numass/control/gun/EGunView.kt new file mode 100644 index 00000000..1c86898d --- /dev/null +++ b/numass-control/gun/src/main/kotlin/inr/numass/control/gun/EGunView.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 inr.numass.control.gun + +import hep.dataforge.fx.asBooleanProperty +import hep.dataforge.fx.asDoubleProperty +import inr.numass.control.DeviceDisplayFX +import inr.numass.control.indicator +import javafx.geometry.Orientation +import tornadofx.* + +class GunDisplay: DeviceDisplayFX(){ + override fun buildView(device: EGun): UIComponent? { + return EGunView(device) + } +} + + +class EGunView(val gun: EGun) : View() { + override val root = borderpane { + top{ + buttonbar { + button("refresh"){ + action { + gun.sources.forEach { + it.update() + } + } + } + } + } + center { + vbox { + gun.sources.forEach { source -> + hbox { + label(source.name){ + minWidth = 100.0 + } + separator(Orientation.VERTICAL) + + indicator { + bind(source.connectedState.asBooleanProperty()) + } + + val voltageProperty = source.voltageState.asDoubleProperty() + val currentProperty = source.currentState.asDoubleProperty() + + textfield { + + } + + separator(Orientation.VERTICAL) + label("V: ") + label(voltageProperty) + + separator(Orientation.VERTICAL) + + label("I: ") + label(currentProperty) + } + } + } + } + } +} \ No newline at end of file diff --git a/numass-control/gun/src/main/kotlin/inr/numass/control/gun/IT6800Device.kt b/numass-control/gun/src/main/kotlin/inr/numass/control/gun/IT6800Device.kt new file mode 100644 index 00000000..db90f3bf --- /dev/null +++ b/numass-control/gun/src/main/kotlin/inr/numass/control/gun/IT6800Device.kt @@ -0,0 +1,203 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 inr.numass.control.gun + +import hep.dataforge.context.Context +import hep.dataforge.context.launch +import hep.dataforge.control.devices.AbstractDevice +import hep.dataforge.control.ports.GenericPortController +import hep.dataforge.control.ports.PortFactory +import hep.dataforge.control.ports.PortHelper +import hep.dataforge.meta.Meta +import hep.dataforge.nullable +import hep.dataforge.states.valueState +import hep.dataforge.values.ValueType +import kotlinx.coroutines.Job +import kotlinx.coroutines.time.delay +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.time.Duration +import kotlin.experimental.and + + +class IT6800Device(context: Context, meta: Meta) : AbstractDevice(context, meta) { + private val portHelper = PortHelper(this) { context, meta -> + val port = PortFactory.build(meta) + GenericPortController(context, port) { it.length == 26 } + }.apply { + debug = true + } + + private var monitorJob: Job? = null + + val connectedState get() = portHelper.connectedState + + val address: Byte = meta.getValue("address", 0).number.toByte() + + val remoteState = valueState("remote", + setter = { value -> sendBoolean(Command.REMOTE.code, value.boolean) } + ) + + val outputState = valueState("output", + setter = { value -> sendBoolean(Command.OUTPUT.code, value.boolean) } + ) + + var output by outputState.booleanDelegate + + val voltageState = valueState("voltage", + setter = { value -> sendInt(Command.VOLTAGE.code, (value.double * 1000).toInt()) } + ) + + var voltage by voltageState.doubleDelegate + + val currentState = valueState("current", + setter = { value -> sendShort(Command.CURRENT.code, (value.double * 1000).toInt().toShort()) } + ) + + var current by currentState.doubleDelegate + + fun connect() { + connectedState.set(true) + remoteState.set(true) + portHelper.connection.onAnyPhrase(this) { + val buffer = ByteBuffer.wrap(it.toByteArray(Charsets.US_ASCII)) + buffer.order(ByteOrder.LITTLE_ENDIAN) + + if (buffer.get(1) != address) { + //skip + return@onAnyPhrase + } + + val code = buffer.get(2) + when (code) { + Command.REMOTE.code -> { + val value = buffer[3] > 0 + remoteState.update(value) + } + Command.OUTPUT.code -> { + val value = buffer[3] > 0 + outputState.update(value) + } + Command.VOLTAGE.code -> { + val value = buffer.getInt(3) + voltageState.update(value.toDouble() / 1000) + } + Command.CURRENT.code -> { + val value = buffer.getShort(3) + currentState.update(value.toDouble() / 1000) + } + Command.READ.code -> { + val current = buffer.getShort(3) + currentState.update(current.toDouble() / 1000) + val value = buffer.getInt(5) + voltageState.update(value.toDouble() / 1000) + val state = buffer.get(9) + outputState.update(state and 1 > 0) + remoteState.update(state.toInt() ushr 7 and 1 > 0) + } + } + } + } + + override fun init() { + super.init() + connect() + } + + private fun request(command: Byte, data: ByteBuffer): String { + if (data.limit() != 21) kotlin.error("Wrong size of data array") + val buffer = ByteBuffer.allocate(26) + buffer.put(0, START) + buffer.put(1, address) + buffer.put(2, command) + buffer.position(3) + buffer.put(data) + val checksum = (START + address + command + data.array().sum()).rem(256).toByte() + buffer.put(25, checksum) + return String(buffer.array(), Charsets.US_ASCII) + } + + private fun sendBoolean(command: Byte, value: Boolean) { + val data = ByteBuffer.allocate(21) + data.put(0, if (value) 1 else 0) + portHelper.send(request(command, data)) + } + + private fun sendShort(command: Byte, value: Short) { + val data = ByteBuffer.allocate(21) + data.order(ByteOrder.LITTLE_ENDIAN) + data.putShort(0, value) + portHelper.send(request(command, data)) + } + + private fun sendInt(command: Byte, value: Int) { + val data = ByteBuffer.allocate(21) + data.order(ByteOrder.LITTLE_ENDIAN) + data.putInt(0, value) + portHelper.send(request(command, data)) + } + + override fun shutdown() { + portHelper.shutdown() + stopMonitor() + super.shutdown() + } + + /** + * send update request + */ + fun update() { + portHelper.send(request(Command.READ.code, ByteBuffer.allocate(21))) + } + + /** + * Start regular state check + */ + fun startMonitor() { + val interval: Duration = meta.optValue("monitor.interval").nullable?.let { + if (it.type == ValueType.STRING) { + Duration.parse(it.string) + } else { + Duration.ofMillis(it.long) + } + } ?: Duration.ofMinutes(1) + + monitorJob = launch { + while (true) { + update() + delay(interval) + } + } + } + + fun stopMonitor() { + monitorJob?.cancel() + } + + enum class Command(val code: Byte) { + REMOTE(0x20), + OUTPUT(0x21), + VOLTAGE(0x23), + CURRENT(0x24), + READ(0x26), + INFO(0x31) + } + + companion object { + private const val START = (170).toByte() // AA + } +} \ No newline at end of file diff --git a/numass-control/gun/src/main/resources/config/devices.xml b/numass-control/gun/src/main/resources/config/devices.xml new file mode 100644 index 00000000..2f515341 --- /dev/null +++ b/numass-control/gun/src/main/resources/config/devices.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/numass-control/magnet/build.gradle b/numass-control/magnet/build.gradle new file mode 100644 index 00000000..f97d0919 --- /dev/null +++ b/numass-control/magnet/build.gradle @@ -0,0 +1,32 @@ +apply plugin: 'application' + +version = "0.3.0" + +if (!hasProperty('mainClass')) { + ext.mainClass = 'inr.numass.control.magnet.fx.MagnetApp' +} +mainClassName = mainClass + +dependencies { + compile project(':numass-control') +} + +task talkToServer(type: JavaExec) { + description 'Console talk to remote server' + + // Set main property to name of Groovy script class. + main = 'inr.numass.control.magnet.Talk' + + // Set classpath for running the Groovy script. + classpath = sourceSets.main.runtimeClasspath + standardInput = System.in + standardOutput = System.out +} + +task emulate(type: JavaExec){ + group = "application" + main = mainClassName + description = "Test magnet controller with virtual device configuration" + classpath = sourceSets.main.runtimeClasspath + args = ["--config.resource=debug.xml"] +} \ No newline at end of file diff --git a/numass-control/magnet/src/main/kotlin/inr/numass/control/magnet/LambdaHub.kt b/numass-control/magnet/src/main/kotlin/inr/numass/control/magnet/LambdaHub.kt new file mode 100644 index 00000000..1239b405 --- /dev/null +++ b/numass-control/magnet/src/main/kotlin/inr/numass/control/magnet/LambdaHub.kt @@ -0,0 +1,95 @@ +package inr.numass.control.magnet + +import hep.dataforge.context.Context +import hep.dataforge.control.devices.AbstractDevice +import hep.dataforge.control.devices.Device +import hep.dataforge.control.devices.DeviceHub +import hep.dataforge.control.ports.Port +import hep.dataforge.control.ports.PortFactory +import hep.dataforge.description.ValueDef +import hep.dataforge.meta.Meta +import hep.dataforge.names.Name +import hep.dataforge.states.StateDef +import hep.dataforge.useEachMeta +import hep.dataforge.values.ValueType +import inr.numass.control.DeviceDisplayFX +import inr.numass.control.DeviceView +import inr.numass.control.getDisplay +import javafx.scene.Parent +import tornadofx.* +import java.util.* +import kotlin.collections.ArrayList + +@StateDef(value = ValueDef(key = "address", type = arrayOf(ValueType.NUMBER), info = "Current active magnet")) +@DeviceView(LambdaHubDisplay::class) +class LambdaHub(context: Context, meta: Meta) : DeviceHub, AbstractDevice(context, meta) { + + val magnets = ArrayList(); + + private val port: Port = buildPort() + private val controller = LambdaPortController(context, port) + + init { + meta.useEachMeta("magnet") { + magnets.add(LambdaMagnet(controller, it)) + } + + meta.useEachMeta("bind") { bindMeta -> + val first = magnets.find { it.name == bindMeta.getString("first") } + val second = magnets.find { it.name == bindMeta.getString("second") } + val delta = bindMeta.getDouble("delta") + bind(first!!, second!!, delta) + logger.info("Bound magnet $first to magnet $second with delta $delta") + } + } + + /** + * Add symmetric non-blocking conditions to ensure currents in two magnets have difference within given value. + * @param controller + * @param difference + */ + fun bind(first: LambdaMagnet, second: LambdaMagnet, difference: Double) { + first.bound = { i -> Math.abs(second.current.doubleValue - i) <= difference } + second.bound = { i -> Math.abs(first.current.doubleValue - i) <= difference } + } + + private fun buildPort(): Port { + val portMeta = meta.getMetaOrEmpty("port") + return if (portMeta.getString("type") == "debug") { + VirtualLambdaPort(portMeta) + } else { + PortFactory.build(portMeta) + } + } + + + override fun init() { + super.init() + controller.open() + } + + override fun shutdown() { + super.shutdown() + controller.close() + port.close() + } + + override fun optDevice(name: Name): Optional = + magnets.stream().filter { it.name == name.unescaped }.map { it as Device }.findFirst() + + override val deviceNames: List + get() = magnets.map { Name.ofSingle(it.name) } +} + +class LambdaHubDisplay : DeviceDisplayFX() { + override fun buildView(device: LambdaHub): UIComponent? { + return object : View() { + override val root: Parent = vbox { + device.magnets.forEach { + this.add(it.getDisplay().view!!) + } + } + + } + } +} \ No newline at end of file diff --git a/numass-control/magnet/src/main/kotlin/inr/numass/control/magnet/LambdaMagnet.kt b/numass-control/magnet/src/main/kotlin/inr/numass/control/magnet/LambdaMagnet.kt new file mode 100644 index 00000000..fae0088e --- /dev/null +++ b/numass-control/magnet/src/main/kotlin/inr/numass/control/magnet/LambdaMagnet.kt @@ -0,0 +1,333 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.control.magnet + +import hep.dataforge.context.Context +import hep.dataforge.control.devices.AbstractDevice +import hep.dataforge.control.devices.notifyError +import hep.dataforge.control.ports.Port +import hep.dataforge.control.ports.PortFactory +import hep.dataforge.description.ValueDef +import hep.dataforge.exceptions.ControlException +import hep.dataforge.exceptions.PortException +import hep.dataforge.meta.Meta +import hep.dataforge.meta.buildMeta +import hep.dataforge.states.StateDef +import hep.dataforge.states.StateDefs +import hep.dataforge.states.valueState +import hep.dataforge.utils.DateTimeUtils +import hep.dataforge.values.ValueType.* +import inr.numass.control.DeviceView +import inr.numass.control.magnet.fx.MagnetDisplay +import kotlinx.coroutines.runBlocking +import java.time.Duration +import java.time.Instant +import java.time.temporal.ChronoUnit +import java.util.concurrent.Future + +/** + * @author Polina + */ + +@StateDefs( + StateDef(value = ValueDef(key = "current", type = arrayOf(NUMBER), def = "-1", info = "Current current")), + StateDef(value = ValueDef(key = "voltage", type = arrayOf(NUMBER), def = "-1", info = "Current voltage")), + StateDef(value = ValueDef(key = "outCurrent", type = arrayOf(NUMBER), def = "0", info = "Target current"), writable = true), + StateDef(value = ValueDef(key = "outVoltage", type = arrayOf(NUMBER), def = "5.0", info = "Target voltage"), writable = true), + StateDef(value = ValueDef(key = "output", type = arrayOf(BOOLEAN), def = "false", info = "Weather output on or off"), writable = true), + StateDef(value = ValueDef(key = "lastUpdate", type = arrayOf(TIME), def = "0", info = "Time of the last update"), writable = true), + StateDef(value = ValueDef(key = "updating", type = arrayOf(BOOLEAN), def = "false", info = "Shows if current ramping in progress"), writable = true), + StateDef(value = ValueDef(key = "monitoring", type = arrayOf(BOOLEAN), def = "false", info = "Shows if monitoring task is running"), writable = true), + StateDef(value = ValueDef(key = "speed", type = arrayOf(NUMBER), def = "5", info = "Current change speed in Ampere per minute"), writable = true), + StateDef(ValueDef(key = "status", type = [STRING], def = "INIT", enumeration = LambdaMagnet.MagnetStatus::class, info = "Current state of magnet operation")) +) +@DeviceView(MagnetDisplay::class) +class LambdaMagnet(private val controller: LambdaPortController, meta: Meta) : AbstractDevice(controller.context, meta) { + + private var closePortOnShutDown = false + + /** + * @return the address + */ + val address: Int = meta.getInt("address", 1) + + override val name: String = meta.getString("name", "LAMBDA_$address") + //private val scheduler = ScheduledThreadPoolExecutor(1) + + //var listener: MagnetStateListener? = null + // private volatile double current = 0; + + private var monitorTask: Future<*>? = null + private var updateTask: Future<*>? = null + + var lastUpdate by valueState("lastUpdate", getter = { 0 }).timeDelegate + private set + + // read-only values of current output + val current = valueState("current", getter = { s2d(controller.getParameter(address, "MC")) }) + + val voltage = valueState("voltage", getter = { s2d(controller.getParameter(address, "MV")) }) + + val target = valueState("target") + + //output values of current and voltage + private var outCurrent by valueState("outCurrent", getter = { s2d(controller.getParameter(address, "PC")) }) { value -> + setCurrent(value.double) + update(value) + }.doubleDelegate + + private var outVoltage = valueState("outVoltage", getter = { s2d(controller.getParameter(address, "PV")) }) { value -> + if (!controller.setParameter(address, "PV", value.double)) { + notifyError("Can't set the target voltage") + } + update(value) + }.doubleDelegate + + val output = valueState("output", getter = { controller.talk(address, "OUT?") == "OK" }) {value -> + setOutputMode(value.boolean) + if (!value.boolean) { + status = MagnetStatus.OFF + } + update(value) + } + + val monitoring = valueState("monitoring", getter = { monitorTask != null }) { value -> + if (value.boolean) { + startMonitorTask() + } else { + stopMonitorTask() + } + update(value) + } + + /** + * + */ + val updating = valueState("updating", getter = { updateTask != null }) { value -> + if (value.boolean) { + startUpdateTask() + } else { + stopUpdateTask() + } + update(value) + } + + + /** + * current change speed in Ampere per minute + * + * @param speed + */ + var speed by valueState("speed").doubleDelegate + + + var status by valueState("status").enumDelegate() + private set + + /** + * The binding limit for magnet current + */ + var bound: (Double) -> Boolean = { + it < meta.getDouble("maxCurrent", 170.0) + } + + private fun setCurrent(current: Double) { + return if (controller.setParameter(address, "PC", current)) { + lastUpdate = DateTimeUtils.now() + //this.current.update(current) + } else { + notifyError("Can't set the target current") + status = MagnetStatus.ERROR + } + } + + /** + * A setup for single magnet controller + * + * @param context + * @param meta + * @throws ControlException + */ + @Throws(ControlException::class) + constructor(context: Context, meta: Meta) : this(LambdaPortController(context, PortFactory.build(meta.getString("port"))), meta) { + closePortOnShutDown = true + } + + constructor(context: Context, port: Port, address: Int) : this(LambdaPortController(context, port), buildMeta { "address" to address }) + + @Throws(ControlException::class) + override fun init() { + super.init() + controller.open() + } + + @Throws(ControlException::class) + override fun shutdown() { + super.shutdown() + try { + if (closePortOnShutDown) { + controller.close() + controller.port.close() + } + } catch (ex: Exception) { + throw ControlException("Failed to close the port", ex) + } + + } + + /** + * Extract number from LAMBDA response + * + * @param str + * @return + */ + private fun s2d(str: String): Double = java.lang.Double.valueOf(str) + + /** + * Calculate next current step + */ + private fun nextI(measuredI: Double, targetI: Double): Double { + var step = if (lastUpdate == Instant.EPOCH) { + MIN_UP_STEP_SIZE + } else { + //Choose optimal speed but do not exceed maximum speed + Math.min(MAX_STEP_SIZE, lastUpdate.until(DateTimeUtils.now(), ChronoUnit.MILLIS).toDouble() / 60000.0 * speed) + } + + val res = if (targetI > measuredI) { + step = Math.max(MIN_UP_STEP_SIZE, step) + Math.min(targetI, measuredI + step) + } else { + step = Math.max(MIN_DOWN_STEP_SIZE, step) + Math.max(targetI, measuredI - step) + } + + // не вводитÑÑ Ñ‚Ð¾Ðº меньше 0.5 + return if (res < 0.5 && targetI > CURRENT_PRECISION) { + 0.5 + } else if (res < 0.5 && targetI < CURRENT_PRECISION) { + 0.0 + } else { + res + } + } + + /** + * Start recursive updates of current with given delays between updates. If + * delay is 0 then updates are made immediately. + * + * @param targetI + * @param delay + */ + private fun startUpdateTask(delay: Long = DEFAULT_DELAY.toLong()) { + assert(delay > 0) + stopUpdateTask() + updateTask = repeatOnDeviceThread(Duration.ofMillis(delay)) { + try { + val measuredI = current.readBlocking().double + val targetI = target.doubleValue + if (Math.abs(measuredI - targetI) > CURRENT_PRECISION) { + val nextI = nextI(measuredI, targetI) + status = if (bound(nextI)) { + setCurrent(nextI) + MagnetStatus.OK + } else { + MagnetStatus.BOUND + } + } else { + setCurrent(targetI) + updating.set(false) + } + + } catch (ex: PortException) { + notifyError("Error in update task", ex) + updating.set(false) + } + } + updateState("updating", true) + } + + /** + * Cancel current update task + */ + private fun stopUpdateTask() { + updateTask?.cancel(false) + } + + @Throws(PortException::class) + private fun setOutputMode(out: Boolean) { + val outState: Int = if (out) 1 else 0 + if (!controller.setParameter(address, "OUT", outState)) { + notifyError("Can't set output mode") + } else { + updateState("output", out) + } + } + + /** + * Cancel current monitoring task + */ + private fun stopMonitorTask() { + monitorTask?.let { + it.cancel(true) + monitorTask = null + } + } + + /** + * Start monitoring task which checks for magnet status and then waits for + * fixed time. + * + * @param delay an interval between scans in milliseconds + */ + private fun startMonitorTask(delay: Long = DEFAULT_MONITOR_DELAY.toLong()) { + assert(delay >= 1000) + stopMonitorTask() + + monitorTask = repeatOnDeviceThread(Duration.ofMillis(delay)) { + try { + runBlocking { + voltage.read() + current.read() + } + } catch (ex: PortException) { + notifyError("Port connection exception during status measurement", ex) + stopMonitorTask() + } + } + } + + enum class MagnetStatus { + INIT, // no information + OFF, // Magnet output is off + OK, // Magnet ouput is on + ERROR, // Some error + BOUND // Magnet in bound mode + } + + companion object { + + const val CURRENT_PRECISION = 0.05 + const val DEFAULT_DELAY = 1 + const val DEFAULT_MONITOR_DELAY = 2000 + const val MAX_STEP_SIZE = 0.2 + const val MIN_UP_STEP_SIZE = 0.01 + const val MIN_DOWN_STEP_SIZE = 0.05 + const val MAX_SPEED = 5.0 // 5 A per minute + + } +} + diff --git a/numass-control/magnet/src/main/kotlin/inr/numass/control/magnet/LambdaPortController.kt b/numass-control/magnet/src/main/kotlin/inr/numass/control/magnet/LambdaPortController.kt new file mode 100644 index 00000000..8a7be45b --- /dev/null +++ b/numass-control/magnet/src/main/kotlin/inr/numass/control/magnet/LambdaPortController.kt @@ -0,0 +1,79 @@ +package inr.numass.control.magnet + +import hep.dataforge.context.Context +import hep.dataforge.control.ports.GenericPortController +import hep.dataforge.control.ports.Port +import hep.dataforge.exceptions.PortException +import org.slf4j.LoggerFactory +import java.text.DecimalFormat +import java.time.Duration + +//@ValueDef(name = "timeout", type = [(ValueType.NUMBER)], def = "400", info = "A timeout for port response") +class LambdaPortController(context: Context, port: Port, val timeout : Duration = Duration.ofMillis(200)) : GenericPortController(context, port, "\r") { + private var currentAddress: Int = -1; + + fun setAddress(address: Int) { + if(currentAddress!= address) { + val response = sendAndWait("ADR $address\r", timeout) { true }.trim() + if (response == "OK") { + currentAddress = address + } else { + throw RuntimeException("Failed to set address to LAMBDA device on $port") + } + } + } + + /** + * perform series of synchronous actions ensuring that all of them have the same address + */ + fun talk(address: Int, action: GenericPortController.() -> R): R { + synchronized(this) { + setAddress(address) + return action.invoke(this) + } + } + + + @Throws(PortException::class) + fun talk(addres: Int, request: String): String { + return talk(addres) { + try { + send(request + "\r") + waitFor(timeout).trim() + } catch (tex: Port.PortTimeoutException) { + //Single retry on timeout + LoggerFactory.getLogger(javaClass).warn("A timeout exception for request '$request'. Making another attempt.") + send(request + "\r") + waitFor(timeout).trim() + } + } + } + + fun getParameter(address: Int, name: String): String = talk(address, "$name?") + + fun setParameter(address: Int, key: String, state: String): Boolean { + try { + return "OK" == talk(address, "$key $state") + } catch (ex: Exception) { + logger.error("Failed to send message", ex) + return false + } + } + + fun setParameter(address: Int, key: String, state: Int): Boolean = setParameter(address, key, state.toString()) + + fun setParameter(address: Int, key: String, state: Double): Boolean = setParameter(address, key, d2s(state)) + + + companion object { + private val LAMBDA_FORMAT = DecimalFormat("###.##") + /** + * Method converts double to LAMBDA string + * + * @param d double that should be converted to string + * @return string + */ + private fun d2s(d: Double): String = LAMBDA_FORMAT.format(d) + + } +} \ No newline at end of file diff --git a/numass-control/magnet/src/main/kotlin/inr/numass/control/magnet/VirtualLambdaPort.kt b/numass-control/magnet/src/main/kotlin/inr/numass/control/magnet/VirtualLambdaPort.kt new file mode 100644 index 00000000..f3190f18 --- /dev/null +++ b/numass-control/magnet/src/main/kotlin/inr/numass/control/magnet/VirtualLambdaPort.kt @@ -0,0 +1,145 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.control.magnet + +import hep.dataforge.control.ports.VirtualPort +import hep.dataforge.meta.Meta +import hep.dataforge.useEachMeta +import org.slf4j.LoggerFactory +import java.time.Duration +import java.util.* + +/** + * + * @author Alexander Nozik + */ +class VirtualLambdaPort(meta: Meta) : VirtualPort(meta) { + var currentAddress = -1 + private set + + val statusMap = HashMap() + override val name: String = meta.getString("name", "virtual::numass.lambda") + + override val delimeter: String = "\r" + + init { + meta.useEachMeta("magnet") { + val num = it.getInt("address", 1) + val resistance = it.getDouble("resistance", 1.0) + statusMap[num] = VirtualMagnetStatus(resistance) + } + } + + override fun toString(): String = name + + override fun evaluateRequest(request: String) { + val command: String + var value = "" + val split = request.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + if (split.size == 1) { + command = request + } else { + command = split[0] + value = split[1] + } + try { + evaluateRequest(command.trim { it <= ' ' }, value.trim { it <= ' ' }) + } catch (ex: RuntimeException) { + receive("FAIL".toByteArray())//TODO ÐºÐ°ÐºÐ°Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð° правильнаÑ? + LoggerFactory.getLogger(javaClass).error("Request evaluation failure", ex) + } + + } + + private fun sendOK() { + planResponse("OK", latency) + } + + private fun evaluateRequest(comand: String, value: String) { + when (comand) { + "ADR" -> { + val address = Integer.parseInt(value) + if (statusMap.containsKey(address)) { + currentAddress = address + sendOK() + } + } + "ADR?" -> { + planResponse(Integer.toString(currentAddress), latency) + } + "OUT" -> { + val state = Integer.parseInt(value) + currentMagnet().out = state == 1 + sendOK() + } + "OUT?" -> { + val out = currentMagnet().out + if (out) { + planResponse("ON", latency) + } else { + planResponse("OFF", latency) + } + } + "PC" -> { + val doubleValue = value.toDouble() + val current = if (doubleValue < 0.5) { + 0.0 + } else { + doubleValue + } + currentMagnet().current = current + sendOK() + } + "PC?" -> { + planResponse(java.lang.Double.toString(currentMagnet().current), latency) + } + "MC?" -> { + planResponse(java.lang.Double.toString(currentMagnet().current), latency) + } + "PV?" -> { + planResponse(java.lang.Double.toString(currentMagnet().voltage), latency) + } + "MV?" -> { + planResponse(java.lang.Double.toString(currentMagnet().voltage), latency) + } + else -> LoggerFactory.getLogger(javaClass).warn("Unknown command {}", comand) + } + } + + private fun currentMagnet(): VirtualMagnetStatus { + if (currentAddress < 0) { + throw RuntimeException() + } + return statusMap[currentAddress]!! + } + + inner class VirtualMagnetStatus(val resistance: Double, + var on: Boolean = true, + var out: Boolean = false, + var current: Double = 0.0) { + + val voltage get() = current * resistance + } + + override fun toMeta(): Meta { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + + companion object { + private val latency = Duration.ofMillis(50) + } +} diff --git a/numass-control/magnet/src/main/kotlin/inr/numass/control/magnet/fx/MagnetApp.kt b/numass-control/magnet/src/main/kotlin/inr/numass/control/magnet/fx/MagnetApp.kt new file mode 100644 index 00000000..296163d1 --- /dev/null +++ b/numass-control/magnet/src/main/kotlin/inr/numass/control/magnet/fx/MagnetApp.kt @@ -0,0 +1,29 @@ +package inr.numass.control.magnet.fx + +import hep.dataforge.context.Context +import hep.dataforge.control.devices.Device +import hep.dataforge.control.devices.DeviceFactory +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaUtils +import inr.numass.control.NumassControlApplication +import inr.numass.control.magnet.LambdaHub +import javafx.stage.Stage + +class MagnetApp: NumassControlApplication() { + + override val deviceFactory: DeviceFactory = object :DeviceFactory{ + override val type: String = "numass.lambda" + + override fun build(context: Context, meta: Meta): Device { + return LambdaHub(context, meta) + } + } + + override fun setupStage(stage: Stage, device: LambdaHub) { + stage.title = "Numass magnet control" + } + + override fun getDeviceMeta(config: Meta): Meta { + return MetaUtils.findNode(config,"device"){it.getString("name") == "numass.magnets"}.orElseThrow{RuntimeException("Magnet configuration not found")} + } +} \ No newline at end of file diff --git a/numass-control/magnet/src/main/kotlin/inr/numass/control/magnet/fx/MagnetDisplay.kt b/numass-control/magnet/src/main/kotlin/inr/numass/control/magnet/fx/MagnetDisplay.kt new file mode 100644 index 00000000..47fba9c2 --- /dev/null +++ b/numass-control/magnet/src/main/kotlin/inr/numass/control/magnet/fx/MagnetDisplay.kt @@ -0,0 +1,203 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.control.magnet.fx + +import hep.dataforge.exceptions.PortException +import hep.dataforge.fx.asDoubleProperty +import hep.dataforge.states.ValueState +import inr.numass.control.DeviceDisplayFX +import inr.numass.control.magnet.LambdaMagnet +import javafx.application.Platform +import javafx.beans.value.ObservableDoubleValue +import javafx.beans.value.ObservableValue +import javafx.scene.control.* +import javafx.scene.layout.AnchorPane +import javafx.scene.paint.Color +import tornadofx.* + +/** + * FXML Controller class + * + * @author Alexander Nozik + */ +class MagnetDisplay : DeviceDisplayFX() { + override fun buildView(device: LambdaMagnet): MagnetControllerComponent? { + return MagnetControllerComponent(device) + } + + + inner class MagnetControllerComponent(val device: LambdaMagnet) : Fragment() { + + override val root: AnchorPane by fxml("/fxml/SingleMagnet.fxml") + + var showConfirmation = true + + + val current: ObservableDoubleValue = device.current.asDoubleProperty() + val voltage: ObservableDoubleValue = device.voltage.asDoubleProperty() + + + val labelI: Label by fxid() + val labelU: Label by fxid() + val targetIField: TextField by fxid() + val magnetName: Label by fxid() + val monitorButton: ToggleButton by fxid() + val statusLabel: Label by fxid() + val setButton: ToggleButton by fxid() + val magnetSpeedField: TextField by fxid() + + + init { + targetIField.textProperty().addListener { _: ObservableValue, oldValue: String, newValue: String -> + if (!newValue.matches("\\d*(\\.)?\\d*".toRegex())) { + targetIField.text = oldValue + } + } + + magnetSpeedField.textProperty().addListener { _: ObservableValue, oldValue: String, newValue: String -> + if (!newValue.matches("\\d*(\\.)?\\d*".toRegex())) { + magnetSpeedField.text = oldValue + } + } + + magnetName.text = device.name + magnetSpeedField.text = device.speed.toString() + + current.onChange { + runLater { + labelI.text = String.format("%.2f",it) + } + } + + voltage.onChange { + runLater { + labelU.text = String.format("%.4f",it) + } + } + + device.states.getState("status")?.onChange{ + runLater { + this.statusLabel.text = it.string + } + } + + device.output.onChange { + Platform.runLater { + if (it.boolean) { + this.statusLabel.textFill = Color.BLUE + } else { + this.statusLabel.textFill = Color.BLACK + } + } + } + + device.updating.onChange { + val updateTaskRunning = it.boolean + runLater { + this.setButton.isSelected = updateTaskRunning + targetIField.isDisable = updateTaskRunning + } + } + + device.monitoring.onChange { + runLater { + monitorButton.isScaleShape = it.boolean + } + } + + setButton.selectedProperty().onChange { + try { + setOutputOn(it) + } catch (ex: PortException) { + displayError(this.device.name, null, ex) + } + } + + monitorButton.selectedProperty().onChange { + if (device.monitoring.booleanValue != it) { + if (it) { + device.monitoring.set(true) + } else { + device.monitoring.set(false) + this.labelU.text = "----" + } + } + } + + magnetSpeedField.text = device.speed.toString() + } + + + /** + * Show confirmation dialog + */ + private fun confirm(): Boolean { + return if (showConfirmation) { + val alert = Alert(Alert.AlertType.WARNING) + alert.contentText = "Изменение токов в ÑверхпроводÑщих магнитах можно производить только при выключенном напрÑжении на Ñпектрометре." + "\nÐ’Ñ‹ уверены что напрÑжение выключено?" + alert.headerText = "Проверьте напрÑжение на Ñпектрометре!" + alert.height = 150.0 + alert.title = "Внимание!" + alert.buttonTypes.clear() + alert.buttonTypes.addAll(ButtonType.YES, ButtonType.CANCEL) + + alert.showAndWait().orElse(ButtonType.CANCEL) == ButtonType.YES + } else { + true + } + } + + private fun setOutputOn(outputOn: Boolean) { + if (outputOn && confirm()) { + val speed = java.lang.Double.parseDouble(magnetSpeedField.text) + if (speed > 0 && speed <= LambdaMagnet.MAX_SPEED) { + device.speed = speed + magnetSpeedField.isDisable = true + device.target.set(targetIField.text.toDouble()) + device.output.set(true) + device.updating.set(true) + } else { + val alert = Alert(Alert.AlertType.ERROR) + alert.contentText = null + alert.headerText = "ÐедопуÑтимое значение ÑкороÑти Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ñ‚Ð¾ÐºÐ°" + alert.title = "Ошибка!" + alert.show() + setButton.isSelected = false + magnetSpeedField.text = device.speed.toString() + } + } else { + device.updating.set(false) + targetIField.isDisable = false + magnetSpeedField.isDisable = false + } + } + + private fun displayError(name: String, errorMessage: String?, throwable: Throwable) { + Platform.runLater { + this.statusLabel.text = "ERROR" + this.statusLabel.textFill = Color.RED + } + device.logger.error("ERROR: {}", errorMessage, throwable) + // MagnetStateListener.super.error(address, errorMessage, throwable); //To change body of generated methods, choose Tools | Templates. + } + + fun displayState(state: String) { + Platform.runLater { this.statusLabel.text = state } + } + } + +} + diff --git a/numass-control/magnet/src/main/kotlin/inr/numass/control/magnet/test/CLITest.kt b/numass-control/magnet/src/main/kotlin/inr/numass/control/magnet/test/CLITest.kt new file mode 100644 index 00000000..e73e3305 --- /dev/null +++ b/numass-control/magnet/src/main/kotlin/inr/numass/control/magnet/test/CLITest.kt @@ -0,0 +1,63 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package inr.numass.control.magnet.test + +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.Logger +import hep.dataforge.context.Global +import hep.dataforge.control.ports.GenericPortController +import hep.dataforge.control.ports.Port +import hep.dataforge.control.ports.PortFactory +import hep.dataforge.exceptions.PortException +import hep.dataforge.utils.DateTimeUtils +import org.slf4j.LoggerFactory +import java.io.BufferedReader +import java.io.InputStreamReader +import java.time.Duration +import java.util.* + + +/** + * @param args the command line arguments + */ + +fun main(args: Array) { + Locale.setDefault(Locale.US) + val rootLogger = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME) as ch.qos.logback.classic.Logger + rootLogger.level = Level.INFO + + var portName = "/dev/ttyr00" + + if (args.isNotEmpty()) { + portName = args[0] + } + val handler: Port + handler = PortFactory.build(portName) + val controller = GenericPortController(Global, handler, "\r") + + // LambdaMagnet controller = new LambdaMagnet(handler, 1); + val reader = BufferedReader(InputStreamReader(System.`in`)) + + System.out.printf("INPUT > ") + var nextString = reader.readLine() + + while ("exit" != nextString) { + try { + val start = DateTimeUtils.now() + val answer = controller.sendAndWait(nextString + "\r", Duration.ofSeconds(1)) + //String answer = controller.request(nextString); + System.out.printf("ANSWER (latency = %s): %s;%n", Duration.between(start, DateTimeUtils.now()), answer.trim { it <= ' ' }) + } catch (ex: PortException) { + ex.printStackTrace() + } + + System.out.printf("INPUT > ") + nextString = reader.readLine() + } + + handler.close() + +} diff --git a/numass-control/magnet/src/main/kotlin/inr/numass/control/magnet/test/SetCurrent.kt b/numass-control/magnet/src/main/kotlin/inr/numass/control/magnet/test/SetCurrent.kt new file mode 100644 index 00000000..1c92907a --- /dev/null +++ b/numass-control/magnet/src/main/kotlin/inr/numass/control/magnet/test/SetCurrent.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.control.magnet.test + +import hep.dataforge.context.Global +import hep.dataforge.control.ports.ComPort +import inr.numass.control.magnet.LambdaMagnet +import jssc.SerialPortException + +/** + * + * @author Alexander Nozik + */ +object SetCurrent { + + /** + * @param args the command line arguments + */ + @Throws(SerialPortException::class) + @JvmStatic + fun main(args: Array) { + if (args.size < 3) { + throw IllegalArgumentException("Wrong number of parameters") + } + val comName = args[0] + val lambdaaddress = args[1].toInt() + val current = args[2].toDouble() + + val port = ComPort.create(comName) + + val magnet = LambdaMagnet(Global, port, lambdaaddress) + magnet.target.set(current) + magnet.output.set(true) + + } + +} diff --git a/numass-control/magnet/src/main/resources/debug.xml b/numass-control/magnet/src/main/resources/debug.xml new file mode 100644 index 00000000..fa5730e8 --- /dev/null +++ b/numass-control/magnet/src/main/resources/debug.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/numass-control/magnet/src/main/resources/fxml/SingleMagnet.fxml b/numass-control/magnet/src/main/resources/fxml/SingleMagnet.fxml new file mode 100644 index 00000000..c6febe53 --- /dev/null +++ b/numass-control/magnet/src/main/resources/fxml/SingleMagnet.fxml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/numass-control/magnet/src/main/resources/logback.xml b/numass-control/magnet/src/main/resources/logback.xml new file mode 100644 index 00000000..3bf27e63 --- /dev/null +++ b/numass-control/magnet/src/main/resources/logback.xml @@ -0,0 +1,23 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + %d{HH:mm:ss.SSS} %msg%n + + + + + + + + + + + \ No newline at end of file diff --git a/numass-control/magnet/src/test/kotlin/inr/numass/control/magnet/VirtualLambdaPortTest.kt b/numass-control/magnet/src/test/kotlin/inr/numass/control/magnet/VirtualLambdaPortTest.kt new file mode 100644 index 00000000..d075f47b --- /dev/null +++ b/numass-control/magnet/src/test/kotlin/inr/numass/control/magnet/VirtualLambdaPortTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 inr.numass.control.magnet + +import hep.dataforge.context.Global +import hep.dataforge.meta.buildMeta +import org.junit.Assert.assertEquals +import org.junit.Test + +class VirtualLambdaPortTest{ + val magnetMeta = buildMeta { + node("magnet") { + "address" to 2 + } + } + + @Test + fun testSendOk(){ + val port = VirtualLambdaPort(magnetMeta) + val controller = LambdaPortController(Global,port) + controller.setAddress(2) + assertEquals(2,port.currentAddress) + } +} \ No newline at end of file diff --git a/numass-control/msp/build.gradle b/numass-control/msp/build.gradle new file mode 100644 index 00000000..97bce500 --- /dev/null +++ b/numass-control/msp/build.gradle @@ -0,0 +1,13 @@ +apply plugin: 'application' + +version = "0.4.0" + +if (!hasProperty('mainClass')) { + ext.mainClass = 'inr.numass.control.msp.MspApp' +} +mainClassName = mainClass + + +dependencies { + compile project(':numass-control') +} \ No newline at end of file diff --git a/numass-control/msp/docs/ASCII Protocol User Manual SP1040016.100.pdf b/numass-control/msp/docs/ASCII Protocol User Manual SP1040016.100.pdf new file mode 100644 index 00000000..0fbc3266 --- /dev/null +++ b/numass-control/msp/docs/ASCII Protocol User Manual SP1040016.100.pdf @@ -0,0 +1,4933 @@ +%PDF-1.4 %âãÏÓ +574 0 obj <> endobj +xref +574 665 +0000000016 00000 n +0000015416 00000 n +0000013596 00000 n +0000015531 00000 n +0000015574 00000 n +0000015972 00000 n +0000016095 00000 n +0000016218 00000 n +0000016341 00000 n +0000016464 00000 n +0000016587 00000 n +0000016710 00000 n +0000016833 00000 n +0000016956 00000 n +0000017079 00000 n +0000017202 00000 n +0000017325 00000 n +0000017448 00000 n +0000017571 00000 n +0000017694 00000 n +0000017817 00000 n +0000017940 00000 n +0000018063 00000 n +0000018186 00000 n +0000018309 00000 n +0000018432 00000 n +0000018555 00000 n +0000018678 00000 n +0000018801 00000 n +0000018923 00000 n +0000019044 00000 n +0000027929 00000 n +0000028574 00000 n +0000028821 00000 n +0000028923 00000 n +0000030394 00000 n +0000031400 00000 n +0000032601 00000 n +0000033720 00000 n +0000034837 00000 n +0000035286 00000 n +0000035453 00000 n +0000035718 00000 n +0000035971 00000 n +0000037243 00000 n +0000039487 00000 n +0000040740 00000 n +0000075867 00000 n +0000088994 00000 n +0000115003 00000 n +0000115175 00000 n +0000115364 00000 n +0000115550 00000 n +0000115733 00000 n +0000115916 00000 n +0000116095 00000 n +0000116271 00000 n +0000116447 00000 n +0000116624 00000 n +0000116804 00000 n +0000117021 00000 n +0000117190 00000 n +0000117370 00000 n +0000117539 00000 n +0000117715 00000 n +0000117884 00000 n +0000118064 00000 n +0000118233 00000 n +0000118413 00000 n +0000118582 00000 n +0000118759 00000 n +0000118938 00000 n +0000119117 00000 n +0000119300 00000 n +0000119483 00000 n +0000119665 00000 n +0000119849 00000 n +0000120018 00000 n +0000120187 00000 n +0000120331 00000 n +0000120503 00000 n +0000120696 00000 n +0000120886 00000 n +0000121078 00000 n +0000121271 00000 n +0000121460 00000 n +0000121654 00000 n +0000121832 00000 n +0000122027 00000 n +0000122220 00000 n +0000122404 00000 n +0000122588 00000 n +0000122781 00000 n +0000122961 00000 n +0000123142 00000 n +0000123319 00000 n +0000123512 00000 n +0000123696 00000 n +0000123915 00000 n +0000124127 00000 n +0000124310 00000 n +0000124493 00000 n +0000124682 00000 n +0000124863 00000 n +0000125038 00000 n +0000125221 00000 n +0000125396 00000 n +0000125558 00000 n +0000125724 00000 n +0000125899 00000 n +0000126080 00000 n +0000126255 00000 n +0000126427 00000 n +0000126608 00000 n +0000126780 00000 n +0000126955 00000 n +0000127133 00000 n +0000127308 00000 n +0000127483 00000 n +0000127658 00000 n +0000127830 00000 n +0000128014 00000 n +0000128186 00000 n +0000128370 00000 n +0000128553 00000 n +0000128725 00000 n +0000128897 00000 n +0000129069 00000 n +0000129265 00000 n +0000129437 00000 n +0000129606 00000 n +0000129789 00000 n +0000129969 00000 n +0000130141 00000 n +0000130313 00000 n +0000130485 00000 n +0000130676 00000 n +0000130851 00000 n +0000131020 00000 n +0000131207 00000 n +0000131384 00000 n +0000131556 00000 n +0000131728 00000 n +0000131911 00000 n +0000132086 00000 n +0000132255 00000 n +0000132439 00000 n +0000132619 00000 n +0000132800 00000 n +0000132984 00000 n +0000133161 00000 n +0000133341 00000 n +0000133538 00000 n +0000133719 00000 n +0000133896 00000 n +0000134076 00000 n +0000134282 00000 n +0000134479 00000 n +0000134660 00000 n +0000134840 00000 n +0000135031 00000 n +0000135227 00000 n +0000135404 00000 n +0000135584 00000 n +0000135767 00000 n +0000135955 00000 n +0000136132 00000 n +0000136309 00000 n +0000136491 00000 n +0000136676 00000 n +0000136853 00000 n +0000137033 00000 n +0000137220 00000 n +0000137402 00000 n +0000137577 00000 n +0000137754 00000 n +0000137951 00000 n +0000138148 00000 n +0000138325 00000 n +0000138502 00000 n +0000138696 00000 n +0000138865 00000 n +0000139075 00000 n +0000139256 00000 n +0000139433 00000 n +0000139620 00000 n +0000139795 00000 n +0000139967 00000 n +0000140144 00000 n +0000140313 00000 n +0000140485 00000 n +0000140654 00000 n +0000140831 00000 n +0000141000 00000 n +0000141172 00000 n +0000141341 00000 n +0000141513 00000 n +0000141685 00000 n +0000141854 00000 n +0000142023 00000 n +0000142195 00000 n +0000142364 00000 n +0000142541 00000 n +0000142716 00000 n +0000142885 00000 n +0000143057 00000 n +0000143234 00000 n +0000143414 00000 n +0000143583 00000 n +0000143761 00000 n +0000143936 00000 n +0000144123 00000 n +0000144303 00000 n +0000144514 00000 n +0000144694 00000 n +0000144863 00000 n +0000145052 00000 n +0000145236 00000 n +0000145416 00000 n +0000145585 00000 n +0000145768 00000 n +0000145937 00000 n +0000146123 00000 n +0000146311 00000 n +0000146486 00000 n +0000146664 00000 n +0000146836 00000 n +0000147029 00000 n +0000147198 00000 n +0000147360 00000 n +0000147522 00000 n +0000147694 00000 n +0000147866 00000 n +0000148038 00000 n +0000148207 00000 n +0000148376 00000 n +0000148575 00000 n +0000148747 00000 n +0000148916 00000 n +0000149063 00000 n +0000149203 00000 n +0000149347 00000 n +0000149500 00000 n +0000149647 00000 n +0000149816 00000 n +0000149963 00000 n +0000150110 00000 n +0000150269 00000 n +0000150416 00000 n +0000150560 00000 n +0000150710 00000 n +0000150854 00000 n +0000151001 00000 n +0000151170 00000 n +0000151317 00000 n +0000151464 00000 n +0000151614 00000 n +0000151776 00000 n +0000151923 00000 n +0000152067 00000 n +0000152217 00000 n +0000152361 00000 n +0000152508 00000 n +0000152674 00000 n +0000152821 00000 n +0000152968 00000 n +0000153115 00000 n +0000153259 00000 n +0000153406 00000 n +0000153550 00000 n +0000153694 00000 n +0000153841 00000 n +0000153985 00000 n +0000154129 00000 n +0000154282 00000 n +0000154429 00000 n +0000154582 00000 n +0000154741 00000 n +0000154900 00000 n +0000155053 00000 n +0000155222 00000 n +0000155375 00000 n +0000155519 00000 n +0000155672 00000 n +0000155828 00000 n +0000155981 00000 n +0000156131 00000 n +0000156300 00000 n +0000156459 00000 n +0000156603 00000 n +0000156747 00000 n +0000156943 00000 n +0000157124 00000 n +0000157268 00000 n +0000157415 00000 n +0000157571 00000 n +0000157727 00000 n +0000157889 00000 n +0000158048 00000 n +0000158207 00000 n +0000158382 00000 n +0000158538 00000 n +0000158685 00000 n +0000158838 00000 n +0000158997 00000 n +0000159150 00000 n +0000159325 00000 n +0000159515 00000 n +0000159659 00000 n +0000159861 00000 n +0000160045 00000 n +0000160189 00000 n +0000160348 00000 n +0000160507 00000 n +0000160669 00000 n +0000160859 00000 n +0000161031 00000 n +0000161209 00000 n +0000161356 00000 n +0000161537 00000 n +0000161690 00000 n +0000161834 00000 n +0000162070 00000 n +0000162272 00000 n +0000162456 00000 n +0000162600 00000 n +0000162744 00000 n +0000162888 00000 n +0000163032 00000 n +0000163176 00000 n +0000163323 00000 n +0000163467 00000 n +0000163611 00000 n +0000163755 00000 n +0000163899 00000 n +0000164058 00000 n +0000164202 00000 n +0000164355 00000 n +0000164502 00000 n +0000164646 00000 n +0000164790 00000 n +0000164937 00000 n +0000165081 00000 n +0000165228 00000 n +0000165375 00000 n +0000165553 00000 n +0000165697 00000 n +0000165853 00000 n +0000166015 00000 n +0000166159 00000 n +0000166303 00000 n +0000166447 00000 n +0000166591 00000 n +0000166741 00000 n +0000166885 00000 n +0000167029 00000 n +0000167173 00000 n +0000167320 00000 n +0000167464 00000 n +0000167611 00000 n +0000167758 00000 n +0000167930 00000 n +0000168070 00000 n +0000168217 00000 n +0000168361 00000 n +0000168505 00000 n +0000168649 00000 n +0000168793 00000 n +0000168949 00000 n +0000169093 00000 n +0000169246 00000 n +0000169390 00000 n +0000169534 00000 n +0000169678 00000 n +0000169837 00000 n +0000169984 00000 n +0000170128 00000 n +0000170287 00000 n +0000170437 00000 n +0000170599 00000 n +0000170752 00000 n +0000170908 00000 n +0000171052 00000 n +0000171196 00000 n +0000171346 00000 n +0000171490 00000 n +0000171634 00000 n +0000171796 00000 n +0000171943 00000 n +0000172087 00000 n +0000172259 00000 n +0000172403 00000 n +0000172547 00000 n +0000172703 00000 n +0000172856 00000 n +0000173000 00000 n +0000173144 00000 n +0000173288 00000 n +0000173447 00000 n +0000173606 00000 n +0000173762 00000 n +0000173915 00000 n +0000174059 00000 n +0000174218 00000 n +0000174374 00000 n +0000174518 00000 n +0000174662 00000 n +0000174824 00000 n +0000174968 00000 n +0000175140 00000 n +0000175293 00000 n +0000175452 00000 n +0000175608 00000 n +0000175767 00000 n +0000175926 00000 n +0000176082 00000 n +0000176238 00000 n +0000176394 00000 n +0000176534 00000 n +0000176678 00000 n +0000176822 00000 n +0000176966 00000 n +0000177113 00000 n +0000177269 00000 n +0000177413 00000 n +0000177557 00000 n +0000177710 00000 n +0000177855 00000 n +0000178006 00000 n +0000178166 00000 n +0000178326 00000 n +0000178471 00000 n +0000178625 00000 n +0000178785 00000 n +0000178942 00000 n +0000179087 00000 n +0000179232 00000 n +0000179377 00000 n +0000179531 00000 n +0000179688 00000 n +0000179845 00000 n +0000179990 00000 n +0000180135 00000 n +0000180280 00000 n +0000180440 00000 n +0000180585 00000 n +0000180742 00000 n +0000180890 00000 n +0000181041 00000 n +0000181201 00000 n +0000181349 00000 n +0000181494 00000 n +0000181639 00000 n +0000181793 00000 n +0000181956 00000 n +0000182101 00000 n +0000182246 00000 n +0000182403 00000 n +0000182554 00000 n +0000182699 00000 n +0000182847 00000 n +0000182995 00000 n +0000183140 00000 n +0000183316 00000 n +0000183476 00000 n +0000183652 00000 n +0000183803 00000 n +0000183951 00000 n +0000184099 00000 n +0000184247 00000 n +0000184407 00000 n +0000184555 00000 n +0000184703 00000 n +0000184848 00000 n +0000184996 00000 n +0000185141 00000 n +0000185286 00000 n +0000185449 00000 n +0000185600 00000 n +0000185751 00000 n +0000185902 00000 n +0000186050 00000 n +0000186207 00000 n +0000186367 00000 n +0000186515 00000 n +0000186660 00000 n +0000186805 00000 n +0000186981 00000 n +0000187141 00000 n +0000187298 00000 n +0000187446 00000 n +0000187594 00000 n +0000187757 00000 n +0000187902 00000 n +0000188047 00000 n +0000188198 00000 n +0000188355 00000 n +0000188518 00000 n +0000188678 00000 n +0000188838 00000 n +0000189001 00000 n +0000189152 00000 n +0000189297 00000 n +0000189451 00000 n +0000189602 00000 n +0000189750 00000 n +0000189904 00000 n +0000190058 00000 n +0000190206 00000 n +0000190351 00000 n +0000190496 00000 n +0000190644 00000 n +0000190798 00000 n +0000190946 00000 n +0000191100 00000 n +0000191257 00000 n +0000191402 00000 n +0000191550 00000 n +0000191707 00000 n +0000191852 00000 n +0000192009 00000 n +0000192160 00000 n +0000192320 00000 n +0000192480 00000 n +0000192634 00000 n +0000192797 00000 n +0000192942 00000 n +0000193087 00000 n +0000193238 00000 n +0000193389 00000 n +0000193537 00000 n +0000193685 00000 n +0000193830 00000 n +0000193968 00000 n +0000194119 00000 n +0000194279 00000 n +0000194436 00000 n +0000194590 00000 n +0000194747 00000 n +0000194901 00000 n +0000195055 00000 n +0000195206 00000 n +0000195357 00000 n +0000195502 00000 n +0000195662 00000 n +0000195810 00000 n +0000195977 00000 n +0000196134 00000 n +0000196301 00000 n +0000196449 00000 n +0000196600 00000 n +0000196748 00000 n +0000196893 00000 n +0000197047 00000 n +0000197217 00000 n +0000197365 00000 n +0000197513 00000 n +0000197658 00000 n +0000197834 00000 n +0000197988 00000 n +0000198148 00000 n +0000198305 00000 n +0000198459 00000 n +0000198632 00000 n +0000198780 00000 n +0000198928 00000 n +0000199079 00000 n +0000199230 00000 n +0000199400 00000 n +0000199545 00000 n +0000199696 00000 n +0000199853 00000 n +0000200007 00000 n +0000200161 00000 n +0000200337 00000 n +0000200497 00000 n +0000200725 00000 n +0000200898 00000 n +0000201098 00000 n +0000201243 00000 n +0000201455 00000 n +0000201618 00000 n +0000201797 00000 n +0000202034 00000 n +0000202182 00000 n +0000202406 00000 n +0000202646 00000 n +0000202828 00000 n +0000203013 00000 n +0000203161 00000 n +0000203309 00000 n +0000203457 00000 n +0000203630 00000 n +0000203809 00000 n +0000203966 00000 n +0000204123 00000 n +0000204280 00000 n +0000204437 00000 n +0000204585 00000 n +0000204730 00000 n +0000204897 00000 n +0000205070 00000 n +0000205255 00000 n +0000205403 00000 n +0000205551 00000 n +0000205724 00000 n +0000205887 00000 n +0000206032 00000 n +0000206189 00000 n +0000206346 00000 n +0000206503 00000 n +0000206660 00000 n +0000206808 00000 n +0000206975 00000 n +0000207120 00000 n +0000207280 00000 n +0000207465 00000 n +0000207613 00000 n +0000207761 00000 n +0000207918 00000 n +0000208081 00000 n +0000208229 00000 n +0000208380 00000 n +0000208537 00000 n +0000208700 00000 n +0000208845 00000 n +0000209005 00000 n +0000209162 00000 n +0000209316 00000 n +0000209464 00000 n +0000209612 00000 n +0000209757 00000 n +0000209908 00000 n +0000210065 00000 n +0000210222 00000 n +0000210379 00000 n +0000210539 00000 n +0000210687 00000 n +0000210832 00000 n +0000211093 00000 n +0000211260 00000 n +0000211427 00000 n +0000211575 00000 n +0000211723 00000 n +0000211981 00000 n +0000212163 00000 n +0000212330 00000 n +0000212497 00000 n +0000212657 00000 n +0000212824 00000 n +0000212987 00000 n +0000213150 00000 n +0000213298 00000 n +0000213446 00000 n +0000213613 00000 n +0000213807 00000 n +0000213967 00000 n +0000214149 00000 n +0000214303 00000 n +0000214470 00000 n +0000214624 00000 n +0000214775 00000 n +0000214923 00000 n +0000215068 00000 n +0000215222 00000 n +trailer +<<3ddd6261769295438cd3e3ecd3051df9>]>> +startxref +0 +%%EOF + +576 0 obj<>stream +xÚìX}L[U?ïµ¥¥0¦‘!Tœœ™ë–uî±`œ$*ÛœYÙº-XJÓ…Ž‘®eÀ +Öò5Ü0-5l™1]fRXLÌ2#êâ’ÅÔ%fd&ü³D÷‡ñÜón?W˜l÷‡½¹çÞßù>çÞ÷x뎂@4ÿÒ@…\%hN_tþà„‡ÿ7ݺqGÊÈe¹œ€Ò­]a®@Ö|Â&Ѧ@¾'y$¡êU)éJånA‚ÂÑŒqF¥\éQêS8($æ£b*V«ÛëÅåpÈ º%¤2çU¿ ÀV§Î ™·Á1g¦AsKñ%l¤Ô9Š|h–žšU'‹ßï€É’‚ ÌÒã¥$¼ µÎgâeѯ;Ÿð$JÂ0ÔHµ$ôÂ.)s>Á)î„=U3Ê‚ŽÒAuY¨ä–©‹„)0Cú¬ê†PÜ@B…ð”ÆÊ+a§”éyp68ËgT6¡$xlTñ6‚W¥äQ….@!h âj„ç¥e)"ÀçxÊ€Ýyaó×E“¥Æ+c¥:·wòLMùw“¥›³_ó—néCÞ–¾ÛH†o¯ã°{¬àšòï'Ïô2R"/öåÏq $QgbËJl.³& 3–•‰Å³Ïù\3–g…9ó·àŽ©ßg¼´ãƒ$L¬P s¦ã¾›U'Æü×:FÇ–Buñ%Óqu³¥gлñÜ'#cS²íî!íôµŠjÑâÉöx&VÉ‚´õMÍ"œÃ¸(°ÞÃâo 0xñ¬,ÈÖò@{³e©0;×ÖçŸçi$NK'°¬½…IÆF'ùÃ$e÷;FÕÅ‘<µÎAŒ ²ƒÆ“²¿ø<Ù>ÿƒxÂt—Öº° ¢¢áa‹16>'Ù²ÔÍçä\½¦ÆA–Kÿ)¿ðàÝýOþ9ëny÷i<;(Vd ÈŽ‚ŸÌ}ðNGœeL´XÛØ£}8û§ž¹ßæ,vwøò/ÑA6YàÍn<éÿ°±ç”ߤ­X¥Šo6è5Î5E š-%n¬ƒ{&¾ø{#Žƒ»ïÚüã½Í ²ÂìH^”ƒÆaïôyKÌ;6úeÄN‹ßÉìÄéÒ¸—{áE=É¡WßÀC+ˆ|R°Õ¶ín ~¸é縉ïvËßbEnäiù ¦†êq}]´ BIx‘ù©Î@Ë‹H¢g@|š€N¡=Ž¹A#;‰Öˆ!ú8»x$ÕrË‚âgk‘›šIˆ§ÐËy—¸)׌pš‹GˆÍ y!k7‘çw,à‹¸šNÇ2 så´õw·;(‰ò´°š~¡è:ý‚@G@Ǻ”J6B¿ Ô‡T"¡Þ¤ò3R3‚;<æEHŒJÒéÈÉÿ_ïKýzÇ3SCBåo¸öã§×B!¤`c÷@Ù’Æ[ð¦®C,‡Ëð’ªOqQÙ¦\«\#ôCüß*¿Ò )Ï·'ë³*Íà‚8 +8ÜЊ£ Úá,8:É+›m(w“ÞèBì€J´í‡^xi Àa¬©e]HÛ¡-:‡§Å¬zÓ‚k 4À¤ ¹ºp=Œ³…pÍN¤l át jç¸uàAÏ uê¢X]”÷0Em‡S´kGý!Âp ×6̼›°w-¸ÆÁøèÇG¶Ê¢ ±‹j–WÑ ¯…vmÔ–¹wã„z(–±{zŒ*c=r`Í ¼æg„ÖjØ…=4ãÊöÕ`ÃY 58ƒœ´ +ó¯šö•!ÊìÍh‹÷­ÌÄ©ÇYƒÈFCXÓ®ÇÑ@š-¸“­Ì¸ïEéI̼’d ˆ]˜w5Z1oS\Õ¡GrYn‡qeQ‚:5VLîšÃêÉCÐ[5QV³™$Ak¹.nÖ·qdãÚ ¤e&,KÙØÊ ž÷« +jaŽ*¨CjŵŠöÖОɬ`$=#—Úiµ"¯ +yŒÛŠÒVÄFû8WÖ·#¿–dÖVŠÆ´d_µˆì”‰¼Ö‘~YßH»:ÊÊHÔÊ3¯ y¨åQw’ÄJÚuä©–ê0âoÅi$Mã`(û:nÑLfqgÅ<°l?CÙûÈóc<7L£Ö$|AO‘\s3r­|0ŸMÈéæ;å”5Q6²ŒUmäÕWñäœj¹¾Ü±khÕD’«“í÷á´“5ó8€øÚ6ñαêÀuǸ*ü ? ¦2µð1ü*\lz×ð >¥àè“ûñ]}û_úw•˜ šõûq“  Þ‹x%h¶’ç+¸<š’ë¸@‹µÚ%çÏ«ÿ`û»Ê. +endstream endobj 575 0 obj<> endobj 577 0 obj<> endobj 578 0 obj<> endobj 579 0 obj<> endobj 580 0 obj<> endobj 581 0 obj<> endobj 582 0 obj<> endobj 583 0 obj<> endobj 584 0 obj<> endobj 585 0 obj<> endobj 586 0 obj<> endobj 587 0 obj<> endobj 588 0 obj<> endobj 589 0 obj<> endobj 590 0 obj<> endobj 591 0 obj<> endobj 592 0 obj<> endobj 593 0 obj<> endobj 594 0 obj<> endobj 595 0 obj<> endobj 596 0 obj<> endobj 597 0 obj<> endobj 598 0 obj<> endobj 599 0 obj<> endobj 600 0 obj<> endobj 601 0 obj<> endobj 602 0 obj<> endobj 603 0 obj<> endobj 604 0 obj<>/XObject<>/ProcSet[/PDF/Text/ImageC]/ExtGState<>>> endobj 605 0 obj<> endobj 606 0 obj<> endobj 607 0 obj<> endobj 608 0 obj<>stream +H‰”—Ën,5†÷ý^Nâ¸|÷’›PX”‘X 6DáD„#ñ&þ³|u]®WÉ€]]ž üM:p+™r\Zv}]û¸.„0ìú¼Ü§G¤þ]~º|ùôõã#ûðþöéíùíö»»wÜ\ž>€Ð,!ØÓÝ}àòòòק—×_^Þ™ŒßQ—øº»Ÿ¯ß/€/”I|}2Þpp!0e¸ÒQ).•EÔ¼ÀÝõ÷åÛëò÷ì7¶¬kui#ؽwìýeùñ ö'~Ás«þ¤— î$óÀ•Áÿ(æå…=¿.¯À¾y[~ˆ¯n¶õp­™ <"€ˆã^#â6DfDqëZ%Ä"åCÂ@âv톩Œ™ôn±`žk|“÷ˆYŽbBê ӳܢ渋ÿ2ˆnq_fÇLÆÐOºHÇ…Ú¡y°¨&7ÌVX„¶üè`Wi`Ã\Æ<~“ìÍrc‚O0ƒŸîb>SkbYMIŽ;z·,ì˜l…žâ ÈÀnˆŠñ%h` ´1½/rv`4dQmÓ[œî0²/E—(Ð;Œ¼ÛCjB‹2úv(ÁC‰Æ팙Òi;¡ƒ†¡·ÖСÌdFLvÌ•í Z”ñs9+4÷è¯Ý±œÉ ÂTZÃc…à"Õ~ˆ¥œÒj˜Fkpò…Â2‡kT»ÖLþ¦Ò'!S„Õ~F¤žÐ"L¥5¬ŸÈágÂîgDÎä;a*-™êc£UJ¼€¶™H;¡E˜J+w ]gï@Bpƒ«0YgælfBdž@:ÝmStZÆcŒc6®HÓå °UÐ"NREr¶ÃÜ®¦ìæ8UnéŒS¹JȦ`zlœjÝ‘oû¼Ê;êŒ ¶³ P’¤K…Ôw¸ÛþÀTIÔYd̤x2LGl4àÔÈSl£¢k祫cï©Ò$Eç:àí³x`ªíáLZÕh°j cOhTÜ«`\µ‡˜O—”ØëxàŸJ§ ¶ÐÔÙôR%é³ÓRÇ‚0§õ,›©ÐhøØU£[¡æó¢ÕZŽôi<šÉSÏd1a›Ó‹C»½˜ ¾jsæBñ8¤D¤žÝši¦c=“À-rî™ %9ˆLf0RÃnÒïf+§„ËÙ«õ±4ÖÛÒíø®gÊ0aó%§dJ Ö8£ŸìŠBâL©ÝRs½0£+!àl”â#; e¢Zëu¼=A˜A]É^*”³0pE›‹[»™ë…Ä ¸ Ò,à{z9­lã™±yPé…zoµ¦šNæ J„ÁÚj› gÅÈ-¼Fœ Z*›ÞåT‡»=ÿ˜qqÃ0æ̧b9¥:£Ní4åÔ€ ˜iÐ_¨dˆ1ëõ/–+É-–S¼¶n˜­öW÷h/Ê1£Œ›4œr~ÀÅkã7J°Úo¹™>M˜Á©6%öTHNzM9˜ôšr¥ÎI쎕Ñj%â¹Æ±û‹ÞÇ#§O˜úÂB3X_m4å쀃öÕ]£pÊ•xP®˜æÓ5·ÉÚm +†IãçÏ2 :e`ÐÕи|!Á)î`Ÿ ü¨¢yP o¹Q!iÍk¹QájÍk¹Q²´æµ\©@»Lk^ ŽÊSç‚~ʼ–ɉҹ7X§ÁmMß¹ *Hf:¼Õží;ܨ2ÔænÚ<ª7J–Ú<Ê•ä´Ü˜yÔ“æQÎL˜G™Q†¡y°•?¸Ã¥¸ýªÜ`sxMã½ép~Î<ÊÍšwЛ4ïÀÉArÖæQ71šŠªÂy ÓVìx*ßXT`ô"î[õÀQonÍhÀÏ0£á>ÃŒ–+fHè‰hÍ( û_€7ŒÛ¤ +endstream endobj 609 0 obj<>stream +H‰”—;vÜ0 E{¯B+@øÙ§I™5¤öÉÉþ›@š1E‚?Ù…çŒç \ + ŽãǯOkÂñóïÇï6RöE~sþ»Ãr 'ïq?.ÒW2ˇ1ö&£¡×w¥œÉË?S®\ÜpœÈ[áÜ L_`¦¤wjäÏùVÁ6œ-eÁ<ÙiñÎóÅq˜pn—Ò(ó´&‘Î\ŸZÊH®k +jÍP(ñ;F¦$÷\Á²[´´¡@؆â`šs›ãÖÙ"¬¤óÄQVÌ i6ÍMÈÊY¦ +ùÄ‘ÎÅ‹A»l¶[PlAsnw²[ C„é EÞã4##dÁŠ;*ÄeØB>°Ðq,ôÜmÁIYè£sfk ^Édžå¥"é+É×åì樴Ð[PlAsm]1»þ0D¶&ÒÕsyšÔUqiM„«>ʹø‚"hbÑN­@Ø„â`šÛVøÖÄa^=IÌWÏÏÓ¼ÜÅž²—W&¤Ÿ˜d+X`=èÑ.­À&:«&˜Êª0 ÁÝ­=Ph ˜î®0@µ¼ÌîÙ^— Ì3Òƒö<9iD®<¶§AA{ +„í)¶§9Äž®>AöH»X²äÎtÎ@,Ž\ë].bå ÍϨT@GQX,+à¢P°9ÅÁæ4‡˜ÓÁUs…|RPrÄçÓ„ó¼Ùd±ËMZŸw–²´<ÇÝ êN°;ÅÁî4‡¸ÓÁ¡î4»@ÌÜ2M¬WwVL Âw@>p×qÜõæ®n7e$7.Í»XŒýrƒ6˜TÜ=ød’b\2øj¹‹V,ZsˆhÜVt {Þmü,“=@=@@7ey)-8ÞÎÑ#´ÖˆÖ *Zs¨èDÁ¢.ìªq#{ó÷²G¨VcÉ[P˵Â3é)®Î;fžãªlSv1‚[4KW,]sˆtÜJ_2q—Vø®j+|€îÁjœ‘¢¡×NÎIYˆuBŠ~Ãõ¾{ЀW. >ðÝq|÷æ»ó­2Y+ùäžÖûVà.#Í=m«Ë×ìÕ’Q½}hÉŽ˜½Ï +7Ü5ÆåÆ9“97á'ØÀï1X¸Âß*0D·ÎÅ]' ™¼P­ d6G¤¼˜ó„³ h Q¾5ÔQñ(FUÌ™8¾…)N®¦ é<þ 0ï¿ß +endstream endobj 610 0 obj<>stream +H‰”—Kr,) Eç^E­€F ~óžô°×Ðã/Þþ'-¨*’Ô§,Û.GFžt/ñxüõϯŽ¿ýûõ竇šý<âü -=F(0FÚjè1Fÿ~}7ò.·Å5T¸ôæZ ÿ/éŦ3Ð ¦¢ƒI%À`lMº¬»"PBgK+°½*‰„¶Át€èKá[HSí]ˇZFú˜Š ÝûvwΧÝ}q>íXAê/‡Vh‚ý.Þ g­jdÛäP/‡4%äJª× ö`ËB«–Nñèqnñ8ç/Î#gZ<Ä‹|†ôú¬nk [ƒÞ„ïúc¨HŠ—ÔÐ`H³/±íS OÍèÖŒqnÍ8çÑŒ/Σ™(È(A(ƒo¸椛²2¸ê?–šgæ•0¿$ )7·3"íá êñY¢¤AÕ©››S8bw*ÇÀ^ÊñÁ,æTN¬lÇtzù.­;À\šî•Õ€GýËs‚Ïb¬ïKÛYQGzZ•ï]cÀ-“[2z7›Ñ¥ƒŠK4m¥•N$]ÇTWÉOF&ö‰"¹/Ј+g%k]¢°z3Û[ôŇÞu<]Õfl@À#ŽýqŒ×8´ ™{ˆÍ‚¶?0ä³ð™ŽŠ0F²1RiûXâPŽzææZ½ ü  $à:–ȤŸêðI#òC¬°™âш1¨3}¬cvF¯dªQh«Ü#kÐvk ­š1¨#Q2½û> CØ£ÓçÕþ Q˧¦¦D,«’®«ETj—b2VD›bú[)Åü½JH´ÉÇF ]O/pÄ°ÏiŽT½jP÷Á-¦7pÆcÎx\ÀøÞœðx€3 pæp€a …z®r!ÆaÁ˜ip˜@@ͨÛéY6Èý²€,œ1Ï¡æ8%‡d]#îîKBø­…rèÔÐý Æa!Áx,$ …ä±€<âËBòXH@ Èc!~(ðœÝÓC³‰-u3[Û{ovÝ-$Rt„^\ªŒÑ^ܭʘ®3óŽ ÛuŒúÜzÞ·ÁX]ã}KÜ0Ê6О[N׿Ý?Ž»•„̆;šå¬Oè +3;Ú„WµãÙ§¸¶žV€Ôæç‘,˜ê +VïhÔ.s³‘¶îýö%’ÎX}pÁëô•@öýP@h%Ðq•Ð¶`8Uª=`Ó ŽÛwð2Úí&JrϼSI3…ÚÚªTèH!ºòRMø›)ºùŽHÕ‘3…c4·g + Æ‘B‚:s¦gŠ#…8F 3^[VŒ³Ý0B¦„®I·$š&¨ÖáG®‚ò:’ä‰"¡±Ï(#Šã‰"QômHað +žy$†ëJæ‹­^êŒ +mR­çλmûÚB› •fź­ÊYËŽWržÔãÊÚ¶ +endstream endobj 611 0 obj<>stream +H‰”—=vä8 „sŸ¢OÀ%ø2ŸdÃ=ÃÆóöÍý“¡ÔÓ ,®8ù ,TA¯×_ÿL^?þûúçë×W +Ô^ñø œ^48ŒùÓ뫵JŒqþùïϯJ7Ôû U +=OfL†Bë1ÖòaJý05°(T[ :™î0­øÌl¨Í:#yLþ0-Ñ[hÖa¹z£F›g¥ä2¡bíöwGãæú8/Ðçø*C]Žò¼‘=¨û·Nóà+¢µE¢Æq6rHŽ²„œ”8”ã•=©)×ó\§š4DW¡”§—‡Ë#´Ù7{ïÆ”k%ô*t‰wž¾«G£Æg ìI˜/9…*¯¤—S#~“uc Ԟ',žRíÙNØZHN˜f¸f¼_‚<†}FN¥a`ZHã =úW Ù0@œß’u™1÷†ÂãqÏ“aŠÏôtÎ¥û<˜fŸ½Ú)Óü:#3á^5û£>zœÆU[Gn§…"ét ŠhDIß3!³lX¥HR„2šfvŽª{¤‚ Û[ SAf,æj( *í¶Hñ-»XTc( Ú»ô°æ.Mû9ô0åë@HÒÀ”#Òƒôn¡db‚——#Ò—SìB— rX~û–‡k NзrD¹"ÃÈ@HýϨ;c‘#µŽ)DFíd¡‘ï@7Ð@•ÞþÐ==Š5+Ó\a+]Ì%»¸­y©¸{yELFkÒôb 2ÅE¬A¦ ˆ5Èô°™b@þ¯A¦˜î;JŨÌa ²I`g]ƒL1Èâ[¨ç.n ä k)­«k)|© SPÝ +%!oXBI1 º[3IAÝwÈBß4‡’bͱ[§ú„™Ö Ün­”/=|d +Š¾¾U)Æ áËËHDk&)¨ld’B`¼¶  ãeÉ$?Q™´B›™¤* ð´k&),º"^æ×RM3’.áÝŸbßÆ‹ær[«ˆ +S í"*L¨°½µKD…e€ÂET˜ólD…­âòŽ +‹äǨ° š B3!¢ÂBh&DTX¨?G……€DT¨"»¶o!z´}Ãìؾ-žVÚ¾…¸‡Ê—7X¨<Û¾…Š„ƒèÙÁ-²áඹ ·Ð†ƒhÇÁí™6ܶ·áà=ôã†ÿéàŠË |ŒH×µv\×ÙqpÍì8¸a6\3u<;¸©óìàÙppó<˾-ÔÀ”K7•ò†™4c«[ÌXkŽžwpShËŒvÌØ@;flî¡n˜±ßI3ÖІ5'ÍØ4·ã«ÚòUÓYóÕf%v‡iËWㄸ_¾põÔ›yý`iˆÙÁ +endstream endobj 612 0 obj<>stream +H‰Œ—1’! Eó=Åœ#r'}Ç[.ß?1ݳ۫–ôg©M&ØW€$Þ§¿Þ«äÇÏ¿o¿ßþ½Qb~äã/IyôœXæœëW)œe<þ¼¿LŸŸLIÔSR¯€LOå`"¤|!c(d&¢9‡DL—‘š¸†`zªŽÃ03̓¡ˆ)13J:*½ŽœçÚ[KYµgÈYƒÙ"†ãuæ*£€R÷O„S- +YË[ ‰—¡\S?Ö©4>¡õOš‘4'ùUÑuBüÚŠ[†­ˆp»«á kèzb]»%…£³« „l¯#ÂAÈö]N•H0xœÑ;C犭l“Î Uþ6WÒbd]òçR£íÄŠ]h+VÜîvbÅm¬¤ceÝÄuåäpn€Q±b†Q¤bÅ1àšë/Ç€QPQä(2ÌVYf'Š,Ãàa«£ÈçZgžÍ^â £ÅqŒ¹ÙÏĸµý{í;å‹Ö¾ƒÐ­(7wnD…… +x訰ÌVTØ›´v$ +:‘Ž +w¢ÏµüllÄ€ç-).uJåX(܃•V¼\buÌ$/‚_ »ÄåV™:^Z«³Â1;Yaça€…tTØ…*¦]Ùç c-Ͳ…/}Kôë±êð«ï0>ZàŽA2Vw -pÇ–ª–¸ã€ëÆü’ªa +¡‡~MGĆ렀¡üŸ3)Üæ‚òHBb¤­|põ”òƒ¤ìíÞ°·ƒÀsPÛÛ1 GÄr6锪ƒ€Z}±»:«Mì ô9¦¥j¡–7üè   íGÇ ŒÕ~túJÒ‚tø8P‚tzß¹¼ÎöÉôxo7ADb¤ŒBÈ™ÜiEwAÞ™ÞMwAhá.GÔ¸c‰Ž@ :xåß…jŽ×¹ Õ0 \ð%Ô£¥³Ez–Ü…j 7´ƒØÞêlèLè]r3êô†¾A¯ŒjðÈ0F5Ï5ÖR0x›£Ìõq½¼Ó¶„j 4C˨×wƒd˨Â/ÎcÂGx¤s„ÿ2„Ôß +endstream endobj 613 0 obj<> endobj 614 0 obj<> endobj 615 0 obj<> endobj 616 0 obj<> endobj 617 0 obj<>stream +H‰”—=–l' „óYE¯#þ•;qè58~ÇÇûO,ûÎdÓý5¢¨âõúû뿯èÊË÷?WË8;"æö*9»à½¯ñõϯ¯?þú•šýùïWG‚#Åäæ¢ „IN¾æSøfâ@È¥4‘’\¢5‹Óy™–gèÓ2ÂŽù§ÊVDöï£ ñ3"•O„]Ìg$µpfȸ›Ô€䛫]ét‚âYj +Áõ ¨5%¾d¨‡BgJb˜,ÕÕÏ mP ×–Zùê +».Cû ÇQÍ®ˆçjý¸Ðõ]•†àB—!ŸOo&È ùdWËÙAQ>‘ÔÑ`Êd¼ÞÏ®ImOP}CÉÅeCqª` †VòÎ×oEùÈ‹5Ä`¥µ¼bÿ¨¼ ¢³ÞR]/Žû‰Õ¥g•6˜Uœa"p.Î@ œl½* %I¼èÝF?å zC¢·ƒv† r©ºî Yû¡ÖPÃu°â–4ä®N-ÄÅ¥zwnA‚œPâ +6Êòz„¿6¦k£T\«w² ƒâ*]á“l({pKPcÁkB)G.‹}Ú\ùtzÛ2¨;jµ ‘èN 4åf¤·»µÞ*jÍ•^Þq¥zB î4bŸx0ÃArâ{S©N×Ev…Ö€¨ßQƒƒÈŸ»ƒø¯D‚ò5Õô>d¡Ê‹äª¨^+ SÜ*KTXÇ­nÁ„:˜2†129îb,瀨™Ã2õ̺ô» h˜¬^Úm¦à ˆ•]¨ß¦Ý¡àÏŵ€• È­bår§k.<:"€6±FÊ TÛ`Ò§H݃Ÿ80…ñ®°Ðl}ÈÒT›-ÓPè‚ZýºBÓ¯$“]Ø6¶šv[Ž?äªTÈ lµBjò­÷³§­¾¢¾Ü©Î8|U-ÕiÇûLSÌ°õŸÓGÆ{ÃRs¶Š×ö—Eß®eY¶?ll!8aÍó6êO›µÔYÛ¥¦µüõÛËa«s³dQJ^ Ÿ@L$sFàp õ+Q»ï X +ÍAr"ßó–aiô¨a!\Eï·¥…§«(!À‚ùQŸµ¡訹ŽYßB@%}X ¹j²PEò“нåÑq,„î05è([¬c8Ë["ßoË>`þþ—4üد_II\xbvCÅÆ5j5Ú¸ÊkÀrn¾‹o F^Êéj!½Í[¨=0»øÙw¨øf7šn´Ù Ù–¼ÄŸ}k˜tV\7hù5™Dû7ƒ&õÀ°PyfôB“êê†Ab Ç¡ëc Ç +ÍplEÛu…*òÃŒ B7•Ã÷sª'h˜h{\0Ô®ÆG±Ø tÛ¬±Ø‹ šÝÒ¥å¥PÞϘ!F÷õ¥m¡N–£î)›ùç,õŸ“ü…Á”Y2º?u” 4b±=˜¨ÌëÉ@¦ªiO¦ªzt9ÍÁòÍžŠJ§òÜ1‰ð–P;Ö™Ý!BÕéÌÁά)îIf ô$³B¢Î¬ÃõÜf Ô÷õ]f ô¼äûáxÞÓt8_‰ãzNn¯^ÒNoÁ”¨]®™×ÿ Ù#Ö• +endstream endobj 618 0 obj<>stream +H‰¼W[Û¸~÷¯àS!·0—WQÊÛtR`ŠÝ‰»/A[ŽÕÊ’kÉäßï9¼H´- âf» –D~<äw¾s!!?=tÎÉûvñËâ¿ CaøA¸L)3yn8I¥¦&e 7‡…ň€4Ë"Ji† 5’3 ­¬¥ÌLÔÊ(L‚¿™ÓLrr: 9»Ef9•z +™N2’§Tá63 aŠ1™3M"ST¢e5…É7¸ ’HLòiCܳ£o1p¶€ÄÄ CÒ7…á32r +ô}²¸}Ÿ,®@jš†KA\aFAHšš+A¤†`×äSÈtÆS™ \Ï‘a¾_WÈA žX€†æ¨ <Ž0–ffFÌUN`8›¶Ã&ç@ƒ0`åXKŒ +ÄLAæd!=ÐwŠdÁcC’Žº©™IC5÷¼éëqªÔ%u¸qÀ¤Æ+â—Å_×è_挹'0DS ^IÖ‡EB–ë/V\ÑÜ@T­Í…dýÞ0²â”+È ð FH¬7îQp²þºø”¼o—:Ùœ%ü4=Y;–ïÈr%¹É©Ið ©Jžš¾<íŠMIÞ—»j™R™4Uó…|<"róàKµ«6E_µ Yþký!l@Ú °Ñ´MÈÇõzòâžvå©lÜ ,œ”ï–l6c0UF»zøøøôD~>µ}»ikºm7–㣛Á>7Î~€‘_ËS›Æs‹«ˆ,p*˜[sýç…](Ç…à´\ +‹va0!3$‚PYú5>œ/‘4åðE0û…iû3Á–Û³9ÎÖù߷'äÃn’3N³h·SOÏä¡êÿS6äOä™|ìËã¾lº š¬ƒšqòó^èÈ»÷•áÔOÉã¾€ýó¤ùRÚßî[ö§õTNÖ;L¦”;öT&ôÈhªw+t7£irªŽNERå‘Zs 2j8æ.˜£L^QöÏãÖ~-P¯&éË-òs åÕ&—Ô çsòu„¦.ía‡öµ²Ò䥭k7@îéyÊMV±ÿ;r5¤yhC¢Èý”‹Sa§–¸Ÿr5ÙÚè)êšÀ´¾*à76SvÝÙ½—Ägâ¢;íJÈÊ›SéV·A¹qr1 ûqÛMä¯O(&RlÃŒçaá Óƒ·WàÂÒp:¹ù›`ÑÎs' f)yjv­ÝNøä¼1•Û0õ¤C6•. ŠºkI…Ø<Ùbå*·öÜUöûÒ>ŸHçÜ´7®’ö„¾ƒÃ %(ÀZf؃…Ä-m– eMX!3.W(=,„™Œ€“ìªÀ>¼–î×O„óY#†¦Z˜È4CAËdMt³T;gÁF›¶§ŽPºõ¤_…[ô¼DMßmÔéMP«:µ¾8¨UF Gg—™1š†°ž…‡¸&ظŽ&̶̡{ }цp¹úp®mx®œ,]¤cŹ‰óî­8Ô%ЮŠ]u[ÇÉz_u˜Šª±yÀi\'G´”'ÐE}®Kòµê÷6|>÷Î Øî±\g7êŽl¤^Þ[ܼÿÁSåR‰¶:Öî@:©C4ç>ùãìˆæù׿Œ`ØÙÓÏäÜT½W4¬RjI®¬ß­!p¡zSCÔ?¦v¸7¨K ãñ`PÐ8(Œ»º0ŽÏë‡Ã)lI!ÛªÈqkUÄf_´#Å5—䡮ǜ¥O:B ‘kœ¾’}ñZ–º£}oÝÜ®DßxwõÖ?‡ª©ü‡àÑmÙmN•-èG÷ 3]඀È7:ädWÕ5T¶öŒ ÁØ÷ò|/†ïMtµyä?AÃxpþ8! ÞŸ…÷œÿÇ ³þ—¹@$øN¤Ý‘}¯ì¨†<•Ú½Í1®w 6Uú†à%\¸¤ˆwÍÕĸÎ#åÆœDøŒ£YF%h]_sîX{Ã>L—0…v~ªcW´rõÖ„‹5æ™7ð’÷ÝÛ«uëSúËßÈCÍ–JªÊ©ê"8(JÊbôa¸ +bÅ•I[Ã/ê–þ‘.¹Jj 7Å»T®ýßÖ\ XQ'*ÜÊRkð,£šà"uFu:HGõü=T˜*"•Ò4\3Æ<€.±w»ÀéûÞƒí%=ÉK(™¯ðRS‡Å6ÿ#évdçÔ˜ ²½$î[ð¦i÷-!{:>2Í=Ó¶¢lJ…èýH5¾À]7þͳ,>íXĘïJÐAÐßteÓa£š*h@ÿO.ˆ¸óm…Ö,ÿQgÜã‡Ó{¥‹¥ƒ#¢Üøü®SÅè;$8L¬/ì~ò¨F8¼K§-&pëà úJô¾á® U9÷¥eºûØÔ®à£Íâx¬}ÊÃ,9Ãh7‘lÜ%£°ÂJð ”ßÕ¯X[±Ù`îRø»9oðÃ=Îã‚Ž¾ãÙ(ÏâKiãÇöM¶]]øâ"ʺÇ}(ݔƿ¶ÖaÿâÆƈÌ|ç…üj+€¾ä>«Îb&MèSèÀ>³¦7î»hH#xˆÈ¿c! 7Ü ¼ÜZ+õ#.å&O¿3TïqjðÏ=>…¶ŠE™ñšNò›Á =† +endstream endobj 619 0 obj<>stream +H‰Ä—ËnÛ8†÷~ +.å…9¼“ZM[d€Á Z££ #£¤*d1Pä)æíçð"‰»)å¤v1m‰G‡ßnBhS¼yZ®fÅ¿í]X|] +L‹Î¶v/¡Ööõ}¯oûÚ¶hW=Å«Û‡j¸oùeýû‚*,ŒD­o+‚ a­ïÂ’p´þ¾Ø÷Ý¡þk…`§„§UËæhãWÛ¡Þúëwᇦ®Úîáƒ_öçd%6¥a©—ÂyI‚§7úçËBz ¿­× +ÁÉîÔ™!>dI0Óe‰8¡˜´Þ9ƒw ³þæv°¸7ù•,±ˆ™KE¨ÛhéÕÁ±Û¶îëm\œS¢Mzž¶måùñ¢_* X/Wkø>áÁæ¥$ÏàΊÒ#î?@þS›ù’¤t爢 N1“"*PþT¹¨å@¢ö¶½·ÝnÛ{w4¨CÕ †hØíRé­Ýí¶íß`EÓ—‡w·¨g\Ìʦ¢*C‚eüS +K3ò‚%@—XtXP¬/Í)Sa#¡¼y˜çEó’#–(¥Ä$-/!Ìfª+ZRðáµåCˆ0YYGæ™Ub$«3È&”f°+r’mTú©ßö®Ñq‚ÕU¶+­Ôk°°š ¬  X9‹môÀQWeáÙDA3û˜²ïqJ¬ÇqçÜøœP–(G(3@2ˆ9v">ß}ØÖ.B™—žŽÿ*%ŽÁ4@¤4ƒbBdGRBS?Áñ¶mª>Æ¥0W+ ÇÕóìVH² ’ “|’´Ô=rüøþúñÄQBn¿€ãˆ‘ÿãdDc}šÖDú½웾~„—&g +¯úh Ý]@­./O:†#Õ+d¼ÈœÀšX‹tv»ïâÔMǦ¤Ì5`z±¹S&õÙ@Gž/T)™<?hé‘æMÕO3(aÞÆè¼ +Í×e™1ÏOTf”tì`ÅûºÙîª6ä»”ª<1ß ý æÏ«ÌMCÊ!ø‰r4ÛÜÀ7cüLYÍ ,Èa·w9W׶ß6u NpœâiÉ0+ö]å#ï ¼èJyPt™Û¬Àò;XÙêYa—P ‹Q̾@‘¿^¢  >’àRæNO^ŒdOÎ ÏL:Òï|ÓnûpÛ>îû@ iÌT)g^¹†7¦´¼ÄÛX„%æ âòŒÑv‚78Õÿ›$TõÀüÏ}З‚=q"hZhBz ¸m>Æ'Ùn§Á(…滄´6â(læˆ2J’1#§xgˆB$ü?TÄ/©q~7õC …È+²yt%Ô~’JB“SÅ$.(Ô—&ÈÈÁE)Nä¨JI/5‰iàäZ5åŒÛÛ|9J‘N.DOrh/ÇGÛ4öŸªKôPXpMŸ¯NÏÔ&N/Q›pZžÔq²Î«T8Ñ!c*¡æë`ØØË“²”PA‰Ïo£ i¡5“ÏW¡(w"ðQˆÆ N Ã„ärýŒ&áM ì3&ødBÿÝzúO€£¤ +endstream endobj 620 0 obj<>stream +H‰ÔV{<”ùÿÍŸç^bèu+—ÁoFÈ]äV1.ÑŘƒ1³3ãÞ)3Gº­H(¶")—µ©hk£¨Õ¢pœ6mµ¹—¢ìJ[8ïD»[çœÝÿÎå÷û¼Ÿ÷}®¿çyŸïó¼/À–€t€áë¼<ôÞŒ œq4vPÌ)r(±hF£¼ ›ÎmIíê`@ +L;#Q€÷$¢ò&0’ÅnOÙT€Î$JÇGÅ¥DZdÝqÀ§€4ͤoï<º2€§¨`2”-”ç0 Giýh¶ yÉêg”Î@Ï7Œã0è8BC­PšÄ¦'se=ä*Pûs¨>Og3‡¢œPûÁŸ¨çrø4t öŠå\“ë?×<€ z¾ ª0ï·ø4¢Ð»*x¿4¶A‘FAÚ8Ó3óµm"žÞÂߦ¶åÁ)(ÿkœ<ŽðyÔ"àñ2*Kƒ˜ + `òY &âÏáÈ«!eAÛÄ—Šx{­uñòö¢mBÖºººùÑÜÖ‘C†‘5òñP{™¼5´$SàûºL®†dhM±°´±´ ýßO@XüûwŽ‘8aúÞ÷a…BpÇ ™ŒÞA25Ï.”Ë^R’¾p/a ÍÂøBï´tØꩱœ9i¹®~ÍÐo:F¦÷ž?Þ´ÇàÙ_Bù1É·?S›ý.dÚèË­øYÓ¥!ñÖgywuCÌﶫJdX]É«ªóY?öÂN÷« Â:Çâ2›Ö{‰©;cu÷´é:›/°8П@‡ÆE\â˜Ö’«¼SIñ n{o­ŠIØèºAÙäe;§Ê%yÚO6M´g ìËóî÷¥OÔ–½uwð[-[Ì š>h¼kYç£1…%É7;—o¸ÿççU•=[:dn)JgwÖÖ4§eä>˜¿åây&Gq°‘>s,`èp7ßifö85£:°{V‰Á€"<Šp±%8,‹U”JeoæD¦^ižuýQUýøÿ#ˆQÌR(k>±Õ¯ .ùŸÌ?Å·˜™ì¿ÍÌÚ,(PhL›p"‘>¡ h€Ë·57OJJ2KDù¨±ƒÃ6çqéâ@!™¼ +ˆq*Úœ=at?ű£P¾ V„Á€¦TúžgÊq M úÃôKjZ%rêWåf¨Ú<ÛÝi”Kˆô,ÃQ³F©å“5[»Í—·zyñDÚ!αùßÉw5ù!—¦Ã§Ô;¸ý¾}^PïÜÍ`Ô%°oz÷+ÂÈB"y +§Ðë}Lßø¦f¶hêÜD¹Ï€jùãì¾–èôJ^ï ô¤~oÎa ßï»%J»±¡Áð¦0zdXä\ín[òÆɪûžæšÊL›Ëlñ Uã0žÞ¢ñºÙB(l3λk«ížÕþv³„W±Ã•á×\0UÔª° +']r±²ÚþImѼÉSw¬7;{…ŸYþãdB†ÀáP6Ƀü¾•N /CáEèLB'º„„$ +O0~ !&S_\´&ŸûqQÄs¾€.HàCi´(Z*h¸Nüˆà ¸Î²á™ž˜Ì7 êCÃŽ±˜¥Ä?ª6T{1ÀËA™&8)(+f*àñ8,¡é_LÓ)*#œ¤~ÿg}ÿZ¹gµ Q»oè=Ïv—ÿ›†Ïµ©©¤¨GÈ­§N¾l§FX•æˆ%€2Ü ™›G‡œƒoJŒÖÔ(ÅTÙÝÓ›Î3Úâ8u:àx«•­ SŠÏ/£ôÔT7¶HØÏ»;z_¿»“qVºüíÍÙµ21èPF¿b¿,Lp°·ß«Ôí8Íäüéà’M ÑB#èºr¸)ìp”¡"Á,Ö²ößóº|"ûæÆ@·Ú}é¶ã´<ß³³å©loz{´¡Î gY×oP–´š{[)#u¦G<ñµÝÖ¥Â$üýékgÓóçÎuìê+×àm¶¿Õ0)uJÖv·Õ"I*»-6iž†ÂR1ú1xaIW ëâN°x'ô6îT½àspþöIÞ¾~¢?Á8N\ÃüQÙ¦¬©#ê–Ï/côï%)Mm§Ÿ½í(qhov›í°Î«Éàä‹%­ïþÞngZiEcÍé³ÚÚ«JìøœåP¬È©ŸS¦ª³šÞu¹(…"Ô§i5UË[M¬ L¯1O*ï7P`œzM#¾ÑiëS›ò¯Žw¥HΊ–Í EÅÉoœ¾úÒ¿åêH3|‡¥÷jåiøÜÕž~™þWöÓùZƒ_0½Züi_×á •çsú&¥²w^>róKkÒ`ê`EÒ@b èŠqºÞcµÿñZå +Ë͘~Ë{‰øÁ +7|k¨Åšx¢|Ä%™ÒÏïÜ¥9¹wÏpû•m÷N(.ï)A§Búop~ñß F¶ÚV)õÞsþõeÕþÆñ3+}™²FÈr9g1‘ehì”K)É(Ò1¸h1Swº¸¢YŠ´*DB%¥KÊM ¥l)D]eÉïÌhq[^¿ÿ~¿Ûüsæû}¾ßs¾Ëçy?Ï“q¼¦÷_æŒÒ§`oAp;Û9ˆðh¬ ’î +É€Rü†° ÎÝ'ÔŽ¹lø;’ 8¿SHFhµƒÉ +b|ZîG+ûÑ6ù‰ó7Û$€‹f·¡0×€ã>4øaß…bSCõ[šˆñi", IÞ¼k«(n:CV¬Ó‹fÇîòz¦ÏN_G¨¨Õ÷ävÐ^îD²©'Û‰óeWi’^ï<_Ê#SËŒ‚hµ(™9ÙÔÔèrXñLak›ºYÙõÆÄ´nÛQfë@ÚŠNÌ‘ºÑi½ M1>–¹¶4; ¹rû¶”tp 5ŒQÚréñ…“¢YΡËåÈ'JãxÅûŠUœ¤ÊôcºÄÈ –U½Aµ]JfUò{ Áiƒfâ-­Ñ=駷ィX$+ßT泌œ¯/žIC)˜Jª¸ÕgŠfS”Ç{KNÙDj‰oDø‡M¯8$d/û±r<¡>r~Žy³‰@åsKáóPû^|Eýˆ‘ÄÎûX~âp&€ù¦²8z>ZVÓë¢â…&ý¡KÙž;Æ{tëÄ –‚òŸ'È"Ñ¢ q€+—ªÀbNBX JrbQðã{0³öèΰØ5•uÙ&"â ¯­Wþº{©BuéEod®õcÏ÷5ÿâ˜=•÷œ”@b,¶ìº¨§u§ü¶aPëò…½ÛÛW ›¾YÜrï +soåÍØͨ+: “Оdd'QÞÏ'1<üiÛ’µ=©ûÑn´¹Jäܪ¡cq<%ž}¤w™íÄzbY…v:̱‹Ñš´3l­&'ë”,·õæ˜ZGNYVWJ•¸=Ÿ¼Ÿ§ÅiUqÊ£×h$ämUœ¡%p«8ö¹ç6E-(lÄ^¡” @xWS-ôå™P›Žß4(S¾ýCjqkk–ýÕ½%¶­ïŠŒªOÄÚMò¤.+î§y‚\Œ ³ñY˜á|„¤(ÿg0÷°0üçgŸ~DÐh‚††$>ýôaøÂå¿ rrÿ×!þÈP?xÙM£N¤W—Óx£m*â`¶=B·°±ì/žX-™ý0¥ìIŽ½™çúÛÖΑš¯mo)P‡èµÂ+¤UßÑG‡¼[ž4¤U¢EíÉêÛÉ|IÂCW§_.ÚO›riù°q? ³iäWÒ3ŽJàïÚ:ÄäŠÖ!Üžx‡43fkÖ^Ÿÿp],PUUÙ½Ï9÷>ä'š<0ïã +“<ÐQtDeð <4å£ù0Ї@‚M*þRÃ_†Øó“¹ÔMÇ1ÓèBhh:CeM³ ±´5㯵É`\Ëœü¼3û>Ôѹ{Ý{öÙÿ½Ï>û­74¶:å½9¯íôoؾ¯OÒ®à)ïä^ývßÕcÿ¨EÁ¡5ëÙ]íFÖ©Ñ :UÎÝšÙö¯hǦºyš× ùõ滞ο¸õVÛñ^FMÞtûâܨš±æ×-í®¦^¿QžyäÌõ7ÿؒžu½èöìÑÈWázP”%‘¶‘+ÿ ža]ý  +f>`QÕ2ß?ó“9sÆLp€&ï(§½Ù˜hIÁ ”Òä*G ‚^«²"D,„ÈËô^1Wo™¼bòÍ•ýHÂM÷^€Zx Ëà-ø |€í¤õ6†Fø ¶Al„*PaQ^†…è1B6B_ØI£o'´ìDXG ;†Ë«°*ùiÒª„ ˆ¦9•E³h-Ž‘ó ΋0ÆÐlš…ˤK®“ä°óOä]+´ÈŸ•¯åß!46A5œÇ RæaI¾N®†”Óä-ŠÀÏS 2¡›™¬—Àe Ç +žFVvIC'©((€R¨#8G0›’/3e t' Èj54À!‚&8ß` Ò.ßíñð$åÓ'±™{ï.÷3 MUê ƒ‰3þ …S¨ãûl¦¨ôWÊ"yºA?˜@Ñî%ÍâM¶„`)ÿXdÈT¦º¼jV>‚‹hž8Ÿb½ÙL¶Ï¦sŽ'Ý~PLüeØBÖÏ¡±@ÖÊw‰ýâ¶ÚÃ{AÓ‰ÄÂVxÞÇ ÊTÃ9ø"~‰ß±46…me—øF±O|n)¤¬'Ãs°öÃMìŠI˜Oc)V`¾ŠÕØ‚§ð +ÎƳgY/ååü˜H%ÈsÄ +e¥²Z½âuy{?óÞ”ýåJȦ~XNÑo‚í”Ùah…³çá*€ÁÚp¾@°×⟰÷a#y9…—ð*^Çx›Ê"™Eèl6{žmdÛX+Á)öû•‡ñhnçy2Ïã3)ª*¾žà ¿(¬¢UHªse³²C©Uö+(íj åE?ðûôή»qwÏyÁ»Ê»ÙÛàm”!”ÎÐJUè É}!Át:ïÍÔqoÃi ¤ÚY1Sp Uf +NÇr\@•| kp·/ö:‚ð>—/䛹Á?åÿà—ø/üþ¢§ˆ±Â.Fˆ)bžØ..‹ËJ¾rBùAõWŸSWªMê¿-¿³¤X²,Ù–Ë+–C–3~nêÎá ¼ =x/çN~Ö±DÁN²“ÔÏS ˜g2êTV‹«Øbld½”êP6ÇB»ˆ¥ZÌv°_ØPž‰£1¦³~ÖÔnâMZ’ŇpM¥ÜN’åj .amj 4 °Áäó#þ[aç'à~-b'|+ü1 ¯±½<‹ºà˜HQ\`ãÛ Ž—ãb8Èœþ·ýÖPÅ7i.ŒÇþø.³±ÔEƒøw°že_Ã5ºÇ«à5,Ó`$b\†=t+z+3Ô85ÿÆÊ„‡=†ÀÄ>Ên0öB®tƒ—°€×¨mì,̃VáçøŠ¾•ÕñLÑ®ä`)݀ŰÊårX¨¸Äç8 8>1âM· +Þ_Øh]JS%ŸfÚ!ºÝGh ç™D §ÎC}1&D Áš‚:¨ŒîøDšb'¡QÏš`šŒ4uÄ oL’{ ZNƒr$Ð<¨’d±~€W +½/À,xœnÎ9£d°V%C&0;ËrÙæGÏ—ªƒáð#AmR”÷À#¾‚\&×È/¨»Ÿ  [ Sa|OYþLFòfHôŽeõ2ƒÏ¢|ÏC¶Ü+{¢?”Ê?À88 +»- +Z쎴 ã‡;†¥ü>yèÁIƒH¤Ÿö¾}âíq½ŸøMlL/=Ú¦õ|¼GT¤5"<¬{h·Çºv éàßÉÏ¢*ô!Þ©g¸5#ÖmˆX}äÈs¯¡ð!‚ÛЈ”ñ¨Œ¡¹}bÚ£’’|æÿ$’Ž’¢%CrB¼æÔ5£%]ךpR¶‹ðµézžf\óá™>|½"Üf#Í^š®èÖœFÆüRÓNæêüÓô´ÿ„x¨÷ 4€0#LŸUa)èCX˜sH=¿ +Ê°êéN#BO7#0xŒ³°ØÈÊv9Ó#m¶¼„xÓŠô©è©Fg»OÒ|n 5Í°øÜhef6°Z«oö¬i +©n{`±^\˜ï2xažé£‹ü¦a‹¾ÿß–ŒwMsU=Ìägx™fn=ž*Íøc¶ëa®Íüæå‘ Òe1nO¹^CE«‘7V™ç2°’\jf&fVù•èN“âž®ôT½Ô3ÝMGcõ³ÐÖ`µ:Ë `ujžñ.Ýf ‹Ôó +Ó£ê»'gá;-âQNB|}H—ŽÂÖw¾‡=Œ”<àù0Ÿ¸‰ÎyPY4#ÒŸ¤†0´""qé”S’ù)IOQ‰Ñ“‡¤eÓ‰”ÒÜž!&ÝÔ7”˜]óÜêýÚOR +ïQÔ˜`¢fŸÀul¤¿ùòÕ?Â4ýoÈ°Ça;ãk ÇßAá˜Uñ°œ*y¦Ù¥bh±sýÃ÷“ø†òîëjÎõIPb¸ùÄ2îK%qŸØÃù§‰Ïeßµ¢žylû6Û¬Pÿy®]bRúHŽ5΋ÆkŒ¶©¡_ÿB6‰d±Á‡Š3¬{‘ãŒ$lb41•è"ˆ +b>ñ,1‘sƒó*^3›*>V}HÛT̺k8¢öÓÍ™zo,™'Í> +i2¦ä‹Ä,miöÇ–œ’˜ñYÅw…Ľö¾¬Sb*ÆÌ=³ËÄ•ƒŒ-Ÿ%ïh³äýÕ䃌ã*‰Y±Ïgñ‹Äšò sÂãqkÍT9B6€t/Ö«|ö}ãM8Ê1Ëíõ®c¤±›ùH=±F|¢ö¸Yî¹ýô³ÜE5¨2^ç{AúÎÄPu_,B1m?§t¼S…Eg£ÁîÆ ³gíil”½’uˆ=²÷Á18˜Ìs¢ÓÍŸ²M2²]òAÇT\Hß +@|Ø€cvÛÈxõªOÃ<U¾Pýù‘ø_pL;«Õ{¢?² +Q̪T¢Þ.dÎ%ã8ÇxŠý +Åö¥îëý¸›ùUͳ©šgTü—9WŒ&®çažë„QI5áf«’>¬Pk_bºgìnÉ£ã%Fìý<‡å=±s2–Ú¨¡®Æâ9Éy÷R÷ó7“¹»‡ýÇxç68÷ê¥ï"yËÈAò%F’]©ÞP6È;…óï ÞÈA5ãøŽà~úa'2x_hŒ½[ˆé.”ü¨‡}.”.ä²–f„ðˆÒÏÄ«z£qãVîÐ6s¾fa†1#Í¡È0Ã\ý‡Œ!Xk¾„Cf+ö‰l&a¢ÁWºÑ·¥èÏã.Ñë¯R®E™¹€ý«ñus-¶ÍŒ½ßb y÷šý¬ï2NƲÿ׃öÊŒ"æÖ.–?vNH;5G‹S,0—#Cõ‹ƒ²ÕG‚Íz.ý–Ã=¥½Rîe/mÙéÛxûÔ:e\ö“6æ!,œ‹Ä8—{òô4uúe¬Ä·µãN»vÙZqØÃÏ°\q3‘Ç;~–¶˜jγÄ–§Iœte¾Ýfáub'Ç>C~Z¾ úb̦îQKüÚ¯‹‡Ìu=}<¬§½—ü ïB»Ä5\ê]§æÜÁwù,âv§]ÀXÌØÛ1<ð†¨¿…ýd+…ùô ÆögOÐÎ#SùÐE8~þ~GÜ.Æqª°w7üGö}p·_Rþý;’ÝÂç´ ÎEr‘v!ãAÆ A9ƒr’ïOŸ¨ÿÒ'ìcœ&êåÄ}íOÖŸÆÚxøq‹‡Ç±P`.b{"QžÃBýë^è+›ÇúAn3ŠMŒÁ }eûNLècië(éÃœ#bòyž„´Uýc™@å.¡·ð{ˆÕÏÂRAœ_g‹_ƒn½¿?þ¾$îí ›¯`yíÇgîgO<ÌBç*ß”ƒä-oYy?«÷£Çÿb½üc›<Î8~wvì7 ÁŽ‰;~_'±˜°Bñë`/PkJ€”ÚYJÂH :Áä„h“ +/ÓP‡:bÛ˜T#4MÓ:Ô7öš9€”Léº5+P­ŒiôíöÇúG–R:4:ï{g;@etóëÏóãîyïÎç{ï¹W¼¿‰s,ú%d^^c<…üüÊÏÎüü +ÍÛÞT ÆóÆÕ#Æ•Ë÷î­ôSò"°GNïAÌm¶ s ¹É‚=õ&Κ?åˆÜÆóÀº¿,ê¯fFy ôEøUÐ7ó9-¿·>°ÇÎ’ÓþßþãæÈ/Sý9ºgð¨ò<«rlàÌÌÅËl¹û çòGäè{óôÿêçó|žÙÎ¥œfñgkïqý™çŽÇögœKòþL¨Ÿ¹öòç™JR9ÍŒçîqáïÆWîžýóc˜ùO?oùw„ƒ$|/ØærèìË@@ŽÊœ@ÙéñKg‰þ+yóß“Ð;yt‚%„ÝÊ|ÿ;ð­Æ‹"6šcçlëyæºåçsq>Äœ‰}ð8?©ÍÀ†À7òÿ5‡DßaȺü=×Ø™¹i¼fœgÕ+É7ÁYøø–¡K°ÚPN¦@ˆ YÚ@7 `"–\É^pŒ‚Ej(OžX¡¦¡^*µûY¿p·eÝ®g„›z:–Õ_ݘա Ù°ÕÙ°å Ùâ¥-Y½`qVÛ<~ë¢ÿX°ÌPFތ샤ìUìü”Èä´a>Ñ3˜r%ªÁ–ªõú£#¡f d'‘3cš,)õ‹X†Mrþ?Ød¶†M¦æ–úÁ'Ù‡äe0 + ìC\°ð‚ug7+d$À(¸ ¦€‰]Çõ>®÷Ø{ÄÂÞ%õ ºAŒ‚)`fïBZÙ;ü}SHncï@ZÙÛøYoCZØ5X×Ø5 í­dc“D¾úœ!{rF¹#gØÊüiöÇäíEršý5¥øäÓÁeì +ÑCgWÐø¢€vÐö¬«°® §L¸oŽ@aà p•,*h{3‰nÒìrÒÛ"ËØ%ö;RŽI½È~/ôì5¡ÿÀ~+ôëÐ.è öZÒ%“`1ê î±B[¡ëQ_À~“ªµÉ™`)ÅôÈõ Ú@7&6ʪ“;e9O&pÈ•Y’|$ôÏȉ¨»eÕ»kLá»ú X %áeª÷äárá=vÞï~Þo‚Å…÷Ùý°¸ðîÜ ‹ og7,.¼m° ÒìÅ_×.ÛöP%ha˜¥ÌÒfi€Ù¿Èm#ÛO’uu˜±SªoQ¬£Úªm¢ÚªõRíÕQm Õ¶RÍG5'Õ\TS©vž®ÂThTýÕ}n“j§ÚÕÎR-N5/ÕGçò»†åÛοÉ9Ó æßçå?+i#MÊBÉKÃòçùõú´„’ Þ4…:§ˆÐç*ùì„=„ŠSIùWÃòsÎVySTôf+¶Æá©y“·S^öBÎí²G›ÃrÀ¹U^“ZÉï–—a¾¬Y‡Á.rŠNk\¢Á§Ót—ºØ|Ò5·™¿lö››ÝfÙ\ev˜çI6É*Í•æHE’$™$£Ä$"ÍKg®«>‚¿nžÉÊù Ä(l+ãBìkTbäI¢Éa‘Í-4¢í ‘íŠ~ksMšmìÔ jZ¨n‹HG‹¾ÊI›3›ôF_D7·-:Dé±Juö½4%Ñ4Íð¢Ãݶ.:B(-=|ÔÁõÂÃGc1b/Û°lkK›¾zˆèÉIßÝý>»J?ÙÕQÓýÜÈTÅ"ú6+]Ñú ý8¡7¸ŠEG ké'áM¼Ü°6‹EÒt‹ˆ# +½8¬˜"Nr…ÇEreãNeã<¸qµ\!®°xDœ§°PÄ)Š×†CCµµ"¦\!q/Wî™ð Æã1e™1eÑ׊§!.§¡•Ä)Bœ´R„l¹RŸ 92rDôd wcœÙ˜’ëù˜’ëˆñý·ŸÞŸ¦šc;ºÂ½5ážšp/èÑ_Ø¿Ë®kÛehGŒW(ºÁÛ³}Ç.®·õ걚޾£&¤ 5w=¤º‹W7ׄ†HW¸#:Ô¥ö†’Íjs¸f[(–jmoh¼¯¯#Ó}5´?¤±vÞX﫵ñ!Õ¼º•÷ÕÈûjä}µª­¢/"Öx{tH"-±u]YbÅEX¯=w¬¥Ìºo­X¼ÍnûÇ9H~NŠ}1}NM‹^xÕ’à’ ¯Â3Å«æ¢Ø’«²hv;pZÏUYQ\ZÓB|}ýñ~b=”ýÆñAQ_?Ÿð¬ôÅõA]XW·…â}„DôºÍ=°±3:d6£´‡ÿ$}u¾¬¸8œÎŒe —¢p5/4¦yÙ^VX˜ |ðÿïÏéuü)ÐØùU]´ÄcÝé`Ø +::ñ[»:£çp\âé!ÃŒSçÛÃ&Y›ðß›§¯?gåæ¡/§³wá–x~:¦?|–ø>…ýª’‹™w©»Ô=ÜQ cwÔò/¢ÇxÐ@f”(x‹ã ã È +jV‹ŽW¯d»¤J‡ƒgt‹½bžÝ^awÌ·TT.÷Ù.°)¤½dK¨Å†ÊŠ +uØíž…¼\FùR–HzŠØ)âÃH–³S©ê_®4q>| š,T0ý OwÚ}Ö[ÏL~:i½A“ŸOZ×X×€6-µ55qž_ê{ÎúêòeöußR7Ñ‹\>™¬P–Ët‰V}-¬fùÝÕEuÆß{{·{·÷o÷þìåþdïÂÉ…Â%\rIð0 -Úˆ ¹Š8’(’?àRi $•PZ¨@¨NK(8’TE(t®Tã8S°€Œõéd,ˆ¹ô{—„NkÝýÞ·ïí¼ï{ß÷û~ï­ÙU6yÐ,ÐîO;Zˆ©éÓ<(Ûˆõ,©A:³þD@ ‰øá ¥ G,¹¡àtÉfe}“üxk³JÁ顼\?ƒƒÿÈØK»;Úz·l:Œ 甕Ï~fÒ¶ï¯âk»·Ã@+ Ì Åeåªò]Ÿt²ïÜüQýÛoÔÕ¿µ¥î»:V{ç_xËîKtà,>]ÿöëõtò¸xô²:òäASáïj¦òÇ[­Tko˜Öh‘~øÒt¤¾+‘WÍ!ÒìÞ˜Fz$¼Ô¾,H6EªBÌ~ù¢DêÜu©d³ÖEÖ õi³7»H—í Dšå6/iã›ÝäOÞ3é$&}à"}Î3V²2Ô'‘•öA²"€Ÿ +. ‘‡ƒåR"Ív‘lg¡‡ø]÷y ÊÊ’³¦ñþuâðâú)§[žn˜ÙTü\Ñc•Mzìþ‘<¼sÑ›sËN­€²hLÌ'K!Óš©ðé&8ƒ˜9 Dqð(ÚcÔ@«ˆÜcbÆË0Ìq×ëÉ |KËAùÐõc?sóCùA–ƒÛ&`üÅ›.)hZ—>ÓHOÌÀ·±ñæÅ‘»)këèÿC“ðþ—ýŠ~ +™"-/`dÖRø= †¶ía*ŒÑÑ[=‚@JA¹Ýc2%•¡ƒ!©|­˜xž”šŒ#10ûHkëü´ø˜›î‡;(Ù%›@Fš  'ÍLoh(/¹˜¯à«':ÚÊ?¹;rñf⟠xY‹âªªcÀ[Š­Ò’;f•šcµ«xGW‘y„‡žrÌãš’áp<, …Ã(0 çdOÓòÒÄ ˜fK NÔàöý¸=QÇÛöÑv_bØÙŸ¸Œ›Q ñèñ^èô÷l?¡ø1&ó8ŒxÂÀ b ¸óP%zm@À«º½ºÃ‘á!ì†Û’¤&Œ±ZNv0/üÁ¥‡BùÇbO,š^øŠÕ¼æ/q,{ìÎÂQRE^€ÊŸª8V“Õ )Á%`Ò‡ˆS½>p¨V¿AW6®£@I<'Õ@0óÒl³HŽööÒ#kˆVðžA“•B ¹x©:a¼Sµw¬À"”|Çœê‹ÅbÉãîè—¤pÀ 'O fôòk!‰Ž^V¼ÖÂí &ÌæC˜µ[ákØrÄ37¹øè㪣 0sXnËu«zZfäçcµ™™i£,Úµ5±Ø¡þú;˜ ÒÑ/U¢úà.—&dÎÂÅ +ï”Uj«l0صÑÑIŒQEqPiE¤§=HÒëAêi +Àb b°º"×aö‡3 ÃL,é: 5©ÜT:K§hôz*iß½)ÿ3gëun€ÿâÕ½?zIð˜á1Á¯ÄÏTl+Ù¬Ûl:gTk9] +)¶üÔö¨cŽk¡e‰m‰c«š«Ö-·c?Ó]29ï¹[§UÒ|¹ÙZŒ´‚–h·zÄ:ÇŽ^¤@à¶Êg_+.¨«HMf|ÜM©AT@/ OY™E0S®“ÌPdI´”×D¸ŽcK«;שŸ]5¸÷Óu¿<ÑÕØØÕõrã£2ˆUxæÊ£‰Ñ‹‰DâÃîÇñ®ÄöonáçpÕÍ•-+_@ïBîxtHñ2ŠAÌ­Vm íd§Fu@…µˆUF«Æz‚ÏóIïyº&„)ßÀïW’E@ùJ“ u'jL&¢¬8hº&r’ÌS¯V ¦\õD$²ÕØ«VÔDíÐõá0Þ„ÆJ£&â2¾¥ÃK¸d +±È^ˆÅBÉLó‰,ËåAÉÝžYƒ ·ÿ=P¯Zÿ`£çà#ç+éÚ€eÖ&ã³ãXÒŠ‚!ÅbaK J¢˜Tn*ZAM¶ªe +Q;ý@–é¨ì6ˆ¬§žËQÒ¯è o·{=‚Hˆ×lø4Fe âÔÓ"*OO§à%÷ êÍf’4¨hM"™°sEÑ™-¤T¶Ò>:÷˜š–ŠNGJí”…“QüÖ(ž©=j-iL = ~€íWŸdû¹³šsnn®¾L¿ÐX­ÆØ`n°¼j0_s^sÝrêOêŽ[ˆKp ©‚,°ïÞB€_­²å”yAòçÝN«ÛíÔ¸À§›1ÈB”¼wtžˆÅ(Né¥+@Ép˜0ÑóuöAˆ6Å:î'MÈ‹\ èÅÞ"RI^$ˆŠô‘ûà|Ù~x ìô̘IéÈe$ 'ÇÈh¦™1q,cZ4Q(‚#µee“miþ|ÈøÄ6OIxì8%Ë©¸ïó‰}ò»o}³oçúWÞÁ',·?üö'¿ûà7KäîîYáå§^>}íÙê_½Óf¹ðׯºïxoó²@ÊS£×U %—'NçHQhüSÜS¨fêágøxƒIo’y>Ã&»Ur†[aðô)Øf½¿—óÓ,ÒÏýÊ>±½‘¹°¨6‘8ä/~F8c.NgN§ÍßµA2Z ªbq‘¸ÖÅ,žª¬ÏHk ëþMwµEu^áÿq÷}ïîÝÇÝ]\. ¢H„µÛx3ãLÇ*ÂŒQ‹-¥FF­hÑÖ©HFEŶ¦Æ«NŒ¯ )" šQ¢©)Ó‡Ž“:éá©-™¦¥t¦µçü»‹˜Ivï?÷¿wÏ9ÿ9ßw¾ãkQvùv¦žPì&‹ºqÈŠS²P°K1-p™IQhq§,kR°—½FRØ*cxi7O}µ¾Ngz+Yo´ÔçnÊ¡$GÍaàñP7îä´M öÐÒŽ”Û´—–B#¹f8³Õ”ú£Ÿ' Kd9k(¿*Î[£°8¡Ob>ãé¨B­´®Ò[âOhm଒±e2‡˜D ~Pæ‹;ÓÔn;¼aÆ|ŸÇQßÓ²ú;­¾ÎŒ‡ç6ߪ]QÓÔûøîõGôùàË;ÞlÚzÌw„mnXÞÔܬ_¼¹²£¦úpAø­½×bÿ§CÀª©øM¡9ÆLÏ×åUò!ù´üKÙ4ŸÏW~"qÔ8‘ÍÜb²;¸…Èö[\òq.q…0Y‘,ü2»L¬0œ5ìD’àrË.õ°Ý&“ݘ^dO2¡=Þ˜ÄâÑ¡ì=´ÄP,FfV‘¥1£ØÒæbXNÅWD˜ÊtƾŒïÀâA¾Ã.:{h«8é¿û "Bz‰ªªàAu(:uG"41bI—ËÇ-æYz¾'”sÇp̈ðÌ©.M˜ÅŸ¨„dÀ3†O6¹±""993 ®S#‚m+aà,¦3Ü3´,7wSöÒh3{õÇ7ntÆŠiõ Þ5òµ±cꣵPxØû3L¯Ç.Ž#ç¡Ÿ‚Ñ4§=¬ii¤ +‡K’ÂiŠ“Kú…Pb!P†œ†(Á:‚"íd 0ò<‚{]â{^èvMxÉ{Òû¶|Wþ0Õjó“CÜVh*tôq@‡êµk¯÷–Óåsz}N—1¼èˆá< +‚Öé24špªÛ%ÑÛ`5CG÷ÜÕê:u›ºO•TIP€$HIP ²`$Á6Ýs…=EUÚá¼øE`I,áR…Š0"­rÃháÁkA¾ ²Hñ Σu ¶ž€ `Å š—^ˆæ³à »è-íå5Mí­KZsOïe÷F»Ë›÷_£Ö{†Þ¥ê®Ý}Çu”Ïö³O߈}ï›±áßÞÜßñTme9 8o™Lˬ—î¢é´šršš6`šS U¥š2Ã>Ŧ$[Å&&œ¨˜Á€à¼€Pp„Üê¿Ó¯¾“Ì$L|}U˜É©µ)tŽÅÐæ¤ÌÑ—zžÑky¥ÆºÚS£o´nJ{ÁÚ’v×zÇï¶èxÄ“â˜0/Ê„‡« ±aÁIz–žnô²Baàg*½]‰Ò³%}=[jxÈÅìzU$f!P +Qü£‰Ú6ÅŽ™ ӈ៨¬ l H?îüh.ÐÃ&^È‹4@âàXŒ'˜bLd áƒlWI-0¡43[Ü<Ø ²2‰[-Aª£¾q)åŸ]N™[»øéEϲ§¯¬ìýþošÿ{ðêÎÛï–”ï]°áµãÏm9#-t®.,+|ê“ß/ÿvì?¿Û5øC:n¥§¯ŸúÅÈýª3•=Gž?° øÎo:I²Þpö)T‚f•lÀeˆÂBF%›¬ÔsÎðHÊE‹æ,ä²ÖÛþFÊ!÷ՌφË:º ÄcŠ3QÅ8‡ÕEˆ¨Ã¨Æp2ÀîqGâ­Š'3áfKÖL§d¿Øœ7Óu‰7ýk§ôßöÖ1O쳞ÛéCzó0Áé*0*0@²H!#ñì”Ij¸9t[TPàÉ›M¹a¶ÉXl8t‰)"ß…s,–¡+)œp!6]Ažryò)>V¾|¢&ããšøEM”¯öxZxrAÅ5‰ŒM$ÝÂsÒsÜ‘b2q%9iës¦¦7dÍ +Í÷Yßð/ÉZÁ×ø׆Vfm 5„[C»Ã‡ü§CWBýú°îýŠÿˆ¿ÝÏgåÕ˜Ù$ì»YPLÁ ݬç†ËÕØdÓÐ$½]§äNt"½—FˆÙýd[m›‚<݉4í«%·áfî¶ü›ãÕ&–ÒàøÞ™¤]RUG«*ò)V\4 Ù®ŠÉãV6J*JFµ´¾Ý¿uÙ†Š™tæåµ]#Ôrcßàs[>=þÆì½7wœÞÚpŒ.T·|wþ¶÷×ËÁŵÔúþGT=ûs쟱¿Ä.œ»Ê‹^éê;Ü +” 5s ÆŸ)ÍBJAG˜ˆÙbcæ¨Ä£Ô,ÙYt a:œÅ1뱃 ù¦‘oñ ÃçR?¯ìï9Ùßo¬'’¾,½MÌd©!ïã&ÆMfne¦Ël)Üäli3̽´äi…¡‘³ô¬.±UŠR4¸É²d©8Â(27I™*„¿`HG‰N€¹©F©¶ž¿7ãŒm?E]ˆõÅ®_Àè6ÐcÒ,É,¢ûª1Éd¦’ÅF²9ÍæÌ’-Iælh*?c¿fŒ]5‘¦XÑæõ:@¦• B¬Q°#Ð0ûŨ23¤Y#¥ü]üðo}ø¹ˆÙ> ´&uÎ0bJ$ˆØdPŒØØ|VçøB×2è1yÝßR‚ ‡E"öáÕ–ÂÉú5 #ŒD§YÝä–Idnš¿Ä“vñ}YZƒ~Bº¤“véôÙpI§%ü3©MÂÒN×Ùs6J³à‚3® f}šDp¸æ +Î-Âäx4ë–É620C|·CCΡ¡-‚=B¸gwkß›Ý~¸ia/ï"²4p{ôòMš¼h-Õ¹ôÈCe(Dˆ7Bb¢DpÙŸñ½öË èŸ¯ÖMÈ)nÕ¡c©ûqÚÕÿô‹/P± Ï5ˆ”›)Yo?ÇCLfjšøÏ×åÍÏkÉkU6*â‚O +«•Ví9á9M,ð+$PPö‡Åë MœÈå„Âà·ÜpØÍɘ¨Sé B?g•Qº=”jE‘z^”鯋,Ö¢â@œé9ôºJßÓ).2è[zð¾PØd¹hÒÏ9»ˆ¤'ô]˜ÜêeA¶'"ŒX*ý].¯Zd‹Oz$AqÍaõÃ_ØKq X¥PbÚÃÊ)îJbH§j4^掔úïTU'ÎC‘ÒŠi´ˆÆò K+(gÒù.ë<ÕÚ²|Óöm'·¥v¢ê ÓgÍ®{vOê}´jq¬¶iƼW¶¥º„Æþe‹”k[~¤¹„Ìuû[ê|bâ×’>ýñº¹Ï”ÐoB>8Γ_VÊgJ1Z^p†ÏC ß;XúÝÕáÔ¹×£ÚÁÐ}UÇËwüû¢UmþÕß0.ùüë“èG¹Š9rùÔ¤Ž—ö¥>ßñÛÔµçQÆÝÜÓˆvï®ZSÌ\T+Ûèta'ƒÉ +ʵ˜f RTŠ(%ÀVô%sCÆ·†ÞWãл9½ð½ÐKÏ“ß@®¤¸ökÉ–dQd^æŬ@0€EM…?jÜ+V†(„eY’8ÂSGªJXãd‰¢#dxÊ¥yd–©š¬¼ò¤«^õ¨  ´ÓêYÂ&뿸¿7OKŠaבôµÏÝCâ£ï‘Â@Wªæ”£‹f”D~ìAá^´âlÛ%tg°…_˜ØÔ0jßÂnKc†ëé$Lý—ùjÕ¢»Ì¿Ëþ+v»GU×½¶w’G¯âî±j÷Œ®±°aä~?ä~y­`¶/;7 Å²yH~>ñdâ(Æ,9MjBbfØI ÿPŠDó¡¢Ã¾ +š¡7µ[«tõ¥;É_YXõͦßÇkÛ +PA(fªHe\ÍŠ-}ôN*×É/Óûã§Üiìì™ò%\´‘@ßÏçeçs²rˆ¨ÇŒhF,7&GùX^4àE8¿Ë—}^S‚§ B4‚r4@¶Ï ·°‰pùnýC@¸‘0ññƒbÄÇÔ¨û?Øß)MÆ@¢ÕÃT¸ÉCxÕöÔ™Žó©½½=¨áý½½;yì蛟ŽLß‚ðŽõ#ßÁ5o ±Ëk[ûÑâógQkïò¾—‹W·Õ?¼ñ»[÷¥n¶-©@nˆÇ~`” ,Î÷sðzЛQΓ°¢v¨gT¬ +k2d°)IPò>cþ†ÉuKcUÏ`U¶ŒVù«|É6r`ÍF ¥ +?ú-à'§áwãøÓÙc:éhp4;V;øªÆ@<¹fœÒ dÇ1N1ùT“€Ž’Ñ‚"„+îûñ­ÁÁ1Q;€›nÕáž±z°ñ8$ÔðáÞ~‹æÀêžéÕål,+·ÇIÅöX8Ñó¢ö +Ûc ÈF轌rSh €UkÛ¹®›ã§p×À]âF8ÁcÂb;GØëÌ“ÐñØÞùǸw>÷Η–a+=æ}üÙƻȷvÑÂ7Û@Î%׬MŒ%Ç]>¨¡©Xæ>>H¥ì±âöÇd SC-c^.®ÃOŠ[[Ý¢Âò­W£éÖ‡‚–Ƈ]ŠSU9¦Ñ¦’ZÆ&Ô Íf6±‹6]±ü4bZÒô"Óky¼Í^Þ‹b´žSâ§ãAý Í)³=GÇw2l$×Ø;¢êRp8^ÃÚÖ¸ßÃF|´u‹U–V/}peá`ãÉgO¾ƒ:?­m]O®fõýiåEÊ‹ ú„¹Ñ(e…É„ŠJY™Q N§©3Õd39G¤§Ô ä!ʬ4 +Ûøç…ßðŸÊ‚Ê£©üY+ÔŠ'RNLzÑУWzèj<Ë鑧cˆ'z<~º~Ѫ΂ÿŒF«e%+«RWQYÏ›‚êx‚tAµ‹ªÊ ˜GXÒdNV Ö ‹éÃ3,W±€:„ná„pYà…Y2]ÓŠ%d‚ +ï–ˆÔ‡7[ºfþ¯Åèú7Ũ“Êø4††Ç £'e¤MŸD‚^À‚TÈ;©@Éó0‘d#!'@¶@¶gƒl§ªúüôÆ#"®ÇFzt7õ׈• ÑpºËåS^%°Q\gxÞ›™ÝÙkÏÙËÞõîŽÅŒ±°ÔQ nŒ¹”È·W Q-qŠÅ „Û „ …(4@‹Ò(D4M¸ˤ-Ei9Ò&Qу6i¤‡@¨u) +xÜÿ½Ý%nQ%ºö¾ùffµ;ïÿ¿ïÿ¿_•ÕZAn´Áä·ƒú&ú[š« â6%lrä]5A šVÑk +e~“³ü& ó± À€™ýêÕA¾õöå² ûQÁ¿S:‰?DÎñÝøÌøÍ ÿJ|qügwv᫵¹„óA Ð +)È’F»¡ëVA/¹Í+ÖÅ ,¸D™\Ø-:¨vÕ‚p¿¦ÂUáá®-ìäVq'wò;™ +Ïø;º@ 9qB}ÿý Ùl>[L4Ÿi«ÔI둃®,]9ºòtÛRaj* a’n,SûIç#7]ä H˜°R‚ yno­BÞÃ2HK&€7#'ßFý’㸕ñB¬Z-©à^ÅðÓ¯eÙËØTà:m ùÍä¾â^6OǨµ‘ÁŠàÇQ{ܳÙsBéiò4)l%—‘¦Èíl÷¸ô„<( "æSª“ãfv®ÓJ_—Ý»ðnvÈ9$`_q:¼X‘åi<µcÁ#IÓx àyHyYcAp¹E¨û²¬’<-óx±w`$4ý0ŸFÐtËíq¹–g£ˆÄQؤŒD¸ƒGh¹ ¢²FEên}3Á/ãxh%øÀ´Æ°:–Ë5èÀ³k‘° +ÊnˆÜ=¹œct0l ꤿˆzíúà†·Aæp€ÚÛü† +/…ÿœñLÜ^`ðÄ…Y³fu€ú=p¯‚ª_š¸uHv“« brzn8iÊS’¦4°Þ”kê)r>6ìÏÃ8À·Ý-·h’t …3ñWHÛsŠÅ£§îØ°ïs!Y·Èܺ\ÿøsŒÌD™³Ö’ˆ‚üªß E£§r~1$F¹WCÃòod6Ò£8·´Å¾Å!+Òη»ÚÔm©¯3´To´EŸíÆj¸„e½%¢+`$`è!.ƒ$ÁYtMnÐzì$¾ƒDÀ-N’–$-=‘8Š+É¡cRéÇ–g˜%9¡Ã”Ž\/“Ëåz}*“¬áÈhJ={½Ę̂a´Zl¤Ê˜åh ª{5èHãÀn‰Ñ™ÃÖ”Új?nV›ý]j—Ÿ=%Pa˜žŸõ¼†à&ûÔBí-L?B$AðÑ¥ÿw¼w‚ Onc´-R{ipH`Šó+õÜ0Ê€®kB%b“Lj€ëfÖåF*¹Wî\øÈÎŽëö{ Zÿ־܂éOÙ[ùQÙ»b¸ç¸=>þSmßØýd@"ÌiŸØÁ_æ˜ +´Êz~©ñ#‡õúc\)™Æü¥þ”£Š¯e9|Ch¶±€_j2r|KªÝø.¿ž]Çog·óÏ3?dÂdÏ3çƒW˜+¡+z$Æg™*~ÏåøúqÞà2Á*£6hMzSl^é¼T³Ñ*´k-ÎXg¼µ´-ÑVöm~e`µ±ÞØÛa|¢_2¢ŽPÝGMÕ9kVÔät¿^ÅÏæ9Ì+Xg…¡yÆ‘d}“†O—”(,Ò%NWÄðé$¾"s}EÇì#&¹ð™K€•!YñÍÇ‘DÕ@®JPD:{‰”½b¸ò¿Ù»°àó({¿vM ™…™6d2Ú õŒz&ß s9¦ÔäÞ¾ ŒF¹cÒJ8Wë +äÖÓër}æ¾½/ýú”ýÖëo ygá¿z ç ðü#ûs½´ª»kÅÞ\vÐ\ßuuüúÖè¯ì—?>fúÌÔÜdFîçì‹6|Ø~·|Nr¾êúkÀ|)Cw¬¤W”‘·.ÖYºRè)å\*u tuÒ5M†2‰DŠOˆEà™øüˆ7R ÇGÊÊk5r/¯U G¥p„û‰ùûðyµp$÷­&y~l~âa±;Öës=!÷+›Ü[”¤W•åÏòŠ +'¡)~MS4ÅãòFq2t;¼š*yxÝå +†"á’ia’´PˆI–Q ëÀY(1ä=BB +GQž2‹”‘}8dÇŽ\"½&=fÓeúýêÚñ?{PŠ ‹ŠÓ{!iBTçáË:°†š„‚¾³p¯Áœ +ÜA@¤Aù,S‘|vò‹)̦–[°SQgkÞÙ¤U ^êdè8‘°©AOòÂ[¶b¦ +Ö^-+…÷Ý&C¼AM0ð;œÁP0äK±`(!)ZNH=I%÷ãmoÿvÝÙV´,˜;Ùòh[u²ùOhÿ¦¡E/¼dOãGŸîßs!žI/Zk÷¢éOmŸ%:Çײ3êû¿¹j3™»'¾àþÆÀLë|9»œ{ŒýÇeÊg²fìl“sA|^éÜtcùÃl‡³;ÞV±Õ'§À+Ðv“.‚LEP^)šŠü‡ó SFÀ‡oZUHF§ÙòLR›š›™7µ3ÑšjÉ<"~GZ-¯ô¯ÐûÅuÒ:eƒº6ýXf3»MÜ*mSžQ7¥ŸÌ씆”¡@IÁW' oÔˆ¸ŒJd0LeÄËÕL7˜ .©º?úo²«56Šë +ϹóØ;w;}ÌÚf™{ñ²56µ1^ìÔ“”g"“P-´ÅBšòŠ’P‘ ’`©šüh«©êC´€ñC µ”ZIPšFJ«TA%Dy¤qEª +x—ž;»ËCµ5÷_ïcæœï|çûöÖ“ú¦„Ñ’žÑMrBæĪq9Ý¢¦Ó 1œsyäˆ"^Õ­\:·NT~냖¦FÓÐäLÔt=(’Hhjœ†gŠœ®o© 8ìàì™H-À*+ |è‡u°A18¸-ü+ùWã?ªf…äøØ6Mò¿5ƒ¿/W÷U|&È:\²ñ95;ËÑ°Ë—ó^HÍþÎêО‹œö,dÁÿ„lx£2À­R1‘/7ø!Œm.±0D%Ÿ¿‡bäBwnš´WÐ6#Û8#›ÓÑÙÙŽ¨D†D&Tâ±dBJ† å|™]3j¬ý`×s¿z²Mwù™e›6|ÿ˼ùª|‘O9Wž<ø&WÅ¥“G?üíSã9RqÎ-Ä1øÚ¶­#ƒ@CEÇY¥2¹õÜGÖ¹ìövÌy/uv[}ÐØ*ÃL¡Ylb­z›¾NßK÷ªƒúIýš®ùz¿N$¢QRa‚QtM ø‘½½|èðÝLU}*Ç(•„‘c„È*~ÕUŸ¡ 0@(O¥Ö\觰›Rü 0HÐ\XKàù!„ŸØ¾Ü/“6t ƒòIùš,£ }mH[÷‹Š }þ"v¿< »I]jÂëíá^‡÷š¸AÅiÆÐM¢X‰UàaÉÿÕþpÓÙŒ/ë M§pçdתU¡G©¹*íÙäáÒŸÀ®YS§µÀëï•N¡9³{Ë /H¹[ yÎS‚ÙÁµü-È愬s²^Aè´ N§·DXd/qy+…öJg…g½MߎV´[P—ÊÇ;ä}¾<_,¾\^®¯Ž¯—×ë›ãÛämúKñ¨×EŠ­FÂ:öö†UK†ìÉ“Ÿ%Y&J“ωªaF£zÌuœx"éy(%{†dÁóù®;6߃§âh9™ô1Á“)MǽX<î9ºª¦ã†Ž­G£¾eÇ,ËvTzq9j[ØWxK²èYѨªRJðž<DZmÖ%“uÖÃ*,|AÇ5ŽW È°lÄ÷ •ƒýG* X—ê+Õy¥R]ªä=¾``þ¥»šÀªþr=€Ïg×.´«}¡]åEþ¿ ;iiãÒ3^‹î_°ØQ,¶Í1á0û·‚€&<œyB:&ž étU@ñ½"­ÂupsÛa:  ü´üÒûo¬ëbüü“¥ÓZ.ý±üì‰ò‡3"ÉXù4öjïßúg£x¾TWþâúþcâoÐÄ_÷Ý>XíØ%ˆ— 9œF)Hh$çäÜ.˜+vÑ.µË˜gÎqæºÌq}'ÓáðÅDÞÂݨîju§œÏžÁ@â¯ù²vj$+å"ÍÚL3ëtJóè<âbº\*Ò5ÚSærg HOÓÍÚ&sÀÙ.½H¹&Øéìt_•öEö±·¤1:ê¼'¦g¤ÏèYóSç²t…^1/9_Aùy`ë6á_5ÊWlµÿñ ŠsMâ1Ëc¶Âuç•Àä‘¥Ä@V"i,;¯1ŽÇ +¤‹ˆfUœD¢ˆƒÆš†–eØŽëj˜3bh¢î2 ‹¸*s]_P‘ÿT‘†¯‹1]‘‘D‘×ÀQ/ÐÖ8ľèDƒµ£>d'™ÈÆ`lxm•|Ʀ ¬~ëcK´ðEó…T,~*ÃÉ'ÿø ŽÙ¢÷ÔDq¢ˆAÛâ¸Ý#?Q?M¡rTöÐñû· +*ÇW…¶âo¡6BA«¡ ÕRàbÖ«/ $9–Эl¦q¤¾@§Õ°ö'6 N¯S +. +_/ÃL${\'‘|ˆ¢Cè%ŒÐ»œf¡U›æ4}Jæ!¦dz4Æ#Â#ÝMâ™›Ä3ŒÐày¸/FådÙ÷˜²Ö*™[Ö/{rúì¯ÃŒ¿”J$­|`jfv¼ÓbŒiŠ¨¨"—Ê ~*úpp¢k + mŒ¤•1ÄÎOsŒxª«Ol7#ˆœáÀÐ4ÝÄ'–’!‚†Ù4VsNÊ#½*‰.TEñF ³ŠªPp%„²²]âJ¨ã»B{fåó§šÌQF{L$5 —Ç'‘¬¬ŽQ]Õ¥ãwnâŽ7®{g¥ûø¾ïœóÙ¸¹˜ïª‘”Åñ‹)C‚Î×úa¬RÜ»cG:œI)ýÊm'\)5IØ ­î/°|eå‡ÒÑ +% “Ž’³¤ ’(Pºûü¤‘¤ Éybg™@Óª¡wEB¿”j˜;ò768òñræý.æƒUóÖÅëÜ“m¦¤ëžmGY&Šh…㣔àÁ,øc,ÆJ LÁ9n„†»ÃñöZèœÓ˜+£©áé—XwE¹W-ð©Uþ=èÞÞl³°¸íno UœmF]d¦[g¹¨À+ ÅÝÏá>ðË„LË[¶“ÌÖ‘Ó¾O2üÿ¦@‰*µ(Q€º²›z{ÑÙæ6¾æûG¶·é_S …íª¡è GìÌZ8î~LæXý`E9ƒÅbg›_~™Â†qþØMÖd¦J¨jzŠ=9GÊ)3¤`Y©TV–”¦ù«sg”Í+KK鲩¹¬©ü5igé[Z{ð°ä/Áy¯ úìb¨#D‡Œ#%§Œ?”ôKþî¿^âxTCùàñThÃ=bB‰M@ul„ÈÒ­@trY<É&'ÏcçN^æX}ÖÑÝâú¹ëc×]énT­ŽËˆUb¡¸^Yà DIÖV ¿¨‡…±Ð9þ"O[| Oó2ì”'ÉŒÀzpžÇ︮˰]^ÉùŠé&+ipx0ª`AÝßó? ” €óéÖ Žö㜛x?ƒ G ÈÄHáÁò‹R™0ÏFñiÓªÉ_"^)*ä…â‡éªJMÓ5¿ß§éE†d‡Ø>á—˜ÔªZ:ÏÎÙ47±®o ªšµkÛ‹yÇë/íÞu¤AÉÑ ÏšúÓ=ž¨|¾yío"y¯4Îþ`Ç—úd) +;×Oyhy&ÙSg¯œ?uëíïw<4]/1•’úØܦÇ=ôÌè˜ÑVšBå¡ív;â\î—àfq\uÜ¢-«Ð¬217Zû,~†7¥¥‚ ´Á´#-ýØÖž ¶8ž“Öº×këƒÝV¯«Oï3þé½¥ß2¾Ì»aYÆ$.æŽùʹ·Í-p7pÏr}yß±÷—â—YœYsM\u~S¡K"RD[l·‹¬eZ"ᨠ1W· ‡D 7y`ÄŽžâ H­¢ù(¨*&LÓÝíCÐqt±ªA‹ƒ[¾,!-Fì< "TA +|y€*ˆPºÃ.`yUƒ©QæE>˜ùsªI²¾ßj¤3­©zeô+£÷@ø£&‰‹…r—i¥28ªÓª*ói¿B3>ˆˆUДßvµžxº3cg¿ýãÙut¼ñ—[:ÞÛ¼¥ƒ;3úÝÞE{?Ù”Ê^}½y®qÏ…¿^:çц±›Ì ÎWA´âMJ[\ÞæFnÙTµç>ÖcŠBÀdE$ûì^ »\°{AÝ „á.Ÿ‡U*=éJ¸*Êsí99.d™3½3õ¥Þ¥z“·Io§Û™·¤ƒÊÁ Ë!κ™iá6»6JÛ¥C®“9§œ'].͵Óõ%ÍÈ…O¹7¸·¹7Â)Æ~±œ‚E5áeí£P7¨ÛØb¸Ý"u&^zHvüT˜‹÷£î[°G³ @6Ag.Á$H0™gúCd 5-Èð’à„—’^…ŠÜxÔ§qTÆÅŸn­[ZTGœg5Ø:l%{ÇbW“1%Ý?ô‘Ë‘Ú¦Ô¸C§éBЗ0“:‘7t¬/ûïÖ¯v½fuÛVì:rðÕ–_ úï/¢<äì@ôKïä®{î/Ÿ]ý3©1³1f_`EªX‘öA'ÍJa).=*q _Â|Œþ‘s‰o©¹†^Å­ÎyÆ×dv[—¹+ÞëÆ€wÀ7¤m åi– ‚\ë‚ ]a*’¦j3è„TGÏ’fûæ™9—Ik¤þ_Ú=4,+ÈÏÈ¢âÆŠ•Â’dÄ@¢Âª;¬(—T¤¨¶Ú¤nW±4ãU= •-ªÊƒT"X<ú-~Ÿ¸*ÉãçoˆJqðû@G}Á:'\¾Æ Z$0B>¡ÉÓBþ8 l¤, ¤úF~¼á¥¥3õƒ£Š.¥@WÚ˜¥àº¯³LË,¹'ãqÀ°æï¾Î˜é«{¶]ÙÜrù•¦7cŽNêؼå½÷ºõ¿~ýûw÷#æµÅµ´|o6íùô“?ïû´0«ÃY4ëÌ1[jëeúqWžæÒ9âjf·!gµèðC$Ûƽ¢<îÅž^îžïN­ðÌ0*ÌZO}°Ö\ìyÂXb®ô<\inå·úïÐw +¥!·¤ë Z“¶Qc4Ó½O9 ÐŠÂæšN:CÆNd³n¬|î +VG›«G·±ÕºÖG*0µõŽ”¸°œâ²øq IA ?}ŽÄá¿] eÖB–V¥„;TŸ@jÒH™©q™#à…‘z0'¦£õ£ý • öx®‡L8Š+d?W:5šI‘>àkI*hë„Īª’R}Bx¡‚)¢Ì“g&óÑWÙ!ä»vÉhä¦ów;žy}´^ìš¾l÷Ï£eú»]ÈÂÉÞ…J²Ÿgï*“:ϬEm;g®=„³ˆC¸ûŒÒ‘dçûrÛˆå†ml4Ú]¿’KŽ T"7º Ö€ó( Zñ<‡Ä¸Ü¦ùé¨ÏË2<åÜïC¾1¯Íêáÿ²]5°qWxfwvvg÷~vÏ{»gŸÍù|¬MrWDr;N­x)Ø)œÐµi\D¡jãBê4Q‰T¨\TD‚JãP!*Šc,jRT,¨TµŠS•ªA‚H)КZ-u!Ävß¼='­vfçnçnÞ{ß÷¾UyhIqÅêŠÃb}®2Dhm(aRÆ&U»u Z­& Rª.¶.WÆ“D 'ïaÃÉÙ_  ûi¦ö%zœäÉ 5É‚+[€ú3ÃSöÔTdÎ@Oµ;Û+v†®íp¡s’-RYâðd–iqùÞ½´8Ù&…syU¥­`´&Y-]Á>zøpMݾíë7eW¯üÊ•'N¨?4pw¥ûk©'ÌîÍ·>tî@Ä—æ®S?D\D–Óï„›-KsKVà®·º\.jJV³[*´[­îÕV·{£ÞkÝi5ÿN\Z(µ¬-¬mYß2T.é­ùÖe¥n«;ßµìúüõ˾©ß–¿mÙæÒžÒ-ïç?*ü£Åñ=žWŽ]R_£c'±ÉeØGö 2 &b\ WjõõI³«©>fzérP6ƒLfÒ§¶ú›ý=>+AÈ•JHk>Òš¿Hk>ÒšïákˆÖ仸¼hÍ—¢àjYôþ=I¦ÜÅ/'O$O%ç“,—ìLn€F‡ˆIÖÉÜ&›änÉz¹S¹-‰Ü–¬-–îÉKz+ö,¡·§ì nöÌ ©3?gä(ÝNÿ4%ß÷üH@¶j”ˆç|ðA.ŠÐš%dwÇsÖÊ+îýì®O÷Ö]¬ÜrSÛÈ÷iÇ[)}èàžsw}rbÇÏÔå''^þí+¯½"Û~BÔ÷¡k¹ô–‰…Ÿö+ª´-(¯¶JíRÇ.­ñk+¾áÄWÕ(IÖkºk™±@„åÖʼ ‚zØc¼P¦A\‚WW¦@HcáÈÀ Ôv¢N¾Vg¢”W¦DÈcÉϦ„‹|ý[ÑãI,ú•Öʈ7í)[½aoÄ›÷˜§¸â5´á;LÃyH#TÎiÂ$ÔPå$ô¥‘¬4äGVEèÙHa© äìI¯ÛXÕ¬I‡*-Å…ˆËÀ‚‘l§© žÐƒeiÜ\fq/PÓb9R‰ž—v +¦‘§ýc»'¶ÿüš±{ïÞøpHÂ>ÒÿÔgoVŽì¿ÿ«?œý%`ò$ +^Õ§“ß…ß­òÄ#bBœÓB'"'¶Š=âpué´˜fN€ÆÒ™¢ +®î¦„kœ™\4³a6Â&ØiÆ'Ø4Skd“pÇX¤••ØbÜÆ™òS2[`6&E¸Œ“ 2e Yqaô¶Aô$uNa‹ÿ²ä· kV•Ó*DåÀØØûÛ‰Ÿ¥Yógo­Ï?9w]ƒgN‘×Ã.¦ÚYY{PÓ|CÓtƦշÕ1G³tyB‹ëõNrÝ÷•ñÀ4‡,š³:­ –jɵÉY6:4 +zJë"t&1y(Ë@O‚ضjkÜgóë–¢Q Þ £ÇîºýÊ÷HçµÒÀ©Rí‹çsÊåý¶ÑU„a'› ÛÌR‘г$ªÚ/ÏNÛÒWˆ?86wgSk®­u¬|ùcW±NžüôþC‰«a›>~õÚ-¯P ê'K¹%ÌòH[ñùMBMÆÿ¥ÍpUÄdö¸ôXòxæÂD,LÊ|ðõ>SIñÆš|Åsö|ª¥"¤Iƒ1¥áBÂ`…3¦1Þ&ÖA*øÌ^ó>õ^ó õ]®å´À›õÀhç«Eg|C¼õñ^½O ²Ú!ñÿ=û#?Ã?ÐÿÃ?5Ò)ÓÔT•)œëBp# #й«ë\e,ÐLWÓL +–ÊR㺈%&§ÉPhLæFk2ä]¾ݲ¥n%¯Hh'ÙÈò W ö1ã3N°’I +íAkBjcñwòëîXškL5ôa>3Å~ s„¼ ¿PÎŽß¾_»´ÈíWaÌ0Ñ!íF‡Š×c½[üAsâU™¸S.èƒú¿bSohŠRC»0: ao6´Ãð‡ÑFŽåÛñ+ôòýKŠExâEÂç'FóíĉQOoÚí<ð.†Ã1+z¸ØÅ& So1j¸|šëvàžšÍȇÿ~,½ö÷¡t“³ä+Z¦´@u@(}惹»èËoÏùžvüÜKtdnûì%·kîë².÷Á¥ ñúî ”&eTÛê +Ž•UÑxÙŠhl +p h7I-§ÖNil\¦55§mÕöhó675"x¹}”ÍaB'Àf*KÙþ“ÏÙ¾a ÛG¹Žô˜QceÁdYä.ÒÃÎç.I^ÅbD_Ø¥·EË22ûÆ´ãg»«=”7ƒf*Ð_¿Hâ3¹½1^‚þ^kÅ+;ÃΈwü¿4j¯k3Šo4D&Û(TµpQ=OKI¡S^¨«µÍÉ€Ã%‚!‡: [Ýšì¦:6WÒ‘ˆöåA}ÒÈÂ$z–³‹î­êbhËCYšÅí²‹Ûeq;¸ÿ(tävYì’YSn—•XÂæœÉáþnœ•ûyD):I{ÃDɉ?ñ×ð?øCÆ%^µŸ[Ðȇ.¶â(‰’ãtÇó206p$ö’•©%͹¶¹y‘âZ‚àJ‘¦#ZŽ¹5ÍnÌÉÒT<½Ð¨«Öò›nE“)/Q»F½´qYyô®íåvÿæ'Ï<_Ø´vëÆz·¬ß»†5?Úsó­½ÇŸ{a¶Eyâ[7¯yô©ÙÇ”Ñ;6>þÃÙ?/h®÷ ^<:Öh*¯Qž¶ÇíwÕ¿ÖL«35œIÊ퀂ÙiÓƒödætf>à 7áz)Ð\”{q3žˆ%.ΠÎÊ æ²PmY¨¶¬Eµe!¬&|‡Œ0ª- ÕÜ%ÔBµeI5†th¡ ³(üY= º:©¼2Óekf83’™È°Œª”ÓbsfÌq"äýÁe^ ¸œ%‚‹U‘8¦.p=¾=Ó?ðyN…£;o~¦€¥ƒ¼¨Â<îÓ0uSåv³ÃYš4SÕ$/ß+å6”fYê1ÿ¼ïòÞ·6Ùh›cËïþòÉ®Ø&Î;~ßùì{ølöÙw¾ó#ŽíØÁ‡,áa¡RA(Áfv ¤%2*` ++”>h` ‰VªT +ÔÇÄ+Te*Ú€Š"Э«ª0-C” UÕ4Á’ìûŽiJ%Çþû|ßÝåÿûþ¿G÷1.vàøü‹+¶t³¯<ß1gßÕ‘?‡ÂÔÝåâE££õ{¼ðŸ(dé9`$»¡Òé.^ÒåzËa¹e…ðœ¥MR¸ÖU«VyçãWƒ:ß›5gÅ¥8çÊ©K½æq-îpu¨k½¿DÑb¶­4-3/“VÊLëÌë¤ ²¤ù9ÞI(ÃõÑìã£Û€‡”C³ï…£<?ú úO ê hA@P¢%©©ríþ¯QÙ®^¹úç‹W>'´k´+&º˜ Zé“ñd< 7`.:b‹B“äH ÂS˜ØÚjµZß"m‘o…°RÎjY_»°^nÃÚzßùÐ ÷×Þ¯Á!÷Pðvh,¤F¸$Nzª¸Zü··àZïF±Õi7©~?°¼ê·[»–2R«Ô+q! +aˆÂI|ÛŒ€”¼ãß Ý¿(–ÔÙ„ìµ4[êAJ%[é*a˜óÄ¡ƒèz€¸"”FMÈ„@ç(#Êƈ²1¢;ÉpIà ØÑSi0B2\˜P$ÁéEõÕ^”lÄO1ùnèû˜›ë$(¦©'¦\Ëä@8™N¥@ªªÇÍB ;MÐÛu¤vß/^hqpKËžrçÑM›?<ÖÓ}r´Í|îõ%Kv½uxôÑO׎<2¹váÊ­+Ÿÿ¦pÁh›é6Á3~4=Óge“lÂ;ƒm`_’-iOZoÐ÷Í)%åKë”:_³Òì[£¬ñµ{ƒ7-·\w,ßÈ÷¼x–“ž¶J^È>%·°mì—òWÞ¨ßèw|ÿcˆ³¹ ¿•·[Ü~Ž§Ù+™Œ;2ŽVG¯ƒ f¬¤AŠžÃLèÆ¥Ãè°@Û*ý +)´Þ¡B¯*èÒÓÓ”=zœÑOùëü ?ÆsEÄä6‘aŒÒI6`!¦³­æ/g=ì)NMPÉ\çâá‘äœf”fþ¦9é$9SŽVE<ßTLe‰Ïœý÷ 7~u±óÐHñG›»ßôâáÑ6V˜ÑˆÊpôå£}ç™~wíÚŸ>»ùÅg p; 4—*NærfÆaE¸7kæžåz8‹èDA´)NÑƘd¥#ÁHbé^ á‚6ìÌ¿ ~R&x½ÿfœ„ÆB‰èŽ‚îaÆ2Áä7ºê/ü0že¹ﺆHs 55äEƒƒ/ï²o»êB¹‚'Ðxè O„bç¡Ùmé•«fÏ;c•;ÈÅÞë\P{,^Ÿní¹ ]HÝ5$]˜jÒ2[¸°;\+.ë¢ËÃëÂ[Å>qGô¨òaÙM6Q3¼ÚÔ†²/4³}†eq’¼Y!+f¥¬5+gmíB»Ø.µ[Ûåv[¬?îˆÇ¢ñè¤éÑi…umlmiO¤'ÚÝ/½#ï+=PöÛ©G¤÷åÃñ#¥§ccjiÁ‰† E¤PD Ei>ŽŸE¤PD E€äŠŒ+XÓ"ÄKd‰3B1g-Ÿ°dÂz4¿HOëMúÏõãúuÝâЋôôA+Ò÷è¬~Ž`ã!ûâTÝp:FÄb4@‚ˆEàóÝj +QØíÎBåÙÀ†ð{x‘⊌sþrk‘Œ¨žQ¼© +X>ÆN÷æßaZtöˆ‚•zVé48ê*UéOØ•§øh‚,=ã¯H ÜV$`<á2‰Âœ’âÞÇ°(aÐ[Ç©ÖŠólº¢·‚­À¡(ãÍû]ºåBù.j‡ŠŒŠ:(;èã9Bã ñ0¢¼a§Ä SRb­> ””Ð0òq*&˜|t5R‹‡:“‹'xâaâ…á¤ôp§«fJ>ÙÒ¦d³“ÙóZÞ=eⓃ³»,æÄ.¬`“%l ù±”÷!ódòt“¯Åöˆ Gl²0Iò¡Ò¸(Y’œ)ÂðYIL\Yþ h2‘ܾ};3ŽP®«3§T«yª‰ÇâålUjzu^ ÈŒQ/æÖˆ7Ó‚l^ìcéSŽ×¶lÝ\U²ÿÒÛMs~’øMó¶s-ÎrwÛÖvUâÛñéåm—¶]ÿÍò¯ïZW7+â-©X¸½±þ¥Ò¢ä‚-Ïy—f—VGüEŠVÎÙšmy÷§ÁœFǾeæ·ùëYF"{0K‰€ìRôêˆA²MB&FÅbÒ!é6Y8Ì„‘ÍU"£1^˜/Îoå7ò½ü^žcˆs:ÈŸàÏó¼…±®âóbM‹oû³ø|/(«çtÞ“ö“Ê2nÍò®’ÿ=ÛÎxÑô“Ï>R‰üŒ‡€á‡Ó¤†wVVâË[“É ú«rF¿W&‹8ÝÐzOÏ\½¡lÇŽÓgÎ(ÉÒà{ïâÙë±kv#~Ãè›»Gö/.3 G/.»ÍÅÈÝ›Î2éèÑRlHQSxÚJ—;•TPTPT)ª•¹“´‰©TK¼Ä ƒf¦Í Ðhì„h”¾µÇùD£ùDz§ùD£Sƒ|bƒ~Œi輆´F0ò@41ìFã qÂ38C. ‡ˆ1$ˆ·EN,‡øX8DzgQ‚»Šp}ª"Í&" ÷uÈž»M†køÇ!„(ô==3¯tˆ Ûmká‹`Hádcœ>bH"±è/Y[\E¡‰p*dÃÃ@L‡Ú”ÞzkÕá&lí·:Ÿ_²¤oFÿ;ý :šªºÙ}#§ßœV¿¤yÏ«lÍ£¿tD¦» Ý;ɲó–ý,£™F,È"1fQ0£ÿS]í±Qwxföö1³Ù[Ûw¾3œ/œ}pˆ³ês„ÖHÃÃB&RŽpjªTFB•B ‰ÄD‘òObZU•Zµ€T•‡Ÿ@Ú ”"’Ô)­´´®¨â$4Š«Vr¨(õ¹¿YŸ ñÞÎkïÖóø~ß÷ýˆ:GÂOÍçFGÜÑ€†T;9ÕÄÐB£´Wd’ßm¯H!Í,² Àt}Pãj ßøS@g5PŠÐwÒt¦€j¡€Þõ`_vA¥ àÖ\”¥M¬ˆ²•hÛ€7’±‘vá.²ÕØJŸA»ñn²Çx†îf=¸‡¼¨Ò_2Ó¡^úûú ûÒO±wÐoÙuôû }Èî  6–Ãb¨–eQkc(`T DmA¨NiáÚ)¬G.I‹pyŒ …*÷BŽ…vVîJ8JTÕ2áØò£9ظGr#9”oo2´1Ý02”ù”2¤ÆÄÇ&ÂÀ²!XÓUVó¶ÒFô%t'õ€JTh4Eœ6?ý£DÓx}|²Z(TZ5ǸÒoÙ¡ÄÞ‹ +-/eM?8ßï¸áVÙò‚°Ï<# ÜÖ8ì†mi2 ,“‹xLv¼¢ó@uFFÜ«#î•ÜˆDŸÜ`©8UYÁ€ôñ¼È\FV{z¯zŠ— +ÏoxêFH‘™†'i‡În,¸É†fI×ÿ +†fÏ)D4‹Fµ 5‚"šIMÇ.Š*¾ž4fd°}ž‘s +h¡¾Ä¸ßyPY¡ú£Ã\ÎWx«Å£ü±MÿŽ±EìÑöêOg´³|P|®Ý¡YÓË¢¬Ýìdy³Èû‹Q›Øm¼hô*oXGñ1rÌü¹5€µ³Î¥ÈUíÏôfä&ÿDLhÿ¥IS“3¶ÂÒ K',yXŠ*lÌá‰ÀWTKW)cä(Ìõ<à÷Ž> ð,«‚.ÆÔo<ÝHéž9U÷UUwàœ3¶ãÛ¶c@º“c†?GêÝHAë"bpÏrìpzxÜ0t]†ŽàÜqóo¹6~Ì~Ê>`+ö0>°T'Ã;Ø~FØ0ùF@;=¼ÃÛïOöLWÅ©OAp)\Gð­è­®ÐÅ×L”Ë1ð5ð‘AVŽ}|7²ÜêKÇÕ¨ó²gͽ÷å +PÙã¸tÇ]*oÙ–wÇÉÙë6öÛ)+EÞœºžör¦.÷£ž€Q¼¸úWê8YXgL]>¥·àp q]ÇÉÖ‡7…£7Né©éQ£³ÂQxÑ XAx7°ÕåÓz‹|ãi´˜œþOw_~÷wuáI},I!ùh/ß¾íÊ (¢ùpC€ŸŠaE¥ª˜}OR‰ôŠ +†„òI´N’Ê}J³‚;*çÎo´?óã…__5¼øf¤¤—Ìθ—õšCdغd¾ëüν®|@ÿ`ÿÅýˆ‰™à2-$<³ÁXh’ÝÙâ"6bŒ@ŒHâ$ É*tiš¢”bM£jDËÇAÏm̹íš`*ˆm*–Ë4N8s/¢‹”¸D}„¨Bì‹6¶3–â[–Â(U¢A&`Yˆu +,VÙû¬4ãßÖè¾€2 ÚZ퀦hÃdyऔ}$Ý {¹ÊëÕòÄ´X€V¸¹ã—¿„g©å*ZËϹœ>ç=FˆÒé* Ý¥ÆÒ*(úXCÑ”ûm6­t]Q[öO7]Éñ¬¦ˆÓE$‹3ŠS*ïD9Ð +Nk”ž6hL0Ç+GþþÓÉù™¾k•×ðË£×—TþA²¸r{E˲Ö;kò}¼ºT)ú++ÿŒÔãÿT1ÒÀ|®˜J2Î…fjÑ@ð”X©*Vâù\ýh}l¤>îÊJg<”DOb.ñD²˜õ7ðL ì$•m)¸²Ð-*jí˜h6›­f{‘µÈ^èñ̬ÈFWÖ–D)ZªÙ*¶F·ÖìÑvÙ{¼½þÞšìÃÞ+â•è!¿—3ßtÏygýOÙ'þçö¤{ÛŸJΚATmÔL&"üA~+<~wúáüà ÊUµqn¹À•àâ~4ṧ·€ 3&óM“E…°,S“/@I7IòÉ·’$9LÚ8ìEà“õÙ.A¾%ÞD ãeƒ§Ñ×L> +w+HY-V§¥¬µ¦,bÁ7úòö†´÷'RÝ@Œ°y“;!]As<æNŒÅݱòÎñú˜;¶PL&3ˆ2IPÇrNR=!~€õ`›°Í9dMÝDæÔM|/×øSl+²t[Ñ(¨)zéšb=À4àa>ÑféqÛäõ……©¼ß¿þÒ•u^“jVžx{4—žû°¿²ý9-Ý +•-ÇÝìœÄ6ÞÉNùþóݻȶ;—N,+­“.' Üspåà-†É;ø+¢®ú~@¡¿®zo«¡1—diÞ-â"[…"«h§»¯'ëMt­»?N7¾KŸÅOÏÒ—ñ Æ!zODÜhÂs-?3®a]FË[S @¯`B®÷‰"&K(#cL@þ]ÔþÏ~µ7q]ásï®´kË^ïÊÖËk£F’%ÙØ K¶„ldÇ<ÈbžF‡GÌ£åå’€Ó„Ð’&™&:M2PÓ¦it¸‡â˜i'PúÊL3Í´¦qRÚÆI¦$‘ÝsWiý]ÝÕ·{´{vï9ç»sh·!€.ævçC>KAsôÝ< åÒ!Rð*n†ãk´|he…‰ù?Hqi…´Sú@2Hì½Iì‘Ô ¹òS ­°Æ;» Ž¹×Å–@Ë(®î±„<"§˜0G¹),S1ù],ßUØžŸI5eél9Ôw=C6úˆG¤äzôDKüwú‹" ¥®H6/!I{—„ÌåÒ15’#ZÕz–œ±Eô²+סEˆbëÍ…%"Æ2WÈe!B8è²”Óm]<ÖÊ­JÚ¸}ùç>N4î{0µ¼/çã92ÖN6¼~è‹'frsRCÙI°›¤·OV¯|~žÓ­âB¬àÚëÔT³â68¢ªÛÃE ~oÔ3à÷wÁ³@am`S¾ØÆpf°_ +(Ç0h¬²Áê`$BX=vº°škhm8´”M4ZdäV°Z-zÇny'SŠU]$Ñ­QCΔÙuž…uΞf“yQò«+y±±ËYwE¼ÃAµî]E[Ÿ"-¡Ž5õrnÿÜ]cŸÌXß[9ùä›gÜÍá~ÙöÀAÒóTeÞW¡÷[tïû9pã—ÑoßPúêÁ˜¢÷8Ä/Ç#(ØÊmà#$_s»É_èÒ4)À\-q{‹¢f·Ñ- |Ô8md'ÔµžÆ0Œ&c7£0ü…((×£`‘($*ËÊB8£C5µáp­Çªñz<ú-Ûj¶Zé–Û‚#¯X¼u¹Aø|誗hÛ“cGB V×˦þ¹ßø䞻ݵGß<ã¹'Ô/[{Ž½ðdŽô+8EÌp%L‹›JøÜË'yžËÍaáå8"Ä01tãbp>ïO3nSìÈìòˆmê”ê`(hqepåyûùËo.è ³¬ÏÈ9~#ÎH&Ä Hh±ACüà–h'å÷ *12¥šÃ‘Ëó[ɹ½{C;¹H;2d‚Gâá>ñÛ"ÍIJ o WzRø­@]*l¨(˜€»†Y0:BĺäQò,áÈi‹[ü–P'z!Æq%ԑ׿]Ïp“8­SH£$I† TtiùuœÄ›Yƒ-›‰¥Œ1RkÑIOiþΊÚŽ?ûû?kóÍ´u.Â6p'õ†×ÑÚÞ¸ç á¾"œè‡"ùŽø¢H·Š»Dº@\E®HDâ˳n¹a6PŒ±n(h„[ÌdCÍL(§FØžh#Z‡ --ÄÈ FÁ‹çáSÎ@gE8ÄñÿB§µûfY»peç`ýø¯ŒO`D9È?x±Q!WRUtM÷4»:\‡ÊO”¿_.RŽ¢ûã. ˜†‘r_¬pª£So…»*\‡èKGLžÉða•¸fUˆF4êSjT‰ª a <®æ¨êu›ÊaÈx)åh¦ÂH„NIi‰ª˜íR±bV ·rbÁtˆš¢„Xm6¹@uXŠŠ«0™9|D*43…J»iõJ¦âP¡zMQºd"_–B…Ö„f'öC¥yù!£Dã ï(y¼=`'Òn}X&þ«ÉáØH*©³ÓÓ»##%S#²{øÆM¨ëTexb” '·Qr…HPÁ]ÀÂ)Üg|Ë÷>lIsÀ{Œ|wGOÏž÷þCÌ÷÷‘·Ælö#Æ®Aºù¿ä84O¤ÁÇMÃ0qÀ("š¯ˆ›°<™`Úß ×˜ãˆ÷,;l¨g—3ØàÀmÇñÄM¨SJÎLàœø- õ´_L\P¶ `~˹˜w9â@À P¶U~0ù*@õ?‚¨Fóõhd&À´o4ìˆãƒF´¯ñbMÍ<”Æ ôk~öŸæ þ¼€ÄZÑÖv|w>Æä¾&€o,ü#À#ÀÒ_,{`ù|€.´iÅe€UϬ>ðÀb€µØßW°¯MèÓô¡ûûZSÛš³¸kú/¾"W…ÿ¿°çH_uYd‘EYd‘EYd‘EYd‘EYd‘EYüo +X+ŽI¤a„;6ŽÄ0åT +æÂ"‹Õfw«%¥àÔ\“Üo¹Ï¨¨œ\U=ej°&®­‹D§Åê MÐÿ¾Ž -^²´sYr9¬Ètòòç:<ú3<½rgð}ôá]©}yãá0žËACIij|…è„$tA|vÃ>xNÀI­P³kÍ3>ŽohàÆ·ƒF]³[×Ü{»æø;_<°I B-„.¼ØÇî 7w׸;jH0pCoìÆ;œÅ×{z”œÎÈH´1#ó(·fd#ÊÉŒ,Àýt=9|û&½‘ ”pç32‰û[FæðþÇ™‡^ÉÈF”+32ÚÃß °6ÁjXƒ\‰W ~‚è€]NÀ¿Ë«~–b(žI‹(vwy“‚¥×Ö­ H¹.–ëÕUB/×ÆKÉ¥¿‚ÃAtÑÅů!¸ºøt(ˆþ’qªP7åÈ{y¿üÞŸÜ%ïË0ÌœĖ¥1·’Oƒ€Hø{˜5ΩòU±.V$qr`-èY¾«ã©²ò|¶çÐ<$tŸj0Î+D¼C³ d –Æ:óÖÕj+‰±r +­Ù 0…7³ü.¨@þYËk³lË1ó¨=ìXÇùÛ`ómäNÜn8øÖO ª­ÿÌyÚh^t> ï º¡h((P™2€ÈWz¤47©Êh$û5¹á?*6u•[$§V¿Z½^-CìyÔ’Ât049…"z"b¿uè7ƒRC§\Ñ"Ë©kŠ4Å)×'¤’…Ń47B‹˜ÒŒ ¨½.u¸¡mŠj'‰G<‹IÈ\œ Aóþùaö粉úJߎv€ì¡;†cXö`/b.»ö®³Ývåâ¯tµˆOw„¶¼±ö®9û‡£—_=O7÷_ [מ/_vv­~xº›Þ½Š¬°Óög׿?–ª[ + +endstream endobj 621 0 obj<>stream +H‰ÔVyXSW¿/ a ›"}l•%ÀM}‚A-!’&a§J"âBDÀBUQ–2"‚ÕŠ®ˆ‚DÆ‚µ¶  ¢ ´HGyl«3Óþ7Ó¹÷{ß}g}ç¼ó;ç=€‚t€á«¼ÖŒ<Ü!Â8£håÒ,h$¡ÙÚi/ˆÅeòÁ:¸= ö¬:AécrŒ–½Éâ^9¡ƒyÔ?‘›Y™X§ @Äì¢ÙÌ-ß„éìÀnQŒ¶ŠÆªcª“wb´A4W”Tçºè&Fÿ„=ß(–Çb<óP‰Ñ.3‰¯„Nbö˜:@ã˜\¶…çp0<Ì?±ƒÏŠ°<°Å#Iå|›?¸*¿€Õd”Õ0òvKO •ŠêàíÒB‰(o’é™ùJ ‘Å•J´Â1ÖF‚PÂD¹9 NFÀp¢‚)! kB(õ‡~ò;¹li:Ø¿Ýt„€bˆ°ËQº!ú¾?éÁá§ÕŒld0ëy{§T¢~JpÒËGZ»¤{Õg’ ä㛄klº *ý'BÀ¡.ƒ:D| AAmQ[À àDÅ¡ A¼P„ú²E‰<ÁVêb¨!UPT[øN‚zű̩h2'ÐÿÍ’Ãe£"&—ω‹BØ‚‹úóx"êJH›Ó6õ¥£Þ^.®^Þ^Œ ¨‹››»Ã}55bÛX£ï?.]¬dc -©4øv….V‚+!ZÓVXÚXÚ„þõ—üþ#2/Þ‡½÷=8±Ü1GÇ£Ó(fæbrñt…âY¥à¾€{ñm+LN÷Lʇ­œxœ;#¿ ³_;ôëŽáÉÝu‡[v>ý4„$ŒIºõ‰ÆôõIã/C6¦Í"TBÄ䛟äßÕ ±¸Û®.“au>¿ºÁgíãçvz *Ú¦{(6³e횃1 '¬î¾‘7»Ó`óúH౸˜ S[óT·©²õÚ{êÕLÃFV*&)ß6Q!+XúㆱöŒ=ùÞý¾Ì±úò×~+KøA“Ù&Ûß~ÄjNæÈ +ÍOíýùYuU÷¦…›$ùœÛõµF…W“3òîÏžrõ<‘KlfN +xt Kè45}˜žQØ5­ÂbA %ø­¥x‚ÑäR¸yèç¯N»ý ®yøÿÄfi´UïƒØêW—¾‹Oá_â›ÏLñ?ffmæh ¶€+Dy‘h¼2Eh´HÄÚZX$&&š'`ÆBÌØœÅãZøLi J] ¥Æxµ¥œ=” zâX‚(Œ¯€“ hIaîzzºÏÒ6Õb>H?«¡Sš•ëиêsz[£/3Cª=SГþ­…~»ç¦$õòº F×ÄÑÃCçÛÒ_œ¬:¼®¡)ìÔ™ŒÁÖ5Œš¯FÓ[µ^]]!·™¼Þ¾ÙvW®z»yü˱­CUáM®H5½:¬ÒIZ¢ªñ‹ðèRIÁô‰Λ›³Ìϼ`I1Cä°?‡²†ú¶•Ž‰ÏAñèL”Ã&ºŒŒ,O 0¾£!’i - +VKÈ¿(RŽ…PÄÅ ¡w÷2RLF#¼ìíRLîã6„dRLFÌ‘ûÄKÒ˜Šr¦º(׌r¢„©‘K§Éå¤ ‡"ÑQ'…¼{èâtù¼ÿ½ï§ýÏÞk=kí½Ö^¿çû<Ä Fƒ½!B ìBH(%æÂ,C¸»Ë9QC|Ö“Å\ôY@ZÔ)¶Ll³?#84„ñqeØï­ì{Û%Î_m“¨.lcåb ŠûPÐ…ýMKˆ„¯i"%¢‰ø»WŠÌµn2¨u8žUSˆ‹&:û’RnkNÌÌ(=× Û{ßÀ2µàÊ®&o%üY}é,r¥ùéTÁí'¦(E9åñPE‰}l¦ôN8Ý8*dÖì´˜£üK¸í,Ög÷§Ëz€½GžåÀ×BÿCí[ñùc F³äCù‰ƒC™6Ÿoâ¥Q +(y’w•ÒåýÑê\¯}ooé4H¬V|š @I®ÂÂÜ`{ R•³\”PÀm™ùŸC¡$tûÌl¶ôgZîŸÉ¹f}Û5´¢Ñ³reme•‚gó“±×Û*ì9åÎä%1V[õVéj¶]¹¹õLóZßÊCqݮ⦯Vwܽ|8gíÃ8Àh(>¡ÜºÞAæÊð]zJTÔã®5sjOEyÐNð”ÉfœšÑ‚$®2×1ƇO}·CIV¡•îqêe<LºTëéée«ð¡DCâì2ÚÌHÏøCIùØwE­³Eä櫃OþFN»ueçe­ËF>ãØ)«ÖÊ ŸX,iRHC¸·R¬*[”¶—Mê¡L—â‹ÄŠRÎÆÛ¸G)¥ŸLcŽ?4š´?2fq¥›ïξŽlй¼¶Ë‘n¢ük¿]€9rÌbwHcÏTæ˜D*–«Kæ¿(j O +$>®PÜ'°ƒh5l–Ùí3Û¡Ý* –KÍ™LÈÙ°&–`2†^O„+.Åk)\ôó×ç= p~åÈ9?"Ó×ë»—Ýv¡vØí½Î]AWòýÎUVoÇåŸÇæ¶e/8íÕH U¯ |Wh~†ø¬9§: ä ‰UP…›ÀÿaÀ÷xKbÄŽj^Â8 ÿ)f®…ƒbH訡q¢Húáô— AI öíþsK”[qj?MDKQ²Ó˜²U ¤X…ªÓ²S7Ø ¿h¸$„äY³­`4 ó‡EÂ0gè Ý7ÃB¡>:,z¶…žC`,èɲF@ã˜ó=˜>LÀž:[íƒØ£££¿;+&,4 ‚óeÚˆâÀatëÄ\Û¶>^µéÒñ}\¬×ɬx¢ŠÕ[rK¯ÔÛ‹V×fâHª #‚̱×^cœÃ1þÆJÒ’ÆÞÄ‚Le¡Ç{þ¦IÁª†RUS›lj\äÂìEyùPùTìïþd¸ýludPâ{{n_jÛóÎf§ÅÓZ“‹¥c~‚Ñ }Q±íèS]:œ¹®ÌgJ!÷ñ›âšâzwû6­‰Î9ê‘Ÿ1Cù·ÿ®IÕ²£[ø§òÚñ/"­ÛšL;ÖªûEçŒ/{©•šµo²`¬ w@3?8I(>•õæ÷¨à¾œ¾4LöX)íLgH¸[êݹÆû‰=³qίX¤çÊéëÎ>hŒWÓÁã 6„ýç“À„1Ôe8¯ô +| Ê<'!Õ*xøG0Eê"Ð"öD²Îþ¬èЈ p9€[­ôÇÚjˆŸ¨ h.ˆŸg2ƒ!…²èÁa"‰ºùGD1ýüçñz £µœ]©–VÔTšÁ’B±ÙD³±Ö¹„Ó~!+ìºó¸ËÕ†YÊcyÅœ/tî(.´Ä§†Òó' ”å éàz=ý­_éùT]K÷oå¾õQlµjµìoèÉžL½ê¯xçS]×ÖÀÒu%­}øþC}ÕM¦7¾¹¶ãµùˆOñXÚtòδç}=ÖÙ‡¢ÇºUwržß ˜RÐ}—¨½b5lŸ*ö“½`ù*#sO:øc—ì­Û~³¼WMOo†M7å=:3¶Á)žýd«þSc¼ÙVÅR¬°sªl DšÊè”ñÞž`󮫼¦+ ÿkïsîxÔ+¢¹q%"&ˆW(©äFˆïÄP‰z$BË”j£M•/ñõ˜!_¼ÒáFKãѾ2ƒ*êmJh©G‡£ýÌ„»çÏmg¾vîÿs×9gí³ÿµö:ÿ:ge‹ß”ßxôï¸È€ “šùFnËøªóÕ _·=é ­¿ëÚp´¬ú»O.ž_8ªñºŠ5éëãâ®ÛU±ó`—k›=odN›{Š÷£y # ÚvL œ±½°´ÿŒ„»õÏ=mPæúq»ÆUwüjm%–åV ùöy€èŽ²6`ÿÞîÀðÿõŒW ÅVÊ¡m‹½ÚfÏŸëW&ÔìÒ§N™ŠD¸ÌS{¡/E:8Ce"ÄSsÕ^Ì­B¸µÐ+Ð07¸ÝävÇ×—c&Áí›h®ëFtþÓO†•XÖx(íp•è‹Íx XÞ8…¨‡YrÜHÆV„Iõ&˜«¹—1’js ×4\“†¼ùh‚®æ.÷iX`öÒ+;°O&Ë`ÄÒNUÑÅ™—˜J#Âœ4—x´·¤µ)G*­oÐmP€ehˆ‰8nž’ik~¬l‘9r¡ÈF±o™Iè†Ý8/i´Ò1˾Tk7&sÔ& –JSenãÏ–P ðñ.Tªt’½ÚŽ©¦9¼ú.K#i§MÓˬáÙ-x¤¢ÔQí$(ôÁh,ÂFfãnâ{©-e”gä}‰ÜÒø)5sÉ|3Ç~€½ÒNÚ©`Ìl£-†òÚ”rþqZÒ$K*å.µã|=Mcdns #‘I†ëùw%Ž>œA·Ò¯Y-­×ìöÏÞf„c±§q†<®1ïßã‰D7Ô[ªÀ 7[Í-­„  b{ÀLöŠ?rUãSüCªU-zž²ŽØ³í‡f9sŽ^ä>€Þƒyïb®Ò.Teq1Š.Ò_ÉY"+¥B.ËeåP¡jšº§½ú„þÒêdÛ&wj‚–œ×áÈå +¼Ål/g¼[qÇ$HÂ%†]àøT7•LlR§Ô5=O/±žÚïù®û¾õU›"8Ye½™‡ØÎ,|'MÈ¡­L”Wåk2_ª>Òõt}íÖõKzˆÎÒ ô +ýWý¹5Ý*³®Ø}ì»Ì™ã›â;cÒ̻̅ÀA^mxtfýŒg5M"¿|b:æàma1ëe96 ŒqÄ1œÇUü+ %ç<ÎþVÝT{ÕÝH‡é£§é5z‡>¬ÏéYÊŠ¶b­îÖ0k‚Uh²ÎX—¬j;Äöعv‰}ØÑÜïê˜èXíØé¸ãxêt83œcœsœçœ& Œjõƽ?ÿÅ:NÉ«vcëuUÅ碩ηçËPfÌ¡†èÉz±þÂ/µK®H‘ÎÓ“Ì&¢žè©2L”V:ÄNÐã±FÊÔ õXݶ‚dˆº+Ö2ùXMÕIÊá×Õ³VUhßÔE$¨7¥RÑ…ºÐ|‚»Dªìu.ëºj„*>ÕóÕ*ú\å©bdZñv5ò˜÷möëÌwµ@"õ9«·´[ýSÊJªÆIékµV/«®RFÅ}&-q_¦!_ÞG¢ì—«R‘­z‹ôSu¸Z^UW: pR‡Ê9ˆ¬Ž®‚$C=TCõÇiö¡J|Ù¢%Žµóߟï„ã±Bµ¡¦y¨&g¥=šbõþ±ï@bÛ—ìbÖÙFÍ÷Ë8ŒR'Àg㑉÷ÐûXƒ §VcŽ™+c©ûéÔO… +™ˆX©Mµ &·ö‹&ªµp4g}Bý?NÕO“ø­¸ødU"ª¹²ÐòP™²©¿ÅÄXŒâÑZ,wì¶Ïb€–ËWÂ*ÿ/³ç|Íù›¡;ùÀF+š¬]Tæi±Ö—ʾ˜H†'DáMrîÁç<ÃJ¥ò®4a{T?öÄcÈ3«Äµd +M1F›f$&`°ÙJýiv¡æÛYj˜eÅScɧìG“bêv*®P¤)î;È¿‡½EÖEjgO³ÐœGóÑŠÃ.z“o瘷T]‰¾þªÜ¤è|v¨* 4[Lˆ"×L¦ò@©Ó¦öÌEK»”µ‹Ä^C‡$öìñb÷n ]»tîÔ1¾_¹b_ˆ‰ŽŠlÑ&<¬µ»U¨+¤å¯Z4oö|Óà&5lPÿ¹zuëÔ¬àtØ–V‚h;%Ûå ÏöZáîÔÔ˜šcwOäüìD¶×ÅS)¿ôñº²ýn®_z&Òsüÿy&þè™ø?O©ïêŽî1Ñ.Ûå=™ìvUȈ™´%»³\Þû~;Ýo/õÛui‡†r€ËÓ47Ùå•l—Ç›23·È“ÌÛ•×Lr' Œ‰Fy`mšµiyƒÝùåÜCü† +ö$”+Ô%)o3w²Çû¼;¹†W‡yrÆz3fz’›‡†fÅD{%é÷/ܽ¼ÏEù]äŸÆëHò:ýÓ¸òj¢A±«<º²haE}ŒÉŽª3Ö=6gd¦WçdÕÌÑ ê?¬W lT×wßg7ãÅ6?¯!»<Ö–˜OùØgƒ½ƒiˆ±1»®Ó,`"ÀmBÅ'¢‚QÄ'KhKÚF„P›"܆g“´6•Q…PZQZU%¡mJB[Ú"­ ’_ÏÜ·oY/´Ðªˆã¹3sçÞ¹sgî¼Å¾õÖ¸o|<þ‹Åóêb;3µ~5¿6Àl2¹3`jŠejƒü7Ç–EÉ(6~!ll`/±=³”íØ0Àçà39§[mFX’X°2˜k’븘¤EK·{ Ãýö‡T $[bfÐzÔoÆWÔõPré–ã Ã5•=¾ÑNX{Få¦#s2«Ó:9’ÓyÔ¸4W…=2"¬Àª<‰™8Ó\þ³z.%WÍÅ4ü‹+°²:pk­‡êI_ ä>¶·ôÏ $oîßüô“á’)‰òÝ$r–¤ zwl•—[eeœ ž:Ü(|¬•ü¬ÊŠÍ}Â2×û =‰Ø®ˆ×T!øÁ _ïî¾0­cu5Å>@+ý½®*["ÁšW3fkº\MÚ»k¥ð™É~5¦Æ’ë# ÷úûì»ýVô•8±F©Aj ZÐc*»šzÂÊ®æ¶X¿(°«%Ö+Q—Xï™]¬?€÷YJKYÈL€ô7TE¯ðÊùþþ0Q—ÔjR ùU} +I™×•)´ªO82Ÿ+iŽ,,eü_Šº–XfÈŠWÊ"Op(BË}ôùÆ¡bŸ”dþ3’FµRÄ#á_Øj=mÇÏÌðUã(5Õ8Åש º`*ä{µ—(„ùÏ‚oÝ+ª‰š.>*€f ¬bÀbà  s-à[¼† uµ{¾B+ô3äÓ[i2°cSûˆÊ´ ĸyì7SHeO†®Ô3sÏØ—Yy“å¼VØm .èkÁ? äyö4ȇ¼ëaŸAÕS|VûÆ›áÇBŒ?Â×zÐÅ/Áx>›/Šj{Æ£1žØŒÆx$Ý-¶ÁüøØ}xÁs±o¨ŸçbÍRõ‚âWöã›êõh-Tý( œ›Ï잉ýgŸþ ¢ì_&ÿ$ØWqÇ·» ²°Z)ïj[ê¬ÄYZ¯²¯clax.Ð$œï Zë  ž‰ö_áãBýmšÞ Œ—à5Ðõ…¡+7^CÞtP­˜Å,û¶ø&M4Bô8΋xS |sî!¦`^³´ï IÚe*Ä8Ìðý9'ÄwßZ‡¸_õ’ý)Ö¨c`~àìÇaÿ*Žß»Ò:Ô¹W {Ø€™Œƒ~·ÌaØ°=öyŒ÷pî|2Î=`†‹Ôý¸xØ…ŒÿQ‰±À8`Àû¾üxøÏÁºc1üx‘s†s“óƒsCæ?òIæ,ßãĆsÌ©™ŠghPTàGÉŽÊ0WÖ ß#û̵Àksnqθúb'ï•k|NΩ jêroYƒœ[´”sŸ©–g(4›sÖ‰µK¥®G® —ºþp}ÊU;)ŸcÇ÷îR7izˆBÐ-Öߣǵé´\=üoÇøIÐ9ˆÏAYƒ×´ïÓÇb; ÏUà.¹v_Ï¢ûžAeÖ@,‹µ³ôº¤ƒb²6¨èz·}Eï/:pÇ™4Ê€£cÊÈÔý·òÿâ¼ÞMÏ`ü7}жµAzg%Ïß•i@À¥÷]@™·\ÙçíTú<Ëȇ¼¹<§…ñû5Ls´zT#ë.ù2¬]¥uÒ<Ø©ø¥ö²ºŒÝôu÷ˆ½Äyz‰Á냮OçQvÎÝK’ºùzÊ5ãRYSÕöd]UÛ”5Ym9”ª¹7ðû,ûÉ·y´›¯é¼|ƒŠÕ›ù™•§ù9v¾ì¼Ì £˜¦zKŽ[§°˽†Ï/ßÇVYOòƒ®×ŸMÓöG©Oµ?ïðYjs똄 ÿEêÁ;Œûæž¹Çn7ž·ÛÕEv;ÎùSc'èuû¸(±{Ò=5D3RoY¡ÛK9NúY*J÷Ñ-I½g!î§Úôp§æËþù¯_—oÛ é/×!×`Þ½ôñØ·µÈ~ÃþìG¾£G¸'z#†#¸ñ.JõÁ<ðôÛøNJVäP%(Þ£V©k¡ÅIqLœ´;ù;P}ŸžV€û;FAµ ýû4zã<ôðEˆÕo(¦þãÉ6ãÛo#åj¹Ô¡^¼Э‡ÝY¬qzÆØ\}‹æ«¿¤µê¾.ñ7µM OõT§ü˜:Å-ê4f£'ϳßë36Ú_–8Œ¾y)e›‚ôÕŽ|Þ‚o»{ø+}Íô“}¼‡¼¯+í0GÓ(—Ⱦ„:Ô$öP7pH¼¹_¢-Êû„r€¢Êeà@ +?¡I{€&ÔØ,å`ª6‹~løô$pÌái?ð°kŸ=nà§C,@>ƒBvØüÊÕe‚÷º—<ºß>1Œ½Pnà 7†ëäžÛh6ö›­Í·O0Ô+è!€±• +<›©@-|ì²xÝwîšr?îåM“1t~3>(¸v¹?ÿ¿Ö{Pà~·OI®â=–9D£”óöEÐVå<úö&¼¥øJðùn<Ý{‚ü»RžuÈRÉþg¶<›Ï¾×ûñâ8= 7Òùð*Õ2´G1Èæ½ïR-Ã8 Ýé»yíG÷A¾Qö³OÈÁ’»yc •0ÄøZÈ6¨9 ͟û +ðÜq_íÁQ]eüœ{îÞÝe¹Ùe ´$„“°YHÈÒ„E +«»›†Ò<¦ ‚<âLy”“ ØŽcÒ€Š‚  +–¤H´c³Ü%ty(™ql§⌣Ž3BPÿpÓ8hBü³÷†°”ISë?ÎÎïûïûÎkÏ=ç|ß‘íuÄK@ž]@9ƒX ŒúáÎƬë£b]Ù±”ßú>ÖwIÿ>˜_D½ Ô!Ÿ½LJÀ+ÁQ‹G÷·y_ܳçW¤öû¨.î’¿¤Õ¹{&îž œ•õùÿœ·77þ×cQ‚½ +x™£.%Ë´EÈ=WÑù bjÐD, ²[Xl¢B =OãÅîOË£ÇÆéÿV·â¼…ñòÒûò€qôñú›¨žžwLXOËK,=÷ùÓ÷ž•Ïd‘¬Q¤»‰B¼-ÔÞ»¹¿5‡ôs%ˆßd¯/áì,ë5=½‰Œ)AÝ…BId?0 *©g?!Í@ Ð¨Ä Éb FXX'ëÄ<;ÐÞ Y Ô-€JV±ŸÂ¾MHöÛJf£í ì0™þ.;$ù8 |öYàW¡ n3õã`á?fÚ_†>|Ôä#°gƒ_‚.ø¦þ,¶µh·Ûäv¶Ë˜Å=ÑYðç%Cé0J‡±t‡¡HʾÁ¶Ë‘Nƒƒà)Ær5y>ùšÍ¶cI›°ôMX¹&¬\Qáj´ê4¦êÌg¨Óˆ:¨ÓˆU)a»0Þ.‘,@z€\€aÝwaÝ…=ÙôKû7![v¡±ç°Ž…˜Õ¶Õ(àØd[E‚á ì,u„=“˜‘l¹«9'‰Î0Ù-ên–ÞÍ çdaÝœÈÊI1jm‹f°äk€‚«q#É>”*Ûhäóóì)²ÃA"¼YifÍj³M-)§ÞK,Hj‘IsâeóI +y,DK×;œ{œÌãÌu–8#ÎZ§­ž5³Æ8+faVÃbÌ–é3ìK‚"˵% []í®¸«ÏÕï²Åµ>­_Ð5[®V¢E´Zm½Ö íÑZµvÍÙªµÚ•õ®×ó¸r]%®ˆ«ÖeãvÚÝÇ6àoHд*Ö8{.{ˆákÄ°OÃN 4ÐòØÍznÔsÃê†Õ +žZ`=Ð`zµQÕFÔÏ–kÖvrP”€Jh:4šŽZýÊfèÌj&mv ¤å+1ýëMúeËm•¡È—æöÒx!m/¤­…4 +Gƒ‘Ù^¯7æ‹ùc±µÞWï¯/¨ïPk|5þš‚š5ì ûÃáµØWì/.(îP¹ûyïP[ª{ª/U_©VcÕõÕÍÕ¬Ÿ.a•%Ïö î5fdKÝÑ¥JþN ² ¸0Â!‹0P¨J$Wºa통›Ô1À†Ýâzä¦OØÛ¤O”„_¹ÇÏðÇ»Œ% k¢•¸rc@ÀÐwü]²vªÔ#íqÈi¯1ë·K;‡´Ú0\puòš«Ãñ«#a 46r…­!×ô É PY~kØ¥¿.¥‹"ú‚iœLŸNñNqx¢e2ö€Žà*äQ)H–2?’Q©ßªÔQ©«RŸ‹‚R@¢p–2/âŠêg¢zMT/Œêèí!’Gteš”šôoR>%e ’™§ßÎÓ?ÈÓßËÓ_ÉÓwæéŸÍífâìêJ¦”.!éKRVJ9'ââú\_ÃõR®Guz‚btR&å,)³…¤ïŸq—»‰ó}Ÿ”£'j„ +yR!’舊‚î¡å a#tô/#tˆ_¤·© iô–‘ƒG§Ñi…*ôL~VNð x øÇ$DýàSFh¯¨ÿ#´?ý$™íõ_%µ²]­öWÌv?40êq#ðUŒzŒä¨GŒÀ X ÀvP‹áÜj„æñ躅ä+¢îFâWÄLªÍŸDÏÛÁËS—Ѫ\ ¤¾ ¹b–©ÔÊá¸á“2‡ød3‰ON:›ø%gP·œ¼NfKv¾½èE;ã¿Áÿº þ8¹IÝÆ þç‹ø«¡þ‰Vü×çÄrüJ Iýgùeßþ«ü$]mð¾@ÒÇ¥@R¡½ü49Žº +=Ë{[x·Oz;|ðâS·…æóã¾:þ²ºÁ÷.ŠiøÇ«á^ø¯uò'üI +w$„Á"“øß—ùc0/NÒŠD'_ŸS)Agù<Œ8Ç'§ò…ÒóÊ"b§_‰ì»íì«í+ìKí íóí¹öûL{¦Ãëð82““‡æPŠƒ82“#‘"‚S˜©yiªª,{!!Ä­¯P‡‚³ŸÊª”ª•e4î­"U«Êâ¥EUIûÈçã‹‹ªâŽÚ/®=Mé÷ÖA‹+û“”¬Z‹ *Lû²ãÞÇמ#”ï;˜-¸qßÁuëhU¼o#©Ú¿µÿcÒŠº¸ÍWö0™þløáÿ°[u±QTQøÌìn[~d[P³H!·³lƒ[J‰Á† ´Ýk³m©ÝEw ‘»Óvö[vf ð@MŒþ$<˜øfH46 Qv©ZJð ÆÞH|Ðÿ¬ß½3´4"þ<(sÛïžsÏùî9gîÜ»wv¯ÝÕÐñlb™nØëÕ¥Qïn‘•·“©LåƒÙJ;W6f“•žÛŸ™•Ë%=1+Or‘ÍÌJÇåÃú·KÇÙE)ò$h¤qÁi3¤p)ÒŒ õ ¶©¢'ªŠâ’.K½œ„ísYFÝX›‘±¹MÞD›E¬Íò&NÃ~pƒ…ﶚ¤°^M"X#'Uc1PžŠqJuG „jl‡p¸äŽÆÜr²ybRV䑤%Ηƒ]àqä:pÔ²™Ý@–fŒëùœnFõá¨nÕ7ŽŒE*/d¬š¿Î¬h>˜ãÒ0+×£f¢’&XÕÈ-ãÎq·MT)§§3Õ\§™8gtzÔHdg¦§âÉ{r½¶˜+>µL°),ÎsM'—q'¹{šçJò\IžkºsZäJuKÉÁLµŽº³ñý®œ‘W­ÄyÞДí~¼~r—8;›"'6Ì ×Ö*5[Yí®<pWKWKwátrטÞ+rbgÓ†9é´çª‡¹!ÚM*EôBbñß²,›ÃqTô¶6‡¶)•¬<ûü¾LE«hz¥s8‘•øëp¼ÏtÖÏkW5¹¤Mi'µSÚY-ä8Y˜×Î+Wù€RR¦”“Ê)å¬RÃû3Ÿvj§”ï•€ƒÝ$ÙhzBät ñχ¶cñFH`n:ÕQã™.…røÚ•ðeÞBë€(°H!úýÀ àG H¯  x˜á–@K ESkåùs +^/~ •µ\9/ÑúºšÐ<ü2¤'i…tHz‰"jý/Úmm þ'­ÿ¶F»¡×ßB·­­©¡©!†Nj Ò-¸t«3D7‰/Ñvć>|øðáÇ>|øðáÇ>|øðáÃÇC ™$âíQ +pMz¨¡û¶Àý)ÿí$&ú X¶°àö 7¼õú3K¦¹EÞo‹sjé+ŒîD™’¾ôôE佞„>îé5Ð_õôZ*Êïð7\ÁcVxºDmÁZO—iMp‡§`×==Ýñôèï{:ê ^ãz˜Ç }Di:F“dÒ”ƒdtHÓ˜Ðû©D€í±Å1*Cç½{A0,EÌo…–vãoFÚºX£>stream +H‰ÔV{<”Y?ÏÜÜ’{‰¡Çmsœ!÷[”ÂÈ=ÊÓ`0fš¹õ–™•î‘\–.$å²Þ(ºl*º- +‹dÛ´m¡R”w¥-ì3ÑîÖû¾»ÿ½—s>çsžó»ßïù}¿ç`>Hx±ÜßsE[Kn(F@cÝßœ6_΢M!F br<£±ûEÔ¨€ä27 ÑÑŹm[R¶Q¼hŽôðCô¤ ~Ÿõ-I]ÿC¸øÄ°:éTp¶³gƒ”·(Ï`ø;ëÇp„É%$ÚAŒØý†ñ\&9¨½€\‰?#™'ÿ…2Ó¯Åäч•^ç@-v?q€Ç±8°Q[-áóø,ž‹ÎŒâF@‘‹Ñ÷S²Ø® +Þ 6k°H2Æ™+3_Ë#R¸±FFòÃ!u>œG’žåàˆD#H²&$„€ˆ­q¡ÄúBÊ(äRít2°?é ÄbËQ2!ú±=‚âº_ÂmÖ »âÇ8q-«É%°D¬zŠq’e€S\µ¨{ÙñZòñõ‚ÏÕÖß?åó!`PC-> «² ˆÅgû³£Ð~¢@ˆú°„I\~u!T“È©Ìÿ @A=˜fT +4žeèý®Éæ°P!ƒÃc'D£þ,þ&6“…úq¹BêRH›•6ñ¡£^ž.®ž^žkQ77wß÷åÔidc~|Ô^(oc -©4ø~„.”‡K!ZÓ,,m,mBÿ÷ÿñ#D€íÅÞûNœHn›¡c1›)¦f"ò)Òér¹sJòÁ÷üï&ö·ZŸî™ [:þ${Zf^gŸfè×íC;NnÚnðìo!Š‚Øä[Õ¦¾ ™0ú*$<Ÿ0e©""ßܘ{G7ÄüN›*1ÃêBnU½÷ª'/ìtÿT¸EçP|fÓª±õ'¬î¼“1½]os‡Çý $ð˜_ŒÀùŽiÍ9Ê[”÷é¶õÔ©˜„ /K>R¶e¼\Š¯ýhíh[FÿÎ\¯>Æh]Ù[ߥrż ‰}Æ[vG€r£ÐD eÑ¡'°57OJJ2Û„) 0e3&—cÎç1$ŽB*u 4(ãU´ÿv¾A4®©QŠ­|lwë‰ÞD®ÑzçÀñãþ‡[¬lmXÒA­»¦º±™h?ã>rmøž~Wó¤LùÛGšýS.²±ùXPƾb¿Ìvp춷ߡÔå8ÁùÑùÓ&À£š@£ÙBÐuãòRøìè¡å(Õ«Ho6“Ïp£„¨—Ï3£jCò¬°ÚÇ.Ÿ!ds¨:pñla¨ÿΗtÔ%QÃå³…)sŸ&*B빪¦A*Í‚:wü/xôWEZ‰»t…7h÷ÊGÓ°¸ 9>-­Ükðùätž×±sÓ‡KQÇÍkJ–fEÐâº\7¤¼¨ÞÔpïÕ³C™ä¬âŒ¨ºq©‘z½Zöœáüë¦QEE1ŸvÚRç ùìŠÇ¬ã²|J¥¡Mňç®ý + EñŒjñ棦I^O +ë7Øù’©ÒúªÅ•CûMÔ¾dªF„YÅZÖ~Û_—æâ¾Ñ¼Ýè^·3½Ñv$ ×çäTy*GèS£Þ–/c¨‚³#ØÖ «•¥ìƒfÂÞ–EÉJŸèžµ _ J"Ü›¸|2=oº¶}ko¹ýÍ‹cÒÇtai[kš¤²íǹ"­€¢ãPT*A?BAQAºbX'o”Í?¢·f‹êiï}3·ŽòÿóùÿÆñ’æ Ë5í/P·|~Ñ¿›¤4¾.‚V|Dî–#qÿŽ¬VÛAWcÁ(gJV´DŽ¾û®ÍÎ.´Ò*€=­Ïqjm«z@Üüu¯C±"/¶aZ™®Înz×éÖ¯ŠÒŸF¦ÕT-j1±60½Ì:ª¼Ë@yìuùNk¯Ú¸_u‚MjJ¼pòqt¼üš‰K/ýš/ ]‡ïPªÌ­<# ï;Z¸ã/ÓâëÃþqꇖà,Ïf¿€³õxCå™ìÞ1é¬-ç n|eMH¨HêßT:c®t[ízè¢\a«ÛgùS™0PáNh µX–àM–<'[ºçö'vrà ^Ÿ²íö‰ÅåÝ%XWhÅþ NÍýÄÊÒ›Àƒ*¥ž»¿R_æñP­ŸÍØ1²fʾŸ3(æÚÆ5’±u•†SÁ‹JL‹–q)W7”f(RZI¡+âšHWÉV¢ˆ[Iä’%÷ -nËë÷ßï÷ëüsÎó<ßçœç9Ïçûþ~¿¶Ùgªþ/°@€°`þ1Ø› ”À.4äP|ð(´,ÂÛ ”d Y1_Jts™Ðw¤Œ SXVx]5‚Iý¸2±ï­ì{Û$Î_mSX¾° ¥Å#T(îCACö×’ˆ5p_ÓDR@‘yšäíÁzµ‹ÓÌ9‚r͘ñöˆUêkÆþd÷Ï^œ­C˜ªiÔ÷s»É/LÇs]x?y9O]ó7‰WJÙGž…{$¹” DL75ñ×W¾PØÞá¢eëã§ísh:jýXèîH÷ªóÆ›š’(v\g²‹”ÂU׎Œ,`½c µ´õú£²s'<Ê£R œ-Ma_Úi‡š» Ï$©G’@¥Û×›VºdäÜ(P}'¤î¾I7íŽÞèžì¬ógºÄ¢¶·™ÓO_Ý\ ¬šo‚É!#•lŽ¥—ßyn…b’TÒ&,JŠ¶êa‚à‹ØÈYëc®roà«g±°'ŽÏ„ž%i"àÈ|\úߊ¯È1ÒhÑå'epØ|¾©ŠAÉ£ät®)—5™ _Ïõß9ÑßhT+iª(~š ‡@I,ƒyÁb R•#.J(à«©ùŸC Hèö-˜9øõewÍœ¨vŠ‹»ÀîP¬ó-Uª,½ˆà:l±ðwMû¸ášÜ™¼gæÍ©+ìz®ëݽچn|¡WýDiïŽ.O«·+Zïߌ؟Œµ¤î¦Öýjp°+}•‹ÔÕÁû”´ØØÞÍ9=™©(ò¯\‚5ëÆðé¶ +Û5>ç<µO#¨‘ÏǬé¡>,»¨ÎöÓÓµ*vÛ8VÃ[a'‹í*+dJ|žM?ÈÓKnWsÏó®ÒN‹*È Wž#dÝHv-à^ÞœP´´¾éø¢ dÄzY顪確i“f‚‡5R~®ZyoÇwlì‰O¨Šiû\¦Ù2ÕÊ©d€%$ÁlbfbaLp™.þYB‚çG†€~xÀ off¦æú™@ð3ƒÊAHæþ·7‚ÿž!¿ó²ÿ˜Fͪ´8£ ýVßJÜÍfGœQ!Ÿw-YC`>Ìà=á¸ÚøolvðÈŽ×}ã|GÉqØ»FÄz nÒ{t8°õIãÑÀã÷~ Úø–½äÁ‘ÃÒ×Çô«û‘9âûTÇ.Y°ËgÅU-£µŠ}õuó°é,l_\¾í:ä¡5QjC¦™q÷Þ=Ù ”áû¢yëíÐýù£=•XÇmŠšêòé9Šñ=[~~+¯;s˧1Í^d¯÷A~ƒÔjWo>ûhœKÊ`]?'9æ̸Eó-%ÝÅ€nå¡$-½ø÷™]+B5þ0n1}:0í鼯ܲE3°íe.’’Rf[?Ù‡¸+´F±à¶Ð±œ?*U)Ÿ/ßDág¦lm¸í‰)5nå”]صQëJyÊ:] ¹H0®ŽJæɹIßä—yêÁ˯ ÁRÕP$€Øp¬8?í³øPÒ3Â">¾g¾òŠÚJôG1èÔ˜`f´±À]ÞyŠ4ð…—Ï#ÑL}V–<3Ô=òHB.aª°¤y¶ÐPÑÿüoä´WWaV'gåqä –£ÊòJ9þóP[ÑzùÃïf’]i“ò†Kcx”Õ’~ÕBá´üDïX壙‡i#ÌÇœ¼Ž²½ÚÅóNº‰¬5*ÓécaêIOœ6”£± ¬ëžÌ~-^µ\ŒmLà½*lÞ–¦Þ[òLag¹#ü´~­ÿÊðÀÙVƒZz¹úÉ9ËQæ¨(:Káýæ>¸ÂU}ù‹>¯CL¸…º¿ue’ê†OõlÚµÙqß êÎv÷ty$[â}Ð2»w;òΉ嶨K_X³]û0]«*lªÀæ¤ú‹†×S@–"„E,ªp“y? øþo ´È±" Så$rŸb¦FBG Ù "é‡ÓE‚h±&8 +Zýç–8ˆb!Õ~šˆ— ¤=ªùG¼! +ËÌu &kÓæ"s p3@å’ÌaDFƒQ`á0ÌF‡îTèi5ô cBO>°È"²ôà`&0#¸ZI$÷…Ä™ñQôP%*,þËdłð“Ä„Fø¬ØßÀeïOÃ?l5R;–Y¦=q«Í=q÷@¬1Ôïrbœô˜Í}œFjÔÜP7®±ò£þ÷·§z©ÖÓÃDíç¯*¶Ü'¨¥³F‚–Þ›L¾½³ƒóŠÚV¬lÏÚßNý+ÉÕéU‚?£QÂ%{ª¥àiË¥›€—ƽ¦Û­ËضM ñ*†ª×´ põ9›IsÎì]¯šë7{å¿ 6"&J=t%ug9Ä Í÷!G‚ž¤‰^Œ“×GHö±R/߶·®²}™Z®×?ei8ÎË/£€À¡Ô›{ìPÊìÀ¬UO̽U~Sô`A…Îeb®?B߉ î$;Œ»WÛòÂWÎiýÞÕÜvkäŽaS0¿/–ĈOÃx"SZÌ0žäw½¿EÜ>©Ç?ÓDÓºgïÐg̃µîtê+ùÉ-OªÎ<«ÉIµ~šÓÎv±ü‡ïj®éJÃß¿Ï>÷æ-ÉÍCOr #I[ÏA$÷†‘ˆ’x´7’tåÒñ)•¶2"&ÓK[¯SjÐjÔ£=!­gˆvé´†jV0¨jF¥Je-e”Qî™ÿ\Æ0kM÷^çÜÿìýïÿýçž“º¥>gý›&¬˜˜`;³»«{Ä_#ÿñjîìm³ÔÎ3«×-tg[Qñìí²åYç\~š^¼¶ÿ„M½#Ü/ŸîºlvZpݨ ÎЙëc÷.Ëÿé©éççt+«Zå|=bæžäCFM͉qŽ¯ó? +Î-lyò|ãÏ#^Ê[ó˜ /¤û ê[j/~Œº÷«4â9ÑÎOV)Ì!òêßû ~æmäôiÓ¹«5ãŽÚäÍ¥^ÖA´= d†¹«îA´ïªA´ŒG4`\øÏåuÌ=óW\f1÷®ûc;¶âïÔ4ì ÛÇ-²QdAâ&݇¸‹7†1XIíÐñ4²›l”€Å´Ú˜m\Â@,Ãc'U›yÿ |†[lÁ9IHF6ó?ÍÈtIiAñûR@ ÀhêÈXv’ç ¶a9V`?½lÜb­a¨dy©ŒwCŒƒÆtÇb¹D=åÿ–b/YŒ"ÃNˆƒG$'o¼ƒ­lS5ÈaˆÅT,Ä*²)Ÿ1õ&Þ…—‚Ä$%C=Àš²'§a<ØŒÃÔŽrÔSêUã%ã",hnl“—¨e1È8ƒ ØÏÙ_s6È ²Fàl¼m|‚ØI´ª=Õ×ïÎ7Ö ˆíéÁÉf=“±€¿×¾ÀO¸&*Œ + ã­98D1¤Q`ØaeÛÄ‘' +E‰¨Õâ5QÇsøBœ§D+[®Ø•¥‡’¥ŒW&(Ó؇2ežRÅ‘]ªlVŽ)MÊE奕³.;Éd¹ü³¬‘u²Q¡þ–çõ€Ú 6ªwÔ;a‰´D[ž°L±l²œ·Z¬}­9ÖW­Ç­×ýfP4ugË5<4„{°“Ø,ÂdµòB I´aÏ8yÜ×1Xñr^BÌ}¶­ƒ°ÉöæIKšÔù|íE:„ +‹ù5ÙŒítV4ËOÅ@œ Ùd2M=,b±…Ñh‰Ø'öR:êDª+Ö( Ú„®÷±‚¦Ò,l¡VêO¯P2Uà¸è¨äQR B’?eÑU°˜/‹ñ ~uP?œÅ%ïZ,_f|Ú…•œÑ­ø–ÞÇmR+Œn +£Q!£Ìb®÷…0Qo÷Y÷£äyË1Ô‘°&[Ér\Å¿pIÝÕÎHzÑë–kåwF²‘ÄÆ]†MÜw¥ÊÓÂURÏÏæÓDîôÆ’žÜÕ9ÏÿÓ^aÔ[jèÆc1טŽ¿ñÙÛ”H·iwÄ.>‘ŠÏy¾Ó´ˆûpè¯ûùÿ†· ¸LÔ…zr?´ª³Õ%êfµNݯµôàhWa5Wôy®æö ¸Œ›äǹ±!½ÙÞ¶=Ï‹¥‰ܳÝÇÓï{2‹¥TrôÖp?×so\eœ˜ˆý8E‚ÂÙ£"ÖïÇr†sœŸeî÷8ƒ h¯3jwÇìw¥ˆ2Ö—Æ’V2j5°Mgñ=GÛðٕȸ࠱,ë&Æ¡˜5ôEÕr>F?FV‡r„ãÝ™B‘Nqô.Ÿsq‡† ýÔïH Ñ›m¤·RÏïƒ××ñÛ+ +i&[цý¸‹4 +}¼£Ù†& mȘ´Áƒ¦èß/%¹Oï^ü7ç‰Ç“ºÿ¦[×ø.íq±Úcb¢£"má;„µo×6´MHpP`€¿ŸÕ¢JEöL—¦Ç»to6,É|¶òBáC .]ã¥ÌGytÍåcÓåLcÎçþ‡3ígÚN +ÕR‘š”¨9íš~Ôa×vÑøÜ|¦_sØ 4½ÕGôÑK|t0Ó±±|@sF”:4\šSÏœ]êqº,®60 ÞQ”ˆÚ€@&™ÒÃí3j)|ùîì_+àÌFé‘v‡S·Ù¦ºÒÅYX¬çäæ;Q±±I‰:eÙ'ë°§ëm|,Èð©Ñ-ºÕ§Fs›Þ`‘V›ØàY¼+“] AÅöâ‰ùºRX`êh›Àzzxù…ˆÿ>²ðvùÕïF)g„[3=žjMoÈÍx7Ö¼° >+ºdº<™¬z1qxžÆÚÄ‚|²JÍôÄôêž%v§¹âš¢éþöt{©gŠ‹SéÑ1znìöÈÈ´ÝF3"šgL¾=Ve/(tD׆Á3zî[šf{t')±6´í½ÀÖ†´¹O?L”<ØóQ>v“>úAdɴȞškE[’ogŸRÌ[I +*An—Væç!ýéé¼ÀM‘Õ n( ÚuÕ¤uR /§2¬V³%êZ’ËÙÒàZbîÕ~0ù0áú¡ä°'3ö»Ë›2jÞcùa%åߘWØö¢…þ¢Òª o^¨ÚÉmQÙ°šm0fsJáQA-MuJjš&­ å’Xc®G†E~†$õò°RJ…â+ {«çÛÿ+“ÒÓoé1=qNëïì%Åu7'Êp~ÎðúŒaõaÑ iˆWdªEeU¡PÒ0[! P¨Ðï+ U‡–F¬†¿ÏëQªCkçU» ±ºšÒÂ…›*1‰Ç”|U¥¹~¥±´# 4.¬ +ñùË‚ª¢TÏ­ìølÁ#xŠ¤Vi¹æã) z§ê‘¦´#¢iR!ëË" +IÇÕ)´,¢Ú:¯Ôáo<ñÚ›³®S—¯ez¥&þÏŽJî "Úk´VÔÑçBóªÔOP•òWZÛj @»ßX‡¨íŸ@½r›:ݺŠöÀ³ÀÀC@&°Xä`!0>=@úx”û‘ò Õš½ôŒEÀN`)°]¯ °í2¦S ë1Ö&ôáGy7ôûŒ6Ú‚r ì•ÜVJö¯ ¯Ãž‹ò6½Â²Ìf2¡#”¯BŸ‚ñ·rÌ™¿NÔY—PÎFß_ƒ}#d9d™ïhY>Ã>r®<Çg¸ŒüÔC¿X4‹‘öŸ¿±¨7£|â9¸Ÿ¶÷¡ÍL¼Ãã1~3o’óÆ° +9hçø0~çëþ‘Rqí%´=qŠs¬æn¯+=ÿï /EŽc¯ƒ-Øk‘ÓŸ¿Žq .$ÏȾÚHSÛ¬!G©@/°…ùT¸ ÆOBû$ÉWp†¹Éü`nè'$Wrìöä^hröÌ·à¿Œ3ÑãЖóSÃœåýâöÍÜb^»Rrz5ó^¹ÀódNÅÉíz”J99.¸åJÞwèw=K|—pL{´>ÚÌœe¾¹’óÂ\ãýÈ{‘%qsÍuöH.üï•\]éæ"&_§=è³ÂØžP±8EÅx ëë!·b~G Ã|¾(´zØ¥,¬åÃðÝ [fŸR‹±~,Ú‘‹>Ú'óÚ§Þ'ú]o·Îë¤ôèíj½,ß ¡DmKF¼íÓêÿ¨oëí´å zŸea>[yO˜ÊÀçJè; Û“£´xV+³œ¼øä» ¬Ê×4MDi¶H¦ò”}¹ñUyînFÿ'”jÆzýÐL&¿vg#ÆRßÆýpÿÅñhç¹äJ—¯‰’9Ãç.¤9û® èN9ø3Ð>~[î_Ü |>Ëûg4ÐlóÕºãgµBþÈågO³øi&ò2QòÝÂ综[°OG³;>ùŒã3’Ï9¾ûÜö‰2ÎÎŽ?Ês¸—ªœ}LòÐÇQçéÖ"ÖeìÑsÆ›V·9ÛêÖNZÝÆnësµõªqØjż³bwjÔ>Ëx?¹w)ç‰ïE÷Õ3i¥sží‘m1¾¼G+ä9@Æzì¿ZªA¿¿ç{•÷¡ÖŠ}‡|¢¿ âEú¦è§Íˆý.í¶^,¤b>Å:”¡Ç™Îö;´ÍÒ¾@|HëDÊ/B “Ö¿a«WêÎØ6ÖéU´ ¼ËÏÐÏô +òZñ<Ô)ÖI^{ìùTOí3 î§=bsŽbŽ'¤Ü+ùľ¯XƒvÊ\DeŽvH#ܧñ–|oþÚÿ„žò$ÑÏý8Ÿ®Pª‰³DŽÕA‹<™w!ïë°?À±rjÔ¿`ýSòÿeiƒØCØ_ <íôd£Ð^ì¥F™[6ñþÑ(™9‚ù•É÷Ä8þ<=n´Ó&# +Þõá.èú `.«éA”·ˆvkmç¡ⱡ/•ï¾§Ö¼_Ì(6m8ùþøÚYÄ»q–Ìñ Ðs†ß5ŠîÝ L´!ëOõÀ&Rçµ¥’Ž>ž’úôªÚ¦©à7Û{ÄKØ{{iŽv’ÄJ¼.Ð56jÅàÝ%ÜüP¹4N»DEÚÇòþÙ¨'Ñ4Ù.÷ø9*•ðÒrÑIË5 åÑÀð~z„ªôexg=‚~¨Sá3‚JŒ&”ó¬CÜNŽñ±•Âëi’ô‹ƒŒÕÇül\Ì;Ûï/Êññr¬±8oŸœ'÷ ?ÙæO4‡ÈzÈ°åµRµ™Úê)¼Ã£T¯ì´º”V*Tέ^¦ùRv¥T(ê•F ¢žöCŽ‡¼ô­ÀQàob +ý}ƒ|…¿ ê¯pvAÂþ<ðKà=×ëfúxˆ÷­®øº>‰¦3Ô\œé¹Ãm²ý~š,žÄ9<Áêbhë(‰aÜIY¦‡²Ô~è+à—P×ÇÑ.±ínÏí ¼Ndmâçè®dÊ€wã¤%ö×x¾Ÿ?kŒŸXߧoÈü /KÛܴŽ+Géå´5ˆóÜ`ØuJ•ùÜOw»ë}£Ô'¬¸2U[@Z¢å™ ·ž¸®·«£ßUñpyàÂœD†xíÄ:îƒÃ`ŽåÞX{+”Ñdä©P”!–þ놗òêZÔ[`ŸîgÄêe”ÅචäÖÏ@®»j?¥3´°-íg1âòä¼jQö•þr}\ž'®|IüçÑ_ðf.£ÔDã·s^ ã|©Í÷XÏ’³ m®ï‰ë{{åV}þ?{ç$pøÝÿtð\!pðÞtoâ½Æ[õ9|c¾FÍDW‰†Ž}ò(Ρ‰/CWŽr&äÀhèVAâ6:òZØÞz"žtÞ•cPŸgû^}Áé/Ãög¿A¼v†¦ÚþC½(ÿˆŽCn‡¼‚öaøUBÖC·r2ê%@á¿h/ßØ&;ŽßãÇI“Ø„`\B¸§ ŽIBÏ 3 )y ”jͦ¸"g Õ£í¤mZ±FR6hIʉ„’º«6­Ôx“¡1š'Kê,aq×UB­^§j0iš_°Ó¨àEÙ4‰ÎûÞù1C¦4M·%úÞ÷îyîsÏ=wï~‡òïPî‚ò_€þ +¡Ÿ7ÆÜèÿ*ô¤ŒG>æú¿õ;œ?>­£ß„öª˜ý-?C|j/Íç"^~Ö(Íÿb^:KÜæö8 æ{Gê–³Ï'žqJŽùü§­¡«ŽÑÂGˆ)5G#–U1·ŒmWñöû*ž¤*¦´ã)ûQ%cg¿Â_Qç¼ èÏò%ôk·êWi¹emeÈã×Ö=² uÞC®Ñ“ÄMO®#¶LJɽMícöÝwán¬¹ t¾p~åìe¥=­´¶Þ¶ÆÞ¾§ý_ËKÝ#?ÞÚkëe*]ÿº­òû¶¥Ê÷â¥j±½û3ïåwØ£oݧÿÛriŸ/i±¸´<X¬¼X{K-—Ç·”§¥>á¾*—Ç%¥r¹n»û·WŒgVã÷VRÙïn©Âït«cáRé÷ZêCùïøæï­tF"Û¡ûKŽõc=Ö‘èYûÜÕ„<öÀÂ!¹¿¹në4 ¡|š‘k¼¿¸÷ž¥¯#–þ{ò¯”5ÇyU7f«±ï¹ü»•ñ¹Š1fªïIÌҤÚÕBÓзoÎ5Îxö9ŽWžsùåÂu´uýN±àç¼ïÈóÊn”Ý¿"}ü%,”ˆB–ÿ8í© þbÚ½"dD<ü‡$ +1bò/“,ÄÈ~þ<‚ª÷XíŸ ÍÊLº²&äAýãD‡†!NRH©*¬<½Â+›ÿ¾å^®¸ÃV°³˜I{|¡h¤Ž—Pþ8‚4ÁÀ×Â…7À÷ñÇHµê§‘v{BÃx^7ªw󕤷#Ü‹9|;¾»zUmЪ)>gÐZߊTòmܧª¸y5é„»¸f…„>Ç ôÔàÇÒwÉþ³<+Cgù×Hj £Ö*á>Ë+I$ߤ/]QJFªx^³Ã"ÐGJ&Tjð',4„çÝÏ×/î}‹7•ð|­µRdçø ªÚd+x^—åºOZºº&”Tð.Ü5ù8F|\=-™nÞ"‘f¾ž!†ABn9Cn Ó4†©ÃÔŒ¡cˆ5 ÅQÔéà‡H‚$Ihyš\iagUfÝúÐ,¿›û0ž9ŒÅÕÕéŠÙ3ŸU»BUó¥«jBÝgùÒ 1t~ ½ÊÚ?Ç[Õ«lHûê%°*ª0t«ŠsÐ+çà,_Ãת‘hP#`FÊ”¸¹ ”½ÃrrtØïÙûr~Ù”¥¿kûyÛ[ôB–åÒxŠ‘aïIÏGÖ°¿ ±GØŸÈrŒÍ±·HÀYFö‚]b³¤~åÇà³ðûà¿´î9'2,“†¡ï/[Õ^ù²ì-«­ÃοYUogj½¡ˆŸýš½IÖ ‰?À×ÁßdYÒ_€ûàY6@ÎÁÏ°d üuÛÃæå7ÍÞ`3d   ŠH€H€H€H("¡ž>I""""®ˆ8ˆ8ˆ8ˆ¸"dã ⊈‚ˆ‚ˆ‚ˆ*" +" +" +"ªˆ(ˆ(ˆ¨" „¡„Âa(Âa€0TDDDDPAAAEè t:]:„BW„B¡+£"¯AH²&„ ÂT„ Âa‚0a‚0A˜ŠHHHH)""""¥ˆ”úp!I,ý£\òÔ°ghÌ…Í• ÓåCäŠò#ä¢ò§É´ò§È¤òÃä¨òC$¬ü iVŽö”ᢖ»#^,½Ð#Ð~hš‚ Må.@† +l£Ñèpk½Ú„6¥-h˦´¼ÆÜÎ^ç„sʹà\6åÌ;™©gÕjÅÒBžSéÒ«6¤Ý*×Í:ñÜN¬³ñßÉ:åèW[é…VºÐJ§Zés­4RÁ µÒé$ÌÐq3ªš»ÄE(ÜèÂÊ4>se•°š?/2t¾h-Fü +4 MBG¡0‚Ú!?$ÔµVÔv“óPºÒå#ˆ×‹X­v¹Ë˜eÕt2ýv5©Ï ¬7g‚°Œè…½aö‰H!Ñ3˜¹Sð)K\ÆíÓEû…%æ`'-Ñ Ûkî…í±çE¤š>L„C¢}¶ïÂ{Kßi‰Ý¨ö%Z`mV YÖnŃü¸ÛBcä2ÜoSëŠOj²ÄX£%6ËÚ.O¤]uo$§Ñ¡«³4æ Æ]âñ‚¸üoX|—ôŒvÁŸ¡»J1ßþ**G„©”õ±?LÛnJ?#&ý£âe´Eý3âEq¯oϸpùú=ªa‰£z†2Vˆaí—Åñ øšØ)öúqÝ_󲛤ŸÆØ©Eƒ_Ä[ø-ñ€?£º¸C|O" 6ëór|ɦb»áöy9$T|úŒo«?#¿ñ‡ÃºÜhÕ®iIm¶UÛ¢5iÚZ­A«sÕº<®W•«Òår9]sW]¦7Ú>Û:ç`N‡L*ïa2E‚”0êbäAb®à=¬g×VÚcf%=ûtóﻚ2´ò¡¯˜Ëš¶R³¶‡ôôm57µõd´ÂN3ÜÖcjÑ=±iJÇûqÕdÇ2”ôÅ2´ /Ô›µÛp“Œœ¨Ÿ%”Þ=r¢¿Ÿø¼Ovûºk»–oÞ±ýc’¸¶ýçÏwk¶ÁüQÏ®˜ùó†~3ôoÒ«?¦­ãŽßÝ»÷ìçg°mâ8~cCq±1¿i¬ú¶Z’%KìªNH2V’I+]³Q*maY& )k'mùƒìÒeUcã6²¤¬‰2-Ú”ì‡&ªJS6EM6©“ê°÷½gœvÿíÙwß÷îû¹ûÞ}î{ß»c/Oª7óú~ùÅä"1‘²D|‘”3‘J.ÒabJ|‰•Óáx +`4xs9ÀP-Ów#™Á žt3ÌQ€ê€S˜œ¡ 4\ÀP¦á(f¸ìŠœˆgeYÃÀ5jEìøÑ0à1P7ž 4”OÆI†ÂIŸ¬uì)­!¯ ^ ‚á\§5äÅš±Løsˆ ÒúÒªÙâðçoc«+alu€ þŸÏ`wç"ã“7ƒ¾Ä€/1i sîÔ#3uL–³“ãL!g¸ÀÀ±ãCLÌŒûã™I_\ÎFnn£¾ÉÔ_<‹n&$³7ÕÁø|D$|Gã©\,šìú/[gŸØJF·i,ÊK2[±®mÔ]Lc¶º˜­.f+¦Æ4[‰Ìï÷&³zÔêy±(sD2€¸•Tw¥yøYæЋ»Ǥ{‰"ü’‚©ŒÑ×)ƒÄT ] ]L댩ʡش¥rLîRÜKø­-•Š-¾nT¢1Po¦u_oFÙÿB’¹JF=ºýœ²GS;PâDþð=¦%ø}‰F·}ƶ{ÆÇÇGY6E¨7S¿¿7Ó¶z¢Ó©x +ÊB¥2ŽÓʲ¢˜X(,ƒ2ÀcÌ{ â 0¨àÖ¥#³Â¬Ž°«ÂXÎåizù:ìà¯A‚{™˜G´[ÄD®ÚÏî/c¹pkQÂý”Éy—ÒríP•IQª–x™ñÏ4Ì´ÏúgfÛ(½:…Þ9¶•Î‡ç84-¯c) ºÅì]œ¯òh†gÙK0˜ +Žb¯ÿ%—HBìèV«£Zóc¥ )–¢"¸¨ Ž—*oUÑ”ãZÍ „^Á<üà4¥CÝïœt $¦ZOó2èh#§^àó„û g°9‚æõèfty-Ú·E1x7o@iT,ŠÅz´!sË*#™.³H²·±lÕ¨vÒ $`‚½F†ë!ENúÒ)GšL÷m¢Xßj¤±Ú:Ø™Èjÿ:à—‘ˆªâ×Èi¸q„Â9%w„Çü9ü¾^ä12ŠèD<—§´ZÆ#ê¥2ÍPJ†%| Ï¢¢‘ht=]K¯vFQZQ,‚®µ­¦½™ äþøw_ǤñõÍ$ +5¿>ÃzÐŒ5B<8¦yÏqÕµè¾Cå¸ç¸ç¼çÒ÷¸{ªz<?¡o8.Ó¹*½à’QÐîzžö8zœ=.}£ÆYãâ*ô ý®ã‚ûBÕÏåªË}ò˜=²'â9åù¶gÆó'ÞÃü¬Òfoñ³Ñä1I„ñ¤yÌ+*[й˜#Øhb§ +Ÿ×6£ +åÆ9+/®TVâ~è²ËkZ1OçÎ?|¨»omuy}$í3¯¢ØfpäL^0=µTtbKs0Í|y +Ëó–NÖ‡y“&Ôrs'Õ›;y½¤¥³èV©¬@z$UIt;ÝÄmÅÔŠ4ÿt*ÒˆÓ½û’ב»pUAòîwtt¤ðH:Æ¥­¢½­½­µ%à«tþ¶šæ¦J»MÐ TÐQãF­yö7‚Ï ¦’Cúü#'Ößþè³çúšóëÏUb>ÿø‡Xü8;ôåÃ'OW=ºó÷wŽçŽu­í €G£C…‡´fiò¡ëꮓҸ~Zÿ#ç%þ’þgå—­‹åW-×­Ë–»Ö2;ßf‰›_­|üÞ|Ϧ»†îBuŠuŽ +³[†±ïFÝs¦2¯VˆÂøUæb"VÅ{bAäàÛŸ»‚1^ÀŠZí¥aJ(ÃÐ9;WÐÄΕ~#6ºüŽ• +gMi +´è[]+NÀZz==²‰AÂÀ#0ÍÈCiÌ4vÚš›*ì6ä«F3ž°­²¹i‹:jÊj8Г:m>q!ó8ÿÙÝ?çÿŠëÿyéãÍ‹“ûö Ø7L÷ï<°wvó›ùµ?þ%ÿ)Ná³ø<þʵ¿}ãÕs?øÎkà&‡À³ü’ЙEDáà1YZ ’Kz†vžçJ—¥Òo¥$ƒ"a‰Ó!¯–HXŠIý'±KKdqøí÷ ÁTgA¸äÂ: Ñd@-'ýæ\eJŒ[,D™ö­¥7µ%h^Õœ—Æ´Z»@È¥¢¢ý÷ˉõ×qþ_ºÕÛô"æ3žß·~ˆÉ+ÿÖΈÿ€_‚øeÀ]‹HWXQÅöΡ2›B±®µEP!ƒ¯u¯R :ÈžBõà'u†°±µó1ãIt’ r_å‡ô/q¦Ý&zsQ¤:célé‘R™lœþÖ­éƒ 9¾u«¸(ß[IJdë°7#ÁÞ¹öÎEÄòózjX*䩬@;:¶VeqM+ +?¬X9Žÿ cjóê7ò·É.ÜYç6îËçø¥ïyó>Û1ÞæóVˆNO£56Q‡Ê_©ÿ„®S**vQ¨{ZñWVxíývÒh¿b'v»ÍWí¯°êe›ÎÿîÚaaJ Bo]íX',°‰R‹q|_UCjhoh 4š +Í„fCz9Ô"![µŒdk£•Xȹ\Cd)œoBHK¬ÙÊJ¯j{K–ÎpzD jöÂÔ¼§Ó΂š‹‰©¬•Å±€Jëî W&vÎ0ÈÀ ,C«Ò´“°ØTÉ"„(^PÙÔÞÆ–`mÀÇY”­€ïM²ûŸO¿ðò‘33ÿa»j€£¨îø¾¯Ý÷ÞÞ^6›ä.¹Do“r$¤œá,‡·Eƒ)Â#ùt¬åA´‚ -#maF>ÒêŽÚ:©_…‚!À©`¡ãG«£ÐÖ6S¥Dj*Ó"8À-ý¿½@¡C.ûÞÞΛ½ÿÿ½ßï÷ÿý[¾âÿ„o¡ÄÁ—ê¾5³åŽáèBNgýøiÞ#GYÏ wwÌ]ôb}íþÕód-Žéaÿ%&fÞ>áÛ‚åöú+E¨õÎñwשºs肋ìö¾Ó>ôî\/6oˆìжëo‰Èæ—DÔˆD(a +YΖ‹õŒEF4ZÃu¤† ÖÁ¶‰#äÉ2h2Âhª­¡>í4Gmyai*˜%à¥Íö¢¥ ”‡½°“ +·Ì-@“ PWRš‚:“ðªœI +¾ÏоЂWÅ’¨¢¤¶Ó@FÜH¿qWùªÁsÉ*µ…”»3ÀòOëÕ¬nZUÕEª0V»JÝ*Ýh$š—½B[©Í øxÿíÏý¿ø?D¢²^˜ßèÿ9öüŠgÿ»Î]¸|ÎéÏÐOÐlô Ú²ãžW›–­;åŸ÷O}¾UÕ‚§¡÷Bm°«½‘  ûíÑtAˆÕEGG›#wEGØèè×ËPÞÁ¶š,^¨`YäÔؼ¬ö%_yLª¬¼¢öJäV&¡:€B;ic[¡Ð½. +¯@Pe™E +FÑH„ÜÐÕ§:¢[±Â  è)|ÃkmkºÛF-œ´vÞs¹÷Qâãïjž›N?0íÖݬ§bèAÿä;»×vÞ×R§/ÞvfêêڳР+Œl? ™šÚ&og Á5ºg(É^a˜1Ah FXŠSã†ÞBp³Ô@Êc®•´<‹XT¸H €dº:£àA«ÓgÒס>Ý0šŸ€VìZŠø¸’ÊÁk Í\ü ÷å\2’õ|åï;çgÏAôÛ úu½Ð–yˆ^g5†Ë“üuþ7NGðMs®åSFŸ ª1•€Å1×LšØ¼6~y½ø[óv/—vTð׋oÈÅósO«Øžÿ*·Yíì<`ß`Ÿ +×tË-7Î0Vð¡'øºÐÑuåBêåNÔ)O&J±Ä¼ÙœC§‹Ùæýô{ôÑÒ‡b{Â{ì·¬ÃöGöI;L*tW±Í‹ÇFÇáíŠT4èÂQ„sZ&¡"Ŷ"ŶºHCÑ n”ͅǵΠw])W%«pUYm§D2.“’HźÊU;®aJÞ>3 êEž}@¾BU=²õé@ð¢›ÁúÒêª!F°e#]:ÈÁÛŽº™dðªVÇîø]/öîýÑQ!9ÜÿSüWíOôïoÝw.?—ëž½á ´èýhþ܉'ŽŽzà±³ÿö/ø&¦z OU+ê|>ëÕÊ$ÁBÖPç0DÓƒ£48t2îêïÆáI¯Ê³¦XmYjµ[XAµÓ굨…Íüa÷*7Àuùµ\v¶u°3 J& ê̼’¯$_Ôôx½ ‰+Ÿ­(' „,·Ÿõä^Çß8ß„×ä”kÚðø äD´ï<ØÕ˜J1%Õ5ÁìeŠ£)yl +kg}ŒÅY[ÊN3ÚÎ@91Ñ8&Ç¡{UëÓH¯Òc•Ô{ðjÒ›.æ²ÁT2ie²Ë ZßF”`=ç› ŽêK'ÉÛ‡£Íö†,á¿´ðt±P,±–ØK +µ7؆l6×4€‹z8.Âjk¥Å(YŒŠÍ/âɲ¢ÜàN˜dg³g/ÿfî̧y ÊBP.½ºjhmM$Pü TëÖÿ}ïñSE™›œwßT°m{æµÿì?ÿtW¥&gwBtpò¿U'6{1®#Ç‘’L(”R!—Lp!Á/¾æÕz±aèDY, KJ–JA¸ «ÁQÁ¶i¦É N»ñü¬™Ãä9F cø +..‹Ø}ÿCE™"Di¾ ]E jt´˜©Òzú˜ýfpÃÕ ·ÓüM¢ÆtÞUí®i¥5GwòZpWÊ^i·ÍòʆêµbÝ®wB«ÙKuú ´Ÿžeà/õí55%†ÀM>N>DÖ“Ò!~*»H9Bää=rQ’qr<ÁËÀ‰¡úlë]ZõKý»3£w_ê÷Š +Ì MZBÅêšN"yoWAY~Gó3¬fX̃ëv†‹2ZÞä¡|û†ZŽP%‚£°p>ý8w 7ùküï@1É-ÇOæ]\ƒ_ýÒÿ&œä3 Òϳ—5¦óbS …a +þBã”Å L®Þ{ý¦½W °¯°4)7á€]%ÏÀïõ±—/L<§T¤V/”„p©gšd(j +ÚÅcRÒ36ìæàì=Wñ5x +ƒúD|.ÁsJY„+¨-ⲧ®!áÅt¸_>ŒWÒçD—Ü-zäYq^FvÐMb‡<,ŽÈð1ú¡8.Oâ~zBœ’ÖÃb¥\‹7Òµb£Ü„Yæ|?]$ËøjLÀ-t‚h‘3ùL1K¥rD8…ÇД+3aƒàÕ…%8F£ÂÈÃÆ‹ÃFIÁB†Ñ¨‡C`žm‚ùn¥L5Y†YÜ ×¦L5À£§=[ݘœ èJ±!5®°›I+Äæ²°?P [ë5À¯¸” ÑHh1!›R6 ·^CBãJ<Fándí7K{ð-|ÍiÍËVtÚôk4Î}ïûVîî%Ù¼6 ËC®&@yH¦.i˜NKBEBg€à”ªb[*-µŠ-ðH¢Œ<:Zì2ƒ¨ P)X;Ç2 +»éïÜÝ0ÝÍž“³³3»ç÷ûžïù|3o,\˜’«î»­”·„8¸¯†.,X9>ÏòxR:½µœm*”×»½.Û2¨B2ó9qLfªq¢^Ò±£ΧڪÕR»ZíllZ´<%-„Aà‚\gs| · ªK:$]åÄ?sïKKœÉUH)nŠ4Oú·GêâöK¸·%•°-O¨NÊNž^Q•"&¨¿ÞÙiËñòYƒóéšLXÁ JÄ ÑRRB§ ´žØô1²„Ê~RDëÈ#ôºþ•\ Ÿ“ëô6QKÈ8:‡¶ÑNúÙ]·&9ô@CRh@Ž˜‡`Ï.l’¥Ø—;Ÿ=(ã>øo ÷ÖÝ™Œý€Û®·¹PÚk/Þ)ì”vi» ^ÂÔ\4\n“×yé:O[ ƒß&mÓ:Œ-ÞmþÎ@g¨3ÜÕ¨” x£þh8¥¾2]Ž”Q.X²_ÁHq+fžºl³2fÇc-±öXWL4c_ÅHÌ]Ò…° BG¥Óóg»Gm<}Íœ„qÂÀôfˆ™VH™)ÈŒ½ò1a?C/€a 3ªþ¸r[7ž‰·ä6æŽçzsñ·®<øÙÅcÇ.“s—wµJNÎý8÷Bîw¹Õ†VÝÎ Þ½u‡Õ%ƒ[p +XÖÙÅ¢Ðëï s³¼Rèˆ×S¬*r3¶v!)ø©'8:VYØŸs»Fºü¨ûƒÏ½ÜS€ìáì ƒ` øE–|¬­±½Aîy‚ïn|}ùÎúæ3'÷îjƲÙÕ]B_0~qÿÖž'<ìyþT®±|ùÃóWé +|1#I 1@qtË~:íªu=J›ÕfíuùU£Ë:j|$+¢$*!)¨L4jŒ•Ü²Çoø]~÷Dc¢k–k­±Þý¢¶Ém‘§brg¤#&ÊA¿¬¹Œ…ÆZãgÆoŒ— Á0uͯëšK è¡`±ÏíÇþ.?ñû‘gå‚Â6ú–]‚t7ȹ¢’.ñ€xB<+òâÖ ›V¥E¬x`dÕ#¹ÄÑÂÀÍÌ=,Ž'Ž €d `ìI£!`oÍ°‚V9õ¤Á`Èçʉey<ÃUµvÕÿú°ýÔÉÆ Íݹß÷¯Y´lÅÔO>lž:oö˜Ã×…¾yï=ýòùQ“:öåþŽ§ïkˆgwsõc–~{Î÷4ÝÆs¯ñ_ÃÙ)Ågíi½žžØÑqï”òÔG!_(N6 MãžÛô'Ç]Ðú-­AYl,N4X«´Þ•ñ'Æ­,]ëˆíˆk^‹ÝØŒN±ÙnŠDS  ¬“‰“ßšhµ6'6[WW,1©Œ×Ç$ÆXi=eÍUæê33¬f½ÉZ¯ÿ4±MÿyâåUý à¢.&D+¢Dô`‚&,EçqhIØŽ˜©Õa¼:¼'LÂ}¤  i°ŠpQ™ŸC³1³¥Ú¨™ªÄ6žñvÜ…àXÂ_òv4íæ1_6^ß áí ¥BsiÉØhùè’.÷7qÏÅ7<ùFÊþVÐüÜ…K"{RCë^½û?0'×@!ZÝÌ$¯æç5É«pÛå­ËºÔ£(öÅÀ,?vÈ—N@y`‚Õ™C^¶:k»¼iÝô¦çåbï}n¼§§•0{ùÒÉ‘†j&+“õêD5Ô±VŸ‘¨±^Q^K((ÓP8Š¾â`0o,%γ:5q8ðQ1àyGY¼e¢9ØŒîÙúËç¦}'ÕûeãÖM7^Ã~¢¹|6l®­(„¼¿öÙAôvî‹\?¾8ê¹Îõ RµEÞò)KÖÿ©åôŠ¯ßÓ[¿_H§Š+Vüèø3?ý!ÆL_¥àI½p†)Zc[r%_)Ì—[ävy»LE,bž#Ir(å7±û—ÙŠHM\‰6±SKgÌ'-¤l'<‰HÙ7 +]Y°ô ®L­ƒó• +Ã#M3¯܈nD®‡,·U/ä°¡<Žx*´^åñ&¾¿Ä_æy¾«¶²‰kç.q—ŠAËGà€e}XE„#5{?Œ‚WâÉÙ }wNóÓ €B?)BôFïxÐö&¹¤hªT +©ÚP8H/íÝ0s#æC‘jà£ë¶¥” ÚÐ +±•Àü¢!Kñ& €ZÔ¢( ?ˆŠeúOåºö|[ùFÞÎ(ïj£sÀïýÚè²¼Iا¼¬½Éw o*G´¿ðr9Ÿ*S{‘ÿµð¢ò¼&å7X†.²»ÎˆçP†¿ãì'ïîΓùn;À8ýl¥ŠÂ`\vX,{˜Å».:|Jå³g°²[גּãfŽP€" +B•ªøUU‘EJMIöK’Ì«šV€vøNCó'(*•%Q¢T(ˆÄÁw¸tàdT÷àJ[1Åãêq»‚¥%Xj&Èf}HÑH]6 g³ÑH6®‡qíž*Ü…§óëáÏãŒÈýn¤^îŸò¬é zkÏØÐʤâ©øÉà¦Ü^\qkà¸ø +ŸÛ{'÷iî" š‡»qñx}öPPá¸Ãê+züêˆ+M öLM#` XWæåÝfI +3‰d1GEÞMXÀùâŠyB‹Yƒ¦ga[ɤ3`Ö*§Gv€°Âcí âx@ˆå î!cm Z õ…ÙR‹Œ„/–ÿÇtÕÇ6qžñ{ß;ßÙ¾OŸíó¿Èã$ȇ/I€R @B )†y¡ÓHÚmÐjˆˆÑÐv£P†ÖñQQT +¬›´6ˆ`Œ mЦNTê>ªmb0Ê,¡)*i;{Þ»€åî±ï^ß{ïóþžßóû –/:¥öy¦t'½æyÖ‘xÐü¿~ÜÑáX¥¾ÌDÌC‡¿ëœ—ß”DG°ˆX…å4¦J^~ Ðh£¥l'Ú‚v°}h;€¼¸Â¬¶8Æá‘É3CFž oT kWà6ã^îE¼›;À]ÀW8_+Ül\ÁåpÝ”ÎîäÞÀþƒü8~ÈÉPήŒ&[‡ +™,Ø( Ad†5—Ö2õ£ôÓˆ§/Žß¨|;r¸¼ïs8ºÓn`ë<¶{š):Çt0˜iF +¥Ò˜ÅãQ@0ŽyŽSÁÝŸ6z·ýÜ X¥X,tÁ?•Ë•àõuÁ€® dÇÑïÝ+oæžýzó(á‘4Ì©;s¶Ú3sžgÊ:ÆvfT)…cY< p>†<Ìqê6 l£ï½ºs•œ‡»3Á¬Åà˜>¨lÄzYºªgü—£ž“ŸCî‹åíÌîIÄyÔÐ +G=”Á,°È.ÞQîRµ+A—Óž s¨¼}x˜èèe“ÿfæ0 ©JªõÛ/pQoܓТËcKãËf|®ÜøšŒ6ãùt±-=˜>lü,z&z)v3úqL`Y1¬±†VÅÖ„óÆN<ˆÏ°Ø¬pÍú›‚©†úÀ,1egæX){z5œŒ„µ#5‘©¶Ùñ:I¶$•P&¾J0‰Ä,”¥l¸êp­iÇ9ÓŽ)pÒ£–9‚¿áÑ?‹ Üs"Üv"Œ˜#l;Ä'ëÓÞ_µ˜Ÿ&œð4ào ¶¤YB´ÃBÖFØ“7IsÉÖ˜Ýt;‚:"Ý‘:bd{M9šAùô .ìowJ° EP$˜ZG9ª6ãÖóPmõç‹[P +ll,a­ImIáB&O +uJKŠÛ°û D¸TL!’—i“(–­œî¨—æ¦f×&!â.Â!7p©©mÌÜúä·#ítlFù ^á襧 §¯v?üÇ;Ú× o7}‘j^׺bIVáñ?ç;’ýby䧯®ˆ7Þ¶¶¡×ÖhϨˆ¯^2¿|KmЫZæw5¤›S[!åû GW§Þ¹D©“ãv=?·9öL «]l—¿KëÒóñGÛÈÌçcK˜v±=¸$v„;êó 4…Mòp!²Až—)ÄôFû’(©Ô`:- @g5@8&‘sóÝß²²Xj¹» +Ü–ëµÀCæˆÏê/ Ââu6ßÃöø{´½7î)ä)RèfR§‚­„„U…ƒ ðž8ËýÈØ;t½\.]Úð‘­ZË^.üx߶­ƒžË¥‡GÊ÷Ê_•–ÿ¾!Ï|¿£ï䯆O½Cªt-¬=•`Pÿ°W¯“ój^{AîU{µÝúËÆÛømá†rCÿ‹ò™~Ÿ½ï½¼gƒOŸ +/W—kmz^è¸yj³Ö¬Ó;=;åýžAùuãœzV»¤k>ÉAhÌ’™²¤¬H®IˉrÀ/#†òCÎÔOÙ0”²a•=8½ –[‘«È¤jEòA4;$$Ecœ2¢ëÜT®$²½°²˜+f@·îbKc™ DW-CN]좪©ÙC@GA&ŠL}ù´¹£w÷ží=aÊŒýé~ùÒŠ×ÿ…ÿÓðÜš·>¸zbÃŽÚß]GiÄ Í8KXd änÓnÙ³Õ<›÷çU-¿hŒû|}É$žG[¼°e,§[…åáVã¨ÏràÂÔØÏI2l…?R#‰iD"ËTô ÁŽé5ëZž¬°ÿK1Ž^!h™ò€±—íõ÷ª.ZØBÞ4§¨f"ý?T˜Måo}´þbù›òõ¡½È(©µ­¯lzm߶-ûOlÈ£*ðR2Ž`e¢ïƒß{ÿôÅS'a½‹`½U€•Gï]¢¨“6~îQß1ñçÊ9ÏYÿßq$êõ†ÐRü ÛæïHž‡ÙáèMÿÇÂgþ¿ +ãÜ#QŒËñ° ¶¥€%‡¯…? Óa Éœ¥D|ÀdIí”6JXÒUÒZ‡˜…²*EÆ$*,'N¯qcf¶õ¸mèô]H)¥Àkw«*¤ù<ë:IwŠç(Õ†]Õ&»“;’'“LR6½¶([ð)6ÌŒ¨Æ 8‹`ùìnW‡rº”ᬮv[®äXB^F¨äe`:EÕ$=:6%³œPpCK^z(B‡ç}þ…Î×EfÎbù;„A Îô’ Y’Ȥ™^²!YŽXË׶9ƒ1ñ—%b®ØˆW€-$§hÓ1‹A×Fð×Hoºÿ›òƒW{QèÓ"RÙ’MïÝôôú*zW×·ZZz¶öØ© o2å›å«»²}÷•=‹¿DxC‡¸ëù´ÞˆÝÐÄ ™L…RÈ3ºÇË\ÓqX ત L)RQ +ù¼2ºùIód#ü, +ÈšÔF¾&xîCx4 ù}Ùœ·ÃÛ饽ÕJm ;€#ˆ±E)˜Æ¡nê]í÷Ö&@ÎiFd×%ÜK¹{”:òa¢vѸCéP&…þ– Yúç6Èð7Õ‡‚YÒq 88‡ÂYÐ1f R?1÷èv½”^¼pAã­[å{'˜tçà¾çRPæ®n¸H/sj¿¼šÙè(ˆZ´ÊþÎÎÄþV±¯~P¨g*P%®¤ëPgi-Æ‹é r>”ŸÑUÓ[µ]ŒÕùbV›_Õ.¶jíÕ­³ +¥ˆÿMèÙ¼ ò3±JÒ"áÙ¢Ñ=E*à‚SÐ¥€’ó¼àÆê™nTÎpc½å‚/s·‡Î4¹ŠÉ?›$œsºÁάáÓQŽÏ0¢Ñƒõ¨(hÄöSÙ”©uOØglŠ”¢RúÝÕÅyÇ÷Û÷îíÞíÞûàð¸·G„ÄE¤bï4çù#Š¯#!¢U“°Õ$“ªL% +êHM!ø€ÚT±±‘¢fÀ´#Ó´5t¦v:v¦i3m2˜™0j1&°ô¿{œhœììãöîûþ¯ßãÓ Y¹-m2ü›3ï‡âík¨uL—ëúÁ°R†â ܲÔÙëò¶Dj¢u…´Îr.ÊéÊð~1@Ød»Šý²ÝŒsA(ØìSXö2šÇæL_S_’gw ýmçF„.ÿ¾ 1ßmx·MûÏ¿ÆöTo9ÔR»yOR™í˜æw>|öèÙ‹m× eý²}lá¯/m-ÌrñÜzŽC`@ ÍPg ¤þÄÝãÆÜ·ÜøM7rcö°ÓaÀ¼Ûã@·ÈáqÅÒ‰oÜ-[6®¸¼KûUð|#S—!¯X]^AW#] LÃÐʪAw´~‰Þjù͆cËs´áÜs“õ35PÐãŸu/jhi?Œ?~º²8Ѻwü zûuijpI€vqã`g1™Å¹roâú¸!î*w“£|\5·›ëA3EÀbqì*ö |³ +4MÑ Éã p¦Ñ‹þJzØɸ¦âˆãIP†µH‹ÄmQ›¾i8^GmyÈw©}³„ ó!T¨*´vhÂþ;ˆå¡¯±Óó˜Êa£®†>Ç_æßç>à?äù +py¸È¸¹$½–ÝASïp×ÉrŒ¼CSO1O±5ôNò y”Ïä³…âRr)Ń&åxŽå)ž#hÒD‘4D‰™L,ÃØ°Gˆ½2)±õ¸=Òh£&J2,ŽQIøƒ)ÙÇî”~Ç–e¦‰˜x¿Ÿó«X4¥· +P}(j]"?‚FnE´UjíèUí/Ú=Ô¥±Q´CûÑø³è£Ví,,=UÍŠAŒ‚EôZRåÞDõQCÔUê&Eù¨jj7Õ(‰IF„–©ø©Gª6Y§™éQ—¾NÂZ»0ŒîTTÐœA,ß®‚µ€…íTBeU·Là ØîDPÈ% +#\u¤)Òy“>Íœ.Ò…¾ÈÕÈ'3)Œ”×#×#t$žåUcpßd|H1~’ÉÊÑi£Ÿgü{Œ$ËJ¶×Vxh=‹¶ÊñÊâj½4€'ã–¬ìpŽž½àEÕ^ä…gòÂaEW\ý¦"„‹éçø,Ø·¯*ñyp”ÁRT%þ¹j¡ògåºBXŸÒ¤˜’«) +©x¦ÿ»,c¢ÒMceÙ(ð=PÒhc•~ÊŒ®dŒolÀÑÀFÈ綨NK(jó;tä2\’ËiŒ²r”§¦z" Õ)Jž|fûÉé0Û9ÊŠ9µ3´ái±YójÓ†Éðá3«V¯^µþ™Dçx +_bFÙ¢G4O­,H6¿1>5;¬£ỔuÇÝŒÍe«dkYr€DP-)Á&,ŸKm@›Ì˜EZ0™@ªâ(ìÄ hÃÐüÈÿƒ6ÞÌz~EQ¸pº,÷0™zäÒƒ‘Q¹þ‡ ÍH™Ò†C+Jÿ0 +@AøkU×r>íìæÙåÍýš »ðdmó+:®­ýÚ‘Šàv:â‹n aö®í®ƒ¼‚ß p«‡òpxJZc[ãL¹;ðNº“í¸kø?¨rׄaj˜¾!J§Ùð?Ñﱨíl+Ý̲х&—ž";ÉØK™¬êì†l<ÛìDz'i“—íöãê¤ÐìunéÔ‡ªlªÂÂv0x¡pÞ<·rÿø±ÛHÕþøÅkÚÝý(÷H}}{{}ý‘n†FliŸ«bi_ñŠÊu¿âŸ˜ à7¦ú¾$§eÔ£Ïô“Ï%ªSkγ² w<—(¾3cÞ/´ÛcL´1æã¿Ñ2d—ì +vZ;íJ{>ÇØ“vÜú®8h¾âÿ,ø•8 #âjq³Ønê°ž +̼`<”o l +ï³î³ï ì q%átÒ´D\nIúç˜@H —Åþâ@q°8ÄÐ<%s~·¨@ È„ñ‚/Ù_vìˆlÏoq4çw9Úó/.Å&Ôæ:è~#ÿL~_íò;ãþ êŒ{}ªÏ‰®ƒé™ÉúËóÚòð¼¸;GÍË*ÐñѼS^€Š +Pa*˜æ/‚暉üØ$7gx%ÍÌœÌ}i@Oùð hôÆ‘I 6êwÀD#XZLÄ‹i„häDáÀ,Ò¿ +¥\›Pë¬W}lG³;»{3»{Ÿ{··k×>Ÿãó%WÇ>Û9×ä–†8Nç‹„ÔÀ5V |(„ÊA%VÕ’”?ê„ +DQ‰Ü +”üQ©RŠTÇvk'*R(M©Ð6RbEȪ TVeGÄw¼ÙsRâV;oæíìÎݼyï÷Þo™pbKÔmHKÙ˜iHY÷0%´7«ïu‰ÛÓ5á- +ø{­4\sÒ•wçh˜©öé™ÊüdÝ:¡ÏOÖ¯«êŽëë^ Ž™¤3Ý›3ŸOϦßO« iäԅ5Ví‚ßLÚ-EìÇ'ßâë馼è½0ûi#ÙKè9M‰ è÷{É¡þ›±¾Iˆ7”¦‹T[Hx¸t¢Ýöp]ÛÃEm¯£+o{¹(šÖ£ÀuCv½}Ø~̦öA×ÃürÉ^·âJk›Î-•ªÙ|!'Ô¥ÜZ~_ÈUQ¬rÊa¼J¥­_ÂÒm]åméÑb(‹íð××Í‚a1œ0 +h¡Û—ôˆ ~!Ö”ðiO&ûft:d§‘ò4„äX X™6âF¿õåã]MV|Gùç_üîÜŸæÞÏ–ÿ9üÈcm©Ú ùåà#K»¹JZsûfk[Sq+Ò¿åó/|ÿ>»iËÃõ‰ÆºxíWwö?óãwÇ1Šê+JÏ)?ìø[o} +¼òõ¡îàÎà`Hsâ”q°£1‹ØQÉ"I™i\3’ÂÜ!°/Øã¶<„ÝU[¶gˆ‘4&!®j7ƒ†ÎZy+@+9Œ(oxÙ¤œ±£ãEë¼õª%Y§­Y¿·-¬°•²Ú,j9îÈ…{åTÿxâÄCˆ—Áª\Ý<Ø3p+©¥ROxÉЂ‹˜‹¯.`!iá%0†Ä#–oS[-ƒ&4v´w4E¤'®ê͵Í;“GžÜõDAgO?M\š™/ø^®¶fnCû¾m›~B~7ÿÞKå³hŸ Ê|Žf°Bú©gŠ|-rN‘™ê¨=RO¤_êüEÒ|î¡zxܲ8ScV&Á„_'%Hcþ¿ÔI,p¿@ +Å üg +XM2ÿV•:T›XùÛîìCyw÷/¾qìâ.âÔï/öØ@œóuRãÅ•¬Ûš×„P…ëÆ$ö>K¹Ýù)Qe=à†ŽœUŠÊ.syZô·tc{ÑK<ÊsPt ½ 6èyèÖGU!iŠÓð×Ò™§QC±ØƒÇ˜…q¡Æ‹êÀ©Î“$¢â˜LñE²6›×Íz³ÍôLjÚ¶æE¾iØŒÔæéT*è´H÷P™^‘Ú°D=í…Œ )„™8Æ,ú–#œ+—ø¨„™ªäìÞvô³öu¿Båy´@ð/ø¡Ã„%â¯Ò³;»:»bHÁ¦ËHóµn[ †CÊh½Õ^Û–hi‘êª6eȈ6£M ©ÙÛ„–å J\SX $¤:Q\ÍbuuLSYPìtäÁíhBR…2S£\šP,æð¸a4B–6+-,Ë›MÐ¥la½°]Ú®ôi;ØI¡'•6ÂO£p†Ž*gØ>jÜ„›ôºrÝä×Ûp›.( ì6_0îÀº¬¬hËì_6Z”™Ê{«éÎÓ +6S™ó5.4ãÞMìt ÷¸:½î¡X;âAÎ æ‘ÆŠy/ŽÝš®"ˆkèALº`EQú'?õ¦N•ÔLe`Rå û]Þ§d0Rø•l€D¨!+\×X@ hš¢P*üÃàè(À[ƒÅ D§|†‘ ¤ÐæÇAÇæL‚S)☳—‰[­U\g`ÕM®®ºÎj²êP=þâK ÷øïˆ/!âÿKô ¨‚·á˜ @ ÿ”î™ÜñÊ„YÀ ¯ ìëž!ž,"ìËÕµù ]hó÷’€ŸED]$ü+&nÒ Ëd°iF´ÌI¯¬D/30r‡üÈóžÍjר4¦]&·ÈumÑTšK“jVí‚Í>2Hž$krÂ¥e­UtE1ªlÌå†s£OÍŽnL®ˆðU¢êè¯ñú´Øàöpß=JÃÃ'ˆíÄ?W"NÕ §ÊÏ‘Coüšì,‘³å—oÌI’\¾EÖ•ÙêÈŽò´ÀŽ`yݧ#ù©hV!1±õ¤Êf(¯ ¡ +¡$ð™$‚«1YQUjêA5,AL¥1‰¢!¡‹ a)9C^E@ ™­Á,¤âmñ¡¸¼ˆÀí×Z™¼è½hm]>Ž¡C ²—tò§ÄfI¾&IhQR¯¶3/¬ˆìÏš]ËÕ¹U¥ÀÕU?–ÐZ'ÂK È{K­Õ€BLËUJ †{„¥ªaTêcªïÆT?AÃp¥‚§UY¼$‡Éf¼ýÂJ©|èÍH1Ž9(¢É¢"Ü ÑO ^]k°DZPFvÕ,ÒaWäÊ+¤±|vkÓÖC§öîÛí<ÜqäQ*(}|Wº\:òétä–ùíA¸}óÿÐ.~ÒȃһÿÚ¨ŠíÍÿ­©=ÚK?°kü= `|ÀÄ9s x'| z ÀÂDÿ 1QmÉ€û€²W6¿Mab[& ”¥H%ôV7x›4Á +Æ %mœgÃ7‚%^;V¯µÞ@ÓK%UÕCOý z‚ 7\—^7¤z ¢‡¨ûÌz1 © ª*˜Yývž}çý˜Yg·onññ1`çÏòSÈ->ÙÛâÓýÀ®!`wH^z¹¾=¤ï8Яø½…ÁÆu`pH IûXk¨‹¬û™'{8p8ø3ph8|ÈMi4F£Ñh4F£Ñh4F£Ñü?@PíCD•êè!q¬Û¢/j,aäPÄIœ Æ,,6›ÍG/¿Â7þª}í¨j]¸Õö[kÇlÀ=>=Íý]ÇÝPGÑÉ…:Fm†:N] õ\Ž|«v@l£Êyê|}꺢+¡Ž"C¨cÔÉPÇ©'CÍùÄ<¾¯eÔa£Ìw2Ï^p“˜Xt.jĽy>yÔênÑ^ <„‡ñÕX`·Þ2Ó@{f‚YŽ8Xjû4h›`ߪ—F–× R¡ÊÖ#öEÆT8?ˆ*2_ƒx¸Ê{‰^Ç-zÞ +æªfRâÈöisùfÞ|B«ÍúUVõƒºªªðYùøaÖ9®P¸wU¼ Ô+ð>ÍÚå`5ýUœÍ¬jþׂH•Í0—ëvÙš·å¦˜ ¶ÜšëÓ$y׫»žåWÝšÔyCÆ,ßZÇi@%“Y×YR–†LÔ—ÎfS¼e É9Ž«•¿!E»a{WíR~br|<ß—óª–“:ê:¥‚ùÏL¾!¦g•ì+–·(nùµsÏ®T¾íÙ%©ÖħëܬÌX¾$Å,Èt¹lˆU+‰í4ìk t3Þó½gžIŒóÊ£ï¹^ vä•Úã¯òJñ fVâêÌÕëøÏ©S9þ#V0ÊÊqž¼ þ¦s´M%\ž®­ovä·Êãë߯]ü`ôIç¶ÎàÿåÑèÕßùõv÷êêŸ%Щ>ËêÎù¿ ±=Œ + +endstream endobj 623 0 obj<>stream +ÂÃÆ€œMcz-Hf#@`#@`#@`'Cc?Wrfx‹fx‹¡§¯ + +endstream endobj 624 0 obj<>stream +H‰:vìXnm¿ºs²’]<~d^´pùúc$ª0Ž™ + +endstream endobj 625 0 obj<>stream +H‰š³h®Gš’]<‘HÙ!!:·ùرc䓘 + +endstream endobj 626 0 obj<>stream +H‰j4_Ù!AÉ.žTd”wìØ1€2*t + +endstream endobj 627 0 obj<>stream +H‰jè£ì dO²/:vì@€—ªô + +endstream endobj 628 0 obj<>stream +H‰Ú°e—²C‚’]<%È7¹ + ÀëÄ· + +endstream endobj 629 0 obj<>stream +H‰r-W²‹§õO_`Ü + +endstream endobj 630 0 obj<>stream +H‰êš²PÉ.ž*ÈÄ/ ÀF• + +endstream endobj 631 0 obj<>stream +H‰² ÊS²‹§š1%@€H’ + +endstream endobj 632 0 obj<>stream +H‰Z¸|½²C‚’]<µ}x@€Øâj + +endstream endobj 633 0 obj<>stream +H‰*©ïW²‹§"ÒõH;vìXpF=uUvHØð°{l9ÕÝ°e—}xu¢…Ë×Óµ›·í0é)uª + +endstream endobj 634 0 obj<>stream +ÆÆÆ­²¸«°¶«°¶«°¶«°¶«°¶«°¶«°¶©®µ¿Áà + +endstream endobj 635 0 obj<>stream +H‰ŠÎmV²‹§"RvHØð0@€ä¸ + +endstream endobj 636 0 obj<>stream +¼¾Á#@`#@`#@`#@`#@`#@`#@`#@`">_t + +endstream endobj 637 0 obj<>stream +H‰*ož¢dOE¤ë‘`µ(Ð + +endstream endobj 638 0 obj<>stream +·»¾">_">_">_">_">_">_">_">_">_k{Ž + +endstream endobj 639 0 obj<>stream +H‰Z¶f³²C‚’]<µ{l9@€Ý–‹ + +endstream endobj 640 0 obj<>stream +·»¾#@`">_">_">_">_">_">_">_">_k{Ž + +endstream endobj 641 0 obj<>stream +H‰²/R²‹§ +RvHX¸|=@€M‰« + +endstream endobj 642 0 obj<>stream +ÆÆÆ¿Á󶻦¬³˜Ÿ©˜Ÿ©˜Ÿ©¡§¯°´º¿ÁÃÆÆÆ + +endstream endobj 643 0 obj<>stream +H‰š8s‰’]>stream +H‰òM®R²‹§);$L³ Àà¬/ + +endstream endobj 645 0 obj<>stream +H‰Ú³ï ºs²’]<%(:· ÀöÄÿ + +endstream endobj 646 0 obj<>stream +H‰š8s‰²C‚’]¼èرc¡‡ + +endstream endobj 647 0 obj<>stream +H‰š³hºs²’]<©È=¶üرcAÊÔ + +endstream endobj 648 0 obj<>stream +H‰Z¸|½ePž’]<‘HÙ!!­¢ Àºï + +endstream endobj 649 0 obj<>stream +ÆÆÆwƒ”4Nk#@`">_">_">_">_">_">_">_">_">_'CcG]w©®µ + +endstream endobj 650 0 obj<>stream +¡§¯Mcz9Rn-Hf'Cc-Hf-Hf?WrttÂÃÆ + +endstream endobj 651 0 obj<>stream +ÆÆÆÂÃƳ¶»¡§¯˜Ÿ©˜Ÿ©˜Ÿ©¦¬³°´º¿ÁÃÆÆÆ + +endstream endobj 652 0 obj<>stream +ÆÆÆÆÆÆÆÆÆ + +endstream endobj 653 0 obj<>stream +ÆÆƼ¾Á­²¸¡§¯˜Ÿ©˜Ÿ©˜Ÿ©¡§¯«°¶·»¾ÄÆÆÆÆÆ + +endstream endobj 654 0 obj<>stream +ÆÆÆ©®µm}Mcz9Rn-Hf#@`#@`">_">_">_">_">_#@`#@`-HfG]w€œÆÆÆ + +endstream endobj 655 0 obj<>stream +¿ÁÃy‡—G]w4Nk'Cc#@`">_">_">_">_">_#@`#@`-Hf?WrShŠ”¡ÆÆÆ + +endstream endobj 656 0 obj<>stream +H‰Ú¾{_ZE·eP©;'+;$(ÙÅ“„€ºÊ›§?g + +endstream endobj 657 0 obj<>stream +¼¾Áy‡—G]w4Nk'Cc#@`#@`">_">_">_">_">_#@`#@`'Cc4NkG]wfx‹©®µ + +endstream endobj 658 0 obj<>stream +H‰Z¸|½ePž²C‚’]>stream +H‰š1åŒù+-ƒò””ìâIE@]ºi%õýÇŽ0|.ž + +endstream endobj 660 0 obj<>stream +¡§¯wƒ”y‡—y‡—y‡—y‡—y‡—y‡—y‡—y‡—y‡—y‡—m}Š”¡ + +endstream endobj 661 0 obj<>stream +H‰:vìØŒù+KêûÝcËu=Ò””ìâ)GöáEû0œ?â + +endstream endobj 662 0 obj<>stream +H‰Ú¼m÷æm»íË””ìâÉF@íÁõ@´ÿàa€vlz + +endstream endobj 663 0 obj<>stream +H‰Ú¾{Ÿ}x‘’]>stream +ÆÆÆ[mƒ#@`">_">_">_">_">_">_">_">_">_">_">_4Nk•œ¦ + +endstream endobj 665 0 obj<>stream +H‰Z¸|½²C‚’]<Õ‘eP^×”…ºi@D]c÷< `´1ð + +endstream endobj 666 0 obj<>stream +H‰Z¶f³®Gš’]<ÕQbq;@€¸¬ + +endstream endobj 667 0 obj<>stream +°´º9Rn">_">_">_">_">_">_">_">_">_">_">_#@`fx‹ + +endstream endobj 668 0 obj<>stream +H‰Z¸|½’]üÐBöáE,Ü/ + +endstream endobj 669 0 obj<>stream +H‰:tø˜®Gš’]>stream +…’Ÿ…’Ÿ'Cc">_">_">_">_">_">_">_">_">_">_">_?Wr·»¾ + +endstream endobj 671 0 obj<>stream +H‰Z¸|½’]<‘H×#Í=¶<8£‚|“«ìËԓ‰7Ž”,ƒò€&ÀMš 4(NŒö’ú~€¡–5 + +endstream endobj 672 0 obj<>stream +H‰ +ΨW²‹Çƒt=Ò‹Û._¿gßÁC‡AÐþƒ‡'Î\œQ¯îœŒ_»²C²5› ›ã&P + +endstream endobj 673 0 obj<>stream +H‰:vìXtn³’]@€…ž + +endstream endobj 674 0 obj<>stream +H‰Z¸|½’]>stream +H‰:vìX×”…&~ÙJvñÄ e‡„òæ)û05F + +endstream endobj 676 0 obj<>stream +ÆÆƦ¬³?Wr">_">_">_">_">_">_">_">_">_">_">_4Nk + +endstream endobj 677 0 obj<>stream +³¶»#@`">_">_">_">_">_">_">_">_">_9RnÂÃÆ + +endstream endobj 678 0 obj<>stream +H‰Z¶f³®Gš’]>stream +ÂÃÆSh">_">_">_">_">_">_">_">_">_">_Sh + +endstream endobj 680 0 obj<>stream +ÆÆƼ¾Á³¶»ª®³¡§¯ª®³³¶»¿ÁÃÆÆÆ + +endstream endobj 681 0 obj<>stream +ÆÆƼ¾Á¦¬³‘™¤Š”¡—¡˜Ÿ©°´ºÂÃÆÂÃÆ + +endstream endobj 682 0 obj<>stream +¡§¯">_">_">_">_">_">_">_">_">_">_">_Sh + +endstream endobj 683 0 obj<>stream +ÂÃÆ4Nk">_">_">_">_">_">_">_">_">_">_">_#@`˜Ÿ© + +endstream endobj 684 0 obj<>stream +Sh">_">_">_">_">_">_">_">_">_">_'CcÂÃÆ + +endstream endobj 685 0 obj<>stream +€œ">_">_">_">_">_">_">_">_">_#@`¦¬³ + +endstream endobj 686 0 obj<>stream +€œ#@`">_">_">_">_">_">_">_">_">_">_">_4Nk—¡ + +endstream endobj 687 0 obj<>stream +?Wr">_">_">_">_">_">_">_">_">_#@`³¶» + +endstream endobj 688 0 obj<>stream +¼¾Á¡§¯‘™¤‘–Ÿ‘–Ÿ‘–Ÿ‘–Ÿ‘–Ÿ‘–Ÿ‘–Ÿ•œ¦¦¬³ÂÃÆ + +endstream endobj 689 0 obj<>stream +ÄÆÆ•œ¦wƒ”nx‰nx‰nx‰nx‰nx‰nx‰nx‰pzŠpzŠ‚Š˜ª®³ + +endstream endobj 690 0 obj<>stream +¡§¯">_">_">_">_">_">_">_">_">_">_-HfÄÆÆ + +endstream endobj 691 0 obj<>stream +¡§¯#@`">_">_">_">_">_">_">_">_">_">_k{Ž + +endstream endobj 692 0 obj<>stream +ÆÆÆ-Hf">_">_">_">_">_">_">_">_">_#@`·»¾ + +endstream endobj 693 0 obj<>stream +Sh">_">_">_">_">_">_">_">_">_-HfÆÆÆ + +endstream endobj 694 0 obj<>stream +¿ÁÃMcz">_">_">_">_">_">_">_">_">_">_">_#@`fx‹ÆÆÆ + +endstream endobj 695 0 obj<>stream +«°¶#@`">_">_">_">_">_">_">_">_">_Š”¡ + +endstream endobj 696 0 obj<>stream +H‰:vìز5›'N›O Zµnó±cÇ 8Êà + +endstream endobj 697 0 obj<>stream +H‰Z¸|}AUW^E'1¨¾cúž} –Qè + +endstream endobj 698 0 obj<>stream +¡§¯">_">_">_">_">_">_">_">_">_">_Sh + +endstream endobj 699 0 obj<>stream +4Nk">_">_">_">_">_">_">_">_">_#@`°´º + +endstream endobj 700 0 obj<>stream +G]w">_">_">_">_">_">_">_">_">_#@`·»¾ + +endstream endobj 701 0 obj<>stream +H‰Ú¾{Ÿ²C‚’]>stream +?Wr">_">_">_">_">_">_">_">_">_4NkÆÆÆ + +endstream endobj 703 0 obj<>stream +9Rn">_">_">_">_">_">_">_">_">_k{Ž + +endstream endobj 704 0 obj<>stream +H‰Ú³ïàÔ9Ë&N›O<ª?vì@€ï!"Ö + +endstream endobj 705 0 obj<>stream +H‰jí›WÑIʯìZ¸|=@€#G + +endstream endobj 706 0 obj<>stream +¡§¯">_">_">_">_">_">_">_">_">_">_m} + +endstream endobj 707 0 obj<>stream +G]w">_">_">_">_">_">_">_">_">_'CcÄÆÆ + +endstream endobj 708 0 obj<>stream +fx‹">_">_">_">_">_">_">_">_">_#@`·»¾ + +endstream endobj 709 0 obj<>stream +H‰Ú¾{Ÿ²C‚’]>stream +ÆÆÆ4Nk">_">_">_">_">_">_">_">_">_'CcÄÆÆ + +endstream endobj 711 0 obj<>stream +G]w">_">_">_">_">_">_">_">_">_[mƒ + +endstream endobj 712 0 obj<>stream +H‰Úððþƒ‡'Î\2qÚ|RÑÔ9ËŽ;`[»(¢ + +endstream endobj 713 0 obj<>stream +H‰jêš‘WÑIZ¶f3@€Ϫç + +endstream endobj 714 0 obj<>stream +m}">_">_">_">_">_">_">_">_">_#@`·»¾ + +endstream endobj 715 0 obj<>stream +Mcz">_">_">_">_">_">_">_">_">_-HfÆÆÆ + +endstream endobj 716 0 obj<>stream +H‰Ú¾{Ÿ²C‚’]>stream +ÆÆÆ9Rn">_">_">_">_">_">_">_">_">_">_t + +endstream endobj 718 0 obj<>stream +…’ŸShShShShShShShShShœ¢¬ + +endstream endobj 719 0 obj<>stream +H‰:tøØÄ™K@hÚ|òÐÔ9ËŽ;`;W+Á + +endstream endobj 720 0 obj<>stream +H‰jêš‘WÑI6ʯìZ¶f3@€!È + +endstream endobj 721 0 obj<>stream +G]w">_">_">_">_">_">_">_">_">_">_#@`Mcz¡§¯ÆÆÆ + +endstream endobj 722 0 obj<>stream +H‰Ú¾{Ÿ²C‚’]>stream +H‰š:gÙÄió)D —¯0ð*Ó + +endstream endobj 724 0 obj<>stream +H‰Z¶fs^E'…¨ ªëÐác}0%O + +endstream endobj 725 0 obj<>stream +H‰J«èV²‹'éz¤ù&W•Ô÷O³lÙšÍÛwï;tø™M + +endstream endobj 726 0 obj<>stream +H‰Ú¾{Ÿ²C‚’]<‘¨8±¸ À¼„ + +endstream endobj 727 0 obj<>stream +H‰Ú¾{ßÄió©‚öì;`=y.ü + +endstream endobj 728 0 obj<>stream +H‰:rìXI}^E'å¨kÒ|€&‡ + +endstream endobj 729 0 obj<>stream +H‰š:g™’]<©HÙ!ˆÔ“u=ÒLü²íË|“«‹ÛË›§Ì˜¿rÿÁÃ9³à + +endstream endobj 730 0 obj<>stream +H‰Ú¾{Ÿ²C‚’]<1HÝ99­¢{ٚͫ7l¢í»÷;v À§† + +endstream endobj 731 0 obj<>stream +H‰:vìØÔ9Ë&N›O4cþJ€Û‹0š + +endstream endobj 732 0 obj<>stream +H‰š:gY^E'UPAUבcÇ SN( + +endstream endobj 733 0 obj<>stream +H‰:r옮Gš’]<‘²C‚eP^eût :vì@€}[Î + +endstream endobj 734 0 obj<>stream +H‰Ú¾{Ÿ²C‚’]<‘Y1­îœl”ç›\UÞ>stream +H‰Ú¾{ßÄi󩈎;`a-2 + +endstream endobj 736 0 obj<>stream +H‰:vìXI}^E'µPÿô…ø)s + +endstream endobj 737 0 obj<>stream +H‰š1¥²C‚’]<-ePÞÚM; <5W + +endstream endobj 738 0 obj<>stream +H‰Ú¾{Ÿ²C‚’]<åhŽeP^vußž} 7ýˆ + +endstream endobj 739 0 obj<>stream +H‰Z¸|ýÄi󩈖­Ù `N +2 + +endstream endobj 740 0 obj<>stream +H‰Z½a[^E'QI}?@€Üp(Ú + +endstream endobj 741 0 obj<>stream +H‰j4_Ý9YÉ.žhòÂåë .©ë + +endstream endobj 742 0 obj<>stream +H‰Ú¾{Ÿ²C‚’]<Ð@ßäªÕ¶ØX + +endstream endobj 743 0 obj<>stream +H‰š:gÙÄió©ˆfÌ_ `CY1Ô + +endstream endobj 744 0 obj<>stream +H‰š:gY^E'QAU×±cÇ Gê*ä + +endstream endobj 745 0 obj<>stream +H‰Z»i‡{l¹²C)ÙÅSéz¤íÙw Àt†ô + +endstream endobj 746 0 obj<>stream +H‰Ú¾{Ÿ²C‚’]<ÕÐØòæ) b) + +endstream endobj 747 0 obj<>stream +H‰š8mþDª¢©s–=Ç1º + +endstream endobj 748 0 obj<>stream +H‰ê˜0'¯¢“ºhϾƒ;Ø*¥ + +endstream endobj 749 0 obj<>stream +H‰Ú°e׆-»*Û§»Ç–›øe«;'+;$‘’]<…¨¼y +@€®' + +endstream endobj 750 0 obj<>stream +H‰Ú¾{Ÿ²C‚’]>stream +H‰š8mþDꢙKŽ@€x&6U + +endstream endobj 752 0 obj<>stream +H‰jí›WÑI]´aË.€9“*‚ + +endstream endobj 753 0 obj<>stream +H‰:vìØþƒ‡._ŸVÑm^DÊ JvñQnm?@€ó86 + +endstream endobj 754 0 obj<>stream +·»¾#@`">_">_">_">_">_">_">_">_m} + +endstream endobj 755 0 obj<>stream +H‰:vìØþƒ‡WoØÖ:i~vu_tn³or•}x‘ePž®Gšºs²²C‚’]<1È=¶ À}€ + +endstream endobj 756 0 obj<>stream +·»¾[mƒ#@`">_">_">_">_">_">_">_">_">_">_'CcÂÃÆ + +endstream endobj 757 0 obj<>stream +H‰jí›WÑI]´yÛn€9¡*ˆ + +endstream endobj 758 0 obj<>stream +ÆÆÆÄÆƳ¶»y‡—G]w#@`">_">_">_">_">_">_">_">_">_">_-Hf + +endstream endobj 759 0 obj<>stream +°´º9Rn">_">_">_">_">_">_">_">_">_">_˜Ÿ© + +endstream endobj 760 0 obj<>stream +œ¢¬'Cc">_">_">_">_">_">_">_">_">_'Cc + +endstream endobj 761 0 obj<>stream +H‰š:gÙÄi󩈦ÎY`CG1Ë + +endstream endobj 762 0 obj<>stream +fx‹9Rn?Wr?Wr?Wr?Wr?Wr?Wr?Wr?Wrm} + +endstream endobj 763 0 obj<>stream +¼¾Á-Hf">_">_">_">_">_">_">_">_">_Sh + +endstream endobj 764 0 obj<>stream +as‡">_">_">_">_">_">_">_">_">_#@` + +endstream endobj 765 0 obj<>stream +H‰Z¸|ýÄió©ˆ._`Mí2 + +endstream endobj 766 0 obj<>stream +9Rn">_">_">_">_">_">_">_">_">_9Rn + +endstream endobj 767 0 obj<>stream +‘™¤">_">_">_">_">_">_">_">_">_-HfÆÆÆ + +endstream endobj 768 0 obj<>stream +‘™¤">_">_">_">_">_">_">_">_">_'Cc + +endstream endobj 769 0 obj<>stream +ÆÆÆ-Hf">_">_">_">_">_">_">_">_#@`·»¾ + +endstream endobj 770 0 obj<>stream +G]w">_">_">_">_">_">_">_">_">_'CcÆÆÆ + +endstream endobj 771 0 obj<>stream +˜Ÿ©">_">_">_">_">_">_">_">_">_-Hf + +endstream endobj 772 0 obj<>stream +[mƒ">_">_">_">_">_">_">_">_">_¡§¯ + +endstream endobj 773 0 obj<>stream +as‡">_">_">_">_">_">_">_">_">_">_‘™¤ + +endstream endobj 774 0 obj<>stream +y‡—">_">_">_">_">_">_">_">_">_9Rn + +endstream endobj 775 0 obj<>stream +H‰Ú¼m÷Äió©‚öì;`9Ü.ð + +endstream endobj 776 0 obj<>stream +˜Ÿ©">_">_">_">_">_">_">_">_">_">_9RnÄÆÆ + +endstream endobj 777 0 obj<>stream +Š”¡">_">_">_">_">_">_">_">_">_Š”¡ + +endstream endobj 778 0 obj<>stream +ÆÆÆ4Nk">_">_">_">_">_">_">_">_">_as‡ + +endstream endobj 779 0 obj<>stream +H‰š:gÙÄió)Ds­0Ô*Æ + +endstream endobj 780 0 obj<>stream +H‰Zµns^E'…¨¤¾ÿȱcA%h + +endstream endobj 781 0 obj<>stream +¦¬³#@`">_">_">_">_">_">_">_">_wƒ” + +endstream endobj 782 0 obj<>stream +ÆÆÆ9Rn">_">_">_">_">_">_">_">_">_">_G]wÆÆÆ + +endstream endobj 783 0 obj<>stream +¿ÁÃ?Wr">_">_">_">_">_">_">_">_">_#@`«°¶ + +endstream endobj 784 0 obj<>stream +H‰:tøØ¡ÃÇ&Î\2qÚ|RÑÔ9ËŽ;`_³(² + +endstream endobj 785 0 obj<>stream +H‰jî™™WÑI6ʯìZµn3@€¡!Ò + +endstream endobj 786 0 obj<>stream +H‰š³h²C‚’]<dâ—]Rß¿pùúýƒ {Ö]]SFç6«;'ã7!­¢ Àe(Ú + +endstream endobj 787 0 obj<>stream +H‰jí›WÑIʯìZ¶f3@€Ò%ô + +endstream endobj 788 0 obj<>stream +ÂÃÆ'Cc">_">_">_">_">_">_">_">_as‡ + +endstream endobj 789 0 obj<>stream +H‰*©ïW²‹');$¨;'C DÄèr-0×õ + +endstream endobj 790 0 obj<>stream +H‰Ú³ïàÄ™K&N›O<š:gÙ±cÇ íB"Í + +endstream endobj 791 0 obj<>stream +H‰ê˜0'¯¢“$TPÕµjÝf€$Ñ$ + +endstream endobj 792 0 obj<>stream +ÄÆÆ'Cc">_">_">_">_">_">_">_">_Sh + +endstream endobj 793 0 obj<>stream +H‰ŠÎmVvHP²‹§:²/:vì@€¹?M + +endstream endobj 794 0 obj<>stream +ÆÆÆ-Hf">_">_">_">_">_">_">_">_Mcz + +endstream endobj 795 0 obj<>stream +H‰Z¶fsI}^E'A”_ÙÕÚ7{Ͼƒœ  + +endstream endobj 796 0 obj<>stream +H‰š8s‰‰_¶’]>stream +ÂÃƦ¬³‘™¤‘–Ÿ‘–Ÿ‘–Ÿ‘–Ÿ‘–Ÿ‘–Ÿ‘–Ÿ•œ¦ª®³ÆÆÆ + +endstream endobj 798 0 obj<>stream +ÆÆÆ¡§¯wƒ”oyŠnx‰nx‰nx‰nx‰nx‰nx‰pzŠpzŠˆœ·»¾ + +endstream endobj 799 0 obj<>stream +¡§¯">_">_">_">_">_">_">_">_">_">_k{Ž + +endstream endobj 800 0 obj<>stream +H‰:tøXCï 2ñËVvHP²‹'éz¤åÖö9v À ö` + +endstream endobj 801 0 obj<>stream +ÆÆÆ'Cc">_">_">_">_">_">_">_">_Mcz + +endstream endobj 802 0 obj<>stream +ÆÆƼ¾Á°´º¦¬³¡§¯¦¬³³¶»¿ÁÃÆÆÆ + +endstream endobj 803 0 obj<>stream +¼¾Á«°¶˜Ÿ©—¡‘™¤¡§¯³¶»ÆÆÆÆÆÆ + +endstream endobj 804 0 obj<>stream +·»¾as‡[mƒ[mƒ[mƒ[mƒ[mƒ[mƒ[mƒ[mƒ[mƒ¡§¯ + +endstream endobj 805 0 obj<>stream +—¡Sh[mƒ[mƒ[mƒ[mƒ[mƒ[mƒ[mƒ[mƒwƒ”ÆÆÆ + +endstream endobj 806 0 obj<>stream +¡§¯[mƒ[mƒ[mƒ[mƒ[mƒ[mƒ[mƒ[mƒ[mƒfx‹ÂÃÆ + +endstream endobj 807 0 obj<>stream +ÄÆÆas‡[mƒ[mƒ[mƒ[mƒ[mƒ[mƒ[mƒ[mƒ¡§¯ + +endstream endobj 808 0 obj<>stream +ÆÆÆwƒ”[mƒ[mƒ[mƒ[mƒ[mƒ[mƒ[mƒ[mƒ‘™¤ + +endstream endobj 809 0 obj<>stream +ÆÆƦ¬³m}?Wr4Nk'Cc#@`#@`">_">_">_">_">_">_#@`'Cc-Hf9Rn[mƒœ¢¬ÆÆÆ + +endstream endobj 810 0 obj<>stream +ÆÆÆ¿Á󶻡§¯‘™¤…’Ÿ…’Ÿ•œ¦¦¬³·»¾ÂÃÆÆÆÆ + +endstream endobj 811 0 obj<>stream +ÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆ + +endstream endobj 812 0 obj<>stream +ÆÆÆÆÆÆÆÆÆÆÆÆ + +endstream endobj 813 0 obj<>stream +ÆÆÆÆÆÆ + +endstream endobj 814 0 obj<>stream +·»¾©®µ·»¾ + +endstream endobj 815 0 obj<>stream +ÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆÆ + +endstream endobj 816 0 obj<>stream +Š”¡mxˆmxˆ¡§¯ + +endstream endobj 817 0 obj<>stream +pzŠoyŠoyŠpzŠpzŠoyŠpzŠpzŠoyŠoyŠ¡§¯ + +endstream endobj 818 0 obj<>stream +³¶»pzŠnzŒÆÆÆ + +endstream endobj 819 0 obj<>stream +|†•fr…ku‡kvˆ + +endstream endobj 820 0 obj<>stream +‘–Ÿnx‰pzŠoyŠoyŠkvˆnzŒ«°¶ + +endstream endobj 821 0 obj<>stream +ÆÆÆtnx‰•œ¦ + +endstream endobj 822 0 obj<>stream +¦¬³|†•¦¬³ + +endstream endobj 823 0 obj<>stream +ÆÆÆwƒ”wƒ”oyŠ˜Ÿ© + +endstream endobj 824 0 obj<>stream +¦¬³pzŠwƒ” + +endstream endobj 825 0 obj<>stream +ÆÆÆ…Ž›‚Š˜ÆÆÆ + +endstream endobj 826 0 obj<>stream +ku‡ht‡ht‡ht‡ht‡oyŠku‡ht‡ht‡ht‡¡§¯ + +endstream endobj 827 0 obj<>stream +ƒŒ™mxˆmxˆ˜Ÿ© + +endstream endobj 828 0 obj<>stream +«°¶nx‰ht‡ÆÆÆ + +endstream endobj 829 0 obj<>stream +³¶»kvˆnx‰t—¡ + +endstream endobj 830 0 obj<>stream +ƒŒ™nx‰mxˆttkvˆpzŠfr…·»¾ + +endstream endobj 831 0 obj<>stream +ÆÆÆku‡nx‰Š”¡ + +endstream endobj 832 0 obj<>stream +…Ž›nx‰‘–Ÿ + +endstream endobj 833 0 obj<>stream +ÆÆÆht‡ht‡kvˆ‘™¤ + +endstream endobj 834 0 obj<>stream +•œ¦kvˆpzŠ + +endstream endobj 835 0 obj<>stream +ÆÆÆku‡oyŠÂÃÆ + +endstream endobj 836 0 obj<>stream +ÆÆÆÆÆÆÆÆƈ—ˆ—oyŠnzŒÆÆÆÆÆÆÆÆÆ + +endstream endobj 837 0 obj<>stream +ƒŒ™mxˆmxˆœ¢¬ + +endstream endobj 838 0 obj<>stream +­²¸nx‰ht‡ÆÆÆ + +endstream endobj 839 0 obj<>stream +­²¸nx‰fr…ÆÆÆ + +endstream endobj 840 0 obj<>stream +…Ž›nx‰—¡ + +endstream endobj 841 0 obj<>stream +·»¾kvˆnx‰ˆœ + +endstream endobj 842 0 obj<>stream +kvˆnx‰—¡ + +endstream endobj 843 0 obj<>stream +Š’Ÿnx‰‘™¤ + +endstream endobj 844 0 obj<>stream +·»¾·»¾«°¶ÄÆÆ + +endstream endobj 845 0 obj<>stream +¿Á鮵¼¾Á + +endstream endobj 846 0 obj<>stream +kvˆoyŠÆÆÆ + +endstream endobj 847 0 obj<>stream +ÆÆÆ•œ¦nzŒku‡t¡§¯ + +endstream endobj 848 0 obj<>stream +ƒŒ™ƒŒ™oyŠpzŠ + +endstream endobj 849 0 obj<>stream +ÆÆÆ—¡nzŒkvˆt­²¸ + +endstream endobj 850 0 obj<>stream +…Ž›nx‰nx‰‘™¤ˆœlxŠnzŒ˜Ÿ© + +endstream endobj 851 0 obj<>stream +¦¬³|†•¦¬³—¡nzŒnzŒƒŒ™ÆÆÆ + +endstream endobj 852 0 obj<>stream +ÆÆÆ‘™¤tku‡pzŠ¡§¯ + +endstream endobj 853 0 obj<>stream +‘™¤pzŠlxŠ‘™¤—¡ˆ—¿Á欳¦¬³wƒ”œ¢¬ + +endstream endobj 854 0 obj<>stream +˜Ÿ©nzŒku‡tœ¢¬ÆÆÆ + +endstream endobj 855 0 obj<>stream +˜Ÿ©wƒ”¡§¯ + +endstream endobj 856 0 obj<>stream +—¡tnx‰fr…wƒ”—¡ + +endstream endobj 857 0 obj<>stream +ÄÆÆ…Ž›oyŠlxŠ|†•|†•·»¾ + +endstream endobj 858 0 obj<>stream +œ¢¬wƒ”¦¬³‘™¤lxŠ‘™¤ + +endstream endobj 859 0 obj<>stream +•œ¦pzŠku‡t¡§¯ + +endstream endobj 860 0 obj<>stream +‚Š˜pzŠpzŠÆÆÆÆÆÆ‚Š˜…Ž›«°¶pzŠlxŠ·»¾ + +endstream endobj 861 0 obj<>stream +ÆÆÆŠ”¡nzŒnzŒ•œ¦lxŠnx‰—¡ + +endstream endobj 862 0 obj<>stream +…Ž›ˆ—ÆÆÆ + +endstream endobj 863 0 obj<>stream +«°¶|†•œ¢¬ + +endstream endobj 864 0 obj<>stream +ÆÆƈ—lxŠoyŠˆ—¼¾ÁÆÆÆ‚Š˜oyŠoyŠpzŠ|†•¦¬³œ¢¬œ¢¬ˆ—·»¾œ¢¬|†••œ¦ + +endstream endobj 865 0 obj<>stream +•œ¦wƒ”¡§¯·»¾ˆ—œ¢¬©®µwƒ”kvˆnx‰|†•ˆ—ˆ—ˆ—¼¾Á + +endstream endobj 866 0 obj<>stream +¿ÁÈ—ˆœ + +endstream endobj 867 0 obj<>stream +‹™‹™oyŠpzŠ + +endstream endobj 868 0 obj<>stream +‚Š˜mxˆpzŠoyŠpzŠht‡¦¬³ + +endstream endobj 869 0 obj<>stream +tmxˆoyŠoyŠoyŠht‡©®µ + +endstream endobj 870 0 obj<>stream +…Ž›mxˆmxˆfr…kvˆoyŠpzŠht‡œ¢¬ + +endstream endobj 871 0 obj<>stream +‹™kvˆfr…kvˆoyŠpzŠnx‰‹™ + +endstream endobj 872 0 obj<>stream +…Ž›mxˆoyŠnx‰pzŠht‡˜Ÿ©˜Ÿ© + +endstream endobj 873 0 obj<>stream +…Ž›kvˆpzŠoyŠku‡fr…oyŠ°´º•œ¦•œ¦nx‰fr…ÆÆÆ + +endstream endobj 874 0 obj<>stream +…Ž›mxˆpzŠoyŠpzŠht‡‘™¤ + +endstream endobj 875 0 obj<>stream +ÆÆÆlxŠkvˆ•œ¦ + +endstream endobj 876 0 obj<>stream +ku‡oyŠnx‰nx‰oyŠku‡ + +endstream endobj 877 0 obj<>stream +lxŠoyŠoyŠoyŠpzŠpzŠfr…·»¾ + +endstream endobj 878 0 obj<>stream +|†•mxˆht‡kvˆpzŠƒŒ™ + +endstream endobj 879 0 obj<>stream +wƒ”nx‰pzŠoyŠht‡ht‡mxˆ—¡ÆÆÆÆÆÆfr…nx‰ÂÃÆ + +endstream endobj 880 0 obj<>stream +Š’ŸpzŠlxŠ¼¾Á¼¾Ámxˆ[mƒfr…tht‡³¶»ˆœmxˆpzŠoyŠpzŠht‡—¡ + +endstream endobj 881 0 obj<>stream +Š’Ÿmxˆt + +endstream endobj 882 0 obj<>stream +ÆÆÆht‡pzŠoyŠoyŠnx‰ht‡·»¾ku‡nx‰nx‰kvˆmxˆˆœkvˆkvˆmxˆœ¢¬…Ž›pzŠht‡ÆÆÆ + +endstream endobj 883 0 obj<>stream +ÆÆÆkvˆkvˆœ¢¬œ¢¬mxˆt—¡mxˆnx‰nx‰mxˆfr…fr…mxˆ‘™¤ + +endstream endobj 884 0 obj<>stream +•œ¦oyŠku‡ + +endstream endobj 885 0 obj<>stream +ª®³ku‡ku‡©®µÂÃÆkvˆpzŠwƒ” + +endstream endobj 886 0 obj<>stream +ÆÆÆfr…kvˆ˜Ÿ©ÂÃÆwƒ”pzŠnzŒ + +endstream endobj 887 0 obj<>stream +…Ž›oyŠoyŠht‡˜Ÿ©³¶»nzŒpzŠˆ— + +endstream endobj 888 0 obj<>stream +ƒŒ™oyŠmxˆŠ’Ÿ°´º‚Š˜pzŠfr…ÆÆƳ¶»ht‡mxˆ•œ¦¿ÁÃwƒ”pzŠht‡ht‡ + +endstream endobj 889 0 obj<>stream +­²¸nx‰ht‡ÆÆÆÆÆÆht‡mxˆ•œ¦ÂÃƈ—pzŠht‡ + +endstream endobj 890 0 obj<>stream +ÆÆÆht‡mxˆŠ’Ÿ³¶»pzŠpzŠoyŠ­²¸¿ÁÿÁÃkvˆkvˆ°´º + +endstream endobj 891 0 obj<>stream +¿ÁÃku‡ht‡·»¾ + +endstream endobj 892 0 obj<>stream +·»¾‘™¤nx‰fr…¦¬³¼¾Á—¡oyŠht‡³¶»·»¾pzŠpzŠpzŠwƒ” + +endstream endobj 893 0 obj<>stream +ˆ—nx‰mxˆoyŠˆœª®³ + +endstream endobj 894 0 obj<>stream +Š”¡nx‰t + +endstream endobj 895 0 obj<>stream +nzŒpzŠ|†•¿ÁÿÁÃmxˆnx‰ht‡‹™•œ¦­²¸ht‡ku‡•œ¦ÂÃÆ|†•pzŠfr…ÆÆÆÂÃÆkvˆnx‰ƒŒ™³¶»toyŠmxˆ—¡ÆÆÆÆÆÆht‡oyŠÂÃÆ + +endstream endobj 896 0 obj<>stream +Š”¡pzŠku‡¼¾Á³¶»ht‡kvˆ—¡°´ºˆ—oyŠˆ—°´º¿ÁÃkvˆkvˆnx‰˜Ÿ©·»¾mxˆfr…¿Áà + +endstream endobj 897 0 obj<>stream +¼¾Ákvˆfr…¼¾Áœ¢¬nx‰t¿Á欳kvˆoyŠ¡§¯·»¾tpzŠ|†• + +endstream endobj 898 0 obj<>stream +wƒ”pzŠwƒ” + +endstream endobj 899 0 obj<>stream +ÆÆÆht‡ku‡ + +endstream endobj 900 0 obj<>stream +‘™¤nx‰lxŠ + +endstream endobj 901 0 obj<>stream +tpzŠt + +endstream endobj 902 0 obj<>stream +¡§¯oyŠnzŒ + +endstream endobj 903 0 obj<>stream +…Ž›oyŠoyŠ—¡ + +endstream endobj 904 0 obj<>stream +˜Ÿ©nx‰…Ž› + +endstream endobj 905 0 obj<>stream +ƒŒ™oyŠpzŠ + +endstream endobj 906 0 obj<>stream +¿ÁÃmxˆht‡ + +endstream endobj 907 0 obj<>stream +Š’ŸpzŠkvˆ + +endstream endobj 908 0 obj<>stream +¿ÁÃht‡kvˆkvˆ­²¸³¶»nx‰ht‡ + +endstream endobj 909 0 obj<>stream +—¡nx‰lxŠ + +endstream endobj 910 0 obj<>stream +kvˆkvˆ³¶»œ¢¬oyŠht‡ + +endstream endobj 911 0 obj<>stream +¿ÁÃht‡pzŠ­²¸ + +endstream endobj 912 0 obj<>stream +nzŒoyŠ—¡ + +endstream endobj 913 0 obj<>stream +•œ¦oyŠnzŒ + +endstream endobj 914 0 obj<>stream +«°¶nx‰fr…ÆÆÆ + +endstream endobj 915 0 obj<>stream +nzŒpzŠ‹™ + +endstream endobj 916 0 obj<>stream +œ¢¬œ¢¬oyŠfr… + +endstream endobj 917 0 obj<>stream +‚Š˜oyŠku‡ÆÆÆ + +endstream endobj 918 0 obj<>stream +…Ž›oyŠwƒ”¡§¯œ¢¬|†•kvˆht‡¦¬³¼¾Á¼¾Ámxˆnx‰˜Ÿ© + +endstream endobj 919 0 obj<>stream +ƒŒ™pzŠlxŠ + +endstream endobj 920 0 obj<>stream +ÄÆÆku‡ht‡³¶»œ¢¬oyŠku‡ + +endstream endobj 921 0 obj<>stream +¿ÁÃkvˆmxˆ—¡ÆÆÆÆÆÆht‡oyŠÂÃÆ + +endstream endobj 922 0 obj<>stream +Š”¡nx‰wƒ” + +endstream endobj 923 0 obj<>stream +lxŠoyŠŠ’Ÿ + +endstream endobj 924 0 obj<>stream +ˆ—oyŠ|†• + +endstream endobj 925 0 obj<>stream +Š”¡nx‰‘™¤ + +endstream endobj 926 0 obj<>stream +ÆÆÆkvˆkvˆnx‰•œ¦ + +endstream endobj 927 0 obj<>stream +kvˆku‡¦¬³ + +endstream endobj 928 0 obj<>stream +—¡oyŠht‡ + +endstream endobj 929 0 obj<>stream +œ¢¬nx‰pzŠ + +endstream endobj 930 0 obj<>stream +ÆÆÆkvˆoyŠÄÆÆ + +endstream endobj 931 0 obj<>stream +œ¢¬oyŠkvˆ + +endstream endobj 932 0 obj<>stream +ÆÆÆku‡nx‰•œ¦ + +endstream endobj 933 0 obj<>stream +‹™‹™oyŠnzŒ + +endstream endobj 934 0 obj<>stream +ˆ—oyŠ|†•ÆÆÆÂÃÆ¿ÁÃpzŠku‡ÂÃÆoyŠpzŠƒŒ™ + +endstream endobj 935 0 obj<>stream +ÂÃÆÂÃÆ + +endstream endobj 936 0 obj<>stream +…Ž›mxˆmxˆœ¢¬ + +endstream endobj 937 0 obj<>stream +˜Ÿ©mxˆ…Ž› + +endstream endobj 938 0 obj<>stream +ƒŒ™nx‰ˆ— + +endstream endobj 939 0 obj<>stream +ÂÃÆkvˆht‡ + +endstream endobj 940 0 obj<>stream +ˆ—nx‰|†• + +endstream endobj 941 0 obj<>stream +nzŒpzŠpzŠœ¢¬·»¾nx‰ht‡ + +endstream endobj 942 0 obj<>stream +wƒ”pzŠ|†• + +endstream endobj 943 0 obj<>stream +nzŒoyŠ…Ž›Š’ŸpzŠ|†• + +endstream endobj 944 0 obj<>stream +kvˆpzŠ­²¸ + +endstream endobj 945 0 obj<>stream +|†•pzŠ|†• + +endstream endobj 946 0 obj<>stream +|†•pzŠ‚Š˜ + +endstream endobj 947 0 obj<>stream +¦¬³nx‰fr…ÆÆÆÆÆÆht‡nx‰œ¢¬ + +endstream endobj 948 0 obj<>stream +¼¾Á¼¾Ámxˆkvˆ + +endstream endobj 949 0 obj<>stream +ˆ—nx‰…Ž› + +endstream endobj 950 0 obj<>stream +…Ž›nx‰kvˆmxˆmxˆpzŠmxˆ|†• + +endstream endobj 951 0 obj<>stream +·»¾·»¾nx‰fr…ÄÆÆ + +endstream endobj 952 0 obj<>stream +ÆÆÆoyŠnx‰—¡ÆÆÆÆÆÆht‡oyŠÂÃÆ + +endstream endobj 953 0 obj<>stream +tmxˆ¦¬³…Ž›nx‰t + +endstream endobj 954 0 obj<>stream +Š”¡nx‰wƒ”ÆÆÆmxˆkvˆ¦¬³ + +endstream endobj 955 0 obj<>stream +ÆÆÆÂÃÆÄÆÆ + +endstream endobj 956 0 obj<>stream +Š’Ÿnx‰—¡ + +endstream endobj 957 0 obj<>stream +ÆÆÆkvˆkvˆnx‰‘™¤ + +endstream endobj 958 0 obj<>stream +wƒ”oyŠ‹™ + +endstream endobj 959 0 obj<>stream +wƒ”oyŠt + +endstream endobj 960 0 obj<>stream +¼¾Ámxˆfr…ÆÆÆÆÆÆ¿ÁÃnx‰fr…³¶» + +endstream endobj 961 0 obj<>stream +ÆÆÆkvˆoyŠÂÃÆ + +endstream endobj 962 0 obj<>stream +ƒŒ™nx‰|†• + +endstream endobj 963 0 obj<>stream +tpzŠoyŠoyŠnx‰oyŠoyŠkvˆÂÃÆpzŠoyŠ•œ¦ + +endstream endobj 964 0 obj<>stream +ÂÃÆkvˆfr… + +endstream endobj 965 0 obj<>stream +pzŠoyŠˆ— + +endstream endobj 966 0 obj<>stream +ˆ—oyŠoyŠ¡§¯·»¾mxˆht‡ + +endstream endobj 967 0 obj<>stream +wƒ”pzŠˆ—ˆ—pzŠˆ— + +endstream endobj 968 0 obj<>stream +nzŒpzŠ­²¸ + +endstream endobj 969 0 obj<>stream +¦¬³nx‰ht‡ + +endstream endobj 970 0 obj<>stream +pzŠoyŠœ¢¬ + +endstream endobj 971 0 obj<>stream +¦¬³nx‰fr…ÆÆÆÆÆÆkvˆmxˆ¿Áà + +endstream endobj 972 0 obj<>stream +ÂÃÆÂÃÆku‡mxˆ¿ÁÈ—nx‰‘–Ÿ + +endstream endobj 973 0 obj<>stream +…Ž›nx‰nx‰ttt¡§¯ + +endstream endobj 974 0 obj<>stream +tmxˆ˜Ÿ©ƒŒ™nx‰wƒ” + +endstream endobj 975 0 obj<>stream +toyŠwƒ” + +endstream endobj 976 0 obj<>stream +wƒ”mxˆ—¡ÆÆÆÆÆÆht‡oyŠÂÃÆ + +endstream endobj 977 0 obj<>stream +Š”¡nx‰wƒ”¿ÁÃmxˆku‡«°¶ + +endstream endobj 978 0 obj<>stream +‘™¤pzŠlxŠ + +endstream endobj 979 0 obj<>stream +pzŠku‡œ¢¬ + +endstream endobj 980 0 obj<>stream +ÆÆÆmxˆnx‰˜Ÿ©˜Ÿ©«°¶oyŠkvˆÆÆÆ + +endstream endobj 981 0 obj<>stream +ˆœnx‰—¡ + +endstream endobj 982 0 obj<>stream +nzŒpzŠku‡ˆ—ˆ—ˆ—‚Š˜ˆœÆÆÆtnx‰œ¢¬ + +endstream endobj 983 0 obj<>stream +|†•oyŠˆ—ˆ—pzŠˆ— + +endstream endobj 984 0 obj<>stream +¦¬³nx‰fr…ÆÆÆÆÆÆoyŠnx‰ÂÃÆ + +endstream endobj 985 0 obj<>stream +ÆÆÆkvˆfr…«°¶ku‡ht‡¿Áà + +endstream endobj 986 0 obj<>stream +ÂÃÆÂÃÆku‡kvˆ·»¾‚Š˜nx‰—¡ + +endstream endobj 987 0 obj<>stream +tmxˆ—¡ÆÆÆÆÆÆht‡oyŠÂÃÆ + +endstream endobj 988 0 obj<>stream +Š”¡nx‰wƒ”¿ÁÃmxˆht‡«°¶ + +endstream endobj 989 0 obj<>stream +­²¸oyŠfr…·»¾mxˆfr…ÆÆÆ + +endstream endobj 990 0 obj<>stream +ˆ—pzŠ|†•|†•‘™¤mxˆ…Ž› + +endstream endobj 991 0 obj<>stream +¼¾Á·»¾ + +endstream endobj 992 0 obj<>stream +|†•oyŠˆ— + +endstream endobj 993 0 obj<>stream +pzŠoyŠ—¡ + +endstream endobj 994 0 obj<>stream +ÆÆÆ­²¸³¶» + +endstream endobj 995 0 obj<>stream +…Ž›mxˆmxˆ˜Ÿ© + +endstream endobj 996 0 obj<>stream +|†•oyŠoyŠœ¢¬·»¾mxˆht‡ + +endstream endobj 997 0 obj<>stream +‚Š˜nx‰‚Š˜ + +endstream endobj 998 0 obj<>stream +wƒ”oyŠ|†• + +endstream endobj 999 0 obj<>stream +nzŒpzŠ‚Š˜ˆœpzŠwƒ” + +endstream endobj 1000 0 obj<>stream +lxŠpzŠ­²¸ + +endstream endobj 1001 0 obj<>stream +pzŠmxˆ‹™mxˆnzŒ + +endstream endobj 1002 0 obj<>stream +¦¬³nx‰fr…ÆÆÆÆÆÆku‡mxˆ©®µ + +endstream endobj 1003 0 obj<>stream +¿ÁÿÁÃkvˆnx‰ÆÆƈ—nx‰—¡ + +endstream endobj 1004 0 obj<>stream +toyŠt + +endstream endobj 1005 0 obj<>stream +tnx‰œ¢¬ƒŒ™nx‰t + +endstream endobj 1006 0 obj<>stream +tnx‰—¡ÆÆÆÆÆÆht‡oyŠÂÃÆ + +endstream endobj 1007 0 obj<>stream +—¡nx‰wƒ”ÂÃÆmxˆku‡«°¶ + +endstream endobj 1008 0 obj<>stream +¼¾Á«°¶·»¾ + +endstream endobj 1009 0 obj<>stream +Š’Ÿnx‰‘–Ÿ + +endstream endobj 1010 0 obj<>stream +˜Ÿ©nx‰pzŠ + +endstream endobj 1011 0 obj<>stream +ÆÆÆku‡ku‡…Ž›nx‰pzŠ + +endstream endobj 1012 0 obj<>stream +ÆÆÆht‡fr…ÆÆÆpzŠpzŠ|†• + +endstream endobj 1013 0 obj<>stream +¦¬³oyŠlxŠlxŠtmxˆ¦¬³ + +endstream endobj 1014 0 obj<>stream +ƒŒ™oyŠt + +endstream endobj 1015 0 obj<>stream +¡§¯oyŠnx‰ + +endstream endobj 1016 0 obj<>stream +…Ž›oyŠpzŠ + +endstream endobj 1017 0 obj<>stream +ÆÆÆfr…oyŠoyŠ¦¬³³¶»nx‰ht‡ + +endstream endobj 1018 0 obj<>stream +…Ž›pzŠnzŒ + +endstream endobj 1019 0 obj<>stream +ku‡nx‰©®µ˜Ÿ©pzŠfr…ÆÆÆ + +endstream endobj 1020 0 obj<>stream +ÆÆÆht‡oyŠ­²¸ + +endstream endobj 1021 0 obj<>stream +•œ¦mxˆfr…mxˆˆ— + +endstream endobj 1022 0 obj<>stream +¦¬³nx‰fr…ÆÆÆÆÆÆnzŒoyŠˆœ + +endstream endobj 1023 0 obj<>stream +¡§¯¡§¯nx‰fr… + +endstream endobj 1024 0 obj<>stream +ˆ—nx‰—¡ + +endstream endobj 1025 0 obj<>stream +|†•pzŠnzŒ + +endstream endobj 1026 0 obj<>stream +ku‡kvˆ³¶»œ¢¬oyŠlxŠ + +endstream endobj 1027 0 obj<>stream +ÆÆÆku‡nx‰—¡ÆÆÆÆÆÆht‡nx‰·»¾ + +endstream endobj 1028 0 obj<>stream +‹™oyŠwƒ” + +endstream endobj 1029 0 obj<>stream +ku‡mxˆ˜Ÿ© + +endstream endobj 1030 0 obj<>stream +ÆÆÆmxˆfr…fr…ku‡fr…ÂÃÆ + +endstream endobj 1031 0 obj<>stream +ƒŒ™kvˆfr…oyŠˆ— + +endstream endobj 1032 0 obj<>stream +œ¢¬mxˆkvˆ + +endstream endobj 1033 0 obj<>stream +°´ºmxˆfr…¿Áà + +endstream endobj 1034 0 obj<>stream +…Ž›pzŠku‡ÆÆÆ + +endstream endobj 1035 0 obj<>stream +nzŒpzŠwƒ” + +endstream endobj 1036 0 obj<>stream +•œ¦oyŠht‡ht‡ÆÆÆ­²¸nx‰ht‡ÆÆÆ·»¾kvˆht‡¼¾Á + +endstream endobj 1037 0 obj<>stream +ÂÃÆkvˆht‡ÆÆÆ¡§¯oyŠfr…¼¾Á + +endstream endobj 1038 0 obj<>stream +¡§¯kvˆku‡ÆÆÆÄÆÆkvˆku‡Š’ŸÄÆÆwƒ”mxˆnx‰­²¸ + +endstream endobj 1039 0 obj<>stream +ÆÆÆht‡pzŠku‡©®µ + +endstream endobj 1040 0 obj<>stream +¦¬³nx‰fr…ÆÆÆ + +endstream endobj 1041 0 obj<>stream +ƒŒ™pzŠkvˆÆÆÆ + +endstream endobj 1042 0 obj<>stream +wƒ”wƒ”pzŠpzŠ + +endstream endobj 1043 0 obj<>stream +˜Ÿ©oyŠfr…ÄÆÆ·»¾nx‰fr…¡§¯ + +endstream endobj 1044 0 obj<>stream +¡§¯oyŠfr…·»¾ + +endstream endobj 1045 0 obj<>stream +œ¢¬mxˆmxˆ—¡ + +endstream endobj 1046 0 obj<>stream +ht‡oyŠ‹™ + +endstream endobj 1047 0 obj<>stream +¼¾Áht‡pzŠwƒ” + +endstream endobj 1048 0 obj<>stream +tpzŠpzŠ + +endstream endobj 1049 0 obj<>stream +ku‡nx‰‘™¤ + +endstream endobj 1050 0 obj<>stream +Š’Ÿnx‰tÆÆÆÆÆÆlxŠlxŠnx‰‘™¤ + +endstream endobj 1051 0 obj<>stream +¡§¯nx‰nx‰kvˆ©®µ + +endstream endobj 1052 0 obj<>stream +toyŠoyŠpzŠnzŒ + +endstream endobj 1053 0 obj<>stream +ÆÆÆkvˆmxˆœ¢¬ÆÆÆ + +endstream endobj 1054 0 obj<>stream +ˆ—ˆ—pzŠpzŠ + +endstream endobj 1055 0 obj<>stream +pzŠpzŠfr…nzŒht‡mxˆ•œ¦ + +endstream endobj 1056 0 obj<>stream +ÆÆÆku‡oyŠku‡pzŠmxˆkvˆ©®µ + +endstream endobj 1057 0 obj<>stream +…Ž›oyŠoyŠ˜Ÿ© + +endstream endobj 1058 0 obj<>stream +˜Ÿ©oyŠˆœ + +endstream endobj 1059 0 obj<>stream +‹™pzŠ|†• + +endstream endobj 1060 0 obj<>stream +¿ÁÃmxˆku‡ÂÃÆÆÆÆnzŒpzŠku‡pzŠku‡oyŠƒŒ™ƒŒ™ + +endstream endobj 1061 0 obj<>stream +|†•oyŠoyŠku‡kvˆku‡oyŠ­²¸ + +endstream endobj 1062 0 obj<>stream +tpzŠfr…nzŒht‡pzŠˆ— + +endstream endobj 1063 0 obj<>stream +nzŒpzŠfr…ÆÆÆ + +endstream endobj 1064 0 obj<>stream +¦¬³pzŠfr…ÆÆÆ + +endstream endobj 1065 0 obj<>stream +³¶»ku‡pzŠlxŠnzŒnx‰nx‰ku‡¦¬³ + +endstream endobj 1066 0 obj<>stream +|†•pzŠ—¡ + +endstream endobj 1067 0 obj<>stream +…Ž›pzŠ—¡ + +endstream endobj 1068 0 obj<>stream +³¶»³¶»pzŠfr…ÂÃÆ + +endstream endobj 1069 0 obj<>stream +pzŠpzŠku‡pzŠku‡oyŠˆ— + +endstream endobj 1070 0 obj<>stream +ÆÆÆnzŒpzŠht‡pzŠkvˆoyŠoyŠŠ’Ÿ + +endstream endobj 1071 0 obj<>stream +ˆ—pzŠht‡lxŠht‡nx‰pzŠt + +endstream endobj 1072 0 obj<>stream +«°¶mxˆmxˆkvˆlxŠpzŠht‡ÂÃÆ + +endstream endobj 1073 0 obj<>stream +—¡pzŠku‡fr…Š”¡lxŠlxŠpzŠ‘™¤ + +endstream endobj 1074 0 obj<>stream +¼¾Ákvˆtfr…ÆÆÆ + +endstream endobj 1075 0 obj<>stream +˜Ÿ©pzŠpzŠ + +endstream endobj 1076 0 obj<>stream +ÆÆÆkvˆpzŠfr…ku‡ÆÆÆ + +endstream endobj 1077 0 obj<>stream +—¡oyŠoyŠmxˆ—¡ + +endstream endobj 1078 0 obj<>stream +˜Ÿ©˜Ÿ©ku‡ƒŒ™ + +endstream endobj 1079 0 obj<>stream +¿ÁÃtku‡oyŠku‡œ¢¬ + +endstream endobj 1080 0 obj<>stream +¼¾Átmxˆmxˆt­²¸ + +endstream endobj 1081 0 obj<>stream +—¡ht‡ht‡¦¬³ + +endstream endobj 1082 0 obj<>stream +¦¬³ht‡—¡ + +endstream endobj 1083 0 obj<>stream +˜Ÿ©ku‡‘™¤ + +endstream endobj 1084 0 obj<>stream +ÄÆÆnzŒnzŒÄÆÆ + +endstream endobj 1085 0 obj<>stream +ÆÆƈ—kvˆoyŠkvˆ—¡ + +endstream endobj 1086 0 obj<>stream +°´ºpzŠnzŒÆÆÆ + +endstream endobj 1087 0 obj<>stream +ÄÆÆ‚Š˜kvˆoyŠku‡Š’Ÿ + +endstream endobj 1088 0 obj<>stream +¡§¯wƒ”wƒ”¡§¯lxŠoyŠ«°¶ + +endstream endobj 1089 0 obj<>stream +ˆ—oyŠt + +endstream endobj 1090 0 obj<>stream +³¶»lxŠtÆÆÆ + +endstream endobj 1091 0 obj<>stream +­²¸nzŒmxˆmxˆnx‰nx‰«°¶ + +endstream endobj 1092 0 obj<>stream +—¡ht‡œ¢¬ + +endstream endobj 1093 0 obj<>stream +ÄÆƈ—ku‡oyŠku‡—¡ÆÆÆ + +endstream endobj 1094 0 obj<>stream +¼¾Á¼¾ÁnzŒnzŒÆÆÆ + +endstream endobj 1095 0 obj<>stream +¿ÁÃwƒ”mxˆkvˆ|†•‚Š˜ht‡¡§¯ + +endstream endobj 1096 0 obj<>stream +ÂÃÆtoyŠkvˆt¡§¯lxŠŠ”¡ + +endstream endobj 1097 0 obj<>stream +©®µlxŠoyŠkvˆtÂÃÆ + +endstream endobj 1098 0 obj<>stream +ÂÃÆ|†•ht‡ht‡œ¢¬ƒŒ™ƒŒ™kvˆ¦¬³ + +endstream endobj 1099 0 obj<>stream +|†•ku‡ˆœ + +endstream endobj 1100 0 obj<>stream +¡§¯kvˆˆœ + +endstream endobj 1101 0 obj<>stream +ª®³pzŠfr…tÆÆÆ + +endstream endobj 1102 0 obj<>stream +ÂÃÆku‡ku‡ht‡·»¾ + +endstream endobj 1103 0 obj<>stream +ÆÆÆku‡ku‡°´º + +endstream endobj 1104 0 obj<>stream +³¶»…Ž›‘™¤ÆÆÆ + +endstream endobj 1105 0 obj<>stream +pzŠoyŠ‘™¤ + +endstream endobj 1106 0 obj<>stream +ÆÆÆ + +endstream endobj 1107 0 obj<>stream +«°¶mxˆmxˆht‡ÆÆÆ + +endstream endobj 1108 0 obj<>stream +·»¾ku‡ku‡wƒ”—¡tnx‰lxŠ + +endstream endobj 1109 0 obj<>stream +ÆÆÆÆÆÆ…Ž›|†•ku‡kvˆÂÃÆ + +endstream endobj 1110 0 obj<>stream +œ¢¬ƒŒ™ht‡oyŠoyŠ|†• + +endstream endobj 1111 0 obj<>stream +Š”¡mxˆnx‰mxˆnx‰kvˆ©®µ + +endstream endobj 1112 0 obj<>stream +ÂÃÆÂÃÆht‡nx‰mxˆŠ”¡ + +endstream endobj 1113 0 obj<>stream +ˆ—mxˆoyŠpzŠpzŠÂÃÆ + +endstream endobj 1114 0 obj<>stream +·»¾˜Ÿ©œ¢¬œ¢¬ÆÆÆ + +endstream endobj 1115 0 obj<>stream +ÆÆÆÆÆƦ¬³œ¢¬¼¾Á + +endstream endobj 1116 0 obj<>stream +·»¾œ¢¬°´º + +endstream endobj 1117 0 obj<>stream +ÄÆÆy‡—9Rn#@`#@`-Hfas‡«°¶ + +endstream endobj 1118 0 obj<>stream +¿ÁÃy‡—y‡—ÆÆÆ + +endstream endobj 1119 0 obj<>stream +·»¾G]w">_4Nkas‡as‡9Rn">_-Hf‘™¤ + +endstream endobj 1120 0 obj<>stream +¼¾Á…’Ÿ…’Ÿ€œ€œŠ”¡ÂÃÆ + +endstream endobj 1121 0 obj<>stream +·»¾MczG]w?Wr?Wr?Wr?WrShwƒ”·»¾ + +endstream endobj 1122 0 obj<>stream +·»¾MczG]w­²¸ + +endstream endobj 1123 0 obj<>stream +¿ÁÃSh">_?WrÆÆÆ + +endstream endobj 1124 0 obj<>stream +y‡—">_G]w¿Áà + +endstream endobj 1125 0 obj<>stream +·»¾">_">_ + +endstream endobj 1126 0 obj<>stream +‘™¤‘™¤‹™?WrMczÂÃÆ + +endstream endobj 1127 0 obj<>stream +­²¸">_">_[mƒas‡[mƒMcz">_">_MczÆÆÆ + +endstream endobj 1128 0 obj<>stream +°´º">_">_¦¬³ + +endstream endobj 1129 0 obj<>stream +ÆÆÆSh9Rn¡§¯ + +endstream endobj 1130 0 obj<>stream +Sh">_fx‹ + +endstream endobj 1131 0 obj<>stream +‘™¤'Cc9RnÆÆÆ¿ÁÕœ¦ttas‡Sh[mƒwƒ”³¶» + +endstream endobj 1132 0 obj<>stream +¦¬³m}ShShfx‹œ¢¬ + +endstream endobj 1133 0 obj<>stream +·»¾wƒ”[mƒShas‡€œÂÃÆÂÃÆ + +endstream endobj 1134 0 obj<>stream +—¡fx‹#@`'Ccm}wƒ”—¡ + +endstream endobj 1135 0 obj<>stream +·»¾€œas‡Shfx‹©®µ + +endstream endobj 1136 0 obj<>stream +°´ºm}[mƒShfx‹•œ¦·»¾·»¾³¶»©®µ¦¬³ÆÆÆ + +endstream endobj 1137 0 obj<>stream +°´º#@`4NkÆÆÆ + +endstream endobj 1138 0 obj<>stream +wƒ”">_-HfÂÃÆ + +endstream endobj 1139 0 obj<>stream +ÆÆƼ¾Á°´º·»¾ÆÆÆ + +endstream endobj 1140 0 obj<>stream +ÄÆÆ·»¾­²¸·»¾ÄÆÆ + +endstream endobj 1141 0 obj<>stream +ÆÆÆ·»¾­²¸¼¾Á«°¶#@`">_¦¬³ÆÆÆÄÆÆÆÆÆ + +endstream endobj 1142 0 obj<>stream +ÆÆÆÄÆÆÆÆÆ + +endstream endobj 1143 0 obj<>stream +ÆÆƼ¾Á­²¸°´º¿Áà + +endstream endobj 1144 0 obj<>stream +ÆÆÆ¿ÁÃ9Rn">_Š”¡ÄÆÆÄÆÆ + +endstream endobj 1145 0 obj<>stream +ÆÆƼ¾Á­²¸­²¸°´º¿Áà + +endstream endobj 1146 0 obj<>stream +as‡">_9Rn¦¬³ÆÆÆÆÆÆ + +endstream endobj 1147 0 obj<>stream +ÂÃƼ¾ÁÆÆÆMcz">_">_">_">_#@`">_">_-Hf¦¬³ + +endstream endobj 1148 0 obj<>stream +y‡—#@`">_9Rn9Rn">_#@`€œ + +endstream endobj 1149 0 obj<>stream +H‰Z»i‡‰_¶’]¼®Gš}x‘²C dÑöÝû‚3ê\d9rì²cϾƒ —¯2€\ ,DˆJêû Ài9%T + +endstream endobj 1150 0 obj<>stream +œ¢¬?Wr#@`#@`#@`4Nk#@`#@`°´º‘™¤4Nkas‡ + +endstream endobj 1151 0 obj<>stream +˜Ÿ©">_'Cc¼¾Áwƒ”?Wr'Cc'Cc'CcG]w·»¾ÆÆÆÆÆÆm}-Hf#@`'Cc#@`-Hfk{ŽÄÆÆ + +endstream endobj 1152 0 obj<>stream +as‡4NkŠ”¡ + +endstream endobj 1153 0 obj<>stream +œ¢¬G]w#@`'Cc'Cc'CcSh¼¾Á•œ¦-Hf#@`">_-Hf-Hf9RnÆÆÆ°´ºG]w'Cc-Hf-Hf'Cc'Cc[mƒ¼¾Á + +endstream endobj 1154 0 obj<>stream +˜Ÿ©">_">_">_4NkG]wShfx‹‘™¤ + +endstream endobj 1155 0 obj<>stream +G]w">_G]wG]w°´ºÂÃƳ¶»?Wr">_fx‹³¶»#@`#@`•œ¦ + +endstream endobj 1156 0 obj<>stream +œ¢¬'Cc-HfÆÆÆas‡">_[mƒ¿ÁÃÆÆÆ·»¾9Rn">_">_‹™ÂÃÆ¡§¯#@`'Cc°´º·»¾¿ÁÃÂÃÆ'Cc#@`¦¬³ÆÆÆ4Nk">_¡§¯Sh">_wƒ”ÆÆÆ + +endstream endobj 1157 0 obj<>stream +¦¬³">_9Rn9Rn + +endstream endobj 1158 0 obj<>stream +Mcz">_€œ¼¾Á-Hf">_m}¡§¯œ¢¬Sh">_Mcz°´º…’Ÿ4Nk">_fx‹…’ŸŠ”¡ÆÆÆSh">_m}°´º°´º©®µSh">_k{Ž + +endstream endobj 1159 0 obj<>stream +wƒ”">_-HfÂÃÆG]w">_as‡¡§¯9Rn">_˜Ÿ©•œ¦•œ¦">_'CcŠ”¡¡§¯Š”¡-Hf">_Š”¡ÂÃÆ-Hf">_Sh‘™¤Š”¡?Wr">_#@`°´º…’Ÿ">_Mcz + +endstream endobj 1160 0 obj<>stream +ÆÆƘŸ©[mƒ'Cc#@`">_">_">_">_k{ŽÆÆÆG]w">_wƒ”wƒ” + +endstream endobj 1161 0 obj<>stream +k{Ž">_9Rn…’Ÿ">_9Rn°´º³¶»³¶»°´º9Rn">_¡§¯4Nk">_˜Ÿ© + +endstream endobj 1162 0 obj<>stream +œ¢¬wƒ”wƒ”¡§¯ + +endstream endobj 1163 0 obj<>stream +³¶»#@`'CcÆÆÆ + +endstream endobj 1164 0 obj<>stream +ÂÃÆ'Cc#@`³¶» + +endstream endobj 1165 0 obj<>stream +G]w-Hf¡§¯©®µ•œ¦·»¾ÆÆÆÂÃÆŠ”¡#@`?Wr?Wr + +endstream endobj 1166 0 obj<>stream +°´º#@`'Cc[mƒ[mƒ[mƒMcz">_">_as‡ÆÆÆG]w">_‘™¤ + +endstream endobj 1167 0 obj<>stream +Sh'CcŠ”¡MczMcz">_‹™ + +endstream endobj 1168 0 obj<>stream +€œ">_G]w‘™¤">_'Cc¼¾Á + +endstream endobj 1169 0 obj<>stream +¦¬³#@`">_°´º…’Ÿ">_Sh + +endstream endobj 1170 0 obj<>stream +Sh">_Š”¡˜Ÿ©">_4NkÆÆÆ + +endstream endobj 1171 0 obj<>stream +­²¸G]wG]wÄÆÆ + +endstream endobj 1172 0 obj<>stream +?Wr">_œ¢¬ + +endstream endobj 1173 0 obj<>stream +ÄÆÆ'Cc">_wƒ”¼¾Á¼¾ÁÆÆÆ°´ºwƒ”‘™¤ + +endstream endobj 1174 0 obj<>stream +¿Á𴺦¬³‘™¤?Wr">_-HfÂÃÆMcz">_y‡—y‡— + +endstream endobj 1175 0 obj<>stream +wƒ”">_4Nkwƒ”">_">_">_">_">_">_">_">_Š”¡-Hf">_œ¢¬ + +endstream endobj 1176 0 obj<>stream +°´º#@`'CcÂÃÆ + +endstream endobj 1177 0 obj<>stream +ÂÃÆ'Cc#@`°´º + +endstream endobj 1178 0 obj<>stream +ÄÆÆÂÃÆÆÆÆ«°¶[mƒ9Rn'Cc">_">_">_?Wr?Wr + +endstream endobj 1179 0 obj<>stream +°´º'Cc#@`?Wr?Wr?Wr?WrSh…’Ÿ + +endstream endobj 1180 0 obj<>stream +G]w">_Š”¡ + +endstream endobj 1181 0 obj<>stream +·»¾°´º·»¾'Cc'Cc">_œ¢¬ + +endstream endobj 1182 0 obj<>stream +¡§¯">_-Hfy‡—">_9RnÆÆÆ + +endstream endobj 1183 0 obj<>stream +°´º#@`">_°´º…’Ÿ">_Sh + +endstream endobj 1184 0 obj<>stream +Sh">_Š”¡Š”¡">_G]wÆÆÆ + +endstream endobj 1185 0 obj<>stream +ÆÆÆ?Wr">_•œ¦ + +endstream endobj 1186 0 obj<>stream +ÆÆÆas‡">_">_">_">_-HfG]w‹™¼¾Á + +endstream endobj 1187 0 obj<>stream +y‡—wƒ”¦¬³ + +endstream endobj 1188 0 obj<>stream +‘™¤">_-Hf¿ÁÃMcz">_wƒ”wƒ” + +endstream endobj 1189 0 obj<>stream +wƒ”">_4Nky‡—">_-Hfwƒ”wƒ”wƒ”wƒ”€œ…Ž›«°¶-Hf">_œ¢¬ + +endstream endobj 1190 0 obj<>stream +ÆÆÆÂÃÆÂÃÆÆÆÆ + +endstream endobj 1191 0 obj<>stream +­²¸#@`'CcÄÆÆ + +endstream endobj 1192 0 obj<>stream +°´º#@`4NkÂÃÆÆÆÆÆÆÆÆÆÆ + +endstream endobj 1193 0 obj<>stream +Mcz">_?Wrm}wƒ”—¡'Cc?Wr?Wr + +endstream endobj 1194 0 obj<>stream +ÆÆÆG]w">_Š”¡ + +endstream endobj 1195 0 obj<>stream +ÄÆÆ'Cc'Cc">_œ¢¬ + +endstream endobj 1196 0 obj<>stream +œ¢¬">_-Hfy‡—">_9RnÆÆÆ + +endstream endobj 1197 0 obj<>stream +ÄÆÆy‡—[mƒMczMcz?Wr#@`">_as‡ + +endstream endobj 1198 0 obj<>stream +'Cc">_[mƒ + +endstream endobj 1199 0 obj<>stream +˜Ÿ©">_-HfÂÃÆMcz">_k{Žk{Ž + +endstream endobj 1200 0 obj<>stream +as‡">_G]w‘™¤">_-HfÆÆÆ + +endstream endobj 1201 0 obj<>stream +˜Ÿ©Š”¡·»¾?Wr">_—¡ + +endstream endobj 1202 0 obj<>stream +fx‹-Hf-Hfy‡— + +endstream endobj 1203 0 obj<>stream +ÆÆÆ9Rn">_­²¸ + +endstream endobj 1204 0 obj<>stream +'Cc9Rn9Rn + +endstream endobj 1205 0 obj<>stream +ÆÆÆG]wG]w">_‹™ + +endstream endobj 1206 0 obj<>stream +€œ">_G]wœ¢¬">_'Cc¿Áà + +endstream endobj 1207 0 obj<>stream +¡§¯">_">_°´º…’Ÿ">_Mcz + +endstream endobj 1208 0 obj<>stream +Mcz">_Š”¡˜Ÿ©">_4NkÆÆÆ + +endstream endobj 1209 0 obj<>stream +©®µ4Nk9Rn¼¾ÁÆÆÆ9Rn">_‘™¤ + +endstream endobj 1210 0 obj<>stream +°´ºSh[mƒÂÃÆ + +endstream endobj 1211 0 obj<>stream +œ¢¬">_G]w + +endstream endobj 1212 0 obj<>stream +wƒ”">_#@`‹™¦¬³­²¸•œ¦?Wr">_ShÆÆÆG]w">_4Nk4NkŠ”¡¡§¯—¡-Hf">_wƒ”¿ÁÃ4Nk">_Sh¦¬³«°¶…Ž›">_">_¼¾Á‹™">_?Wrœ¢¬«°¶‘™¤'Cc">_">_Š”¡ + +endstream endobj 1213 0 obj<>stream +ÄÆÆ-Hf">_m}¡§¯©®µÂÃÆ'Cc">_­²¸ + +endstream endobj 1214 0 obj<>stream +ÆÆÆG]w">_[mƒ©®µ­²¸ˆ—">_9Rn9Rn + +endstream endobj 1215 0 obj<>stream +­²¸">_-HfÄÆÆ + +endstream endobj 1216 0 obj<>stream +ÆÆÆ?Wr">_Š”¡ + +endstream endobj 1217 0 obj<>stream +…’Ÿ…’Ÿ">_-Hf…Ž›«°¶…’Ÿ-Hf">_€œÆÆÆ?Wr">_MczŠ”¡‹™?Wr">_">_­²¸•œ¦">_#@`y‡—¦¬³y‡—y‡—#@`">_€œ¼¾Á'Cc">_as‡¦¬³œ¢¬G]w">_?WrÆÆÆ + +endstream endobj 1218 0 obj<>stream +Sh">_G]w‘™¤˜Ÿ©·»¾-Hf">_k{Ž¿ÁÿÁ÷»¾fx‹">_fx‹ + +endstream endobj 1219 0 obj<>stream +ÄÆÆk{Ž9Rn">_">_">_">_'Cc[mƒ·»¾ + +endstream endobj 1220 0 obj<>stream +G]w">_#@`#@`">_">_">_#@`Mcz·»¾ + +endstream endobj 1221 0 obj<>stream +¡§¯?Wr">_">_">_">_9RnŠ”¡ + +endstream endobj 1222 0 obj<>stream +¿ÁÃSh#@`">_">_">_'Cc[mƒ[mƒÂÃÆ + +endstream endobj 1223 0 obj<>stream +y‡—-Hf">_">_ShÂÃÆ-Hf'Cc°´º + +endstream endobj 1224 0 obj<>stream +œ¢¬?Wr">_">_">_">_9Rnfx‹fx‹ + +endstream endobj 1225 0 obj<>stream +³¶»?WrMczÆÆÆ + +endstream endobj 1226 0 obj<>stream +ÆÆÆ[mƒ?Wr•œ¦ + +endstream endobj 1227 0 obj<>stream +ÆÆÆÆÆƈ—?Wr-Hf-Hf-Hf?Wrwƒ”ÄÆÆ + +endstream endobj 1228 0 obj<>stream +³¶»fx‹4Nk'Cc#@`4NkG]was‡·»¾ÄÆÆwƒ”?Wr'Cc-Hf4Nk4NkG]wk{Ž©®µ + +endstream endobj 1229 0 obj<>stream +¡§¯[mƒ-Hf-Hf-Hf4Nk[mƒ°´º + +endstream endobj 1230 0 obj<>stream +­²¸[mƒ-Hf'Cc?WrÆÆÆ©®µ[mƒ4Nk4Nk4Nk4Nk9Rnk{Ž·»¾ + +endstream endobj 1231 0 obj<>stream +ÆÆƦ¬³Š”¡…’Ÿ˜Ÿ©¼¾Á + +endstream endobj 1232 0 obj<>stream +ÆÆÆG]w">_tt«°¶…’ŸŠ”¡­²¸ÆÆÆ + +endstream endobj 1233 0 obj<>stream +ÆÆÆ«°¶Š”¡…’Ÿ¡§¯ÄÆÆ + +endstream endobj 1234 0 obj<>stream +·»¾‘™¤…’Ÿ‘™¤·»¾ + +endstream endobj 1235 0 obj<>stream +·»¾‘™¤Š”¡­²¸ + +endstream endobj 1236 0 obj<>stream +°´º°´ºÆÆÆ + +endstream endobj 1237 0 obj<>stream +ÆÆƦ¬³…’ŸŠ”¡©®µÆÆÆ + +endstream endobj 1238 0 obj<>stream +ÆÆÆG]w">_wƒ”wƒ”ÆÆÆSh4Nk€œ€œ + +endstream endobj 1 0 obj<> endobj 2 0 obj<> endobj 3 0 obj<> endobj 4 0 obj<> endobj 5 0 obj<> endobj 6 0 obj<> endobj 7 0 obj<> endobj 8 0 obj<> endobj 9 0 obj<> endobj 10 0 obj<> endobj 11 0 obj<> endobj 12 0 obj<> endobj 13 0 obj<> endobj 14 0 obj<> endobj 15 0 obj<> endobj 16 0 obj<> endobj 17 0 obj<> endobj 18 0 obj<> endobj 19 0 obj<> endobj 20 0 obj<> endobj 21 0 obj<> endobj 22 0 obj<> endobj 23 0 obj<> endobj 24 0 obj<> endobj 25 0 obj<> endobj 26 0 obj<> endobj 27 0 obj<> endobj 28 0 obj<> endobj 29 0 obj<> endobj 30 0 obj<> endobj 31 0 obj<> endobj 32 0 obj<> endobj 33 0 obj<> endobj 34 0 obj<> endobj 35 0 obj<> endobj 36 0 obj<> endobj 37 0 obj<> endobj 38 0 obj<> endobj 39 0 obj<> endobj 40 0 obj<> endobj 41 0 obj<> endobj 42 0 obj<> endobj 43 0 obj<> endobj 44 0 obj<> endobj 45 0 obj<> endobj 46 0 obj<> endobj 47 0 obj<> endobj 48 0 obj<> endobj 49 0 obj<> endobj 50 0 obj<> endobj 51 0 obj<> endobj 52 0 obj<> endobj 53 0 obj<> endobj 54 0 obj<> endobj 55 0 obj<> endobj 56 0 obj<> endobj 57 0 obj<> endobj 58 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 59 0 obj<>stream +H‰Ä—ÍrÛ8…÷~ +.©…Ñø¸tœŸq׸Úe©fªÚ•cÓŠf(ÒEQI÷‹ÌóÎA‚”ÅÄSIU¬¨LàòÃÁ½çüöiI¢õîìÝêì·ÕŠF$Z=ž)”èÃßæO¤SˆÊhµ=ÃÑú #Œ±ˆV÷gçÍGxêûÙ]|±¼¼ºŠnª².ïË<ú_´8WHÄË‚9&Œ£åâx†2óÐ]Lý¼8§ Jb4ËŸ¦ØO%¸­ç.hg(–š£¢²Y ¤ ­H{p †×…ƒPp0Jºsˆ#s½xŽäÃ#™À©só=þ`H{0ŸÒM±)Öpfp6å‚袮ʶ-ê#Öú+£4‚³¥Ä,íÊʼ ¬úVüX6Ò HàÄ–ÛŠ(hî<ôËçÑ£õ: çI­Òî8:Ý^– pØ]Ìkôå€Ñ$™$f·ND$¢’mO<€x›åYºýÆ‘>9E-0›NÑ1L<:!AÂ=oûºí¼o öâ,³¢iØÐ~«ÈJ´íÏZöHKÈáí6-`…ÎÕ;þü¨F:˜+¾K5‡Àc†=aÀ3@Êf™•¿Ç­’?nòt›õBƒŽ/›½9®¯¨Že>nÚ‡8Eû°À8€ËÁwb÷âÜó§ÌI79[‰Âƒ2<Ïf” é¶'±„žrofkS%9E{îMHr<ˆ'³&¬{nþ¬î¦bëøbµÙfoCðg‡"Vc®ð½K«{ëW¿óšVpÕ‰b'rbK˜êr"K‡Rx Ä*Ä<4(E"`Öõ(¹Cy“¥ÿµíò÷€ûíÓâ\²‚ÔÈù°×ßlõ2ÏžMOÍÛ™6(Ñà\B0Ë3õ­ ýä¥McÔÞ\L’¡ƒæ‘zÐm‡“5žÚ5D€}•uÃœ‘ ºÇƒý¢êíý‚‚E­Òö¿ÓªfUm7ä±`6†ÈÕVÔÐó `. b“‘?<\§; ؤÎÀ¶¾-ÔçxZÆŒ&l*ã1÷ðªWažt“ÍÙ§»ÄÐÐâ˯Íæ +1ðR#Í--ÖÙõÂ$·t¨•š•t´âÓ0;Æu̲ékò%Èï³@nõ\÷ÉÇeuUd¦Oc¤OÐ=8gäÕƒsÈŽXeÌ̇8еY€Z=†Öœ(>¾nZ3•zVº-žvj-ƒóZçë¸G¸è1ù£å z >¢ßø¦ÜÍîúÈl´”ëMt7Yu³qKfmŒ‰êuCP¾ÌÙ1 À¬±‡Ñ¸}anËoÙ©ƒÞ8¿ÕàˆðdeÙ#Ù ˆùS†éó>qˆY&—]7–RŒˆ¹ÜW÷VÎÐYÜ:y]]§ç„ã©mÙÁöH„=¸ØB ½Æhç¸-ó4Û 1d„wuYVUÖ"wœ7eêV €[Öô¨·«Ú#:hœ¹ô±v½ƒÊdLÕUù.K·<>ÓœÌçšZÖ“ð:¾Â#ö°CGS/æ“?³ªì† $j¤;¿Û?>fÕûì©þjËYÄ‹¹D‘IÐñõÈVþ|!¤±WàeC¼­K¦t6÷Ëðzd¾•?]B<œÅï3‹Ôâ½ÍVÕf½Î*S/’ó78M*'ÑðõHxXþ€1>0ì |À°†ü‘36àmö œÍš<:³Æh2‘쀭G°ë9ù³eZ£gÚ¯÷y½yÊ7&J4ÉN +=&ت´É®1iÎ=@ÁôþÆlŸcj[„“€¾ëVhcžðˆy=4Îà^ÂÝù}ñ~“®‹rw`¾Z˜F³xV×ëh4<5&bP‡Ó#ÍõlxJÑŽÁÁ²í>~‡r—Y½*ë4¿©²]C—Æ{¨ +ÒG•EwO ÞòÞ~]Ú¯óˆ úÙoŽ¼ÁÂñÀ’c[·ßÓ][ðÈq=ª¼‚Û1h‘j«VÇ.«oY5¶_Èæ S„™Su_…AÉ´qæ¨z$ŠžPU’ ãi6õ_e^§kp”èù×ú°Ñ1›Û„YÊ`¡i¦Rv©äWaÌ»tk“ck¸J·Y«c¦Ññ¶ü–]ä9Ô*æíŽ1WÉ« {¤­.oÄ$a‡æ!°ßf/¸Š¹“X'_,Ød¸=]6$ÀWäÜ +øcYmÓz‘ÀÆÿÞ4߶MÔ_WéS%œì E™(öÚtæŸG0ë¡ùsVð¬>šyËr_ÝÏ+c)ÓpUš + p"GުȪõßP.Ö(™y×3„Éç({„¶Yf¡Í…³e´±e¦Ä¶›Ö·?6 y\p +ò$bîb‡ÀlŠ3vd=rÜR[®~¢ë,oÀ‚KøðW]¥÷Æüž"À'Œéém@Õ#¿9>H™€‰è#×>TTÐ Ú®l~¿uc®C%ê@z¤¶!•–ÔL©Iþ³ünSƒùævaZn¶+ó}½1wŸr:£³³£DøB‹Ðùí‘Öz>L EýÐ"ô)GÔ­¤…ÚÔ ‘ÖxÌæ¶È/8Î7ë¢qnç&Bòù4ìüÂ3³8¸Glsô€c$ÚÝ1£`æÓ?6ë¯sõ#÷;Êœ¨}Cçúžlƒr7äçO]'0{äÉ3äIƒœwÈwö»Û¦ wŠñôæBæÂmiÓ£¤9…¶G sÜüQ+=ðX7Ï]i^®¯Š§}}ñ-«Ì*êù+Ø{`Û<õý,N×Ùe¹/êèî©*m|R¨{¶‡g ¯”÷eD?/Î5{ËVßУÉÿÏÄ Ú51€8Èà'¢Ç K¤Ðw áŒr³ + +endstream endobj 60 0 obj<> endobj 61 0 obj<> endobj 62 0 obj<> endobj 63 0 obj<> endobj 64 0 obj<> endobj 65 0 obj<> endobj 66 0 obj<> endobj 67 0 obj<> endobj 68 0 obj<> endobj 69 0 obj<> endobj 70 0 obj<> endobj 71 0 obj<> endobj 72 0 obj<> endobj 73 0 obj<> endobj 74 0 obj<> endobj 75 0 obj<> endobj 76 0 obj<> endobj 77 0 obj<> endobj 78 0 obj<> endobj 79 0 obj<> endobj 80 0 obj<> endobj 81 0 obj<> endobj 82 0 obj<> endobj 83 0 obj<> endobj 84 0 obj<> endobj 85 0 obj<> endobj 86 0 obj<> endobj 87 0 obj<> endobj 88 0 obj<> endobj 89 0 obj<> endobj 90 0 obj<> endobj 91 0 obj<> endobj 92 0 obj<> endobj 93 0 obj<> endobj 94 0 obj<> endobj 95 0 obj<> endobj 96 0 obj<> endobj 97 0 obj<> endobj 98 0 obj<> endobj 99 0 obj<> endobj 100 0 obj<> endobj 101 0 obj<> endobj 102 0 obj<> endobj 103 0 obj<> endobj 104 0 obj<> endobj 105 0 obj<> endobj 106 0 obj<> endobj 107 0 obj<> endobj 108 0 obj<> endobj 109 0 obj<> endobj 110 0 obj<> endobj 111 0 obj<> endobj 112 0 obj<> endobj 113 0 obj<> endobj 114 0 obj<> endobj 115 0 obj<> endobj 116 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 117 0 obj<>stream +H‰Ä—ÍrÛ8…÷~ +.©…Ùø±tÛnÏ”§]¶Ê‹q¥¦‡q4E‹ŠÊL^¤Ÿw.€~,Š¥tW¥\‰Dƒß¹8çÜ_~¿ÇÙóòì×éÙ/Ó)Ép6ýr& Ufþ7?0U’QY‘M_ÎPö|† +„ϦOgçæGøÖÏó‹ûËëëì¶kûö©m²¿²É¹,x~‹CX¡ì~r® +’׋¾~ùTwÑŸ¡¹~ÜäãôogHÌ/·?ñ’X*•Q^P¦“>üf¤gN'ÓŸs\0Îq&dÁ$Ùôj8˜9c~1¯šöùz¾Xõ¿Í«OMéo•…Ä¥€_4~šÿFľÑãb|\“sN ™ÇýÏ` ЗöpÈž%ê»ç„ƒ¼äæ) ©°’â)U¨ ¨ÀR8ž¹â‡`Sc€xLÇb›|¯ç}Ý}« X Vîœ „-LE ç/T>ÒÆÃøœvIM¦íq‹·q{t ¸KYÈÒã.?,†+gÐÿ¹êýDB5gëŠÔÓ½>Ûn;öp[l:)„D2z€]F`w°K1˜emÈëÕçYû¡«ÿ³ªç0° öûF'Ïâœ1µ›¸X÷“ÝÓN8èwTð–Þ@¿,ù‘/#Ø;Ž ì+äàó.\Š„Ï7:Ü´Ÿkcú™²@œ®¡§[á4`§;õÐáîÝÑ»<çH2÷|ž—*~2?$ Gšo^κnµ¼¬³¦©ºïÔ¸¼~šÚœyägž¬Yü¤´`âŸZKF‘ýªÁ[S&s `KJ̶®ð¬P„BŽu‚> ¡X“ùQ¸­VÖž¬Pc$ø¦à)‘ å‰.‡»‚l_ãÄKân‰Â8ž P– ì§ãM h~»zYdÍ8e R†cƒ—âé«¢©É†«B$g[dóÁÅÔÑ‹’§knŠ*'Ø(Ý¿‰PÆQNP—a_–‘S#¨šoõíD/­ù‹Y?³ÛÕí×®F;ß[V¹:ö]qÁMÉtUE#Ø;Ž ì‘\+Mü«º¯MYÂye Àw—Us¾4¶&Á‰ŒÚÂv·&|Ü`àþ„ ïЋ˜±wãÑsÅ!ê7Çþjö<ë«æ¦úßí¯fä…üs>½˜3èo+N¼ÝåìJûvµx +ÕÔ¦6+£8.µÙ±Åc{góŽÓÈÝ ö¶8t‚8% •o³P4t›d²+œŽjx¤ØjFo%5¡ +>&ê8ܾ='5`ï8&°—d­-1ßžôö70£i7º1qÍògm/8;SYìzϵa_K0§#òwèæâ] +«ˆÀáL@€éo¤‚í«4”àCcûοêâSæ¾´*ÊBŸ4‡^Ÿã­ŠTòSU¤±! ‰¶ƒ-úv8ad„0#ä]˜ +«RàKÄ3èr=ÿÒšДât»Ÿ¯Ý†!§©ÎiêSšâmÁÍpãp¨-‘ôŽc{Z†U )oK|}SûD PûÞwiø™7â(#ÔpdÔ ·^ iY nªåò¦î¿¶Ÿ/Ûy-JæÝìÓª·Š˜ÑðŠ#ÓÄÄt>s[ƒ> ø›Ào”]%RéÎX9D=ÇÝgF*Bƒ‘g‚˜‡i+ÇmW/—+øî{÷1ÁOá8¶£B"±Ã8)†5^½a„"ø:X € ‹Ð«€ïë¦~ê÷rÕV.NbÖÄTÐ…S÷] #‹Ž`êøÄ3eŠl5›Çü®mšö[ÝÝ?UMý¡z²¥¦ŸhyÛnè”J±ý&°Wv,_Ø Q‚É»LuÄÞëh&P¢µ +£Öx¨ºYõ©ÑfA^6°Çz½Ý[aö¾Ò“XÉØZ6Ö!}ÞŽ=€î&P‡?Ãò"}y‘&òî./šª{qµQlΑUÊVx_àÉfÅÙ¨+Jžª®£¨àh[Øîâ¥`R8¬ Rp4ÝÍü?\^6­Ù¦x^kë ùEÓœ +ð[pRÎvšÒDÀšqR…¬DDí ‰Ä²Š#ö^Ï:^Z–Åæ†àÊ’‘§¯ºþª~®Æ-aPGlZ”Ív›ãz °G’SçĨ‡â¡&ømMßI Ç„isК´ '‰Ìmr[Q6ÚàAÑM‡×;ªÍ!¸I‰î@!‰ã› ‰àk%ª´§›¶}ÕÜvµ¢Ì—½J¬ºú²j® ÔKDIÝ¿Dd¯oàqøÈi>nŒâav’òCxžawÜYF9 *ÓtK íû| ÷uBÁè»vÞ®†Êîëù²í²‰ÈÿÑö³/³á“•åÜÏÚ¹þ¤‚¯óäd„§‚FœF ©%L8#aOáæï÷w¿_œ24w¾¼™PY)ÄáÆ=<Ëd •’p•À]õwܾ•˜5ÕK=ïm"‚ûÒö YÛë¡Åâõ}‹‹aÌ +ØÊdìóÄWæÐ- ¯ô¦‚sÓŒO¶ã9HF~hT° +¼ŸoAL»ÙBwµzr.ÊŸÔ‹“„D°©$€¤ +bsstí¹]®çMÝ_~­æÏuö8) ž/&ð›œ9’ÜŒG¤ô3Ì•÷íSÛ˜ƒ×Räãä\ÊS%¨94QD®&bë[£¯ ‘ëõ{q05x/æUÓ>_Ï«>{\Ønͨ\ßa»T˜œ³¼…Z]Èô°nO$g¯ +‚©þìQ•p,E?µ^‘ˆõ1d› 6œ­^ fIOÛ¾jn»z©i/'$‡‚¨u® ¡lëõ\ï ”âÑZ0°&®Ç‰Äƒ€”˜­ Qä×û¥£™ bϾèØ~r5{žiðm7ú”~T¹e8ÛÀy;b†Švü¾92FLªTÆpÄŽéiÅÆŠº<~ÚÝÃåíêÅíjiÇ5HîÞ“P5t4† +U®éŽ]ýŸ÷rYM †Âð¾O¢‹“Ëä²V¡…vÓ–Ù "ʵØ×ï™d’µÉ(YºPäûOþKcK˜áˆÕ‰i%0Vµ—Rõí¥ž½4P]¾W‰h­¦-©ÄÀªŸ]b£Sî`Ìã¯r!wÔ\“Ñèûˆ=ê)&€—ð]ujà|½ÜÇ÷Ôe!á™]6AÔgOG†³wèµæ@1RÇö•:k3ᆠñÆòÚ ؙ¾ÿf;írï`?„$=;²Ø +øl IeÚXN¦4MSoö,bºb¾ ªp‰MYI´—$øQe}ÞüQhX8÷Ö~Ê3®]«F‰Ê͈žD‰X·o‚"¬‚ÐFïÄrí„è{Ž­ûÝOÁKWq®sŽÑåAU²{ÞÀØ%§à.ôÆÊrñ¾_5ýÌ‚D.õ?‰|-‰ÌÈÎdù€``C7 LÀOh¡®ŽªÉÛ¦ÝÎ÷¿­u³p/:Ê úË೚Mˆe.†i•´ªÐ#ˆ˜¶žg‚%(Ì±çˆ ƒ22ÔŸ³Å±i ¢äj” ,Óýt½ +Ę +ã#BiÈðð‚›ãæ“y³^>šåjÓ®{'ºÕRoסœ A”½zYÄêõ öÅ×ÓŸÜ/Ít + +endstream endobj 118 0 obj<> endobj 119 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 120 0 obj<>stream +H‰¼WÛŽãÆ}Ÿ¯èGÒXqØÍ»¯cƒ6väaw8­Öˆ/2›yä+ì÷¦.Í‹FÒÌ" Œy’"««OWsêöÇR<Ø›ï×7·ëµR¬·7YPä"„?ºˆ‹ U"Ê•ŠusŠ‡›0Ã0k}³¢KøêxóÑ»ûðöÝ;ñKß îjñEø«,H¼¿È0eÈ0üU(ÏìÓÜ›^(|'ò0œÿyý— -ÎWIž2+ +%AãJ˜¬âš^ì¯ÅÌcο¦+Î= “ ¤ä9ÝxÌ<– gþþÇ;?JƒØwVW$KÈrÚ +¥Ëän™hDˆ¯x™4Kƒ„òÃERJÒ™þÐéCcÚAè®}„ÿU×Zá²w¸?ß» ›†Aq!yxã¾kÅ°3bc¬î«=†Ý–íÇì‡]9àJxq‘AüõBª)šäpbÛÕuw´¢lŸ„ù½löµ±ñADeÅÐS(8•([†zžØG¯lmS ƒÙˆ®½Ñ¦z„ëcU×¢ÜïMÙ‹ª~ꕼ2œŽ1Kâd™h4Ÿyӵݗڜ¢õ‘Š¿¡ +ñÞUq$¥XÉ@Y:'œÏ{O9ãm׸ƒw€¤¿J¡º¶¬…Þ•}©Ó[L˜¶po„ÝuG.Ú<€â%Ù<ãà-l×Vƒìo‡²7₺?Pq©·îà•M¥ËÁ¸f€‚ÊÓœãzÇ]5Úø¡Ë¾¯ÊC[,xƒ‹ÕçcÍÝaôf8ô-œê†–ª«Öl ­³Šã REDåÉŒ‘œ·ºmhè óvþJA¿ ,üÁá+*>.£ª}˜Ú:KCw¢ÏRT}½ó  Iÿ5®S\`¬¡ç¾J<*ž±næ’I’]©›j(ëJWÿ€]_)!)¬ëIb{´Ã·cߎë®"È´P +ñSQ”\\þOGûÝér*ˆŠD-–[´PΘÀaA\(C¸ˆ"™äÃ2$2Ï"B©·/éNñ….D×l¹¦£Ûž*n(ïÅ'¯$v+èù'߇t x_swv ÇÁécø„¯}X RŸ|AÇ•yÜÂgG²r›$dŠ+Èè^ÜŠzûÝ ðD3<ÙL”ôÞR;@©=µN×PðœzJ©[CÞ°ŸŸß‹¿V-=7XMÀûØ ô`BI†t?î4u…™22Tð¶ë’~±t¶×(H +•¿¶WW»†ó߀ÌjÎëæÝS_Ò/Gèƒû'L»g06Ïð¢Ë!úä~Žåu¶g—ü+G¬û×ö|±üÝž2Ö–Tf¸÷ Wú¡„=h=öP½g±D£HfŸ‚imÁ ÷RðZµ0ä²Äas·»‘ï’‰ïÏUÁ +ú4Ëòø\hæµ$­-l +K±‡pHHfÈšºk èÛxžd6çP j/æ`l,ÀGÑ”ObW®7Õv ÁÁg<–õÄÅ_8õ©ë’íµ¨º.ûjû„ÔM.bʱ…ÿbÔ>’Ö(”ÊÚ1‹Qy9d̸šV×~ +uÔaÁ)Á°]}èë'T@8º{*ò ]Z¼ÿüY+v7üÓdq(ŠpØ•žìKÎIDN|7䢂ÚJVà©6#H˜„B +DMCñêÝÞùyÙЈ+òEã¯Æµ €h€‹~”‰]c¢ÀJ)gl'ã¸/!Ì[. ++¶}×ÀñTxºC'¬i-$ú52 ‹ Ï'ï|r>w€$×a¼Y‚eØa9Zº/÷ô|_£WRˆ=<H A³¦C@~tçŸqSiPÈP]v,Ê94²IR]H™jm¨é›ª-þ<ˆðY9z#÷)ÿàøP¾)or?c:Ë2¾ÌG°ÍRKÍ’eþAò¸]Gi® V~æ殹^Î +™ª­P T¾açcÍž;Ÿžw N÷ ÂnQíÑUîG×çÉŸ÷•~ÃNãŸZ>©'—®;WüJå‡Ð­ó +¢1®@1΋î¸vØ€OcMýÈljAÐó€Á>Ô°Ë“£4\D*±×ÙÓ +fÙt|Ô÷ü†¿ºá$p Ö;WT¦5=8|hù†‚Ë«´“RJïh¿å…gL·hôõ7×”‹¥Ýðô0ìDÛÄJš +Z²\"Œ³â +—Èø\<jë9yfPcédäÌnSוjæ–ð–Ìà Qé‘÷1½s̵„` ˆ[œDöl«¤×øXܨ4$³)aXºrïã¿??[LY‘ºQÏ›ÄQžaYäùy u!Æÿ+õ?¤ð_WD”x%¯UÄ×)a”fAžŽ:H>ª9–ÏOlþЂb[3ªkNÿ…é/†!.Ž.zi§¨N©&5c!C û1…ÿ Ïkl†’Úp¦L;§é2óXGdŽ“€Â¹È…@#ÇYWtN¡‘˜‚(yîG)Œ˜?³I +# h²‰æ.ÂŽ’÷@OC Û¼GÓÉŽÁˆÃ^t[š;¢rNg¤°àF9)jÓõ†4ÙO ãý+ üÛ‘Nš¿Y +=|5 <Þ©sî†ú8Ðå^ÁYàŽcŸ³=ƒTÊiªã÷G(éîVÁ¸ç LÀ„¿ LÛ Âl¡I”·5šÖÎ\ÚnÙÂbßÙ +h…žµ<ã_aÂ!o¢¹÷{ØÌÀY3„†p!s‰5ÔyÍÀa?V4/©©àdøAÉŒ}âcë`f¤çí¦Æšç¸ð•77\ÅÕÓ2zÚY°ÍigטçlP¨œ‰„æ6K§970ŒÅƒtüBõGìùR÷¯ËßšÙ¼Sgí4ÏQ2¿À51Ž¢ˆO +vðNتÙ×, ôÓ‰.Œ¾”.YÇb@pµßBáÓtÚmªG˜]é  zC¹-šßíPÕb\beŸmÂo5Àw_ÁÅLe iš–¦ÇüŽæ›Ã€™Po®7ÔÔüÐ}<[÷,ã³=çN4s°›VðŒB¥/½Žn´Õ §ë8•ƒ'†:ºh¨_ó *Žƒ|´ Ì oìú +ê9àPöÎZZG…„J¨çc,I*åUÖXt®Ø†¯ò JA’bË6×êçÏàbž8;ªÂÖ±¸&ÎPÔW”½A%{Nœ‰“B‘·‡zplgFÖ/’\.ë÷œ=3nuGÑ*õß[~|db«vŽ+4»û "ÀzD©ÚV½¨ßX¡-v壡ýLb@Y½0?ŒÄ«—ó{§“ɇ¶aZ§WžáŽóÑ»~óÆ«¦·M ˆÞû+8’ªµª(§æPõ#’•[r±1¶Q1¶(ÍÿÈî›Å N#_,Ö»³ofÞ{#îëá»wTÃYLçñûÀÜ/—L> endobj 122 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 123 0 obj<>stream +H‰¼WÛŽÛÈ}Ÿ¯èGr³¢y¿,Œg³˜,Œ5v„äÁ±%qM‘I2òñC¾7§ªº%JòÌx7ñÒ¬®®:uêÔ«ïïµo~¿¼yµ\†*PËõMæ¹òñ/âÂKCe^˜ªåîÆW›ßó}?QËÕÍ‚/ñÕáæƒóæîíí­z?tS·êõ/å.2/qîÞ~ì©ø¾ºs…:ºŸôî^*¤5‘CæÜ¿.ÿtÀ`È›ËU’'^…Š/Ši'r;û´§“¸Ë_ÈóÀ¸¾H"/Ž‹@¥™g…ZþA\ "úÆy½šõ·Š>òÕ"ð‚(Èh ½X òâ^úQJKxSgöieGóa,¸]«i«ÕªÛíJ;m¥Æ=_­VZóE5*7u°¬åµMÝÊóÑÕ‰G1°Ñ꾫%8!ålüÁ9ÔM£ÜÂ)]x‘9ãZÝŽ¶wSÜï\zZ‡©ÃC¶wñÆSoÔ}9Öxš:+wNþŠ[ãzßÈö »?¿àãsþã#Â\Ap3/àÝé$¬#5ÎǶ㻃»Fóu…CÖ•rTë®iÌ’ñÙwùÕ<å6IA<Ë’ÿ™dsÚê!±A*!t^Æo/Ì…^T$&ÄÎ?\|ƒÙñkF“]áËŠÈKŠ0—3ÌÌ1^”eÆ$Éþ7 ÅŠÜøÙz–+)è$÷½œ«Éã^Ýœ•òøØ®¶C×vûQµÝT¯ëU9Õ£§uCdR@ºÑ£)ÜÜóóÄœLeG£\c“Ð=êvì%KSëvº8Õ% ŸÓÄKÈç#íXØEÖmuÐ( +B\ˆŠZD€ *íõ8îëvCHp𲊪'wFrˆ0HþÍ¿ïøf`éÔh¬4êƒk2ñòÄfçOn¥âVý@»â°Tg¹3¨€Ÿ8cßñoK౬'7G)ªÝ~œTٌږ²¦j´’Ë‘\Éc†ä¡­QãÏ%E}&Z’@>æò5ß R³\É£ñ" +ˆÖ¶–0m΂½-{¾ïuë©ïþVîúFŸ-ÅIàr/-`—‘ ÅE¸0ê9iË@l…pcÐj]7%±Zâ Åň¹ÿ”‘Ùѱ«Æ©œ€½­¡é ù;ãתª7õT6ªnû=å"qL`ãÀKý8¿"¿èd<3äÇVSc5%«e+šn#–@[Å?ÀǨÌW•êåÝ°•%ý¨vX†w=_v„™Ø¦²šGÛ¢ˆŸ¨êËaªKcã’ ÕžŸ žƒ–—\y6¹§–/¤DìN@xÖ<Ø»Yü_=ìꩨÔý£šèSå0Ô )²i?´#z$ò‹Þ¡ Õd3–s¦Çs¦• 4'’ºGÌ ÐënØ•Ó¼ÑZ›iŸÙœu5c^UåT*Óx©PÑz:¯i½xr7…Ó!¦ŽB{y†óò”¶6¨7—Eö+™=³£»j÷‹# ›cܶ5Ð" ®ß¶zÅ>²ƒ/Qp +ŽOƯ¹î BóÓ÷o ‰Y,º †×Ôãñƒ[ä^´sb\ ÃQÙó£¢H®úWu¯z~ÑÉ‹a‚~`‹zñ,‘×Ì{ê/$ÀJÚ^XÌ:dWVZQéGb?wl×zº+pò¹ÏX&:¡žÝ™çûó”Tö=Éé ‹œ-3“Ý?ö/xÿXöGÕq`™Áe/ÚÈ}É·«h,ŸÐ]LêQð=¿èŒMx[ŸÃtÖÑ—Øò"•vê®ÀøCÙÔ+[«©ã*éGnX|$t°(µý3J¢èJ7È60ãÔ5$šI(RgVOÔªì.ß#Œ÷ W¤–ZE°¦-§ôA#ÅØz]ŒÈ¶£4ç$žQ‡ðs;&¹ +¥»NÊ~¢@+yÙ5 NÊUêle–¤D.(1rÕ'¡(æld§‘+;µpw“˜g-×hÏæ‚ÕôÁÜ}c1‰ ,Ïâ+zù•úúÝwHå…¸Ëßåõß—½þdz~«ÄF‰f(·™|>“Ø/8fgØŸÒ5Aär ðg$#.¼¶KR68™ÏÕÙÉxz¤S—®=e +_^Ù‚Wñ»ÀKŠè·Æî]ÝþüÖÔ]ÝÔÓãs±{ÑÚÿáŒÿŒ“x¤ŠX¶UžÉÊAŽ7ò +9>åLÒõ;‘nF®D‚ #ÿŒØŠHl Ê +t–ÀŒj8B*A½ûÉÌ£ƒìÉÒÜL£§þˆ‡ˆ³Ìd8b˜ýøÝ;qü8˜N÷»œá®ÓpØÒιU¢[QX{#iù™¤À€W›7ª[Þ»4á4@¤…cne«dXí!ÊŽ¬•>jVTVˆ¥y˜Úètû¾áÏQÒüåš³QVcBìöÇ¡æWHj”{7-kG™E'¥\ñßΕ·«ñt¦rŸª£VK–GÝH­&+¯lõˆ°!@{Ä›KjÜÒbÒV”…I<;¦áOÝ®Õ#¬”ðy.@é$ÿd&ød0OŠo°4SY˜p“W-·¼ªã¿{‘M¥ÖõÈm4ùϯ46”¿)ø£Eö0Ÿi[0aN42žÄ­Ÿeù€“ÕóVf(ÇÄÆðçÛ¡3¿”ÂÀ©I•ÏÄ“M—_]I?ÿ:‹×ò.pŠÌa8cW’×t˜$;ËÙIÈFÈŠÇy †{³™$Œ$Gï÷ýcA`;µÇèrÿ8CˆþR€\¸A¬ÇÉÍؾE’_y™$ŠªÝÎ=ßk3‹ªÙ ˜NÇà·ÀHÂôH|gfynák3¼hžX<6¦Þò°%ô¸•e{ÙYÈ¡2…¹ÚÚº§–ôQöÆ|¶D6ü¦¬[ U|´ŸsAg‘…uöå©Ëœƒ§¨£¤xT¢ŽÃPO0»F•£µ³2ŒÎzâLå$bv?è™5#y0ÛíøÄàOnK”áÓlÇ¥a9ã„š'oÙçQ°Î¢9_«z­ÚÎìïe˜¬TÅËj×'Å­I`5}XИ.Œ^ù—pã¶ë…+9n©6]W©F?è†`EB¯\}<”Ceú)¯žCòa‡ÄNøoL +ƒtô¿µø¼¦<ˆ84q´°EÝPÛ¥›Æe”m:bV¤ôPòãhKáþ=ºÐèCWµ¶ ³Ï +pDføê‘éE@ S­gíÝÔÜId$*¹g—]]©®ªfj2Dšƒ(|AˆmˇúßWK‚0Ý{ +–šˆ ÁH8€‰ð@kB”O +Õp{ßÌŠ¢kÈ´yó>³hð"Öm@þÔ}~¯O¨Æ‰6’'\{4+íÁ†'î™mPBX¡UØ…§­mà«ä‰³É 0ùïXÃ89ÄxÁr%—TmkŒ§·5Ê?ºgntvwÄëáãTáCVôavè8± ú4T’”Ìbt$“ñG×ÔûÀèB—OÍÍ$IËܼ1ÐB·è†/ðå&eˆPÏ4òj€¤™n[ÐJM„J`˜œ€‘+ej ¶—]'ù„îW!¹0‚•¸JÎüµ‡Ë:ÎRŸù› +îM‰Å…øÜ¡Tb¢³Ü9‰Éø¬’Ï?ó8_7ouÝ + +endstream endobj 124 0 obj<>stream +H‰¼V{TW¿3y€l/Ç4À$<#Ô’&Qby&µ­JŒÍ‹™ HÝnI´¨+ + Zñ¸ +jQŸ¨§¸º–ŠVT\µê©nõ¨ÅG-º+«¶îXTôxºì:÷Ì™s¿ïw¿ùÝû½.@  p@ŽJ§™’ö¯N”Ü@Üš®‹”­õª)Àg+”eÍÒÄ [¢23Ø® :ðåàâ“o+0WÕ„n 8η˜Jó#Rb$~ç÷ + C^{ðŽ|0Ú‹-„÷jñ!]p>¶ÐÌÌ-¾3k:œßƒøY&«Ñ–ÚPuC’{̆¹6Áa\ÿ0‹ÁL̨{ýMø6+Í<}5@ßÉêma ÚÒ~€ð <«  é숛à×ô?â ¸S\Ç6¾<¹üápÄ­wŠ« ¨E©îÎwР<Àsø‚ |„‹8ãP„[¯Å3pÉ ’ÀõÁe ±¤ƒ\@+00ðÄjëçq÷Ìfî;Þ‹»Š½3¸Þéuw¢I¸iD½8•qßU‡^IÜ°Íù¨À þŒ'‚B:©÷äs²¸."W…!( ÁH}q1+ˆ<² ŠÔ‘ ¦±#¤Q¸”U¸‰Æ*0¥Õl&(#i0a:k>Sb ,Þk"éB‚¢1¥ö.Å£q9Þÿ¼ï;N¤xœ,*F#ÿMPpÔ½¸o„8Ž¥w,FÐvš$´Èr}Sh{®ßn}ÖÕBõÔ„C8ú\gYùÒóî?ûü¹ë£Æ°æÊbººóHîÆÄ;Á<û=0y§ž©­r9HÒŽ–Ü4ið©'GÝo}RõÕö=S2o[OäÅ ß~RÀ+ù¡òBæ¹Ç+ýL:WâhW^ê½tå› ó—S|ýöèääþ(ÕKná@^{ì´ÏY¼¾ü¯ysÛ6Éì”pË8Ô7O¹ší¿O;5Ó¯ìW]ø¼³ÒŠ#Ä*…"²w»ñã›ô*qUÀÈ—(º—Mì>­ê¶÷õvøÇûÆLÝ~d’ìú¨^kÍß;â?<ÿ°&>¯êŽþÄ7§OwÏþ‰»ú2ê¼´(lW[zÃñ¢&G{üánÏæ“ÅÛ¢1†} ºØ|¶‹5·ùBÁÍdêèÂæ¸aí: /lÅê¯*’ýd¼q¼[%Ñó3Ò¦NÅ+sÖ¢âÏ‚{&L)ZuWpy¸:óøYÃO××>ùùXßYM]É?ñÞ}ŸUkÏÕkä=7öˆ—ªŒº¾xéòœU3Ö£ÙÂwûBnßÖþꨬ]´ZÜŸ] qÇ\O6(„ëŽ ø®°âòx.W<bÜdœõ [Ny2RÞ§F\Á£q(âóV!ÃØ艑‘¿áßþR$âºâ|yŠ"wcž\.å·¾”4lžÿ¾gÍýåRnõ—ïî—ߟ7[ºÈïQó{¦wº\FÍ:×s¬fуLåðFɺ¢ŽŽ½iÈG…ÇŠÿHÿp!¥mÊc»Ó=$ܹµiEzN¾±<9ïrèjûìéË„×ZÑ%dÖ—þ¡ÑM~;dg>HT?˜P¡Ÿ¼¸«só ~]tÚ8ƒ;uðuÈs/{ï²¾Í{ÌBíJïワæo¦aÀüŒ†ãY¢Jñh鳆áDfþßIüÏzž8`'FE ü¡F…)MšÆ¢°p,•4RVRxÎ#Û`"ó iµ`Å2©>Œ]Ï¡Y:©²W‘`š.„倱Z¤^¸ÇÀQ¸h‰<³Õ’' Æû‹ˆ·ø¹y%äh¥úÍêÝ^£Çk^i—K`_ȶ˳šŸä¯Ÿ»µ)«zÅþCÚõë~iRM¾ú»‹Q« §š$¹'f”´ìö¸»o“'­ÊÚ{÷ðؾÝkF_뛾¼®@çÊxœÝ§ËBF\ÙÿÈ»õ-¦md¥ðÔæÃîÉá3›å“b“j¶U\ìž‘Ùh>ìšÉL°4Îô²<™8ló­„ù™3ÓšP„³ÁéƒÀ%»ÿd`ª¡ø‹IÆ«ÇËÙÂuTÀ_æ5ïók§”¿ÖÞï²$ôº9×ß@;_½zbYqaUàúâÞ8{1|~ñóá .e;„¸|œ­ð +Ç}#`—:¹c xd}XÙU‘”ºuNN‹ÃÉÙ£/$iÌHP ™O ‘ýË: Ù襈|‚",FB‚,yÉИ†0£Š42¦RmÏCŒ±J0¦ÀžŸÇ3»lÜfP#Ãv)ضÂLX,2 @š4 `ûf(6&C®‰e2ÔÚó `f¢àuM`Y«ÃÍÐ ÄaðáQd'h†NŠ³Ru¯“ÅÈ£ G °Ÿ+Š (HµÚ-Œ²Ê&‰ Ûkå°E ²t +ˆ³•RdA!ÃÞ2¤ryìKæ0La2aZAÂ@Ã~MäE`JµV¯Ð¤ ¦)´ZEš^£Öa*N™¢Ð¤ªU˜"MõoÞ«ª©+ ÿ÷¾— ‘Šè1 xîZ â6bhHb­K@*Œ¡¸°´ŒKR«ƒu·jAP+j¢ÀèN•Òb] §Ô…QQÇVʸÈ›?Ñ9§=Ó3ïææ¾»ü÷þË÷ÿ÷]¥"DyŒ—ȺZ¥PMŸÀéƒå\¨NΩƒðU¡³m§RȤz9‡]^«é•s8]hà ¹LÏéÕVQ˜\«°~—uY¯P«8V*Ó+dr¤Ã Bä*=²m=B¡Ó…âyœ4T¬Ö"/¢&upŠRÑγ<\£•ët\§T¨•L:ͺKç¨ù‘keÁØíR­å‚z••<ߥœFŠ<ÊB•R-§ ÕjÔ:¹§íÙ +¥’S©õ¢@¹MIJ¹@¦Véä³B‘y…Té‰$*…^ÖNÓÁ¬¥ÒrÓ¤!Òér§“ËEV9*¶=¦Éq•R‡š–1 $¢ÉŒ1ݱ· #„!šK4&ZagˆÖµ9‚4=#ÒŒ$2¤"½ Ü)ñf·lqâ јÌE¸(#NEÛ6‰XÆEDE™Mmc4%Ø|F”Òöq"ÕÊBê%ÚëŸá÷kܼc<ÞkôŠ‹Á@wk(aØ,*ÆN_¬½±Š^Nq²È3q9ð;DîŒ ]£º<÷_…ÆÅ¿Åí¹ßÁí9«Ûë ñ^œ‡„é';Šï/àëýr4à~s8à^¬J~5ìê-´ïb=²ûå>õƯ(ñÈ_y+pÞâ.W‹K{¾Š¿×¤¨ÒZñ‡Ž.Ùù\£›“Û¹eC’èùkqu1Áy£žý(þ®% ’kû×%ŠoT,L w¤úŽ„Ïüq11ÓZÊkçG‡È·AUî?ÅC\ïcÌÙøεYM M“Ä%Jµó óöeµ|]âx빓ÝñÛrY³¼€ Üü´ÞeÏ|Ik½ 'Q„¬ùÿpw¿& ¶öhS +`Wf•÷€ZêÉHºÞä,&W½^’n÷¼·{'!+éËöùaõì•Íýžh‡ð¦OysÖÛÔe¹½$Ú;rׄŒÌ`‚80à?*l—cˆãño +H†|ƒ(ìÅàx"$ïš1ØŠˆv@$tä{6D$›Ì†ä´$Øné›™±Ä)äÞ‰­?ì³}òþh`Ø–ÛÿnöU'‡=ûÛôêö'U?ÿãAKÀòÇåƒ[Ë’ò̹n~T·3·8çûœ:OÍÉe‡IéÐMvk–ž¿þЩyïf:×ë?ïì×*NÍúIzbÏê¿Ýî7lôtØå÷ƼX•W¸*_hÿýî,‚ñJÑ©,¡$‹NÀ!+ +2‹½3wHܽ]ÛŒ.~a[½ÉŒî¬2$/7š–t ¢×+¨@Oi›ÒI—`‹J I˜Ws:ôSk˜ÒÉ?oŸ¶Õ*u¿”Êdr :¦§Õ÷ÇëæøÔZö}ºÓÓ¡ì³Õ×%]¯w톢9ÞÎ]AÔëEÇŽ †^Ì$¶¢?‰¯Ÿ¿D2÷ ½¨Ù¶×±<¦ìŽÙrÒק°»=³ô:-¼Û³fÒÃÀ_žu‰:Ù\¬ØQ½ÇcwàĬ÷ËÇTæ,u®/RõS:/®¹å_óÕÒa¾þÜQØêk2Ý®]Pdÿ¸Á4zÁʦá‰WS̳OÌ.¹¿¾áúТ%´ÆMÏîú4ÆÿVµpDACªÛ? úbš8WSž8<×yû‘ø™eÏ3§ ¹zhýè ŠEé‘iõG7>n®ºQQ*ý îßË]P9ùFéß=Ý“¶¦ÞÓí[ði󊩹ÁÇŸoU{¦”†ÃhMMvjÚäsCæõY|wP™xþŸŒ9cÎÖ;>êúý½Ç”…NWeiñåvwÞ^gÈn¯Ùh—æuñ«t÷µ—ýàÚû߸bJì6Â^¢% ä |Á¸ÚZ 1´¯@@í„=)ôdYÑ¡ËbL4 àø£E üŒÆNRZÿÄ:ËÞÁº\°uc"Á €¯o¯7-«Úæ-­‚ÞbÁ,0‹ÞÃOw€·áÈn‡Q°ÎÃ-ö…¿ uxyxJ¶ õ*È#ö$žI*YI’ƒ‚1àzX„œ–ÀE¨%½q¦€ÔÐUÌBv‘%„He€:2Lå`$Œ…Å°öÃax@ö±+Ð@Ž*á(œ!}¨+³YÍžãS‘Ÿ~à :,óÁL8Üóu¢)´”žôi½eù‚ßR†YXÂ0F.‡÷à#Ò—d2ÕlJ½†À°È@ ópxH‚ P„:»·á1JH”d#¹Aç³^ìl6›­¬æż?êLFžS` l‡ƒPŒÚ¨D X€'žÄ Ë8€Òo#_’oé0êA'Ò$ÆIaŽ±fö|>j¢VôÈUäÂnØgà +<"$NäM2ŒD“’Bö’›¤•Ï\dš‘‹,¶­f[£ƒá0f¢^ß…t”l@>Â#¤q }PÒ¸ƒËZ²‰ä‘ë4yÈgY1Z½˜=ϯæ÷ò%h{_˜HÓá ñ1Z)¶ÁNÜ­ËiÔEÔÁMx€öåÈÔIYD2È·äu¥Åô4#f¼˜éL"³™yÀ4±Nlû\0C°¢õŒåª¥–ŸÊÿ™ßÌW‹šˆÚ¶Ê†øI@ĦÀJDó'PˆçTÁ7pOj€Fh%¹÷"ÞxZÑ-JKÞ#H.ÙL‘"òWRJ*É%r™4‘'”Ð~4„ªh$Í¢ëèZDOÐ+ôÚÈ£f>c¾aî1˜ç¬++csl‘#ZÁbÁý[ó,oYÔ–ÿ0^õÁQ]Uüœ{ß&! d!@>Ê[$›%¥›†[’]i#„»)N_B—LÚ¡C§(ØRè#kù®Œ€£¢uä.`ŠSAÀqì_TAhÕ +4”ÒKyþîÛ$$íàtÏ»oÏ=çÜ{Ï=÷ÜóÎÙxë=w©û&õÇÏ(]¢ðºY”À÷s¼_OZ‹›¸™~‰»÷]¥kôÝ€¶Yœ oÊÃyy—r9ßËãùè_ÃS¸žÍÚpKx)?Ík°“à?äú†ða~C€Q(ŠaÕR1Ÿµ +Q%&ˆ¸˜"¾.êÄÀ7Äb±L¬íâ%±CüTgÄYñWñoÑ)>7¥†Ì•ƒåÀÝr¬LÊ¥rµ\+7ÈïË2%_“'äIÀiyVþÇ£éÆ,c™ñ¸±É8f¼ã‹ú¦úl_›ï)ß ¾ùNø®gø3¦el€üÉ»á½~ðåïq¥\-|XýÚ&Q¦CçŒ œòÛ´+ãØY§LóŒy²£O§ãÞ'<ü/€Ó=ý8ý/"³‹vòwpߧ[.à¹âMÚIçù!D¾6܆q¸ûáU[m¶"ŠÜÆŽòxöD›µ´WlâN!ãž¿ÂΤ!â8`6DÌ>D¯ð²‰vÉ<ÿÀáNœó2£†]¡wÅFZƒ›yŸ¸HqqÏÿ俉N>i8L÷ rí¥ßk+ù¢ûóÝ¿#êUG|±Ž7Èë|J{R­²†6‹ïò×D„Þáoaï¯â?ÛÝo„Üsî2YçÖR§±‘Þ§·qBˆÈeˆ{ßDÄÕQsüóuÄ@Çtû_ |ˆŠž3ŽÑC< ÷îqZÉÇ!³™ƒt‹½ðI +Ðb> âZnÀÌkäT:!Ï £W… ·ä¢›×oÍ’É›[nå°½ŽÓ»´qùì?NütÆ­ùø2§NP4w2Õ‹§ÃD­X™ð5 A¿×3¶fl%ŠÖ&fÏšÙ8£¡¾nú´è¤‰ªÆWÞ_q߸{¿rÏØ»ËÇDÊÂ¥£¿<ª$4ÒúRÐq×ðaâ¢Â‚¡CçèÏÐ?7'»_Vf†Ï‚©,fÅmS•ØÊ(±¦L‰è¾Õ Bs/‚­Lâ}e”i{bf_É($ç}F2š–ŒöH²ß¬¢ªH™³LuºÆ2;¸©>üù+iª+>ÕïÓ`#ÌXá‚S±mÆT|ù'f×`¾TNvµUÝ–)£TvÐ`ªÀZ’₉ì!¢ V™”ÕZ©b«&¦Š¬­‚’¡Xs«ª«OÄjÁ`2R¦¸z®Õ¢Èš¬òžU{˨Œj•é-c.ÔÛ¡õfªì°ÓÞá§;œÛjµ6ÏI(ÙœÔk cÝUðä¥ÂÛ]L>¨:±¶77 XáBSwg­©vÖ'zsƒúLbŒ¡¸íıt»¶ba9Ñêë­¤7ÕfÅ4Å^dª~Ödk³ÈÆ;ŠV÷Gºç©8f: +¨&¬dsÍ°Ô`rVì/ŠšE}9‘²”`Úš©y]HnÿÞH[ÏÃã,‰ÙÝ'Úᾶ> âíIlbWBýœe_^öÌpÓ«ÆRh¬7—íËóÞY¡Î´hU•ª ßa•A28¤†xfè=ƒzÖªòn5þÜri£~v9ÄøÉ)‹×Õ§¢¼nFSâ uϺÆÄ>d·Õöädj$x‰ƒ&>¤UôPuÏÔ=ªeÐ>‘å±£D«<®á¼þÜ&–ÕMcšÛ!Ò4¿GÃ/¢“Ö•í§ÛP±žvÇÜ–‘BÍ›I}’@ã4Í÷j^/7ä÷ù2DåšÅâ°XB¿ãÅt +Y¯Åÿ ‡s‘å?J?Fæü3äÅO!Ï¿Êxµ ‚YÈ›ècTcP­@eúrº(ò¶5TN³Ý+ o§mçEúÕÙ!d2¥,h *¬côg®u£"Lë\—ŽÐd|{Ъi#2¤$mr¯Qœ~áîAƸŠ^ærœŸÓOh’{‘Rü[ÁÈ‹®¸oaõݘ_¯t yÎm8‚¹Ò°± 0[KÏE<óoá_ó@èº p\ +‘OOºÿB†uõU+µó`äpGè¿È‹l·N Ùè@dg[øYj@Û„4Mž¥Ç<¶cÜ(Xu<ïååâL§è%÷"öþGÊG¸ƒ$Nê¦hày°­‹cuü¦ ¶{pɃü _Ù¢ •C;j*—ÏŠ jœ/c‡)äV+G +ØæyXìêÙG e=N©U>OkéÝØÇnÏÞGdk®Ó £7x'¸Åk³Q/u·1°›nÛaµ±°”nzžp"º]‚uƒ^{’'ñÿrÖn£½î:ÃíÈw¯ìnúMßFõú°{•íq¯¡~®ßéÖ ¼R ×ÒéÞð;rå¦ÿ1^öÁQUg>çÜÝ +’[’ E°Ö³»XhaèY[e,¥´"å¦ãRƒX†Ž¦dQ‘€Y,Iak1;0hí×ÐQìØ§í¢¶¶´ÊLg:ÊtûÜ—,ÒŽ8g~ï={>Þç|¼çî=Ñšá ü¢ÕÕR:_:ÏHÒjˆùî‰{õhvñúRA}ÚœÔÓq¾Ý ·Œc¬ëeÊ+W^¥Hû.SŠØô±v¹6Ϻ´ž³ˆ©h=Ëk*ë‰çòZ–õ²: {zPöé¯DÜHÕd¾•—Õ_¿ÇËböº]-gÕ{õFÓ¯·•Þ#FÒÜ1ËA½PNê(wtJƒÒ99£_gibÿËì§Q“Õ_T‹z‰ÜQvp›©WïSÿ;¹Mä×vå8 gõŒ}9·‹÷Õgû ©&÷,ä&õ#ü0«(Ž7—Þæœdl›Øí?³Ó¨UŒb¤ºšÛÐTuWéߌ#5<ŠýxÛĽo2¿þ®ªH_a§nU®ôJ¾‹ÇÙŒ 9$ÕÝœ½/²›iWã¡·û§¸-.eVÝ¡^—þkJÿ¡Ï­jººIÞ/Ž6‡ä°’qÜDÉz,dŽÈ¡Ò/ê:}5éœÞDZ©Wz×ónÜ¡Oëf³ßŒ­èÓ¯êwUNuªwô8V¶¤_ÑcÔ{ú‚®W[ô<Îúy}FýA½¥zõ,õ¦nÆ.ÓÑܾƒWþÌ•z®Z¨ézÖù4©—º‹)'©ìù”Þ‹ßw†ýž|êiœïý•h»…·ón3Gwó-z ³SYDYon3‹ÍÃfÀœ1ƒf6 ÌDÓb¶˜Û½‡¸ÎWãÍ,³ÈÜGº‡ÔFýâ?J…r2›ÌÐå¿/«ù¼i2›Í‚ò›äãêNÑG©|zÊçäãêÿÏÐeŠÎJY2†²ÿhk>ibQÒäæˆÕ$¼(Ú@œŒg׎ðŸp„(µDéë”Í%âwˆ7uôÎCÞ£…ªò5#H'½GyÝÆú(@íè$q©Ä¡t}€J(æ=b~Ç1ñ½ÞÃ…?•>ä­WYdÔìô(ç­7ûâJù”¬£dJ¡,ªðÖ]ªYKÍZjÖR³öÃo­é—)ïF8]ì„Èz\êÛNÙ¯“ñvª×ÐYtà[¢SU£Ã5*çÝOî~Æ—å·Vc½!žCõªPØ(‡¼Ku)”Eíh„ê ¯CƳš6«eÎä:hÝAëZw¨ß¢Joõð|:†G]ͨßD†öí´mW«‘çµ{þð¬–P³„Õ[±RWøc¬±ž3ΫàÖêù…‹n¸˜i™zÃO<ßì+ÜîOHWyôkdxkŒhô­ö«“ÕÍÕ­g_ÔKÕ9½45ÊdýlsvJ¶5›ÉF…Y¯þùX6©;ÓWyõ,@= P/ PÃzÖ3z¶¦ÞôZý\ú&¯NíC'QÔ®Žú:zòË«3¿žF-½ké]‹¿ZÚײüµ,H­93Ü¢†5´¨a 5ø¨¡e -‰_t½†Fz5Q‚ßÖžžãÅ(‚1ÈÖ«¤W%WIÏJj*ÙúÑØÑ^œñTÒf9MWa{ÈŸ@Fêd‘G®’\Œ%¯"ÅI1R%¥%ÏÈñ0âCÓJsôó^×Ý“.™Ÿ«¢ÈS]æ×´ê"ße~~‰~EXu™Ãhj—ùzBÏIÝô ú>:(%ýh?@y)éC{Ð^´’PH!¤R)R)R)„B +…B +!…B!Êh?@y)éC{Ð^‘œ$ÉArBrœ$ÉArBr$É ÉAr$'$ÉArœ¬,$ ÉB²B²¬,$ ÉB²B²,$ É +ÉB²,$+$ ÉB²¬!H¤@H¤@H¤R)R)€@ +„˜þt »  ¼”õ¡=h/ŠXEÓrh;ê•’Ã(¢¡¡¡…R„R„R„RJ‘ùaa…Q„Q„Q„QF—¾…¸xUâëz½‚~&±ó4Ú-u;P/Ú‰vIÝèI´=%%=(‡¶¡Ç¥ä1Ô¾‡¶F1-Z-„B …B …B ¡…ÐB¡…ÐBh!´Ph!´Z-Z-„B …æ 9¡9hšƒæ„æ 9¡9hšƒæ„æ 9hššƒæ 9hNhšƒæ 9¡YhVhš…f¡Y¡YhVhš…fÓGáYáYxž…g…gáYxžž…gáYxVx¼@x¼^/^/^/€@ „@  Ð¡Ðh´@h´Z-ˆhDýqÞM¼¯ îÿ“Ì î3‡õ½Dߢðm¢ñA¢r=±·†Ì‹7“³‰¼V"p +‘ØLD6wIâo"qè Ìa€Qw3úÌb³™dvãûi|ïÂ÷N|÷â{¾ŸÂ÷v|?‰ï'ðý8¾·á;‡ï|oÅ7wVÆî›ÇRm ³›&ùQÍD³P+j@m¼Ôõ^>\µªâÓ'®ÆãsÌUñTºÆ¬2-*¯¦1²|G¶(ö±[Å®L5äïæ/ä›ò‰oça>qO>1?Ÿ˜—O´åÇõŸ¸&ôÌÔ'²‰G²‰/d ³‰Ïes²‰Oe7f“³‰t“þ@×Ñj§Øíb{Ä~UììȪ bÿ)¶]ìÅò‰bt]!¡ª†t!3ÍOWé>VI+_ïV™xôÜUH÷‡ôêBf>ŽBæ<¾VH:Ë Éi<Ú ÉVK ÉëxÜRÈÌãјjÎÌôÿ•Lûg“{üç’‹ýxH~ÆïÉœö»“Sü ™ýý¡ +}ÌÿVæ>M”MóïÍ´ú+’‡ü»2¿(9ÙŸK÷›3QÝþ,ºÎ¤®5j[ð§4ñxÁ¿£©ª©ª-wÜT¨˜ÊékSÓc¹Ûb¹±Üu±Üµ±\K,wM,7!–kŒÕÄÇÄ«ãWÆGÅGÆãñÊxEÜÄU¼f¨t65Uñ†®©¬Ž•‘­|µ‰,«ŒŽ. Ë~jVñs9¶Ó¬:jÚŽŒõ¸(|)£9y·ú/ûÕÛÔuÅï½ÏpìØÏï=?ÛØä9vgç%/ÿŒ‡Ôï%8ü©K’ +14¡­]ŠVL»­4“ʨ¶¶CÚh‹¶iZÑöÁ` œ°®©_úaM§u“6¦‰£%mUTm€wtebûPõ‹ïóùóÎùÝóî½¾ç¼û²»CÅåñH7<²½hŽ á"—EÙ-CJÁWÈýãÙâø#Û'Êdª8›É† ý›Û…L®Øb¨eŒ@ï®é:èý5}ôõ5ð¹b¯’-[+›‹}J¶¸blÇÄ)Œ_ÎÁ]‘¼Q¶L”q…šŠÜÚ‰9„±t襕•C/årH|Fói\ÚZ—¹ÛUãÊ'Íw—ž›´CÎ0°òþ‡Á3÷•¬Ò´µ +§ #èˆ:b€ŽPБÈ·ªøƒìøD±² +¦]S²e=ƒ"SÙ9È.¹‰9k+ÞLíÖVX‚,lGöTqOÖpþÙCqd?t7²}à`à{j86â…ÿ!OR\” +ŠÓQÄÀEüú]¸S"ÙS‘ÈíX ÌZ,ü-#I#Ûq$ÉvÜÀ0øŒTÅð}·1|ßg0Ñ{b”ûµ¯ Ýb´á)º•Ç&N­@C¹µU¥ÈîM[¨q4ýZ`½Ï|€ìJ®Ø*Ú#CHÓ| +;€;&-Ž¢lV +_Óì;˜7!ü†w€¹±æjl¤.H7êr‚ÙUsù®iÌã7j.ÌnxÈf>¿«pÿ¶ßhÿB È7<•¹ó«Z E)Àµ˜]QPVûÜíÏ Ý“‡³Ožä™¼)oÎ[òÖ<›wçßo>òð!á*ä0³C8Ê=M£S­@Øø`åÀCåÁ' ý^…$LëA‹õ$Ø̦“ j°˜O2 Yi³šNbä_ñµ9üò)#ìG›nŒ°Ë›Ø›H¸9@©«³ÙÝ쎃j‰n„˜…ºý …L ´\þ¼rÉô+ægh9;‡ì•«úY‡ Yûx×Êž• o_ÔïníiMÈ}Ìëìëú&Ì[íSæ'ìîfAMu +Zêi×LÏLâ„£ŒÏ:ÎD.DÞ·-Û6Ö.Ø‚¦”©ß2g;„PJ:S¯º^ Îß ^ þaÐ}ϯšœú#[þ\Zlh »š´×ÕUm‹ªÉ¶ÕªéõËêEõŠíŠãŠjiS¹€Ã£Ê¶¤­×qÑa‰;¢ê”cT³Z&?Ñ{mÁfs´©ñ.‡ÍiálµÍ…OÑîþ„ á +ÌW·5$PHîJ )…Âx1|=LÂôÝbïèO„¥‡wÍ‚î ð¨x9…S´OêÑ5!©S"øJ¼˜ RWÁ!IÉhGFËK™k’¡ØLƽÆ"60RÒ•ÜvAûS©4²ÌÃÙ¥ Óm áÅ0ùK‡Ëøc+ú±ß¿þu„ßE—¹ƒ<‡?Fi'/꜖MïL3ég˜uîug{J6æ ‘—'§/O.±K“ÓûàߟÜÊGK~7—ê „|Ú\š;Õ¡LNS‹¢L³W•}ì eߘØ%¸â漩µ´Ú*³¥n¿fƒÕ(5¹¨ükÉÁRyùt£+MS:‡!NŠÒa§ +¡ŸÎª_j÷jj2èÖÔ‡¬¦Òþl€Ê…’Ë_“BU:y*ÏÓ¸Õ{/•Jbõ¾ÑM%<·Š?Íz´OU½œóYö<Ì)]xrÁøqKK$lñ^ÑÛD<‚)n‘-ÀV'z{“ÉžnÑ+Š=ݽÉÕ ¹…^«É^ÃL™G°Z-ͼ§<ñàîïx¥Âé»o/÷J_4,wØ\®õj4fjùæÖn“;;_ûÓððã?lýÁ¤ ©+ì,¸Ur\’V8VdýÉolüþ‚Ôh—„øp6ïŽÅ»Ä ìݾþþ£/w¥Á`{“'Þý¦Â_}òü9æEcë8ÄÏò³"ÃònÁíaES;‘eh‚æÙ*ŽËOá§È^¼—<Ór@n$d4F\2ÖÛwÂÁJ +5‡#D[Zä7ÉïÈ»d…ÑVØ¥ŽFÜ`w4:]nŽã?ã²7ج³ *Ê|Õ¥»ÃÛt»ÞHBRS0°ÒçõŠ5ÏÙð¶ù÷2–i®5Š^A½1ÑsɃ=eƧ7 ˜€(a\®¼s¦‰Ó1¨àqð‚ÀóC.Lhç‡y DÆà +XpHètaLØ%ÌG« Äìr,†1iI’ÝÞ`! ‡µDd¯'&¢H«Ë+yɘ{ËR,÷]‡D]w‰{E³X&Çô0jç4n”ÛÉå¹ïq?æ¬Ü ¬ðó<éà5~”Ž7ñû%Ò9»†h>»aÛîB×aÑ"=N¶ÆçñOqª«‘UÓ“pºiìú9ÄW^)ù»5¾\¹ZŠpT.–ü.*Tò:«Rl¬JÁaÈÓðœÛûØyÒÇVcéM¢‘ÄYè.…¾âtG —8r§ í1íî1ò/Å¥jyI>ÀÂUåtÞÔa³ªLCªV}Ê´røÙªr[‚Ã8!ž²µ[&`l\B¤é’§i’¾ÆsF’MO¢}Õ9‹©Îy¶6ç?—š ¹ Û`àH§£/WQjå)\‡j"R£ 6Ç/h}‘Ëw¢]ÕyCh ‘“PBD¦ý} ô.F™Ç K$'ÅÊ•¿Óe6JµFBtáÊ•JUy•®<Ä™§už÷ÛÓ ñ;QU.9ÌD˜Z=’=Õa‰ð|χ廌+Ž¹Ko{»Úú¥ o wõ‡¶]8›::âY†·‡»6¤oýæ×øÀ­o“·H[äÁ¸¨ÈqñÖ9¼éV s>Ù0µo4ßDµöÕ:}ÄüíÓd½^§:Õ©NuªSêT§:Õ©Nuú„mb¨†WYÐѶmÿBž|¯fB1àÖ¨€¾^©Àý¿Ë«ž×&‚(üšiÚú'<¼”lÜ‚¶)%Á¦”d/ždØ$K·;av·¥§¼xôhZDðÐÿAïêÁkÁ»/RŒßL&/UìEq—yï{ß¼÷æíÛÙY«Ççãs×5syDã/W暣×?\ߧ14K/aMóÎ;ì—î9\n:\ž¥^é™ycåšÉYúèð Ý÷Þ;\¢9ï›Ã=*W.·®?wõ”ßR@4"I}B3`4´¸CŠRŒÜy15ai`#øØz0˜ñ>кåÅ53-^VÆÔÃLBÅ¥O® =Yo™¸—¨îЊeW‘@w3@ ¹ê"_†¡i2²5°bÃݹkë5ÕD°w¡5í€SèΟ? ƒ•¨!Æʹ]̰۬OhÓ¯©m*JÁLªÊÈF²/BÉ' %wTªrPÜTz¤´Èc•ò( }^¹ø…Ó¢IÆ=•†É¸"n¹ÑXªC¬ø¼š$ÜÃ<ã®Ì¤Þ“Q³½¹Ñj-4U¡c©·äþv¯¾¦’¨‹$;Áµç'§hÉ]¡wXõ¯|Örg¹Ô2â8åPê\­Š4GªÌÿïwu›6iƒZ¸~Þã´¹OÛX½kwe|fŸÿ^LÖlÕ¼M7x„èBðÄÿ_²9ͽW1s'ömìˆ'8êoÍ¿°ÿcsÚ—>÷?U?ðµZ«Úãÿ(øÀFŸ½;½9ö/.*oªf¢æ"èѶ@Ê + +endstream endobj 125 0 obj<> endobj 126 0 obj<> endobj 127 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 128 0 obj<>stream +H‰ŒVÛnÛF}×WÌã²6¼_Œ @ê›+,¥/F(j%±¥¸*IÙŠ<öšýÞÎ…”ËI +Ö.ÉË™3göñË™ëvòÃ|òx>÷Áƒùj’è,ÿxf:ö!H´Ã|;qa=qµëºÌ‹É”—xênr£žÍ.¯®à}c;[Ø +þgšèHÍÞ{nèz±ö\fÎ4Ó¾2»Îl¦Ÿ¾ ™s~žÿ8ñРÏÎe¥‘ö’,ƒ ÒAHž(ôì’O;ó_(òT"÷‚!tYIìqêÌç“mÀÇi•JÜ3S·¶+gŠ¾Tíx:Q+Ûló®´5\Úí6¯—­ÄwBé~¤½¯R‰“Áñq2ßXÙª²w¥ëLÕk'Ò±‚‚Ìó²^òO ²mœ)âûÖ,¡³PÖi»Î;98ø‘S­=}ùÂr˜~¢C7¡ðæÏû"Çzù}½ö”ݧCp"U¯Êõ¾¡”i£áC=Ôà ³D ±øh'3’©¶Ûmxi|ï©SEÚ¨CÓ!æBQw˜À’¿‚ncZ#°N‘_Qg0õÄy;Š' +ÁKTÎXˆU‘×°0P¶ížŸ Xè%ï ¯Е[£u êŒêbÕŠxþ‘aÄ'Äèë–lóM"D!Ä~,ö8›åF½Ï›|k°Iˆ­q!¶\xØ]œ{ H{±zkkfŒq\BMNœ…/°¦cSYòyÑnÔu_ªåßZêÔ:¨/ÚÅ1=Ï•ü2Î-ƒ8Ðiˆ éëà˜}KµOîÚ§ï^=)šjõ”Ñ0ƒ  @½žQýyaš2¯ÞîIhÿœ'ßÿ!¦ùØÇñ9nÇÏÈÄÇ{6þ`¬<„¯È©:¾*Âb‘ÇVÄ:ˆ£àë³U®Èž²& ‘—>æb`qÀÉ„sÒ°ë¢*MÝ=‚†j§–˜ž: 1Dƒ¯D¼¥ÐáX~LÁ@¡9Þþ¶/ûÍxJ’72å±'d`}ø¾ƒSôØ,2Lq0Ð@Œìä·p¦êÊ[ÙU‡¾Ô(+r';D£™ ƒ/=16jÄùµ½ÀtngŠð%”¯V\—².;T:xójvýòlMÛ昫¢7Ü4±*R¬‡KÆ_3ì‚Ê|‚nh|7ñ²¡ñ7†úB˜È¸RõJê©®«zòQqéæQ2IøÓ’ Þ²Ë-ò'À« +?_j¸êè¼­«Ã©×ÙãY¢é1Q¯×¾Ú”Ts*9zØa %s€»²Ûà¿ziïZìÆ毰kìm‰Ø¦Ì*Š1/†Ë¡çGé˜ÀÉÉã /èî¶Z"Wß”|a(ûSÙ"y¾ïoëžÎ",Æ€YÞ,ïr$Òò››ÿž´n X{‚g˜}’žM‹‘0ô÷ó-ÝåDàøXYÞ4ˆ-Þ_Ðh)o×Äfì<’Ëäïâ Á†:qãøëò&}ÞË" ùŽ×»Št¿¶˜@LÎúsÔ/æ“ÿë2d + +endstream endobj 129 0 obj<> endobj 130 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 131 0 obj<>stream +H‰ŒUÛnã6}÷WÌ#UÀ\JÔ5X,vƒmš¦ b½}`dÆQWƒ¢7kýˆö¡ßÛ!‡Ž…ÄÛ6lÞfæÌ™3ãwŸV1l¦Å÷õâ]]'Cý¸(xU‚À¿H+ž' žäP÷ ›…àBˆ êf±ôK´z^ܳóÕ——pkF;6cC´,xÆV·±HEœóXXEËŠ'Lo­î´Ä½‘̹‹~­ZÄè0ñÁi••‹ª™q™ºHF.&+¢ú7‡<%ä±|±ö+žW/gIp¥³g+ÝéÆBpR>ç䥘ÛÇ>ó{v«ŒêµÕfŠ–)æyF^ÄHÕ2æqZP<’øJ$ñµÒ¦U]”£í/»þ!Ê1YKª2á1«Ÿ4LÚ»‡0øåÎö‘à9s Ž`ýÃa ØWä“,Ë.‚$=¤…žå[ÔtN©Þ9_›¶£ÿ"|³)’ˆAÓ&dTÆ‚¸¬<ä’—)~æ\æÇ"ij"¼ž>Ü\½oL÷øÁÃ!¬RJ„¹kâÊѤÛÿ>?øcfZ ¬²:¼tÌøý›‡¯C&yA!Ó˜X;w’©¹üô©_‡Ó…r|ûaJáÄE +âyxv©c.aö4ôÞÓRyZ¦§q×ùÜ×ðà/u°ÜM$<+ÒƒlÞÌí›×G+˜ëoýkZÛí!ÔÅIYÓn¢æD+úI•Œòă<´áE½øG€|¥B + +endstream endobj 132 0 obj<> endobj 133 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 134 0 obj<>stream +H‰„VÛnÛF}×W ò´,ª —Ë«H“ p$F̤Fhje±¥H•¤ì +…¢ýÞÎ…’Ëj`ÀÚåΞ9sæB¾üùÚÀ]?û)Ÿ½Ìó äËY¢³|üãE˜é8›è †|=óánækß÷#ÈËÙœ—xëav£Þ\¿½¼„«®Ú²­á_ð扎Ôõ•ñCßÄÚø>\{óLÊm·¾udcÁy¿å¿Ì ì\VQi“dØHÛ<ôì“O•zùïÄ<æÆnóJ¸ÇY¤Ó€oj&["Ùkç…H¤éù§í®/Ö©*'DŽrœPŠuqê˜,‰õªèŠµ\×{óc¿0_'å›mÂ,ü0‰åÒǶAß™ržÿÇblŘìðc?åPŸ!F¿iù·ñÐÞ¨Þ³:FxÞ\â3¾˜qpÄV§!þµ‘‰^(WÓ“H¨Ï«‡þõ§¯Ê®^¾fNÂÕZKlìþæÕãÄ8ÿAL¾ô®{³ÙÔUY UÛ°ñøìœùW”ujŠûÇAœh0Þbѹ¾?âËþôâÿçeWv²=_¢\„?a|¨Ð³ ]3¶yÿW±ÞÔîbïàùœD¡¡V<Ÿ“S&¾‰ïÀ§ûoÀ:7qà#È[ÏW— +(ÕCÊ<É!  !ICR¾ÀYP¢Øð~çh.,qvEýB¤Û ¹›tÌ4:|$ð5ÒÁÁïÞfLäÞƉÆдù^ûŠ¢aœi?ý~®X-+ÍõŽš+V}ÙUŠ÷™æ¶ÉyXŸÝÐUîž`Õð’ô¬ö°*z½«å ÜwØc°}Û*œÑ~/T2±Ft¿Qå¶ë\3Ôl¸ó+U£alG(‹n´„l.–ìxÙÖuû ðÍÝÞ|}RÁá1À}|NÓL-<²U;ÊMì‡8VsDß6ÕUc£b±C‡l{§ÉeÜàˆ 0Wr¨ƒ4Q)¤cÈ|Žqó/¿Ã Nowзk×Ê™Ó^Š‰|fÒÞ¨·m³¬î¾žÐÑÿÜVü¶ÀdßÇ;ÛNNÞÝAÑ,ˆ kŠ{Þ~EU·5f¼…µPê(¨‘øn‰~ùUTµÓËý ái&ìQ±ñ]üñå¢o-Ç9·Ö$:üF³-b«â~ä2IÁñí:™ÓÊBñ!ó¥´ +‡ö0e¤lUG”­zDx¿àÿ0Žàñõ›êäŽdx¨ênyëä(Ž‡•˜4ã3ªb©úD§?V=U6fc+šÝ°ªš;h¤ª~i`{5p)l¾PÍýºr —=Þ‘&³ÜTªF@Ra­0:þÑñÓo“I¹œ™¡û¤Ïn]HëðPû£¿Ào“ÃGÏ™Wåé$ÍWœ–eË(ÝÕá©…Qúeðõ-,¶l ¯(ª\v)ÍC–[xÓŽ–:R&2\"<”í±bµÆo³Xa7ï¥0:4YðJL-$yþ>Ÿý7ÇßÙ + +endstream endobj 135 0 obj<> endobj 136 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 137 0 obj<>stream +H‰œW]oã6}ϯúdo I}‹i¾&ÛM&ˆô¡ØÍdÕ*’AI „ýû{KQ”-Ê”Äë ÙsÏ%ï½üt»ÂÖ[qöËúìÓzM,l­_Ï; +-Äÿ‰ndûÄr›øÖúý YogÈFyÖzsv.>rÔ·³ß«Ë»;ë‘åe¾ÉSëÿÖò<°½Åê#aßÆY«åyd“Ý•ôý+eiÖ8‹æuËÿ¬ÿu†ù ‰ o?y¡gã Š,dz·aj$pfÔp.¢åúF¹Û*ÇÎ->µÚýȳC"Z·/î²×Ü’h÷·o–" ±böï‹Ç˜Åï´¤¬Xž»<ÈŸZùÈ0÷éÛØk}%(±ß‚òléÛÑ‚.ÿ-íb§],¤ YçúW!ùª'ºôø;Š].žÙ’¯Ç‹béØ>½ø"%ñø0jŒDp‘å;vèòßží"k¼•¾H[ðˆ/<%î;þÏoÅÏ|ÀŠQ¤ÁIÚ/¿Îb‚l2ʺaéë(/Ï£‚ì¹ ‹-œ¿+·#꽃Ô;Hl#¸â¹Zä‘Háå_à7C%ù¯¿—”eqú9fÛo1›=†A»ù€pÃ:†æ³á ÿŽƒ Tüfr]Ü–­‚É3(ëX„A7ŠlÞo0ð:/ãô±™ì*Foãêm6õ¡¸õEÈ›¦^E½~Ü&Å7l3œžwücÜ€Þ¨îú"s#¦n¨"AIùÑ7ÜLÆ{“¤<Ò¬4o¸`zÙpõüu_€qÃk ×@ áŽ¨0h¸$(ñÜÈ~‘ÀdÐ|®,Yž>gIÉït³Ãµ#Ž T ®³]•`üP4N° +…Ü^Åó3`º!7˜®HPÎ}φ_fUT7^Ѭ0›¬]| +5æëǸë¹Až9¿ ç'|ýì˜ß,ÙLæzà?(Õ|.$'äZ…ɀﲔõv–sËIZO^ïÙçh°9EÏ 0ʶãMH˜Lw€U$(ß.±£ú¸ +“¿PV$y6—mÒF +eÆn¤Á©gk•¿–Í°.•Ìæ=˜2aÒt>>*HéŽÈØt&#¨Þ¯oã$+ ”»;dZòZP_æUVÎzÍÓNFù§¼&>V‘ ¯ù°ãÀ­VP§¯’·¤¹ ä¬œõ›ß NàÇüb0& îÑ›y!1qj5Lnoâ+@ã|Ò‰Nh!*ìàùE§ùÛ]¶«Œ=‡*èL×J¨{@®ëU˜Ø¾GB|w¢vm}À†¾©JãqKÀüzŒSo9ï 4áĤó¼:*Hó¡o;ðz>€œ_åÛÐ-Ë${›·^´E°‚ nßN fæcq½2i¾ç¨Hùw¾‡9Ø.æ5Ã6 +c•=TCÛ‰ªc6èžr–?|x!Àd ÷ñ÷û¸˜u· ÁÌr6ÔS×’{Îà®éÉ'=v\ ²Ù#¶sBÝVa2Ö‹M™üEo’”_9²r¶]:b[B`®à·¯ÚŠÃ|DƒÁ~@‚LwÑ)÷žL†|S¥éj§ôâêòâ}gÚ,¡ºý©ÕPEÌO)žH¢^Åt¯Ä*d<‰ìÒØ.*¨a§*è¥NB}¤a~DñEµ"&]çWÍ>d:mç„J®ÂdÌ4þó‰yZ•Iž–¨€®ÄhÔªÓ£×`RböHé(h¾€MWa2äË<{MÞ*Mé]žµãÙ\ܨíüPÝ ¡Rk•ÌîzÞŸÝQ)"ÖˆšÖLù¶G ªœ&Œ§]G)—¿PVô–ö—·"Ä;·[F‹b|Ñ#Ë·Õ¦¼»R•¨}Þ'–¿$ Ñ=ÔÓÍþ”öå¢Î”Õ{ÌJë3·ïÊ-ùéÈÙúc×7§CÝÄ,ÞÆ}¯ï›#–~¨âžVñ´ÑŸ/½å×ßKʲ8ý³í·˜ÑÁûòŒöV¯ó2Nw*Foãêª1<ä¥u“”%UbèÆΑÖ¿Í–4ë!¸S%ËÓç,)yFöwïUg[®Øzº•{èz}ö÷¢òß + +endstream endobj 138 0 obj<> endobj 139 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 140 0 obj<>stream +H‰tTÁnã6½ë+{’ +˜K‰¶$ïÍuvn‘l A´L+j$R ¨¤ù‘~o‡¤bËIV>˜$Þ¼™y|ÃÏß‹ê!ø½ >—e1”Ç #ë(þÜb¹&i,#I +eP¨J(¥+(«`á–õ܇›b»ÛÁ­VFUª…ÿ Zdd·1]Ò8%1¥PD‹5IBÑÑí…†ÄbXh颿Ë?‚ —ܯV¦¶"liÓt>wfsS›5ŒiTþc‹©¯~í¢×2’/!‹Âx*µ +B€BÈAéò¥0ÿâéÿSa¸Þ +þx'ÕŽf~]lN¶UòØÔ£æûVì”ôÒü%†èNµ­zz«:tÒÀ'ºõŠ¡Þ‘“‹§|7)É`…C¸LNCÀlà}x%¢IáÒMoÉ¿øA¢­Ÿº;Y2w.(9M­g;µwÂŒÖtQJÖa¯njô¿Êo¦6]é<*í¡Üø‘ÍHNqÔ¨M²8—æ³Loƒ å{¤ GæAÀ $n†hÁpüU´D>-ì„ /äK”“,Þ÷mS¹Ì2áá¹i[Ø °Òƒ±´0ð£h_ ©¥ÒÂph•uôz,^ËtŠ¬"÷¡­Äwds`”„=ˆpýgq‘þT,–Ï <+ýˆe˜ˆÒƒvIT ;ž)Ÿ•`^ .kŸ@[Ú¡9…¶¾~àîTÜßó´ÃåŒß=âÎ-2æá^¯?GMpm{ò-OåÌ:þÕõGˆ‹&ß)Ó ¶×¦ëñ½áöÄÉk6;¿ÍÎÔ‹¹×''Æ+oß;ÑqtSŽ·ïz¾D /zêciz{—‹´þèkü/Àa²/ + +endstream endobj 141 0 obj<> endobj 142 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 143 0 obj<>stream +H‰ŒTKOÜ0¾çWÌÑ©cçá$+„Ô„(RAln¨H›ÇÊ PTqì_èïíØÞl–îÒV+­íxf¾G&spºäp7x +ï (BàPT^Jó þì&Ω!Ji( h=w£Œ±ŠÒ 쳞¼kò~ùñì .u?öeßÀ/ðƒ”&dyÉY̸ œ1XúANC¢V£jo”†ÐÄDÄ”ó¿Ÿ<ŽC îvIŠð%4Š Lë°SƒÍ *áÜ/¾ò±#Ï£M»sôEžÐ,œò¹å~MNNeí§4&ÝàÐgvxš‚ÈغJ°]æRjÙªQéÁb¼pÅM9zpÊã;lá’>÷/hN”Ïðá‚#lâðÕûK±u©+å'XcXõví|Œçdð#*°¼=,6ú8ss+.Ñ,ÆA#±Qf¬FpãO7> G燥nª#KÇÑŒ¢È!æúÇI£ÊQ÷]]šþ²]¼óÈÏÍnO4t/;ÅC‘ºâo¢–ºÔîâí—æ´%)±îÝ¿Yê”çNùwÙ®µ˜ö»–$!å™ëË]ßvYoá¿’¹'.Î÷¨d19‰{üžæSˆ%Øg9³bYE®ÇŽM 2”º^ußíéñhNâ›Ù&n6\©ñA£®ñ^AScç§d¡¯@M½`Ÿ•pg:¢’ˆ“rìµ› +8}üÜØÆÅk2€|”u#oU¯ÁÄT‡¹¶‘Þnµ„¸P(ð¾{hoìSûe¤D# ?C°‰‰½ìÜe|ðiŸšÓÊgx”úÙbÞÖU¥´êFçF0ñüÓÄpöC8?ÍÄÐ if‰&Ž('p/ÝñÖ.OR+X5r¬ÖQí@‘± Ûz§o³3x(eg–ã‹ì=ïÝ´Âɱf£`t öEuUeßÚAÕÚA%»[êÄb×±y ïöÐÜÓœ¼R­ÄRê4ž~8¢¦Ùgr™xÝswžÞo ”à + +endstream endobj 144 0 obj<> endobj 145 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 146 0 obj<>stream +H‰ŒUÛnÛ8}×W úD-`–Ô]A Mº…·@4z)‚>Ð2•¨+K%7-Š>öö{w†”l9Nº+%qÎœ9sáËw7îzïMá½,Š$•—ò<?»ˆrž¦¼ßÇà”Ä;©0Ï +ÀQN¦ŒÌÌ%¸ë“îévÕÁì’pt½À/ußÃŽ²=¼®.‡ÿ§‰bI*>RXœÔ|èjþ’j>a}iêíPwí³½åŠö)pé£v¦íAÁ V~„mÓ`Ãáô®‚š„Äÿª3Eè^WØÇ0Ük¨jÓp¯ÕÚz BžKÍÄ08øJœ3´lêV“9²Ž™ñ6%ªœ°‡ºi`¥QÔc[¿£žÅ]Úñ3 +”L2†8ýà@Ö°Ö[·l×u{G`»m×Z’nlLÜNêS(†ŽbÙµU}‡hÛŠÙ.[Rƒðê¶ÌŽèáHi>ª}*ô,øQiìdÄ’¬ô!BRSûú«¶/{‹¾5X3;ûƒ6zíÖ%IÕhNõ¹gÓ “È ·œMtŽÛá(¼q„À¥BÓ˜•Ê8`QטpÌ6ÕBe[}UÆíS«FÛ.LJ¿|?«¬ÛRÎlÞKw¦N?[=·Lµö95¨GñÍ4%rk;ûmⲘ0Nr‘ì)Él¤4 LXö”Ž„Y=×îíXn)ÕY¥J·að%>£[lÜŒ2 8#ö]Âéh Ø£œ´Ûl8£ë)¤ ÉÂ>«¯8œè§Swv€ÆÓi¸Q˜º »Œòöw†át’¾-¼p›C + +endstream endobj 147 0 obj<> endobj 148 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 149 0 obj<>stream +H‰”T]OÛ0}ϯð£3)Æv¾+„´ ˜Ø$@m^&´‡´¸%#_rÂBüxØïݵoÒÊ@S¥Ô‰ï=çÜã{½÷e&Ȫs>eÎ^–I"H¶tb–&„ÃÏ.‚”E’ø1“É*‡“•Ãç<$ÙÂñì²n úqöùä„œë¦oMIþ׋YHgç‚\DLpNf®—2IUÛ«j®4‘&ƧÎý‘}uJKŽ«0zâ‡Ì M…ܱá憕 +ßÍ~ñŠþÀ®P~”†,‘c¾&ÿ‚NOܘ֮'è²Aþ ;J"“(Ꭽ\„tžë¼R½ÒëPòÁ8‹¸æ &‚4"{„I§MíF,¥Êåð20ØÇ`PŸ÷: ¦Ê £kûq&hçú,xû2Y×'8¸äqœH¬É·´àM½löo»ƒ³oû ].¬è +šíéq¦‹ö¨Î祺4î¿«nï´yØÉ’aò"«}'cͳÙÈ>Ø×…Æ×ÍyÙ Éýè-ëÐXìÚÔÆýΫ¶T“íÜÙUºÅ B_É8ûöÌ›$X)Îyf'LP"9Ì´Ý Û:¼5¡~l&ét§‘wÁÉy³±|l¬CÓXí@ÒMýJco[*×w‚”XÏTõZ¿ 4$é¯pE7Z«º'‹¦^0]Ýè(ìvMò*â>ïi–&j%¦X†dÄÂ(Ia>­'ãzÅ8 Úšæ‡á*:¢j ¾Ž:êaߤ‚”^7eYÔ+C½( #ð¶(K2·Áj¶ôÞÈ¿åœ÷âbá^ÃuG +8~]©K7` KB짞ôPÜp h,ËZôf~7x“ÙÑ×®'á*ڙݭT×å+T?øæà;ÂņÂGŠ{ìEÈ—Ö8»¸46$ôÁžÑ=:RèÁa×<àNû÷šþŠÌ•1U V“Ç;Õ=‘Ɔj{K‘ǺybÿºßoØñ&žª*= Àðënðã™ÁHn èQæü`<…²w + +endstream endobj 150 0 obj<> endobj 151 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 152 0 obj<>stream +H‰œV]oÛ6}÷¯¸ÔV3¤(QRPXÓns»&A,dÒ=(2k•%ƒRêÃþFo/IÙRíĆúò½çž{x™“_æîÛÉ›tr’¦>pH‹ID“þÙ‡ ¡ÒQ_Bºš0¸Ÿ0Ê !Í'SûˆY›É-ùi~>›Á•nº&o*ø +Þ4¢!™_q0.)g æÞ4¡>QëN­î”ßÄbà¼?Ó÷Ž€¾-îžÂ˃©L™•«™ÚÌT%<ðÒ¿ ùÀ‘çb`Ÿ}™„4öM¾cìòñ¡êÊuU*=«‹z ^…&’F c6ÆáV„[r•él¥:¥[oà’OÝb8ª6å” r~kÙsé’.šÚ“4!Êcxí3\°pÁ–¤¥5}ŠõP×Ê £]7ö^{ÏIë *Þ¾ô”p}œ¹&vq HA㯒 +9(Ämñ×›öì{•Ì—˯s]g–š£,„0¤l¬^–we÷ûRÕ?— +Sw—Ea~ùçÕž\4ÿŽÒÓ’®oÎUªyY¿4P{6ÔÑóe´£÷[“V‹7[nGÇëÚ&"¿xm#çMÑm2­Ž…¼æúì+ºŽ…\ö»â¨U\Góîïlµ®Ôé¶ÄÓn}Ne°ï†½ý—žP +àéþÛPÐÀ¬ ÜØÆ™sê~¡¹=þäÈÿC`lV¾ÏPÂ)Nšá~Ñ8ìÉÙ÷Ž c£´]ݽx$m +°ÿ^aký +Ûð—Ü1•ñ1oˆaŒ7FÞš1"I›ërÝ•Mýì QûèûNÔt© UuÛhð$ɳîäM]”÷Z- k ÄNºI/Åh‚-‡1W»VÁfYV +Š^ÿ2Ý› ‰{°¥·ƒ‚¦(^Ac²Um¯o¼ÏsXZ/A‰£WŽ"hêÞ;¸†G‡+ß'zK>‘e®›ƒ#H‹ây!ùÑ”œ]Á¦ì–`,ÐÔÕã'Bºìã°ÞP¢Õ +?$« U§KõÔ--?¢2Œ£çV7¦ž›À'Nð {ˆ¯‚Ô¯ìšó­•ûà:Ài$ñàeÇZÚAÛ¡B¨à~'2\",‹­X xâ½5šSô˜¤Uf©Z 81šK²Y>¶¼l¡2ÛÀ,ì•z¾j|H&Ã!Ù‰#'åö¤¾V« õŠ‰¶°ŸÛS<$w§ïÁîx®Ýs¥¬à 1ÿö¨¼³­4Ê ï)'P ªËʪu½d®?Ô܈m÷‰±ߊo?u(†Oú6|)3hûaAû^`•X¢ÚGtz—N¾ 0í‹¢ + +endstream endobj 153 0 obj<> endobj 154 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 155 0 obj<>stream +H‰ŒVÛnã6}÷WÌ#ÀZQÔÅ‚Ò®»q7î¶Ú‡.ú È´£®$¢Ü8]ìoô{;$eI‘ä´6`‹Ã¹œ9ähæÝÇ …½œüMÞE‘ ¢Ý$´ç3p𫼹¸ÀBÛ Ê'ì'Ží8ŽQ2™êG´zž|!·›—Kx(E%‘Á?`MCÛ'›êx lê8°±¦sÛ%üPñü‘—à*F”;ëèç E‡®nžüÃómæ©0¹‰ªØŽŠJ¨oE*ðžOYã@?øÁÜ·gîÙžReÿ…lıLøÒ +I±³{N„ÑR1€Ø!3§v¦Ó§¾ñö—qÎ+^JkêaÞ×Æ™c‡©›R›zsþ¡&Ímøs]Ã_ §ØZ¾M ·psNN`QÌdF~祀G½›-É·Z²å';¨ž8H¡%ÊH^Ui±× V@ªø1ã5.‰iHNK((>§CÔt`R]st‰Bÿ +F°˜Üôœ=RIÃå\ó8‡³ñð7°YÐèè#i(؉›gùþ󧛤Ìvï5$ƒ—1¦ÀµÝðÅOjù­³þÞ1Œ®Œú/x:=µS[d<©JQ, +^î_”äæµèû”„ ¨e×òÛò²Q7“Å©*ã¤úMd•Ôv]Á› óTÊTƨ'3¼Ï«XÊÛ,Ý9/*mx¿ºÓ½K÷OCå»qeãwͥȎÕÑýjý–ãžöݸö*>­y"rD°åÛ‡’Ky,ÍIâÖy=zâÇü',Êmü²ø§…¡¶/¼`¸:fÕk«V20,“Ò,/¿ILx!µCó^Ò÷ÿRñ™™kÅ)ο>¯/Ïwl×»\_¨:¸‹ ˜Ç;øü©É SsÐù8#Õ J†Ÿ«_‹D»tǸ¥W#5ðºÓ1vÐ, +úãÛ~G©[[¥)¥nW«WLµ}¯_MµsC̹UÒ¸Ú}¿†ÞöÖÓë«— &€¤2>U ¨UîEHO¥­€–6÷Ýsw†gà½uÏYÛd˜i2T“ ˆLÊô ²ü~JÛ~ÊL?]óêX"Ö¯-ºrI™Çš®øѬÄ \/kÊ°o¥»4Q[§ê¨RßïZPY3œU°¡JÄExÌ 1ÕkAÔC‘ 6å $m¥b\ÖÓ¤8z0lžžô·nôJCrËÃöªº<Ϊ³RÒôy azÆÐá°ó®hæ…DGÀ½ ­f>0"nDzV¨E`&…J@É+Kq¥ÚpIÊÿâ˜S=)Q«‚3ôÇšÀÄÝ!zÈõ“x†óBg…þ}±”ÇÇ +ƒŽŽ´ÌÇç'­ÌñòcD⪠⢖ê_Œë“,MÔ1£È#Hc-Žú/ÛÂQòç×Ékó0iè—Ÿª§XJaŽF1GñÔ0ÅL-ûªˆòPVé8X7³q4ʘ릞˔T¡^V/¶Á„åöæ8ÖVÊyò\s¥Ø*õMþ*¯-}†&E´u‚×£\[ƒ‹hòïO0. + +endstream endobj 156 0 obj<> endobj 157 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 158 0 obj<>stream +H‰ÜWÛnÛF}×Wì#ÙVÌîò"Ò ´qR¸EÛ úд(ÖÔJfA‘IY1?öú½ËR¤,)MÐ<´ubkµ—™33gfgŸ}}£Äª|5Ÿ<›ÏµPb¾œÌ‚,þÑ Ê‚D‹pèDÌ×)VH)c1Ï'S©Ýä÷åÍ‹ëkñº©»:¯Kñ§ð§³ ön^+I•JJqãO³@{vÓÙõ­m„Æ=¡‡âü_çßLÔ¤œGñ Ô‹0ÂÕ¬Y÷ uKÔê©ÄŸÿŽà#¯Â½1ü$‹ƒTãyF¡ïÊv6ïêæºZÖ‰q>8‘3‘¤r,E‘ Þx¯McÖ ªiýi_°)2˜)ðÙT*Êñ•Ó­÷ŽÓšwSo›Ü^W ?”g}X̼·ÂW`Bêýl›ZÜÒ’á¥Ö.DQÑ̾õRtwV´5Í (ÑÚ®+ªM´ÂO¼ÎÜ––q Heš…Lþ$?t™C×ãýÑ‚àÐk75}Vˆj¨ìwƃ'•dWfäÆL$`L“ L†H¨Aõ8Ïwíåß2†!âðpaï/û¿¾}|ž7åò’Î?ãíßCtðóÊ.Ͷì^T2šø©.;³¢-¼¦pèfÕ0­GÓú„žw¨è‘FWx ñqRñ'Õ0«‡Y}9}žìÍ8‹™¸Ì Nÿ1ò™‹q5e®>Ïý8 +àS{—þTy1°‚"ÎÜNš™sn“ãWø¼äiGñ#1¦g¸ÿÒG 8L™þ㦼ê„<噎á*ŒÇT;6å‘ÎCÕÃP÷NxüJF3iú!¤ÌhÏË·f½)íEoàiæAÕ ¢§¥ëð‘ÇQ¡è-:uR8²¢»…ÒÉäy ¶ÀŸ*ŠC +GBØÖsÐMéaJ G/Eòz=çF:_Áõ¶0’3–v +w'ýHqð£À.û9ÜðÉ) WïT©©T¿)y!ÕÞûNÓ“5}!Ç–ö)–:F1MbyÅ~áÄD@Néÿo4ɱ¦÷­1 +}ÅLžAÑ/œ˜úg(Â(²s(²cÙß 8,Içv/iŒ}—†b î'NG¤i¢ŸÖr¦_áõxmÞ›®¨«³ •Z©ýÍÒm›ªF@ÇC—KéKe±K* Ñšµé +?©ºæ–/ mGÔ‚’¦î¡hgµjŽ¡u +ÑÌÄŒ`Æ@6\FlLCç3/ß–„Æ(´&Ó¨Iëü +5f¿Àåg!ë÷‹Trþ¾ögð Ý5ˆ‰÷‹¸N®Ç3*zçzÌk×$ÚÊõ‰Q€º‰#Z×Ê/ømiîC!åëª|¯ÜˆöÎrË £¢ÜPdÏ„Á,K¢‘gÔ!f,.¯«Îú¨®¬hMäí0> CãÎÀ¦¦±ô¥E˜™·ámäȃÖVt5pù–(ç‡å¨Ø‚åž,.âM ^qØÖZŦ,àiÒ¶Pt›"7eùÀox%‘r¶QPÜZŠu ‡A`Sï€à%¢ß]»žVdÁXeÍ +°Ã SVp:ªÙâK`W ‘”wGWg»±y±,r±2¨…š+å=ôÅmc0›œ— —Ñ,û÷I~gÆ”á/93Ÿ=]‘·yoĪ¸·[³“ˆZn#éþBÜÑáš2;{oY2y"ßBÔ«N”Eˤ‚ >öÞxr$äÑö®Þ– TTպ´ívíc'`¸*–KK²`Gëˆë8LÚ“kÛž6NÛQ2¥{‡¨„¦"süˆû|¥á5ÌÒÖ@Äƶ[Z-;¨:à—âàBPâ,<Ç]m×·@0×=äZ”‚.s–[(q`pEæ·õ¢IãkL#ì÷pac±§ÀK‹‡Ê¬™}ˆÛC± ܨhûª!:ŒO¹¡*GC`ß“PÚœ$®¥<î8µÜ +> endobj 160 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 161 0 obj<>stream +H‰tTËnÜ8¼ë+úH+š¤ù–x0ÄÚÃÂXòˆc)+‰IcÃ?’ïÝnR£qb{æ W³ªºXÍ‹Ï· +¦èS]”¥å>2¢Ø€Ä¿¿I ‘kHŒÐ9”}$á!’BJ™A¹‹b‹«ž¢;öñöêú¾Œnv;×ÁOà±»ý¢d*U.””pËãBhf³íïíšjFpüßò¯H! öäá.3HI&’”húÀmˆ[+S†—ߣ8ÓBm²r#Ri4”[_©¼Ê;öÕöÏņ> endobj 163 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 164 0 obj<>stream +H‰ŒÉnÜÈõÞ_QG2˜¦ªX\%Šcµauâžè)1à¢!)ËFßÈ÷æ-\Šì¦$«úíû«³?ß(q×n~ÛoÎö{W(±Ï6¡GBÂ¼Ø \¡CÇ Ä¾ÜHq·‘Ž”ÒûÃfKG zÚ|µÞݼ¿ºŸšº«u!þ'ìmèøÖÍ'%=©GI)nìmì¸VúХ巴.âh ÙÙÿÚÿe£€¡KÂùä‡ ^hßÑŠ)Yvˆ²%JµTdïÿÊ{¬¼Ò#:±úAì;‘‹ô¬±FÖe^$eZuWUV‹žMïƒ#='A$M.Š\ðÕú”4À¦K›ÖÞz`ð6E:¡Ÿm•£¼4>'ÝUÀD×ueNl¥¶„ÿ=#kF&I­íiV²gõ9µ}àÑ>Ôô­lÀWVkk'ötéUû”d'–2 #dÉ6{æקöíîÈÞ’:¬¦V¤ˆ…à›Ç²LšŸ7]Ò¥xÿþ{DãúÑHóîÐåßÓAQ©3÷˜Æ”sñ¼\%žÉ.ªä[ÁÒþ™¶g×µIºÿÓ1ÁÇú–Ñÿ±Å«ÀûIš2oÛ¼®^-äcòcWíó’ñð ~·Úßíçíg’Ïi™äU^ݽHkúUc8œRé¼ßÆ®:ÛeÙKvN‘Û}8»Lòâ%g>K0;)üùïï_I5š¹hÍÛE‘…+u°¨—Y­–w‚˜È.~$åC‘¾ US ˆE ÈžVNìM]Dî"½!Z/Ô;VéÄîƒéº€4üj a––ÐR]W»-ôCií./ÍÎÑ‹D²yu¡–@é2>¨‚XÇõ„ˆ +E\׫˜X8ÄQE€Hµ4C=ªŸ‘)ÔÐ u¬2,0,‹aV˜˜‹jí™c‘.$M¼†/´Üt†NU²Še³jWŽÞ»_=‹hVÅql?Œ={e&qRºaèxÃp\™zššgÆ9ÎŒÀj ¿£žY k˜õ¸<ÑÌéš<ýŽ\|«Ý=ŸÄá±i0áu•å0à"ëî±I@+‘T·Â¹OûÈq}£rªG=‰ê× +tN¡‘õùÜ:â8Zhs¬[‘<<(N[f8¾vcfŒ,ì¦ ØÙRA¦gUUæw÷¸=°¾§¢ QÄ"á„‘ç· Ã#.«™C7eoªS›”<æýéÖ{àœWt°+‘“:°ð=)Sø!`û°¶IGÛ´"Óeò©iÙ£~·lðæ*ôåÝç6.”Û¿}šíTÆF[›µ»žõd¯gðöQkgoÑ!»¿âxÓ_®ñsbScoDÄö·wçÛ‹W77W½€‹ýÆÓ¸G»žt\ðŸãEh~!E6Ù óω"ªD.6*„õ5 œÐåƇuÒ ¿æx&>ݳ+áb^‚³’*T¦ AÜëâô*´g=ƒz#^˾Æ+» +”ã»Ü×ÖjÎœr\uTÇn¬¡ °Öô3õ¤â§hÓ‚ñ]zËiJ5Ð_ ÇÖðáøŽQS!¸88öpoT¹0²˜îD^Løý=[`¬zÜMŸ*ôÉÜ髃Á3Ö]Ÿ¹ëû‹1äÈ…øЈ¬~àÃCH{ +jçË}ÚÝS{I¡áî +/“žèph3ê +Êi• °º5˜ŽTQÕÝÁ»ø2T~ôl¨ØåÂ/S¨˜| +ÕpÏ«¡òÂBµŠ00^*¾X%óPy*,Úâ N±£‹¥¬o B‘‚7)tóú!mxHñ€ºÒ¡z~^Ã`Ãøb˜g1vèÅÕâ€ãhâëÔ·ÞØ.†¼ŸÒÑ®~Õ¼}_W]Sd­pvZžàÑW?‹'›†<+rØDJ¿­9±@ÔÁY8ìirì±`\Ùlœ (Õ£u±z.¶Ûù”DoÕò9¥³<›Îœ+å"w¦tfò)‡{¶ÀXMgŽ“!?˜gó|`oÀ=¾žËÒEpŇ£çuŸY¼Ó|‡5œ¶Kti`]ØÚæ„æƃ)&¡ç|Kàx+Ò~…G?qF{ŽôÂøùŒÎ‹>ŒSØ S0rê˜45d‚µÚ— ŽC×dÇ‹X’«Ê¹ç¦H2ýÉáž-0ŒHN"z¤4‚u¢5­Á üu)–},×êwzûÁZAƒª£&”üÈKžÚ¥èœW0·¡d¹9aí$Ý´¢k9Ûô§¢z¨ÑS#ù) äYÍ/0$Ðý“ƒTó'ÕbV­bÈ2é}ýȲ°eÀB[Ðr +|€r¿rC'ˆN¼²ŽLà %‚ +ô¾ÈêÛ*(ÈO'ØÇ-ÊÂ}†~BCÐGDVÒÔ,Ó¶Mîš:œÁP4Á¼'Okq…›9¬ý´/¦­D©Õ¢'±ûf‰Çó<¦\™å1eI9Oš1™|LãášÍáf;ù³ü00rFу•óŽuÓëĽ6¢Iu¾ÆÕм¿e3è ·Áúx¦Úÿ`-¶” + +endstream endobj 165 0 obj<> endobj 166 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 167 0 obj<>stream +H‰ŒWÛrÛ6}×Wàl+˜àyK¤ãvR{lN^œ>Ð$³áECRQü#ýÞî¼H²ÕÆ3,v‹³gW¿=h±í¿f‹«,ó„Ùf«4.üÑGªÈ~¬¼HdÕÂÛ…«\× E¶Z,évòýÃõ͸k›¾Y5¥øG8ËX…òáN»«#¥]W<8ËTyÒìzS=™Vx(ãKTçü•ý¾Ð Ð#ãüÆ`^ø¡ò4S±ím»hUêÔÉþ^,ýTÅqˆ´…©/²Ö·htS'ìf×ç/¢©EQ¯Í¦p"Y;K >½)_»ñ1[>ž>Ö© +Á!$xNøS˜nÍbs,¡}­’äX@‹B,tª$±çŠj¦¡J=”〔£ä0ÞœH°Á±ÿ!být“ñZ£0ûFý7Wíãj0_Ԝޣ%Jå%ãuéñ¼ïà¶ÎŠÊ8¡ +佩œH%2w@"’E]Ô[@Nº©Ìž X"±*ÇúÜŠ6‹MÓŠ6E™W¦î;Ñ7ty¾«tÆàÓˆ‚™>{ &ˆÎ¬šz º=Ù‰bƒZ_DÞ\Îkš_‹Ï9}ü`×EÑ1R–:V.@N†Èêh3G§–uÓCÅ-¨oEgÀhùž#€!z¯õÜgò™L–{#EYŠ'ŒÊ®i{³y‡:QÅ„Y7~‘bpŸÕür'0òÞ ŒÃxs"qiQÀâ'ÏW 7ÛÛÖÆ‘@0xrq bb¨G™µÅ ¤áŸÈB­öN(Û`"z\…Ûïá~á®é=tÜãè»SÜÆÊdJÜlNaç$€ÊÖYzÈ";ÐJ5ß/Ð {B>`¬'\ç’†þ‡8¦~ 4~ø>€=ŸìÉáØ1&o­$â²)'v€M2Î~Í«¢ë +'–d!F•^aÔf*Œ( Ô²ÞÓ'´O9}¯…!UCØÜóxfXÿÑ›¶ÎK¸ ŸNã…@e„CK€¼dx”E ˜É4D£!×€)yÆ9bÛЕŸøQÞ¹F znÊ¡´D›÷_œD^ƒP<·³¶ÊaÊê'¦QÌXBOõ͵õí«ü\¬€@ÁbÛ|):þ,Ð÷¦þGWâæN4Î2“5#·|véŸÁ±ë¯ÎI„„¼Ä'6_«“ü-gã‰Rx0Ïõ‹dä9E”ÓÈ+KƒÒq)—Þ¦7ªÕYêh‹ï¯Ð ]îíBB¯öÌ/óÁÀ/°òœ·\˜)”;fßÔDy)\â Ò „[š_Äá™&Lo±Nz²»†ÆÃòX‘žæXGóÍìÊp…~¥²áUrÌrN¢ÜâyZÁô”CÚæЈ{„dPdý;q[_Ý)0FÿQ‡ðšªùMˆá½h†ñæDâtÂò;˜ÊŒ§ææýKÖÆ‘@0xLa§õ,˜–ô8Zö§ Ì45V©‡#†ä40ê] Í“;„–Ç;&ª#®ÉˆÛ¬^¢gÂä;7×¾‚Š˜OyiŒ ô&ÚÒüÔa…ÂÎ,åÎ,”Ô}…«MaÊ5fͬ Ì„)h²æM=‚M3¤OæÔÃQµÔ’ÊàÚêÙò–Ž:»ïNJÂ4ÕÍžD:K†vñ4Ó;Ö*¨ËÙh÷¼ ¦#Ú¢:·TãÚåðœ÷â«„V0¯_ Š×[dDÌø}Ç‘LUŶ]CÎ>+ +\•Ñ'*ÅSÆ#¾Xó‚|F?·`Š}Ê‹’QJ¸$VMõÄ$ijyµpè|šn‰+ô¨õbO ®©@Üa¡Äç¦ë™D<$‘Ú¡AÝa<8tÛºim3Ë„2{½‰áw[Qƒ·ù}Ò¾bÒÁ³ímØ´¬NÒ´œAC9—§ñæDây@/#5‘ +£óeôq¢‡#jykûଧ³ŠiÇå|ÎN`‡›ãõÁòLÿ«ëG¾B]´‰}¸Ùh¬ƒ’qêªXÃìì.‡' ‘Ž©rz¾µ‹oÝ;âcùf¾už3±éøÚÆÝ `Z«왿öÈ©„LÝÓ˜%¨H [±\UQŠhlÓpfak Íw3U;«ÆÑ;ëŒà­—a_ CRƒ•zSließ‚›ÌPÅÝ ý4="×–³€\LÞ–/ãÓè“ OC9ö‰œ‡í> endobj 169 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 170 0 obj<>stream +H‰|VÙnã6}÷Wð‘*j…Ú¥`0À4™é3Aã>ƒ‚–iE­µ@¢“ŠþF¿·w¡l9v&l’¾Ë¹Û!¯~yD5.~Z-®V«Pbµ]d~‘ ÿ´ˆ ? E”ùa*VÍB‰j¡|¥T"VåbIKÐzY<Ê7wwâ~èlWv;ñŸð–™ŸÈ‡û@Å*Hý@)ñà- ?”¦·¦Y›A„(I4ç}]ýºÀ`HÎy•dà^D‰Åè¦aßúVèU†Ê[ý‰àcD´bøi‘øyxÐ/PÿQ®:ë¥~&õî~0^ìrtŸW*÷ƒ¹ƒßsÙn;Fwš™ ü|nšP¡ý˜òè`½÷°Yñ–†ìh‹ +| ¾áWýò0ˆý(<µ ¯+ÖY ‘æÊéP‚„ã½×ƒnŒ5Ce®9*ågwøA\@jo)ˆ e¥Ï] I(¤ñ|²ˆÆA²JÄ*(ÍàÀžJÏì)gï7ã%`hì;ún1טò2nxãpAâ(U–åh’‹È+Ô‘J8ŽX2¨Ö»—ñý—OïÊa·}ÏM‰?x6ƒ®ÌM·o-îÿùð\Ñæß3µ0Éjw-¤ïYïHeÚœ«Ì=ÝèÝϺ´Ý@:¼œk¬~8ÈÝjkH +W«º1—§HIrÚœ ¾E9”ÃûW]D"¡ŠÒW¥\ž÷Æò8Aòãߺéwæz²6•« †,H,’0ô±hÍ‘..mž¾(zBú¾²øòi|”BÌË+0Q*°´Q`Rƒ9Ù©ŒÂ ò£Ð¤…âêTáPQ”%Ã(š£¨”dA¬¤8ý›ñ2PËHý×Ir AÍ Oa½ ÇW—ñaDÞàN}ýMät¡¨Ñq@#Ð[ÐTŽåP÷¶îÚ7YbVZ†ÓÞÝVØ'#FÓŽ8㡹ƒóT>éQha±|¢Ÿ­ôžä*C_b[[k6h¤…z$ÿaì«iðõ²ú/¨Œöð:÷¼xö"9ÁŒ$T¯«ŒõrÈâÜók³íèêp´7®\^‚Y^e?°M¸µà¦…;ë»[å€å`µžr²v(^ kPM3l VÚðžSᜀ¶hêràö¥#V.!Ȯݰ¤õ·&ˆ‚l”`n6ë#ÀVÛ"œÌˆîûÔ/‘5N@GR 5d¨¹ƒÊ¸À楞9/³ +—À°{—©©Ý©±r#`dDRÁ×5iž­9לÄnË]L„0Ô‘3fcºÉ½×c©w>rÄç΢ñ@ +ê¸éM»Á¦†¬D×RN*@(÷$VA}ë Q…€’ïhô·©£œ÷“ŽZ@SÏhjtvº¡ÌÓ+^&v¤nï¶Û4d~ÅÙ<šì ÞúøD«qÒK Ã-_€ß*3RK"ÿ¡¯Š$Ÿ›ˆ ! Õ€|óRÛ§3Œd!ðá…— 7ß®«ês:vP´ÙãÔÄp“ÄYqšJƒ¡Ø‰©Úð`G#JÛ¹óÏ Í£±æ.k@µ½ãΑ¨7ÜÑD8HÂ¥nÅšÖ“bc4 +o.öóer…ù"®»e˜³©³ø}uƒ°7xná1ÅýÄÝÁß^´#@h>æÍ5¦~ÂI’FóROùƒ!S·Æòý…/~ФðÎo°©R‰<3„xŒÕõn<ø-µ#k¼ýàr˜ÀÓ®ƒ†³›7=ö]N]ƒ^Q 4HPÐ,W‹ÿ)a…f + +endstream endobj 171 0 obj<> endobj 172 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 173 0 obj<>stream +H‰äWÛnÜ6}߯à#U`IÔ5¸NP¸AÃ^¤(‚Û-|áû~ÌVÅb‰C8uX|ág×çì²mú¦h*ö7󖩈ùõeàG~ˆÀ÷Ùµ·ÌEÈõ]¯ëµnYh÷HnÅy_W?/¢rÅ)¨g22²jjÒZݾÕÊÃÀ[ýnÁG>“ü$EÚóâÂ"6ªjvænè/<ØÁ· Þ¶!4=Ä¡È£YFó×;t·ÌÀúÆ DÀ+ˆ¾’`†¾ñ§4‘9 ]xN\”ˆ”%™?Ädà¥jU­{ÝvÞ2å¯É._¤„s Тð¿#ü úØ/9מÿ»´YÒf»`-Ÿå;QWÜšóî®Á¿à^¼ÑyR$ 'Ø8"ý4ͬH²I¢ÚyÌ̶ysèÞ~úð¦h«í[DDHe€X¸ýüÞ¨u¥7vøKi>7UßáXÝOã+Ý5ÕЗ±³ Û«ÊŽÏöºU;}Þ ¦·óϪô›£¢0F„ü%:ßpÜ@ÝÓ´Ó`TL³³ýŽ¨óÛÌÆÕ þ×4zô¦EûöQÒ¨³â=QòéÃÜûR¢÷s®gl =Œ\à;†±Ñ÷ŒÍCÎ:r.cF¿¥-?M%4ý€îàG#6ûùÿ3aCü‰E2r"yIòÎRH»¢-ïl¤ž 09ËÛ`jK¡$:¾ÒýКŽ•% *äm­0äjMó¡gªªX£™²I…‹;üA~uðMõ¸¡Ó?t"LD"£¬y/<ªOH½óÖ6–€³…ÓN°H3C½†yD\»˜—ðfËJèŽÛ6UÕJ³CÍðºê€ÇkÃn´Úà¸<²ÑœÔò Tè@mQf«™­AÀÉš•S{UV@þ‡ªÁ¿šmÛ¦ÆÝTËؽ2ÆÐcŽvÏÜ“Š¢©kqe6¢·oT¯À.´S«â†ük5Âr&£G ôrT6o1š#ô©ºü¼E= jdoç2³ÓÝ+HuÐ~¸Ñ½gô \K¬¹ ‘ 66 G”k]\ÜÑ"ëI[­èõ„[ú"JàJðRnPFaM÷0QÀGJ^°_oÜVZ€¬ C¦oáÂå‚J) +9f%eÜ:›Òe~vÒªåVJ° b3$äˆC¨V3ªGC ·„ Îí²…ŒR8ÝÐHÞ#–”³C …¶Ö„( D”ÊÑCG¡;z˜µ*n§$i’;¥+­à‰éÛ¬/Œ†;='\1³Ü]ÝôÞ*Ë '#Y懧¥uJ7SŸKÓŒ·‰_—¦¬¤›0à¯=l‚.CÔR(ÃÖø™ ã¬FGZ¢K'ZÊ,ä(\G+YIÅ;Ù +tûœiS×\`¡R÷ˆŠ×OÀÑ„ Ž}hÒVAà8ò¹kâƒ^QŒ\ÓÅ1&3dÜO³,ÃVÈKã>ôÕ@{qù~öîüòYjß(aN¦.ï-梎‡=xàpû"!®Ë¢mðI€© 4‹“‚&ôÅl: ZFA y ]ÚX)Ý@G +Jö\YœÌèà´îtו{g=Ðᆪ$=î?ÉXPG΀T·®U)Û1h.¤GßÓÆÃå.º˜1äOÔ‹lë.RHçDëÑ'¤ËR†a*’Éÿ–Eþ/ͦܗ›"”²yȺζe´#—d¼+ɽ@¬à hðÄ< ½íˆ2Yo‘í lÅ2<²ÎZá.à¾9v!{eý°~/c›‰äÀË•íà°`ta¯ן¥•g:…»….S?‚·(:­RúàÖPèíj(b×b¬—‘wã,ÉÈWÏÑÅTæD2H1À´>+·l0 ó`Fn ¡$@ÂãÊŸ÷ç¹…/>LJ땮¦D‹lwÛ½†7ãô=¹û½ü¢¡ïW‹A’+Ë + +endstream endobj 174 0 obj<> endobj 175 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 176 0 obj<>stream +H‰„UÛnÛF}×WÌã²7»Ë«ŒÀ@…´6l"/FÖôÊVË‹@R¶ƒ"ý…~ogg(Š‘lG¤YröÌ™3³³ï~»Öpß/~-ïŠÂ€†bµÈä2…_2â¥L D™4)õBÁýBI¥TE¹ÉÄ]O‹ñáúãù9\víЖmÿAf2×—ZÅJ§R+×A¸”F¸Íàê[×ñ>‘ðpÁ×â÷…F@CÁÙJ2 Q"£Ø‡©9væc+Uyò1“×Ñ@ÓO—‰ÌßÏŒc€Œ[µ÷Ûa³ΛUÈX´Ah5ÓùQ +F2‹À ±ñVÊBÜl:DIäR A˜#¦–©¨@Kó•Ó<`it,£9MµÏÆÇê)”Ê Ò\Í)é„ó»´­Ýàº>cLꄳR2ÓXÍPK/QË3b¯SÞôgÛ)rwÂßq;Gììý˜Vø2”¡®\àUè7-ý7úkѪáx1RÂüô˜àReYî!9§ˆÂ”¬}ÿÔŸ^|~_vÕê”(1ÕHá_ÿ±n¾´ÕГmŸ'ûÊõmµÖmãW_lµuG8&É'œv@ßy5BñjÆk‚ûþ*¯éYñ "ï]ÆeÙÔœö¥oÉÏÅá–\’ϧg[o*w²CÓŠá–Ô>KÀvÎcHblû}7ëež§E4÷›{áâó”ÀNE´Föêr3DþèÉÄ^™>j^®ô°CrpâÜàïn®Dܶg¾mSÑ—Ýzã9½yl8iÊß$|þ¯Ü°íšÖ˜<Í”®¶”›½åõv[U`I(ŸùóŠ!Èñ ¢r= ý wô¾éG,?ŸŒ€K¤L&•ö3jª}´ç3æ^Bñ€(š-N]4:hW³ð>¸E]÷`íº²·•ƒU×ÖÈÃ÷€Ž.CÙÖ5N’\ØæÎ3 úîì`¡C‰8†ßÁªíÀYZ•¢ 3"Ö£WU!h&Ú§þµÙ4››8Â4ÎÿÙ×DšÚ'w–ù+5ƒÕëÔ—¶"ùêu³®·5<¢#«¯_M2Ì€3¶÷ŽKYÚ‚TÜ:([j°æÑufî~#™Ï>|Ü©¬_anöÌÈ|wŒ9ºñÆ) µ}ž˜Ï ñ[¢„lhÕñˆÄÆ—ÔŽõf6ñ®ùq[$ÆóL6Íý Æyžbóz†Ýä‡åL¹Ò™8Ü‚múRpäFŒ³"ÌTŒ74áV¶àŸΘ©Çhi Á“希g©ò(Ÿp¦ïx÷Z¯ƒõ2 QÎEµnî¹”‰hGÿ%'€ÛYíŠùãÔ8ìѹ oÞ«»{ýÊÕ–(tˈ¿û¼R§»úxë~~*ÿÅXzâ + +endstream endobj 177 0 obj<> endobj 178 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 179 0 obj<>stream +H‰„WÉŽÛF½ë+êØLF4›»ŒÀ€7Ža{` ÊlIL(R [–ƒ Çü@ùÞÔÒ’¨eÆ3€ÄVwW½Ú^ŸüüIÃj˜¼˜OžÌç!h˜/'™?Ë!À~ˆg~B”ùa +óÍ$€Õ$ðƒ H`^N¦üˆ·ö“Ïêù§—oÞÀ}ßÙ®ìø¼iæ'êÓ½â@§¾øäMg~¨ÌÖšÍÂôÒ™H‘8ïËü—‰F!+—§$Cõ%~“šèÎHw@ZUyóß |,àutÀO?%~Ò}A‘õª^Õ¶hÞ´ËHʹá!çã«|3[?oÑLošø™ê<íÇŠ,Ö¾7ÍÑš¾BõE,º¤ÓÌӱؓ1àlq¸rFêgæÁØÍqø¬î‹¾ØkúÁ›Æèõ§âÏÀÏ4nª}ÏÐm¯Ø +Ê¥÷]ë¥þL/ÀOwCGr˜ýÄ°¦·ENÔGã%(cØvüÝzx^«Á‹üÅóÂABû´3ð$2ȲœDŠMWáùi?<ûðö§²o–Ï Œ4ãP´ýʬšÚ–ë×m±hLE?ýõ›ž¼ïþ¾º&ùÕ½y½1|Éâlê²ïSvm5\ßë}W|»‘fÚsu 7®ÏKï 9û²k[SZS½+†?ØÈÝîìaõkÑìÌëÑý¿ùéL‚üt’!k–r‰âŸ[îxÐβ/ûgÙÉG J¿Ÿ(Rt3>óú[±Ù6æéAšDÜŒ}iäç1$‹*>¬¾Y°e]@@ºöáíÑ9ð¾»±Kú F›ga¿Ü¤ðœ…àÉèÂs–ŸF0þ‹óÑóltú…Csöœ=»Ã#Ÿ|‡VâI/>Ò\$UýŠª:UCÙ×[[w탬"îŽ=!šühì®o¬†eçÅÈ ý¦ 9P,d½³`×–µE?A%Âãè)Øv½õräi„ëTŒ&r—FŽÓÖÑ“  ¨—#isÌÙ¶6Ì€9=ˆ5_×xh¹±¶nWЛ%R+ØŠ¦&¹Tõv€¢­X˜ù©¨„'©¨ˆ 7ukXö~mì%B‰êQÚŸPP¦ˆ’t7ݪFšÎU ŠPÇj»mjã4OuâQ2»Ž‡>!ˆêÄû¡ðs®€ñ%yY+6¶\óOE»âoDKú“ã% Ï¢¶ƒ@úIŸ!¹;R\D(ƒz‰D7§1¶v­Þ,GU(j¹Eã ¨‰œò¤Åz¸Ø³â~+²$¹d|"Æc¢¸Ùñ;ûM1ø/ØÆTU¼dµŠ×­‡v*(ÐCâœXa«Ø º²Üñº?x‹ŠšãeV^'ŲëaaðÓ@Ùq:ìZCÁ Æ“Ò¯ESWœ„‚cñ~pR‹š#aáõHÍ=êÈä bM3Ù;rɯõPw|¢ýQò_Γ³•lô5/ rX¦¾^q²–T']Û“䇺x(-ÿ¤²êö°å è$Ïz‹LGY)—YFbûªrZAKÓÛê<‘ùG§Ò°ØtèPè–ü;I¡}Áš¸¢±KÛÇ7 +z˜skÑËì†E§Ù£5-|&ÉPH°àu²´¶1l@ñµ“BÆDz¬+çZb¼½ ¶=Õé%Ðô’×ÜàÍdõ•[ +É—hQ0¨—Ðv–L\ÖˆQíz¬-Œ£D1U¼;ˆ±8ˆ§Iôhí¶[)“éú:æ¯Xø ùpOê³CÙ¦v+¿QÙ 2Iѹr«Ž‚‰2£!ÿrÀ57|s{ʼn4ÌÑ™¯‹r-ŒœQÊ8ŠDCíejÕ^¡Ë¡• ¼¼_Ó^)>XƒãúšD³8žöª¯áƒ)‹m¬Ù®¹ c1ïë–W•ùvçÞg´ŸDù™°ô(L;{Œ¿òáù¼¸ƒ—wDÊØòà–8ôƒ<ʯÉý:Nº³' jgy„œ—ÄA”Rœx]‡‚Wœ{&m­ÖÞé•\E9UJN­¥" d«…!–Ú ¦òa”‚û;$–)ú{Çïxb¾õD™¾âÃëCOî%Gïô2?W& ‚1¨Ø-Ä]^Õea©¥²EÚG5Ñãtƒ|n)┟e×÷Ò”\C9ã iI•,È\VÉä¥G\èçòØ!¸ù¢Ä0ó>:Ï—¡ù‡O/Ç%ßaDø†E1NqB0cÈö1J>Cµ/SL +ç'`² {<11ÃiLKÕSí$½°Fµýw`M…Úµeÿ±šü˗ɳžäÆàé,‹©±Î);i2½AΖ„"æƪæB&DÊd¬—d)¹Cc×ZI$ Ëbwj_ä ¸÷{ž/1·^ò[ªkétNJ÷ÖL³1ÃPŽaï Õ­¨.í¡ƒŽâЊ&l3 e]“ÖºÖÚy‡Ý™¾èåû[ïV§*Ò‰ŒëͦàÓ³ä?†§ÕþCCûèíàõ|òÿ a–( + +endstream endobj 180 0 obj<> endobj 181 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 182 0 obj<>stream +H‰„VÛn7}×WÌãnÐexÙ«¨“ u 7®µoFÖ+ÊQ»\Ù1‚<öú½^$1’åH€–;œ93g†œÑÛßæ ô좞½­k êå¬ U ¿v‘V$ç +Âs¨û…‡%”Ò êv–Ø%Z=Íî¢_çï//áFÓØŽüqR,šß0šR–F)Ìã¤"<’ëIö÷R7:"2pñçúC@n»UV {©qÓ;ß…ñM׈§qý· >uÁ3±°+~^e¤äÆÞE, @t;vÝø(Õå°ÁÀüÈœñŠ°c[KܬÊÔñ¾[[ÊqR"“1føÛ#ü³ctCFEJ÷\ÀSñu8ÊEn KFÃl4wÑM£š^NRé8I1ég.” ë–0ÂÒ +³öÁÎrgôç8Ä9©"SüõNY8e›&Vò2õP·2ÎC¯GûbÔg‘ŽÉÞ¾øó÷´(Jé8WçÝ“>ÿtõ®UÝò܆ãÂÌ™íkf~¿=6ÝF~?ÒäY¹×ä¯i†˜'0ë7~ÿÒnÿæ'×Öæ9o›N¾ô·UÁ™wmL`Ù´Ó¨N³øwïîÀ{«Zu~pâ¬-§"­øîh¸»PY_›~Ýɳ-£®²‡·‚\¼+iY’"ßß#v|áÓUÈAËàš$iAwÑ£ˆ£ˆg¡èÂháIËB·²Š…iÍ ‹õ{âøËnMÊÁÖ&€DDüP’Š@˜z¡H!KË3¯šcµ& …å B^¾€É«„‚{a˜,-~ðÂ…²2”æ(¥Ç»>EÁ ùI×⥠‚\¸¶ñÁ´<Ò­Z­§Õ8œl[öÈðÂ5Ü[9mÔ !Σv–+ÛÄ6ªA‹7€–ÓääÆå¨`úb·$¨±³ízæìAëÌ”’­·hº/^Ù 8Ž@QeHlwüÅnr?§/½ k£‘X´ˆÁj@ß~¿ù‹k5.QÁîD*N8z~ƒ/z%׸­šÐ´™`5¹¼$Û»pˆÏ‘Þ¬mðëQM1^<ìÅ覈 }Çß‘{Ä-¢r`Ÿ­)‡]m ØÚŽ}ß ŸQ—«-²G0 +v±0}?`Aoh+¥ƒ_SãƱð£ÕdË×A`0»è]­4àÐ]{±œÔ3–©I5 +×Wó_L€L´JvÍ$­ö„iˉM)ÃÄåä{ÿÜù·dÑG4JÂ8tÏ ¿®ñœH+^v÷lÁÍ»ñ³ôþÙ„z\NO[C²-¢÷ñàOÀ‰i»ö·²o0%ž ÿMý£Ï°Š» ~Ô«_ãnãc=û_€§òdÏ + +endstream endobj 183 0 obj<> endobj 184 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 185 0 obj<>stream +H‰ŒUÛnÛ8}÷Wð‘ZÀ4I݃"@ÖÉn½Ak#úô+S®·ePrS ØßØïí”e&v“M€˜Îœ3s9äÌþ\3´í&¿“YQpÄPQMR’gˆÂ¯]D9I8 +SÂT4Š¶J(¥1*ÊÉÔ.õ4yÄ7ëùbVºíÛ²­Ñ(˜¦$Æë£e a”¢u0Í ÇrßËæo©7˜›tÁç⯠ƒ„Üw«8…ò(ŒI™2«šÚÔTÅ<ŠŒøȉgá˜À®œü$IÆG~føøáÓ|¡ª ! n]ùçÖA7‰}²³Úêƒcüqžƒilò ,ûŒÓ ¬òÏÎØ ],Ž óu,¡ÁÑpg[’%õ5±Ø9Z -ÙKÝSÐŽ¯œ-JRÇ7e„E9ˆ¾µòYâH[{cPø;08t`ƒs²¦—SÑ!Õƒ bÈÑí[û©À3Ü!lt_Ià O)išf&¥ód÷y8¤öÝSw½¼W꺺¶JœÂY Ø„?‰ú›üÐn¤ùòãæзèwåìƒPQÿ{Æåq6r +6­n˯%/ÕlYUç¿Üºý¡£?[~“z¥e×´ô‰Åo>œý_¸53$ßK5›×m'7¿D²×çv-‡¿Íñý¾—öÈqªj[4oÛz§¶·í“º$luhö#ü¦,e-5ˆÚ÷þ¦º±„BäìQw·æv·Ýõ¢^¨ý¡§¯à¶O|&{“9š!¡Ô¥¾~qI-ƒÓ0yí¾¸Ûä‘Übf_Ë«c6F]ºÜÞ÷%!É"E!‰’ÓÄü‹á ÃåAï%Êò~tƒÐxmù¯‡8]ƒX*/4\ä~ü¦>±#è¬ø‘Ž\ú‡—¸#€ì—ÀÁúÈ/]A×ÅÇ0t¦3 ‹Ðå˜mÎ1f;Æ‹>kÉW£ìyÔ;«7^|žf$M¼!º'÷Ö<¹ îJ½Û÷»V]xòý¾ããØæÜM¯Ù´êPÿT%¸ÌMøµ";CSL`àeÃ$=²7Û/< +þ€ÄÖ°Uí® Ý8˜Ïç¯÷Ú0±œûQ¸+&?”Ƈt + +endstream endobj 186 0 obj<> endobj 187 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 188 0 obj<>stream +H‰„UÛnÛ8}×WÌ#µ€hRÔ5(¤i°ÍhŒXè>}à*tª­.%5],ö7ö{;$eYµãF"*œ9çÌ…ÃÕïO½÷¶ðVE‡bë¥4Ï€áÏ.¢œ&!ˆ”† ÇàÉc”1CQz]¢×³÷@®6×··°ÖÝЕ] ÿƒ¤4&›5gã åŒÁÆrµTó—ÒA œÿ¹øÃãZr·ŠS¤SšÆq§†›V&~ñ·9ñ\Ìvåä'yL³Ðø?ëJ뱿m·~B#Ò9ÞŸcæ ³…—‹3²´f•¹ˆv¬Ä4%ÏÍÄÍ©dSh^!ùìâ:’œļ ìLMå8IIBSH2¶”Æm5ÈZjÙ¨AéÞ"Ìý…‹ŽÑ”cùNy”còÞÙ(xâœ>v-¦"'ÊgøwòpÆÂ;'+xŠMP÷Ê£ßuöÝúhÏIï š ¼ý˜$a|| +ðÉÒ43.&aiçruožûË»oJ]o/­'Rp+ƒ˜íë/Ò´ÕZ«¾µ2ÿúwÿU k †««ÿN`Â8›aÞ+‰9Ü r{‹q·Ý®þ”ºY½•_Õ©ïO䮪k©ÿ9iWˆ³ô.~s>ë±Ù±]•¥ª•–CÕ>­îÚ—¼>Éú›ºîÆv°^vuÖnÝõÕPu­KÉôñz2ÎFZêR_uª5 ™H^ow rksó]6»Z]ìÑ8sp¹múA³bŽ,:F~ÔK‘B)ÀTœñ‚»s²Žú܃)ZØ,ë +óƒ5[â¼ÔG6‡¢œÃ9”xi[ÌõÜ?lo±ÈÁ+%b8ú¢yÄ +w ß™¾ÔÕÎ0ür ¸)`-Ü€¼WèÛü„”£ Z»¹*˜½Þ¥A¶vëÊi|l«'7=Sš >¼g3S˜:ªÑœLAµ…á‹‚^µN¹„v<Ç85~¨ ÉíÌCÎŒÂF©éJ\TìШ ºé²3¨üæ;–·•õ{©ý˜> endobj 190 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 191 0 obj<>stream +H‰„WmoÛ6þî_AôµE I½Úhti¶xAР6°iÈh•%C’{CÿÆ~ïŽ<É–e+“ ›&w÷Üy/› ò\~™.çsI™/G‡„ÃË Ü1ó%q&}2_8yqÆ9÷È<Ùf»^Gôãìz:%eQq‘‘‰eÌ£³Á].|&8'3Ë3IÕºV«…*‰Ô2Õê¬oóßGJcG^æ‰ã1ÇÕfVh;ж¹¶Je`ÍÿÔλè¼pö +ÌÝ÷Ç å~¿ÐûéÃÍu”=M-Ÿæ–-éÒòؘzˆÎBHÁB¿£ +Á»Æ= +1 kˆ€e{,]‚¹TC0˨Ô_’~C°=_E ™×õõ“4(›„ÉgñCÞõLx ʨŒVªVeeÙ.äc‚à8 ¤ÔL¸cè'!÷iÕùÖxfŦŒÕ4O <U,ztK,N‡à˜í8"˜óþ,"#V©„¤–y¢¶ˆŠ!pu@Á–}ÈsÐ)–¤E•1JÖZ›¤tùh<¤ ‚°[¥†s€ÑÑì¡æOP騋Ѹg¶–ÄF"ÃÀ…$u¸{$AÒú—¨Ú8 %ãBv0…9¥Í¯”%~Ee%ØÜo²:]g)ˆJÕuš?W_­ac¡›§^YØ©ô1ÿ_”©ëj]˜ï\ÇPÐ +07mI@}‰¦À*y Æ{tÂÑ:Ûƒ“/‹÷¯ÕÕç»÷q™-¯Œ3ܸ¡÷P½Œõ£GÿìKImtöÌBI"6#Ûþ–ŽjÔºƒÇ^­ì$yº½ÜßOf³sò÷QUyð=]”Q¹žûqâ½ôÞÃu«ªz(ácS¢ÁÞÜ©‚.üy”í*UièOž÷wû ¢ï·*}~©Í®£™s[®‹¼.ÓÅFCC¨‰³6TýR$¨Ü Ï#Üc™æ™ª…ŸŠ·aQ‰¢òœi#à €sÞ·ªŠžU㜟ˆ :—qyÕ»4ˆäŽ¿?Dƒç¯§±‘¹ÙF«u¦&­6ÁQÝØÜ¿câÃ5èWr溂è#kŒ æáu%ý çÈ~ÉqœÞ%ÙUõùnBðH‘ÎÃ;«í!X…DŽ0Çmó~â|bÞy}ZzòÕީЫêg~¤ _ögDŽjmôEºEݸÁŽu˜Rò‹¸»*ú«òt•wsÖÊ9CrM/¦r;rïÞµæ:©níX^Ò>á#zûÖwðÖÿ¤o}ŸVq™®u †»þÀA84Ó–H|Q«È´ôÒ4åïÕzȘ)jñó—º·´hµÂöž›Ï›;ÕjmfTœ.uëõh ,úT”™_ÙˆAY·½8ªâ4%šuf[# ¬ ¬ÀœéÓ.Ð@Ù¢C¢,+^u +:ðJþfFò„˜N + éZiÕ¤GeTeY¿"ë1í]PEêæÐËÍÊç°£7¾ü?g©àèÂÆä*¤24(Ä®íSëF†¼Ât³–§àò²(WzÚä$ª‘X©ivU³Ý-¹¬T÷ç_@Gê—¨!:óC ¿öæümþh ;’¤ dC¦¥F}’#­Ôì8cdZë|çEMV*Êk-¾Pmœ“'5uøë¡©‰6¼Ï]ˆ Ô\ñ”,v 3×9¸¿›5*kI#sÅøMJu3ý7‘BMï + +endstream endobj 192 0 obj<> endobj 193 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 194 0 obj<>stream +H‰ŒWÙnÛF}×W ü4,*šûbš¥›&1"5}0ú@Q#‹ 7#»BÑŸèC¿·wR´%Åaˆä çž{îvxùnኻ~öj9»\.=áŠåfÛi"ø£‹ µ#Oø±íEbYÍq7slÇqB±Ìgsº„·f·ò§ÅëëkqÓ5ºÉ›Rü'¬yl‡rqã:ãF¶ë8baÍSÛ“ªÕªZ©Nx¸Ç—xœõÇò—™ zdœ¯ÂÌ ?´ýÍTl;FÛZ•^b-ÿDð ƒwý=_1ü( ìÔÞwü.+ꢾySë7‘‰^Õ}cEv ;†t æ)8s6BLÝȇ¡ÆóÙÒu-šn­¬|ï„nD–ë]V–{±†ëz¯·ˆcGë½5÷mWªÍ® ¥Þ2°ézÓ‰Œîk L%Rdm[`/²}' æòAäyŒ¨ s<Ë“™.šZX‘¬v½:ûª€8Ü•#1…¶Å§7µ"S ÍÚòàʳZ¨{EO;kîLæ|7wm7Hc„èÀ™À¬”(j†àn=Eˆm%€I$hº¨”-~ߪúÄfÄñX©‚ÙŠíÈâ)[þIm8FÞYsW¹R‚or ¬üÛ‹…δ‚P•% +6q©¦ÍY/®ñÕßzEA‡Þªnà˜þ&`iÀ0ÇRÑ Ò.1hìž-–Û¦W#cµÙuØA.Ĭd›­JB …f: d¾kè8dó­Üƒ}LAN›:«Ôâ^u=¯Á£ ­ÀÏõÈÖëNÑmO/õ˜M@ÂÈ$f¾áÃØ&>üøIú:ÌÇqî! +{¬Õ`ìC­ú“Zu;ö•àÁòµ©ÓIÎT{ΉÈñ§¸!“s“uÀ‚¬yÉ~Åp `.´Íi|ŸäWlZgÛ~Ä‚ +´\Šq3wš˜\èIê[z®òb³·Rˆja9° ++zœ”‚ã#-ÜQèNIdz”^&€®£Ç‘¡ä¹ÏŽ8ñ²1¯Là“Àqô‹I“T6˜ÏVûê¹N mì»|½Ÿœ‚ÛêS95%:P,©UÄÔŸ8:G…Ç#…ò`~&vü3Æ(•}ÛÐ/§K ùæjLI×áŒJ)›Rùvˆ0Ÿ !_<ô/?½‘wåæ%Áa¬¾ï#‰Ë ÕYùq‡cïÿ6š&¯-€ÍG·y÷ò™\7(Û€§>‚;G ;Òž·eU[ª«ÁÀi߃$@3§ !rÕ÷âí^¡ž¸Ð.Ž‰˜À×Nõéýè¸SÊ„øõCìÌac›ÿ>‚Àµ£èy>¹+ßp1÷yW´X6':ôÓK‡%Lcžû8M‚“*˜èmX¿P…E/êF‹|×ujª4Lo«;Üê¡(ü(œN¾x4ïš‚ÍÊNeë½ ä)øGm€?öþr¡èàä†Øq„“ œJøThW5L‡zÓ@ÉV•ã4©×€6f?7àÒZé¬({Óc@©{œv“ÃÇŽ¶)ê5¶ÄšÊ…/I æ ïðyÇmù"®ê Z%™R@RÓ“Æœcf äÔ7›Æ¡S⳪2ˆ\–1,_û+ Ûû¹Yq.#†aáÁ°p°ßð¼õ`Þí5DZÞ4L&e@-Ž}K=6ZÚp7••ÐF3-)†‰ûmeT¡XDN•À˜áÉmÖé"ß•`Ä.´aø-‹œÕÂj Zö^æY|~`®@G¡€)Myõ‚›û|Œª”-TŒ²äK3ˆ ˆs©ê;ó™À *Nw;žL[­mÚõhjø'§†ó=ñû™‰¡GQàpÅšÐTÆÑô=ä¹R°´¥uô%ÀaFç`€Wæ+B‹mv?T:d„“ž“+b¹W¥Ê5ëÆ5KM[ Ò +©TO¥tÐò- +\ôèVó/EÏa˜¯scö¨É•,:þ¡È»æ žà µÇ7pÈHŠNÖAîîtSA.å$d=Yî™ã¥©ù蹕ÐHëü"@¬:o/ LI (ÎÊäQ²C3¨²5ZÌfÔÓ²jÁÚª( éŸb"£î!£Oóuò¹±‡L_ˆžy؆n(d.7ËÍâäËŒó ´o”¤Cnv|´ ¤R váÄ-1˜µáXòW<^*ú~%°ovœà&øJ ‰fì>‡¶S-‘ +©´¾|ÐÜ–ŸÖë’:´ªël§Xâ…> endobj 196 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 197 0 obj<>stream +H‰ŒUÛnã6}÷Wð‘,j®¨ %A€vw[¤ tƒDo‹>(2«ÕÅåuó#û½ ;µ7-Ä$Å™9sæÌðݯF<ùÅÏÕâ]Uňj½ÈuYˆþh‘–ÚÆ"ÉulEÕ/"ñ´ˆtE™¨šÅ’–`µ_|‘?=¼¿½wÓ8Í؉oB-sɇ;¥‘±ÚD‘xPËRÇÒmg×?ºIÄx'‘èNýQý¶0à0¦à¼Êr/’L')†é9vŽ±#Œ*ãRU"ø”Á›äÅ­¾-3]Ähÿ‚¸YÈ{×¹Ú;<„ôÏ X [D§ eÿEÞÕSÝ»ÙM^-SÈõŠ³ˆtn€®¥Ñ&-ì‚m,ý>ÊêR:Áÿ`Á—¾LèÖò²«(¸ºw*~;Òï à¾‘^%Ú‚{Ú|3uèÊHàãzïo>ºn¦n}s ¦R°Ùu£R]H¸ ¿±¼QKó:¡“PIlÔÒU ÂËåÔà~oø˜­«¾KBr¾¤;ÿ®ûmç®èîwø2¯ÒL%•ýºotÔç›ULÕtg¥o¦v;·ãp¡dÿ‘ÊÊ$†ûYÅÐ]tã¸óÆ ïøè5j“Õ¦UÈ|8ÍØÃÞÊäie=¬DëÅHV¤¯BvÏâkݵ+B[rˆ^SÍð2†'ê5´ˆ¨…ß5 óïýz×!Ò÷#í‡A6¼éûz Å +ãY©Å톉•Ç R9&€èÀŸÛ®cÞ–ÒÅža)2.ßÔÃЪ3{àjC]T‡.½«¹½Ô2†ˆ;ÚL qЫë.7Ì^„[ž]= Ü”+$|`Ö¦*ÑIV&'\GH1cª±ÇLg5B-ŒÜnÝ +ÂàêQϪ„:ˆÉyþ¾›€WK ›P¤ÒÕŸ¬Än«ÄgS =ÖŒaü2vݸGa7˜è¢G5„"x¦x§KòM¡S›Î1¬sÍ øƒúÀ¦€éZ4i_`ßSR÷ÔàökXS_c N‚“¦§’cáQêQÌ)ʨrY'Ànëýέ̤Pqkò³)üßM|x +î]‚aH­ò—¿¾Œ÷·¦/ã Äß®ÙÃÀ¹¡Œén¤½ŸQ©M7zþN=q‚Øè H‡g ví@û'¶Ä¥ØÔ[–#¨Ô¦æDŽ'p,ÃÙ‚Óz5/™äÒ’úd¸ß„O%#(F±A,”’F +öu<ͬٺՑXÿzr.̕͸w_’¡ð qÁq ‚ÝmÅã3CÆPmÐ0´ËOŒl2›œ +ê|ÖÖ< =ºœxò6V‹{´[£ + +endstream endobj 198 0 obj<> endobj 199 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 200 0 obj<>stream +H‰œVÛŽÛ6}÷WðQ*bF"e]A€v“´i6Èú-èƒV¦/$º¢wQô#š‡~oçBÉJœÝŠÖ$ErÎœ9œ™ç?ßÆbç?­Ï×k%b±Þ.2Yä"‚?$…L•Ð™T©X7‹Hì‘Œ¢h%ÖÕbIC8uZ| ~¼½~ûV¼ïlo+[‹E¸Ìä*¸}GI§2Ž"q. ©sìMsg:¡pðºð·õ¯‹.TdœG« Ì ½’:A3 ÛÎÐv„V…ëß|Îàc=¢çÃO‹Dj:OØq¤2†~kZg;qmÛ¾è׶iÊvãÓ™™oÑùËc>ƒOԨ߿ޱµumO‡0•YÐî•LQ¡!¶úq¢*[aÛú^ÜáZ¬*ã˜Bæ‘JÀþú•ç^m)¶å¶C ÇÎ f#N{ÓŠ~oà¢8δ4páRCÀeÄ“Œs°ÉóvçÑ›EÁð­²ì2Wož¡É{Qvâd»O{úbC8›3l`©S™k¥Å2–qRddMà½MCd•Änú= Ê^”È¥è Ï7ˆ:Ô°Å"q}I>ïà£3}8¬öeKŸw||#§8'“ˆÆ8ëYœ“XfSÕ¤ÁÁ›C]6aLŽh #LƒQMOˆh>ŠT¥|7Q¯˜‚÷eW6¦7Ä'§tÅwE2‹á5e:;k!bç^×}ÙÓK§€=¸mÝÎlÄ)þ¹i¿KK]¸T²À¥íö‹7t~tßÜrÃo™P| > ¹EàŽ–~[ < +O3K8¹š¨‰#æ¦ ^ +‘j™'b•+©'RXz̵i{Ïî‹“{yóîEÕÕÛ—‹1k­P€Ÿ‰üõÁü1×› -ý=;µþö^L«Ž§ÏÕ…,ÆE(bˆ=)hÏë?ËæX›«ÑÄ$Ĺ\©'X7í¥ó3$àÏ£§ßM^ Ášñ>åy’A^OŸö›ŒkVÆ+TF¸ª;ûƒmWu|Î8±öÙtèZzÑ0½TC×7ôü*©;jSñ¼Qì ·Þqt(]þ¥¯ôƒi ^Ð +’è'w"‡‡ô€W”ˆÎôœñ»Ö'RN—>&ŸËz0ØÉéµÌ5£ÿz³£Í'N¸!á‚£`-Öj^AfPô˜Ñ)ó¦>óB]8•¯òÑ?Îâø°÷vÀŠ²o0ÿ®‚¹¨èé²Ç +ÄÉ~´~ñ .3»Ù£ÅR‘§C]ÃÂÑv=¹ÏŒ”¾DcwQdéÜ-}vË÷'7­¿Ø“ù }VýPÖäÝg·b¢*†´§êñb; +Ê ··C½™J÷àPnm¾¢©•Œc_2p…ñ‘¾m·Dø<*ž"ø±g‡Ü=¯Vûζ…·a‰‡Óžºrg¾R:8æXæ¯b^c’3ΩuÚ@qx—·ã[XÐ×6ˆúðÉI;ÓšIeû”šÉX­òÇ•J^8”úÓEŒXzé; ±Ý>¥¨ê&/Hoä"—‘.Çš›£Š›¥sIgO=…¿µÐ›Ì[,jáâ°”í= ÙД +`tÐRèÜ(öé‘F™šu]31ûŽÕù$Š¾ohÈ G À,°‡¨øBcDÿ‰£–œÆøe¸2 zöÄkŸ[Äñ+è +øì dïüŽà· þ;Í!³Ý”›9KqGècN+}¨@ƒS·Ö|^;rˈàÎSFø^™ݽx3Ò•ª<{ü¡^hŒbòáx8 ?{è9”ž÷Øß©@v‰4Vn¡A;•ÝÆ_Q^¯ÿ xGdÄ + +endstream endobj 201 0 obj<> endobj 202 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 203 0 obj<>stream +H‰ŒUÛnÛF}×WÌã²7\.¯F` mÒÀ ß‚>ÐÔÊbÃ[¹d\#Èoô{;³CZ‚-Ç…iµ—9gÎœÙ}õ~«àÖn~-6¯Š"Å~“Ê<ƒ?nå2 A§2L h7ÜnA EµñÝOÝm¾ˆ7Ûß®®àz짾êø$FÂI|&„\Ø¡w¿WÂzzERk^(’ +X¥Ü)”C¢eáw"ur”Y‘ùõ½üôáu56ûKG‹éj­‰ eÒÄŒ4ú¾žå©'ÇŠ_pó“¿ÕxùB™iœG2HK9–ÏIÄ™änÏ»ÊvhÌÅ +q^…8 eœý\ªÕãÜOx`6?9 Ÿ><ä ÀÒÀñÅÄu Ãìå´ºfg¼%g$ÂVc=Luß=koæMÎgÚJÊqò2QWsSŽ—ç}[ýó®øÙ´%öD&F¤Š¯ö}›‹gûðÏŒ‰Rß}œ1B*¸áRŒ‡Ç02/yÁÃ0‚š:s4˜ÂÎÔ ØžŸÚñ_¨;˜0æhìàæûÎýXµ¥ã¶ö0âÐÜ3Ño©(ÒX²³w‡ú{æûÂNf·HèrîÜôä…2ànŒQ›ÁÍM]•X:ÞÈЇÞ-Í|‰4;˜‘A8*þÊå¤&\ÞðÈk©/¥ç2S”Ù~ìÝ°õ¼ +aõîU·G¥BÑ +Zõm‹‹EÙí t'p@ø¥½ç`Õa\Â.‡fÆð|ÉR©Ò> endobj 205 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 206 0 obj<>stream +H‰ŒTÛNÜ0|ÏWœG§RŒ/‰“ „ÔZQ¤‚ؼABp %—Uâ­ªþF¿·ÇvÈ.¢@µÑÆIì93ãñÙû¼âp;Š`¯(p(ê ¥y nçT ) +Š.`p0ÊK ¨‚È qÕCpIÞ¯>žœÀù8˜¡Zøa”Ò„¬Î9‹W”3«0Ê© zmtw­GvŽ$.üV| 8 +WÜ’˃L¨Œm™Î×Nmmf«)Ââ»%{ò\.näé«<¡™XÖ;î—äSÓ–]Óœè0â¤7¡B.g}ÑtÚÓÙšòŒ˜¢)¨ŒÍ°Î>ãž—cÙi£Ç)Œbt`߃1šr41â”Ç9J8ší‹“Bx'-0¡œ …R¤ b|Ýt×îš6Ô0ùïÕлûMˆës2à^ûokÿ̆Ú*Õ½™`è¡F:3òd¤#ö¦‚õ4zÎ×ÑU^ä…­‚%׃»÷¶'S(©²vÚ‡ýÅDμ‹¹s0%iã?ú­ûìWßäê÷áàa:<;=¨Æ¶>t´6|Æ?.‚¿ Gœ› + +endstream endobj 207 0 obj<> endobj 208 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 209 0 obj<>stream +H‰”WÛŽÛÈ}Ÿ¯èÇfbqI6¯Æb'k,œ…w™É¾,òÀ¡Z#Æ©”gA~"ùÞœªjRÔH²±!Þº»ª«N:ýÝOw¡znþtóÝý}¤Bu¿¹Éü"WþñM\øi¤LæG©ºßÝêñ&ðƒ HÔ}u³â[Ìzºù]¿»ûó‡ê¶ïÆ®êõ_å­2?Ñw·aaê‡A î¼UáGÚîG»{°½ŠhŒÑ´œ÷÷û¿Ü„X0bãr—d0¯L⛘ÌìÄvF¶²ªñîÿAÎÇâ|hæøNÜO‹ÄÏ£y~DóáðzýÎ3p±-›îQ8†áÌ•ÔÏTšn!Þz˜ÈJ·e_îìhûÁ[ÅXð­,øYˆ°­B?Œ 8ý£˜7sè¢LB÷ f{‰ŸjÄ,Ê£ÔéΘ0;¹»ßZÕ–;/ÓVÛrÄU;[žÜ{±¶;ÛŽjØò‹îЬeÙ«ª²iÄÈÚy'ŽvlÎää™ÏîƲ?–Ã/Ààª_ÃXbñN¬"¼ëMõŽ¦²¯ìSÄ>‘;ø6TeËïZ{êÒ"V ñè}»þH¦b=©B³k&*¾4Û®kùÓpj‡›\—… ³s/yàßÆCæ4»é|Œá#_œ£÷ üq껺‡[Ûßz9lÉ5£?Q"ã½Hõá?(Y°ÔÊ9Bšø0P$Ãô ‡\åœì˜YÂ¥ÏY{¾sø”ÍÐ)Ø:²cô“dí +}u§ãZG÷M츑${Š é‚B5P_]ø%Owkþµ™&øÃ<¿h© päÀWY![½&7§Üiÿ¹zÚZD  àò7å@jUx×hÔ×aT%`qƒ­’y5[tÃÔ@‹p˜O绘à`A@ù†Ž±ëPÙÒ&*il[NrBñÒ¸ˆöÖ®¥a0ëÜžà”äA¾ŒÊ9ýr×é_x©5³kXÇz³A°Ð¤¤;…²ËH sw²v˜Þ0;oŽÐbË_g†ÁZWÙ{Wíº¦£A$mf×õTL©÷~WRí|u׶¹Ð"Þ*‰r8¨õP8×Ö­ìÙòï³Gd@µCø°r¨kq’{äÏÜ€ >ÂqŒAÑIœHŒi;ãXƒ±AZ8`ª‹5Ê©ã{Äw¾ºÃÆÉ;61jˆsH–žÌÎÃjn#pRœ€‰µ˺AÕK¥¿ïüXŽÓV–_é|šêDzvœ(D(Jⳤ¥nʇÆ~5âçåvéкŠ…îO"ÏõCQÏæ¨r¦uѤò¢ˆNí.4Y¶àÈs#¡GÔY²¬É×t +ŠJ4]ýVuÇ­Ì}÷8(’ +ÆN¹ªíæêÏΛãÑQdžÃTÑûJ§êZê Xí‘*˜Þ?L7(n[F¯ÈxH´—a¢Ý^3QƒÒ¾ +?)¢|B§Œ–ž˜ ˜ UY×¢±€>ìž0Êò@ Ï¢ÔŠÈsÆ‚[ü4Q×Ǻê»ßDÆ9‘‡®ý#S@Hž¹QuÈ·êA +hÜ*„j/@ù±ËÿYD)‡=¡€I AÜ9±Ø:!KB&;Ÿbiñ=FoåL-Rø»~ÚJŽˆ§õV“øÄ Jß#/€ñu-ù` s¨G†ö¶ì…¸E½… ø41_¬TŽ•«[˜8Ê(–DvYןkP–“® oæ€Ø©XÞ9ÈŽ½q™tN¼®j““/ÜÈ'À¢Yƒ!´˜¬¹Ç®§k¨ÑTåÈÏ \ËÈ‹üa®Tø8~îØÇ÷òæÙ±dT€=%ð“1ŽHÖîðx¤*$D0=1JçÉkÕÅ4â‹R(®¯§òí¶ìK‘Y¥h®Ž½YœãbfšÈbë´>Ši&†"ƒ‰×ÄphÆzßÔ¶wj$Ÿ@Pø&b+F•Købƒ5÷h< +cA†Ð^wN³®9P d²úÜ5cù8Uòûû›ÿ ÌlŽ + +endstream endobj 210 0 obj<> endobj 211 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 212 0 obj<>stream +H‰lQ»rÂ0ìõWž + ½lÙéÈc2¤b‚R1)Œäac›áOò½ÑƒEÆ…Ï·w»{ëÉóBÀv ÷–L¬• ÀnˆaU Ü?±Ð+$(Ãd¶#¶„3Îy¶!Y,ýÖ‰,qºx˜Í`~èǾé[øš–ãb.¸æ¢`‚sXЬbÝ~tÝÊ@†…Ž¾Û"<¡Œâ©Ê—•3¥ƒL—´MÐæA•¦ö‹d¹`Ú(…aš ö1à A0‡L0¡+ú©åáOŠ€Š@ ñ¬%¾º®¦+ñ@sï÷{¸£ŠU˜LÞ®V—U®/‰È2%ò¶ïýºÁõm…PÃpŒ¦qñ= ›c ÓõzJË0W·Ô`¿…¦ï¨a;ê5°ÞÅá5Œj?”X·gÖ.š’Úç¥}pvÄÕNžì¸z8‚°öÄÜçv#ÐOŸm +éw ?ÁàZ€AT +lƬÓ'tÿ‘±”OvörÐõÅøŸ,ù`F€“n + +endstream endobj 213 0 obj<> endobj 214 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 215 0 obj<>stream +H‰¬WÛrÛÈ}×WÌã 1±¸_¶\®Ò®ÇÙRÖq÷Å•ŠÈ‚ƒ‹%U*¿‘¯Iþ-§» (Rön%RÌLßOŸùæý­¯îû«ïÖW߬×òÕz{•ºy¦<üóK”»I ÂÔ µÞ_yêþÊs=Ï‹Õº¼Zñ+v=\}Ò×·ßø >víЖm­þ¥œUêÆúö£ïEžŸ¸¾ç©[g•»6‡ÁìïL§Zj:ÎùëúOW> X¸¼Å)Ä«0vÈÄìEvJ²=’ªÃØYÿ”Dy?œà7Q?Éc7 h¿hœÐúz³ù®èÊ]Ñ Êžb]p¦Fâ¦*ɼå!>{à“þXtÅÞ ¦ëU{¿K<7õᲕïúQ…ߊêáì¶ ·ý»ØM4üdAâFô†~zò¶ÞÕ{'ÕF »bÀQ{Sô#oîœH›½iÕïx¢ë{gTYÔµÙXíD±›£8iŠf·9¾/ùaF•}ÉÊ—¶’O§á†ã“Æ6dº•láYÄ3çé²ÈàÐ ýØ"Ò»÷EÕ|h6æÑÉ,Ê ½È§õ®6åе’äŠLW%¾‹Âï1jÉ骙§+:iÎÕ±žYæT4íØ•Bæ¬|aúä%çAþˆµÈ©$蜾  µñG°KË¡íX>ÞeæÑ ]_ð%8êShN6©ƒÔV'«“è’óçuzDb†ÐÕ °—Vþ…„¡j-?Ùó>R>DÆXøûž€qÎ@œ«$t³HÅ1v‘ÜŽä¯ú7?þðºìêíVIô ÔÑô™—žÿ —.–"ª¼`>^E/—V1}eÍŒ"ð8¼´ø%Òå/Åà¢Ð9¥y/š0¥¡](ãK+)lÊصÓÌÙê³aÙ½ùJç” EQàB(\/¥Š„4ç5ï‹ý¡6ßNç_Ά(À)ÁÒAáÅÇÎØSMñË÷è 1âçÇB¿¹ÄÏÏüñ‡Ù JQ©ó?–{çšÆ4”XÞ’|æƒb«„‘ÎEþŽ{!Œ“Á*lÆ«lžm¶’6ŸÓZËWqšEy{0eµ­,}U»‰Ó9»Ü©ë›Ÿ~ŸFtÍÌdy³W¡²løì ™]P‡ÔÂKàÆi”“9—'§Œå5.’Ù›8òÜ(ñ}ñûj"Âäm¾Å92d†U戼ÎßO"º7Ü +×>´P›+8ÔƒC…Ç1iÚ}Õµ:ùô‹*åF銀 +^Û* e4ZQ@PO×¥…•+«6YéÇ“•ÿ‹7œ¦Œ})XÔBDéI†w0à?R{? +ù'úÚäC†©ÏE=ÚP…š lÀÝ„_þ_6¼P§Ö\ãRTÜ ÏÅ—œÀs6åßKS¢Ù”h2¥ ÍXJdÙðq(qȤ‰rø"† }“;š±›wVmqŠïÆ™—-½rÞÂØÉ`TŸ‹î‰Úp‡ªàëHo·p +\º»/VZš×)ÎÛkîpý« ]­äç—¹ó†Ö!±>l§ÚTt ¤UïÛŽ ~Ûò·n_åq'£jÍç>7sAö€ç 8ÍSwEÏÈ+6þ}ŒeZµ[ÎSƒÝumS•êž?3V丸ÚÄ~ÄQ<1¡Þ ‘ºáØ#»¬§f*g"¨¢y ÃI+6u%W¾ôœù»­l÷ÒÇ)9ÉOȉŠªî•¸ªŽ€|ça1L¦,¿Â*l¼/¸½p³íÌÊ>KXªº¸«ÍE/¹Ñy¹ÍÜÙaÂâ3¼FYNæŸxžë‡¼žÎ^ïy¯²Þ¤ò"N”ÁÓtŠçTP\¯;¶‹Gú%®'Ì0gV?W}Õò ‘=o‡³sRm+$$Œ,­´ÑøæêOφwTÔ¢a?l×[,Ûf+­ñ~ì¤ËÜIcB£EÖóÌJš£uÐA– §¶¶¦š™µðôÜó ›²sÙºŽ³¥*k›ú‰àÃ(G™b øK#ChÅÎÞ—ˆÚ“´wë«ÿY ¦Í + +endstream endobj 216 0 obj<> endobj 217 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 218 0 obj<>stream +H‰\’Ínœ0…÷~Š»´¥âø0î.i«*]EwuÁ€™Ðò3OçMú¼½Æ$UHpe_çø\î¾$œVòàÈs +$¸Žn+ølEny©@®Jp#p"‚ ! +p ɶO]É3½?|z|„§es3ðXfxAORäB–\ +–Y®¨??ý*öhqì§ûF$Õ&žªÂ <è‚ë<ÊŒIÛDmU©.™ûE2m¹1U¥á¹0 +Ü縿Ty28^†ÐŸ‡uËg¥ÃCA~PPO-h¨·õ…e +?Ú¾ëü²7XSB?Vè欤­¾ s: æ!Ô'ÏÓ¥2)yió2ÉenÍîÝÃfHµïê´‹®å–ð3ýîÇš•¼BOF÷{ýÈ4·4¡o&p‰~¿ü>çy‚ÖKÓøuí.Ü·í«h½4/õ ™ÓðÒä87˜}ƒEO[’h¥Â7.*s‹)_I£¯×ËâS“ÇÔ®ý0ìóÅÿHÛâ†.ßáû˜Ž~ÃMþŠq˜ªoo‘&"_“ÕŠEiÿK¶¹÷‹#ÿH3· + +endstream endobj 219 0 obj<> endobj 220 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 221 0 obj<>stream +H‰¬WÙŽÛÈ}ﯨÇb2â,®†aÀ;Ž=p¦1-Ï‹1ÕTI¢›[¸´ºÌoäk’Ë]Š"ÕRÛ3Hºq»Uw?÷Ô÷ïn|±ë¯þ²¾ú~½„/ÖÛ«ÄÍRáÁ?Ý„™B%n‹uuå‰Ý•çzž‰u~µ¢[Xu¸ú,_ßüðþ½¸îš¡É›RüK8«Ää͵»¾ç‰g•¹4í`ª[Ó‰e”Äíœ_×®|Ø0 å|% ^¨ÈU!ª©Xw‚º=Ô*U⬿ ñ!ï«ãtÇæÇY䦮37›k't}i´£ÜLÞÑï|IåXµlÈŽ3“b7qêÙ )>Eã³¼Ö®Ì`ºÞY…àû ÞÌs·ò]?ÌÀø7ì†:†0H8„‡ÕNäÆb¤Aì†x§”ŸœÜ­÷FÔºriÄ°×üQÝ´¸sBi*S¢ßÓ‹f,7¼í­¹.KV²±Ö±aŠ û,ÿZ”àÀÇW€úÅT&)8tÉš¿5$s•î{–÷ò"–ýž?`%Ë Ú€ïs]×,½ºÞໜEë{Ó f#Šzh„fú¢Þ•FtFo +𻦷;ÞóÄ9¾AxŒo˜Ú…Ä'2ÏG'k§é’?:¾Ïù*Åò¼äåsK#”ÃñqC¿ÆñÑ⤲áðy„Ø°½!›×š[(¿R®ò#ÛLoßé¢~_õ“By +Gy¡7âmiò¡ÃHªÚ"‡ïlæ;x¯¤¦¯E}|]àNÒ½š ‘´+Ö~ÓŒ]n@½;+_ÂZ*c#øãqë”a<"É•ª¬OžK{Í ±â7°…4šŽôÃ=¿y€>õ¹1’²>¥æd‘@{à¾ã‹µ‰mÉhxo²Zv©šŒ¹µF™ìÛ†®y +]A~xq„ßcìÈ72+7 ÌR7M Áî}÷ çå¡õÓ/ó®Ü¾"›8dJ)´FâgD¼þo~[ˆ‚G$pl_›/ ¿†zîtþH¢ðpIf®D’¢ÇKrÇš±‚ü|Ióeóke§7¿ùÄ ûþlPò.ïøÃó˜ÍñÓÈRžbùó´g‹¦ÈHæ탮ÚÒ¼˜\ÎlÃ@ùJbÅtãÓÝ€Í0ü"ÚÃ;wjaÞIžÛý§/F,qþ7Û2%Eˆ¹LÎD­±³ìT4O¶"sÍœˆx ‘E¹<'rR'OE¾•l•2~;×3þ!øÐXìó®h‡¢©/Loõ5ð‚”ôB‹šÁè ZF/}'¾Œ•P·0+á X…ãX áà†Á‡3½75 Ð<Íd³”vÒ€Ö2û\—`Cg„^.Û¦,yõ¡?º•ù@~)AªÖª‹P˜Æ#aj±ìöED„ãÖ(9v†„êÁý]tÃNô€eênZ“ÛÂÒ +±Ÿ¸†a^ïÅëŸþÏFzèê#à 1bL1Èzà'$c»À’0Cw?ŠK3å%°ÔôÕIznû>”éÔÉHP0ÀH<¼(ƒ<û¦8¦“¸—‡P„ò†9PÛ€Ù”_%sBµR7UQëR´†?݉œ·¡ö- ´0%+%m‚?VÔ‰’fsæåÊš}êåÿâàG0%Š‚݈Äb$!rKt¼þC)ż±ù`Û‹ÉC¢«Ľ.G›*%i$ƒÀéæÿåÃ3m~êÍk «zÇý_ § 7'Wþ½t%<ºN®h\L/v–é0>Lî1‚4ü¶ Ó”£pT”=íàÙ`eÀ 1WȉðÐí¬b•!G¼ð È,Þmè×8Ô·f8~Q#£”^’-Ò½ÄKc±áRqØ@$O8 ×[Ýc…Sýa ‹Ûq¥é¸h´ Fðº‚4Z1Ñ#‚ÉV*ÎézàÖHÙÎ(î"–ãVMÏ]È7ò%é†+ÎCj!ÓGÑ*°pÁÎØÅ{k6ÅŠá¥Ë¨³ê€USaÔÞëÂm}Y@¬C¹ÝB°ðìFØï³—a¿Ï} +Ç° +–—žûï¬ë“槀¾P³úÞðxˆdÛºÖ›ÙqÀ(^5­¢Þ6ô­«4CwŠ0t-@ó9_:wsÁσ ”ž¸Õ=!/ûlè÷`,•¢ÙR}>áÔp¬ÙÑgŠŒÎ3cÀ(Œ8ÆèÎ02âÌ„3–ØÚH!Ìõ8} ˜T˜7à8* ÃúÉ! óÉùx?[ÞT8 3*jŒÔÄÆ º({Á¡¢Cdç°§G=L®,¿âa-–;Mã%â ¾Ó̾ç´¥¾-Í7#~Þn—Îm«0ÍÐý“ÈSÿ`Ô“cÔ{>ÖÙhb{aD9à)ÐÊ$Y â9KàÐCÔ;ÅCù €¦bÊ`V¿}ÑÐCÍk@¿}<'‘¶CbFÂÐ2ûß±ûQ´Àl¨EÃ~líÔèhÃzË£q7v"|˜k4DîÆàσ Rã Ïc-ØÍÏðí¼ûº>y×üR0P0ŽðCSÿ™ ÀGˬd¢ï¯Å-7аª– Ú/ mþÏ"Š[¬j(˜+¨GË Ë5VÖ’dL1 ¤8ò×)z+«j‘ÂÏò°ç!N˽@¡‚üïÒ¢,ýœWl +šÑ–\3b A_{{ 2v8` x±¸oʈ¢;3•8 ã'&.Tç©äGlõϦÒtV쨬ïúT™ççÕˆæœgœóODœSvf¿tMeN'8IöÛ±p¬½p‚ÂÒôöîÃXµÜ}0X©-myxáë—‘3B¦ƒèë4ö£CÉDéÀ j6€úŠš©(KÛòtÔ1"d0]A0Þ? {x¬ˆEÍçÍ·ë«ÿ»ß:ë + +endstream endobj 222 0 obj<> endobj 223 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 224 0 obj<>stream +H‰ŒWÛŽãÆ}Ÿ¯èÇfbqÙl^†M¼Y¬µ­_Œ<´¨ÖˆŠTØÔj#ß›º4Ej$ÍfoÕ]U§n§ß|xPâÑÝýmy÷f¹Œ…ËÍ]–…ˆàŸn’2Ìb¡ó0ÎÄrw‰Ç»(Œ¢(ËênA·°êx÷‡|÷ð÷Å}ß ]Õ5â¿"Xäa*îU”D* U‰‡`Q†±´ûÁîV¶1Êh‰Ûÿ\þt§`Ø”ó]šƒz¡ÓP'¨fǺsÔ¡V©‹`ù/4>aã•>m@wl~V¦aë•Æõ`ðzýPà™lºÚû si \RùÄ6MÈ\X—…¹ÈŠÈïMh¨”7¿7½ÙÙÁö.Xàfoy³(Ì ¹P¡JJðãGöHŸÐŒsFóX¤a&Ƹˆ³0Á;­U~v·ÜZÑš]F‹akø±bg;Ðâ>H¤ÝÙvnK/ºC³æmWVT¦iXÉÚ[dži6l²)a›>çPµŠò°¸iÎÎÀ–Z‚(D·C£”Û¢v±¢w6€­sɶö–Þ­ý»¹-Hh…7()|ÊQ¸ªêÑËeoèR=J±UJ˜n×,½µ4E©ª£Ç5ýÚ@¡Q,(dÇÞÁY/¤N6/ÿ2•Du§C­R_ï?˜ºýØ®í× K×Q¢ðF¼ol5ô(†´®£+9ªPä{žŽ#Ý p`hÓ°ôѽý‚™¯ŠËT¸N¿À©Ëç¶þ7¥ÖáĶ˜D1€0Ç#£¢%`@Bí^™•‹Z•gôªP@] ùŠb¤$I SBñK7Ô•'} (°oѤc–â1ëxZnˆ3퉽ìˆÀ^V0‘tlj»³›Q._Æ°áË*œøYìùYÕíØy_™ +@ i¿ÞÂümé=E¨Löº2?Qºðûªá°§<{µ`:gÝ Ô£{@Fï>}f¤*xX!C¦|dš<Îzôµ|»c·\Ï—ûVÔˆ¥¢ª/è7OÃqSèCy‘¼„mr@{šývÕÈyð:XdºÄ,¿ò ÈjFä9#Zè‚‹ÃÑò‹û-ådr^ÎÓc–öž¦bœ +qÜZ¨¡Á(mŒÃx`¯ÑºÕaÅú§I2XÁëjÒèÅ„Ãò“GD†êå|½ÇDqÎ^TÇ,¬žÃî:ˆ*—:¥¦’¤©?L¾8Œ1õk×$ï¨N­_¼õf3( +ÈDT¼^4òÚBåõÏ´Õš*¥¬¹ÙXxÀ¢Î¯Ø˘:¿âÄJá”Ë›qÐ’}/òš¿dÎZr<•{?Š FýÆÜ€vD«n7~Øì ξW²î–›³¡,Ò¸e$VÆA]Ô-ûlé÷k€Ù>6gË'˜Ž-ôÙ`8J:¯ø)—§IÊ£;õ5è)p†Ô8<ð´ +A!šÖ’2Ì¥Sm–¯ÁFÍç5&'â9±¶ƒ©ˆ #Ú Ñ9nyÀ£+ó¯xËä#X0ìžÀÉ¥¯h¾,äimwªþ<Ôi©Ïb9껡;ìiŠï»Úw×n°Ë@@=“…êÈ% ³šF‰– f =‹= ¦cYœO¶4QÊ0-ãbÌN–†ŒáÉ\Ãn°Êº¶yÆöa÷˜£PÍ@£hÓ* M$°yžrÁoþT¶®O50¥ßkn~þÒC×þUðx˼D¢ïÅŠ hØ +€jÏ åW&3ju†(yØcVCÂd˜A-¯™-µ˜Ys–UÍ(½÷<Ë·1V5Köq*fÔ§a&ú™‹DÃ÷H›0ƒÆ±EñÀÜu‡šiÁÖôܸãÌ×Yªgé|YìXŽ•¯[Pqð¼[cùc;¤ eçuý¥†–å =´ïãH°g„ÝÎI7âe$u¶Ð †5Òo¨dÔ滯 +ŽÍÏ ®Yò5žpCÔ@ºãQð‘>6B†{oáK•HÌ\Óp¤ ø5 ]ñÔkÆéDyüjõ=ùóOJ1yzœ*«nT¹ârj¹ÄŽ¶ü´ö“øö6át4%s^VÇ,ï—wÿýq5„ + +endstream endobj 225 0 obj<> endobj 226 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 227 0 obj<>stream +H‰””ÛnÔ0†ïósé Åõ!'¯ªJ¥­PA¨U7w!ëm6›§jWˆ×ày{ö„J)h¥ŒçÿçËdŽÞM%ܹèmU• Õ<*¸)Aà/,RÃsºà*‡ªÜE‚ !2¨š( K<õݲÓéÙå%\ÝØ5Ý~BœÆT’­ÌÿHÛpc[ÿiú9ãkùê&ø ö×I°¯Dm*™Z uß{RXÌlñ„ƒ c0ï˜Ù±^,§ƒðî»G¨CTƒ:#”í'CxäÀÎçÁpMp¢2.UVòL÷.ÈD½×[Oΰª°3øöP/ãÚ˜ÕcM¦(0 NĽ +œ©)ëÂfˆÅ 㺷Ž‹‹*ú%Àφ– + +endstream endobj 228 0 obj<> endobj 229 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 230 0 obj<>stream +H‰œT[kÛ0~÷¯8òÀªn–¬R +ÝZFWJCã·²×Q»¶±lgÝûû½;’œ ¤]`bÉ:ún9ÒÑç)‡Ç>ùX&Ge)€Cùj `ø„²T † + e“0xLeŒåPÖI†¸ë5¹#gÓO——0éâ^Ìᤙ¡9™N8SŒkʃišY*ˆ[®¹w_#‰‡K¿–_Ž€"ÇQndN¥ò4Mä6ž›yV¢XZ>{ñ*ŠçrFQ¾¶9-„ߟmÅ“kWõ«Î5®Îf³ëªïa“ØS£©]°],°îȤêªÆ ®ëÓL¡íãhˆQÃ1¹ŒS®,ê>˜M|ÜÄø"&93´ —íà1 ¦JÕ—¾Wó•A#ž xlFŸíSFuÞº4§–ôËEx·)ÖsÒ§Y\œŒÒ1Îb6„`AKZ(üÕTêmšü4O^ûÓ›«“º›?œiQ·”Ò‹"~y]öË~ï”–°`oZw§þ¢¨0·Š2=vLP÷^4Ñ 5?ªf9wÇkŠ·ÝçFм8ì½aú:¸¹ÚxˆŸÆ½}*F•ø—K¹màÜ7vTÝ=-‡§E{ QÕ¦QÅxÌg³ªØ˜Ö7æ°ÀéÒU/ð¼jRl ²L= fm6”¶ñë@¡üæƵ@--´èm-u{¹Y]“q´4«~H „{BÕ+tdH×ygi‡ùÏx™lÓ|ÄnÄx´½›»zð[9™ù`w„·žIQæoAaòÿ8}ë[âÖanEvÈ'ÈKŒÏŽøeòW€Š7Ss + +endstream endobj 231 0 obj<> endobj 232 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 233 0 obj<>stream +H‰ŒVÛnÛF}×WÌã²7\rIŠF u‚”ߜ>0äZR-’IE‚üF¿·s¡nµ]ÌÝñ\Îœ¹¬_ÿ17°è'¿ç“×y‚ün’êl +þðÁf: !Ju˜@^OXLA y9ñùˆV»É­z7¿¼º‚›®Ú²]Ãßàù©ŽÕüÆ60‰6AsÏÏt¨Üfpõ7×AH:‘"wÞŸùõĠÃË)N1<p }¥#c´—¸±ÙTGÉËÜ0²Hì=5X¢ú²[m†UÛ<1Ã'ó9‘A¾äQÄͲpüí¡€$Ê(è¡mPxãDñž†oªàzË×zõøêêPOldiÈ—â–”z±èÈQ-Poû†‘vGïÖbV¸j9ïƒpx®†˜|U½HœHžÞ³=ö®ª$±HÝ_okÞ (š +f£FjËÇLy9sA!B(ÛZ ÑLL¾zšÖ&³Ñ 6Dƒ!¦êA0FxmŒÝphüã³f2Aºê™šö˜g‘óE»µÒ$8ósRðpôÓ°UÛU”€QìH”ý/¾5ÚNãÇÃh¾ÄÕÎuŠªBeQŠû;eå¥Ò‘’¬Æ,SÎÒ­:¢ïN´\O…ÚáÛ„¦ü<ˆ„U¸ ‘“=–“:°:¾·ÔÙVúŸ§+=?Æ‘VßÝú‡~®!Ÿ˜óãÈîßê/®.°Éè?ryß_`3fê?_lqü!Ÿü#À¸¡Ix + +endstream endobj 234 0 obj<> endobj 235 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 236 0 obj<>stream +H‰ŒTmOÛ0þž_qi1Ž8M…6`Cˆæڇкm¶¤©¦ýýÞ}iy)T©=»ç{ž{îåàë$†E|.‚ƒ¢C12ž@àÇIε•q©¡h‹@p!D +Å4ˆ¼‰¯îƒköir|v—]kÛi[Ã?£Œ§lr‹DÄšÇBÀ$Œr.™Y[Óܘ¤óQÌ… ß‚JNVš!<¨”«ÄÁ4„9láPY"Ãâ§#ŸùXíx‹èë<å#éÞ{Ʊp;›²¿ #ÅG¬ O˜iÌŠÎöÄX3µíðïÙjF–Ùѹö(kž‰'€)^–]Ù`ணµS0Á³åb'9r;yvÐX*Òر +ST©yRΦ›M(yÎPt9ʧ)Ü”½™Aõ’W;»403ÖCc +ý9%d'F¶…ÛÞÀÍP3¢qv.xÂ%ZYJ+Âú¼”ÏK ëÞqÚÑñõÖ!Æižp¡‡v÷l_“Š2ʽÏé¦lÖµo!^V#Í$OGïUîiðˆæõ®(ç;^xS%¸½­‡'£¨uN\ëhÖO»jm«võê¨ùGò¡ß5õûñ²Ä)[-ŒÿíýÀ`Ó÷¦¦›©ÅéjÈ&_œj‰àE Ç®GSf{œ4A¹3™R§lÃá n5+ÓHâÆÍUB3yÍ zCÀ@Gœ¦¹Çåõˆú,r¥‡šÛÚV뺢H{¶Úëø|ZI•„T©\ÕÜcåPƒø£ô<”ã¡`ݵwÕÌŸ ”5nºUiÙ¬­V‹Þï»\v|:¸kk[.èžÅzR§7·ÆvË^™¦ µ_ênåýêǸ0Üjt1O‹à¿áʶý + +endstream endobj 237 0 obj<> endobj 238 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 239 0 obj<>stream +H‰”TÛjÜ0}÷WÌ£\°¢‹-ÛK´ÉÒPº†>„>8Žvãvm/¶CJ£ßÛÑhoé6 Å`diΙ3::¹œIX Á‡"8) +Šyò<qÎre h‹@p!DEDâ®çàŽ½Ÿ_]Ámß]Õ-á„QÊ6»•"Òp)ÌÂ(çŠÙÕh›{Ûƒrk4sé¯ÅÇ@bBEà>JR„p;˜Æc§[8Të°øæÈÇž¼Ô»yú&Ox¦Ü~b,KpÇ>Ùrx +#ÍSÖ‡¹Úƶ~°¡ä†­=Õ½`G¤ OÁdbyÇn˾lìhû!Œb„™ø ‚§U$—qŽ5]ìõÜH«´—–È »3ž3TV¥ &p_öjd˜8]齕[ÔÍa|´`—¶û®­½ÀÏ„Š‘+šڣVÁ"tÙË0r¢Ô-Œ<Ñ xÆ,Ì»ž²7Ö/üÿÞý—¨ch‚mG_i´…¥Z5Õ*öM×ÃèXâ·éÕg”³aÕÑ·uÜ$òÑØë“]c¤ðÉ©+9ͳ߆k³;^gz×ùq/óéópvs}ZõËù±ÄKkíx1÷ûåâý<ØQ¼ÃuGê÷Ã×Oçšä1fsæ‰çk"ùZrZ3]—Íji'[ˆ¿ë¤Š'Ù?tø²+ ·U@‹zSJ¸¹Þ p0¿Éý¦Zp•½­ñÐþ°\¸ÃbØPõõj¬»öUÃýq•©Ü;àü±Äk–¾”Є©sUH“ ÞÊqòäp`´#š¨ôQëìi×fÖúúOGaB'^LЛü¸W˜—{÷ÝœÁoHp‘= + +endstream endobj 240 0 obj<> endobj 241 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 242 0 obj<>stream +H‰Œ•ÛnÛ8†ïõsI-*–u Šî!‹¤jÔêUÐ U¢o-É•äM€b_cŸ·3Yv“f³0 “£áÌÏ3ÔË?WnïMá½,Š4k/•y +nå2 Á¤2L h<7ž’J©ŠÊ ÜWÝy×b±z{q˾»ªÛ¿à©ŒÅj©U¤t"µR°òƒ\†ÂîFÛ|µ=„äc…󿗞ƀ¡KΣ8Åô`bi"JÓpî”r+Ê*¢È/þ"ñ‹×fàF,?Éc™…´Þ)ÖNüµ¸²å°÷#Ñû +Ÿ¶±-ÏÇsþßlGÛ_u5Ϭ„2¬õHì‘êD¦djÊy-–e_6# ~!“3Ž dªk ¥ŽrÜÔ;ïÀsBÆŒö|–ËUГð¦Ã1:ESqk¡™ÚŒ|µ°lMõ =‡(ëMë7ì<¯íxz×Ò ;öîaqõyΚ k>žÐaOl+aèŸ(Z.†]çþ[Ê¥ÅàO|ir6ÖŠ çŽn‰‘Y„ÏDšd>NƧÙÓŽ3/ûênxýñëªß®_;u,ÝCº½þÕù þ9YPün¦UÏÓ§‹€¥Æy$U2Õ®“ù#ÞJî|Þß—ÍnkÏ)~!NCgÿ ,mùí-šððQ8Ñ…»{>ÖÇ3 €ûÃ$ϲ1J†Ùódœ ÃÕóŽª+µê7»qÓµOvRð°• ·ÒÊnm50R¯ø Æ*©è21 ‡îà +¹³“rÖz²®»Þ”}u[:s?B餄F¦yá^Qŵhk‡üD\î9ánŠÐèº-[ÇANLQ'´PqŽ–t,Ù}"͹éZŠÄ fœô(ÓˆŽ4)»BM†  ·¸ÜYÛ¾¼±~„-*ae-,ê·Ë·4Þë&Ìy‹.rxŒœphæ³6-’XÔÎVS†Ë½7;¨º¦)[~Cb¼ˆˆ."kºÞ/²üj,7Û^u-ÜvîÍ;Ìó‹ã9{OD‚Ö‡>êÖ’r½¶ÝÒ¡}Ï'.GY}ßo&Æ/I*BÉi°!þó.4s5>@ŸlS"þ )Ñ9~Îð<|_h­J~í„cŸ¼/¼Ÿ E¯âÆ + +endstream endobj 243 0 obj<> endobj 244 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 245 0 obj<>stream +H‰ŒTÉNÜ@½û+êØÉM/^G)D1¾¡Œi–`GnOˆñùÞTw1c–ÈW»ª_½Zžw>/\»èSíT•ÕU”‹²‰O0’RdL.tUI¸Ž¤R¦P5QL¼u³‹ý£#˜ýØ7} ¿Ç¹HÙb®d"U&””°àq)4³«Ñvvíc ópü[õ%R¨Cr²ÒÓƒI…I|šŽrç>·ôYY’òê»'Ÿye¶Á"úY™ŠBûû±J<À9;±µ[óØ ËK‘3ÛÙ%Ç“Ú9â4uæ»Läò/ì”°çõPwv´ƒãq‚€3Ã, +;+¡’ë8ˆ6-|ì¦N©›”?6Jæ¢`Õ…®æ©H~þQ·S³µ…±‡µ³pÕß#:KnË1cÍhƒó’ŽànƒwyÝZX‘«¾ƒîц$k?¨c f—£€ýz ˜j (ÙxÛ/ñlXÝ>VHÅ*nšøAÅoÔŸQÓÎ<‰’¹UÞKÏX1Ç ò¶t˜m‡¢$Me‚”y^xH„ñ˜4ãÁ6Œt÷Þíï6C{µ]£æÝ›°__í½·^Dë´ è7aš¡È1mOÑÒdÛƼÙÒUbÖݪµ³ Úke«òeƒJEú’èÕ‡wïŸo¢@ø2A>+Q•ùóåÓâÞßC›pà7!c®nW~Å^‘™Z¥ ÍþßM¿Û6¼]ÐÏXç¯é›ß[\§KM>‰†î \.ƒB  ‰5¶Y—%J;ždÒ,e_ñ—÷Ù±:îüß$a¨®m;yÁž‘S3‰*A¡5DåÏu+(¼!ð´_ÿ'®ÍéÌâÄ«yÀò4»s3ÔUÉÿ°Šþ0+Òqs + +endstream endobj 246 0 obj<> endobj 247 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 248 0 obj<>stream +H‰ŒUÛnÛF}çWÌã²07ä.¹$ @Ú…,æ)éM¯lÕ¼$'úýÞÎE·ÀQH—äîœ3g挞ý¹Làn +~¯‚gUe jäº, Æ/ÒR;6×ÆAÕ1ܱŽã8ƒª "^â©Çàƒz¹üãê +ã0ÍÐÂF¹ÎÔr‘Äiœ8Ä1,èÔFùÍì»?‚¡=VQ¸ðïê¯ Á€†Áe•å6Ó6%˜N°sÂŽ U¥.¬þ!ò©Oì!¯„¾+3]:ÏŒ“”|Po}=mÃÈ"Ë1Œu®|ç{¹Ÿƒ,Ö!IT?O ?.|ý Dr=¡ìt®ˆ¿ÌpQuçg?Na”bðK †Ð Ê%:IKL™$6…H¼Ö;"aAtë0à RŠLZdªº÷Ðo»0G¤nèÔ°‚ ‚çÍ#.jF5F§Y– q¹mF¹=ß=B9+S»]Ç3ÝsZIJ%ïyý¹î6­¿ÜC|_Ž,7:+~UHÜSN(ab¿æÝ›ƒßEø©&6Ö¦ø¹"ÌÆJ÷¼¢îqjjÆõf^ýy¿?•Æ_zôÌŒ†š|KQ¬jf uÏknz´WwÌ™ª‰üwÃ[¸;ÉodA~2Èvã<ˆv6ÍŃgLOÞcëRק +áN½Ì šJN}TdÈ·lÿ÷C Õ=¿õpÕ¯h†®“@ý-Ü­?yÉoçÔ‘§J]Í‘>]ýyÝm;Xv‚Øײ˜†vK"ïÆŽh×Ë+š¥éiTÉ(xc—Ï’"¥jÛ/¸}=~­¹€½4Æ–Ùéx²G.»ÿœºi\mfLLÝ„Q¢ZŸêvKOÕ*¨Q±º}¬¿ðÐÜ?¥‰¢qS ‘žX¯¾Õ„ü°Oöjl¤I°£¬¦/[…—½äG=H¿Ü3Ûõíž3C~Â9¹#½€â‚LRcÙ¬Ñ|N¢›ÎMR{ðÂþ¿çÚcõÎí‘GõÃt‰íTª:B¿®‚ÿjŽ= + +endstream endobj 249 0 obj<> endobj 250 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 251 0 obj<>stream +H‰ŒUÛnÓ@}÷WÌãáe}[ÛBâ&T¢¢~C<¸Î¶ íÈë´•¿Á÷2')„RÉÙ]ïÌœ93güìÝy W>xUÏê:êË ÐU ¼È*mH X¨»ÀÀU`´1&‡º "^¢ÕmðE½<}z +gã0 í°†ŸF…ÎÕùYl2[çaTéD¹Íäº 7BBwREî¯õû F‡ —U^`xHsf¦“ØÅ6UeEX#ð™€Ó½^ |[åºLÈžÇìà‹úè¿ £T—j Δë\/ûé³ëYÞ„1¦á>6Þ ÈUGp­.À–æ·`¹;kƦs“}eèðDœ]ÄHmë8«×›™ÔdÏo’¿à´_¸»°DTÈ®µ™.”‹Æ»¬úE˜ëX9~Þ… ].aZ:èûÆ5×0 51º*QsXîÆVÂHÖ—ãÐAgääý¶Û@çøUÃO/TmÑBŽ‘Fbm’$£]­Ú‰†Ï®E$&ïÖr¯P‰…Bí±*Ð…jCí˜)Tñ!z³ºÊËßô–>PÒŽ(GMz¦ +t@oHÓ…‚•ÑüîøV³Ðç„–¦Y?Á²Yì$8C¸/Á?ͬ{²é(mÔµwžËŸbY é€ã ÷ÍbáøÍŒÜL0ŒóÑHHèww¥Ð€Ÿ‘,Ï…†CçHô|fš ´Ì–n‡ís)0Îèø”ªD³ŽZˆàv¹j—Ä +cε­‘rÍ™ÏQTs<òîqÈc¬“É‚Ljq†müÏÁu v7÷±±L>19~ú®ý άJ=Ô”Ô¤^®¸íÐͥ酘+hÙ÷öR½0¢¯6(Z"Mk'íñ6¬Ô|…ª¸¶º’*Ò ÌÜû9ó·uðK€3Ìÿ + +endstream endobj 252 0 obj<> endobj 253 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 254 0 obj<>stream +H‰ŒUÛnÛ8}×WÌ#¹…XR”()( +ì¶E‘‹µÐ—`‡ŽÕêbˆr›Åbc¿w‡CYvã¦) È”Dž9sfæèùÛ•‚;ýQEÏ«*Õ&ÊEY€Ä-ÒR˜t.UI¸‹¤RfP­£˜–xê[tÍ~_½º¼„«q˜†õÐÂÀã\dlu¥d*•JJXñ¸ ³»Év7v„ÄïÑÌÃñ¿ªw‘BÀ„‚‡U–cxЙЩӅع-}T–¼úìɧ¼Ò ­}Sf¢HüùÀ8ñ×ìO[»ýh;ÛO«a?rƒt×ö²¿µ÷ÎQ”3bFä` +y +«²{Uug';:§yÀ¤ÈŠ+¡ÒSx=“1‹’ªJz2ă§"g–ãÍ’Âœ +†ºj­r‘2 7µ³·Ð,;ýõž'ø† L[ Ž°ˆ@¢„QÆ×v‰,±“$Ć²ç™P¬ãÉS0 °w6ÃèAé5t^=ZŽnpŒËÃýaŠgR'S?ä6¶‡›¿I´Ë~3Àzè¼5¤¬F£Êཬ׫[Ÿ.ÈG ›e +"Ùƒ&IY¦?·.ô©z´Ð6ÎWg&pè8¢|­Ãø^mÌƟ׻¡Gf÷ö˜3ýa—}_ŽH‰ ¤æ3ØÖ_y‰¾máóÞM0ôhŸÍ=sv +5£Ç®0>až‡ŽHù.n¢ô3â«'œ{Ú†<°r?²K¾äÊÁͽòóáÛfšZ;»zpq¨'p³Ï(éÃC7ÜÒ=q,YKñvU²4ÊLõT´ŸÙ½^t=|>?Ú®F܃fˆûÅ] Ó—3þ›*ú_€»P&* + +endstream endobj 255 0 obj<> endobj 256 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 257 0 obj<>stream +H‰”UÛnÜ6}×Wð‘* šuYA€ÖI +7(bØÊ“ÓYKïn«(É.Pô7ú½ ÷âl¤X@;”È™3gæ /~¾‹Åf +~ª‚‹ªJD,ªÇ PåJhø‘‘–*O„)T’‹ª ´ØZi­3Q5AD&œzîåwW××âÆ óÐ ­øG„Q¡2ywëTǹŠµwaTªDÚq¶Ýƒu"Á=F¢»ð·ê— ‡ g++ ¼0™2)†é8v±5F•iV¿#ø”ÁÇæà€,†Ÿ—™Z%xžÇþ^þjëi #£réB OÛÙž×óíÀFÛzãɺ+o:g›y7ø­ŒüÈßY¹*D¾ÒÁ½¼©]ÝÙÙº)ŒR`è’=hUÄ@r«8-!ÅwGz› Q…N ýy²W`3 Ñ‹Ê-öâ,JY‡pv%Û‰Y5*[i,*8Õ‡Â%)û³"ÌåóÖÎ[¨È<ˆ\ à rðdé­ÍÀ†ã nC2Ä㧷tb²-0ÄÖ¥¢³µ÷käâl-Òö3'í1Rú¦ðH¡Â “CÜä\È[KYOã@ÿ=†Œ1Ö”—‡2ÅšëTRJ‘µJá™+“Z„YçqØÐÄ‚#ê©üož§·Ÿ>¾i\ûø–PjÂgŒA|?ïKuØÿ©n'û÷ÉÉêضl/_ï-Æž•©Ò¹á~4έ¤=ïÿ¬»±µ—ûÿÍKV$P˜ÿÅ 5â9!'È ¿ï÷öéã!^z ôM†ŒVÉêÛü(ÃMõ› +z¹q»£½*ÓsE]m¹×û k` /²æ0Q©$¡T¼NÖ(d€¶t¡öÊX&{”¢‚f`"ѨB’P‹))Tœd^ßLlrÄ”0&& <¤r蕨(|?h;<ÈeVÔ´h-N‚iGZn~ 8/Ø}З‚ýŠˆf8Ÿ_¤OËúÉpÝ?J×yÒÖ_B%>ø1#já·V~F üy|ôjdO/¾{ÀLP~2ï%%‘Ëã$óCÔðäH»XaBF7<íÖP#’»y+œÝÐÀ]xo[ïò<Æ<›ÅճݕŸL¶“6Kl­˜­y˜ëÚiÂ’’¸8ÎÖë]Oö£BK(öªÐÇÛð¼­Í!ÞþÒ»µÀs—ƒ#¼L—0&Kùwcõ—Ðu?»a½pÎÀήG‚F¼ý<`ã´þi7aáÁ)Gy_ÿ +0žh"U + +endstream endobj 258 0 obj<> endobj 259 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 260 0 obj<>stream +H‰”TËnÛ0¼ë+öHCQ"-A€æÑ" Š±N zPlÚVkI%#EŠþF¿·Ë¥í(u‚¤0 ó9;;ÜÙƒO“]p\E!!†bŒxžÀ Òœk ɈK EX‚ !Ó ¢!Þºn؇ÉÉù9\Ù¶o§í +þ@¸b“«X¤"Ö<&a”sÉÌ]oê[cAº3 spá·âs# ¤à~¤FÅ“Ô…©}ì‘‹-\T¦DX|wäSO>Nv4òôu®x&Ý}b'à†}1e·£„§Ì†‚gÌÔ¦ñóþ«±í±)ëËùÜS{h¤æ#ЙxBùW¥-kÓÛ…QŠjŒ=˜à£b§9¦sº‘RîT•Ò«ºãÅJ ž³ã¶]™²ª Ù¬š–}Õ, šC¿4p‹ç¡[¶´‰É¹+«.Ó´5[[wáÁ/Ù¬)gU¨1ífAkßÐô b(U'z´Ÿå }æ×>gÝ]Kÿ£‚ÈaÂ53~2Þ) /mN²æ ž¥øÕ<Ñ;M]LÿjÖ=Õð‘ﻣˋé]͈žçž$‰#ÆÜöàä¯Â®ÍÁÇrՙ߃;Å;<¹7Z?}¹<]•§\èMÕ—tòéätæìgYß­Ìxây)ÔHr•½M +pÉík0 „‰½æòb§Àö«j$‚Ëìu-ˆÇƧ®f4릶ºë«¶yÑ-ÿšE{³œ´MoÛU¡f÷KÓ‡¢-±Ï8g Ü"¾b†¾¥+BŶnQÞ-jç…ni µ;Ï C2t”ž•Ô\æyŠio Å„”'T›‘R,ûH¢ Ö4±n"‰EÂjßxšžH?²#CO’Sgǘф¼£Ù¼\¯z¨:÷ Ãõ÷е”.qŒ¶$‡>~^;jˆGÙõ fQcò¹á”sÛ”SÆ6…³¡O;¶Ýjõ´äÿÓG¶ôÚÔ%¡YÄ–ìG7Æ’³ýÊt)|V¯«^ + +endstream endobj 261 0 obj<> endobj 262 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 263 0 obj<>stream +H‰œ•ÍŽÛ6Çï~Š9RÅŠ!E‰’A€¦›i°È"Ö©A²D¯ÝX’«ºÛ¢¯‘çÍpFk»õ.,RäÌüæÏêÅOK ÷Ãâu±xQh(Ö‹Tæ(üÑ Î¥À¤2²P4 ÷ %•R Õ"¤!ZÅ÷ËÞ¾…»¾»ªÛÁgÂT&by§U¬´•Z)Xa.#áö£kV®‡Èï1» ~-~^htQp%)†“Hû0 ÇN}l壊DÅo>fxmŽhÄø6OdyûðÿQܺr˜z׸vüÅõÝëi½v}`ð&µÌ<ç†ÁNò\ Z™‚ÍÔy=¸+û²q£ë‡ ŒQ‹kv¦dªQN ¡ã“¹9 9kÖôˆ$HÕßðsà¦Lx¶P§ÊJ+Šƒšp»5×ü…ÆгIYo1±D´÷°¢4åŒÂ†(ÔIW𲆗 Äi9»Þu.†}GÏÖSi1yO®êiÅòå$]ÖÈ,Æ+= )ðô¹Üøì^†Wïß½¬úÝú!2¿1Æà ¿ü„Åß4úç̬ø7_L«ž§Ï6S'y,• ’ˆŸ“‹³ÊiÏ›?Ëf¿s×!žV$I#™dß®d—:œ1afßèçý»£Oùª.FÉ(ûº*d¸ˆ¨¤­ª~»·]ûl‹¨SsÄÜK7à }p;ïňjt54®¤ öœ û ŒðÁ›š T`ðBM²¢…É×l*|ÖÜK +Üþ(wÛšÁ"¼ Mž`æÇãNd“áþÉÓíë®'ÔÕÜÌZÌ1 ä™'L„}dÞ º‚ø +²+Ðʶ! : ˆð‘â¬yÓªÌ2_˵[—Ónœ£a>Û²wÿÙ©œåbÙÞ½%IŸM„ÏDBÖ$jB¢æ‚ÊŠçò¹íhZ;¨ºo |ÞéSªñf<÷Ôø+*­`Cï; ‰8rHÎ7`Ó9Ž«i&áDz»~€—+w.Šù[ú‡aï|àXÔ$-Iuÿß;8a6Ý~§íT (`õi÷€~·í=½&Œ\@S>@µ¡We{ï`Û>‚~ð`àÅnÚÕ4€•—¥=w²å×Ï€®ÝvžS#ºŽm‘J +4mÇrµC±Âù0‹†‹sMÿï{ÿñ«öÁ5%TOŸ–OÃ5^ù˜?—&Ú*{Ù¹ìùM±ø"ÀvDç + +endstream endobj 264 0 obj<> endobj 265 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 266 0 obj<>stream +H‰¬VÛnÛF}×WÌã²€6Ü%—— 4i‘F܈O ú@S+Y)$Ç úýÞÎ…””Èn\´0`-ÉÙ™3gfÎî“ŸÖýìe1{R «Yªó BüãEœëÄB”j›@ÑÌBXÏB†¡ƒ¢šÍy‰»ngÔ‹ÅoÞÀe×mÕÖðóT;µ¸4ašD›0„E0ϵUþfðÍ•ïÀ’M¤È]ð{ñËÌ CËÁeåR ‘ÓQLa‰Rì¢*gƒâ xðJà'¹Ó™¥ý‚8&Ô…/û}ç¿~ó]ûr¿Zù.HÐE07ˆ½]zÁudç a¢SH²ðÔ¿qâÿ²ìÊƾëƒyŒîžŠ³P§ÙÄ&Î1—WGGJm$”PYë.Zþ]h”)y‰,›,sqí¡i—üÖ‹ ´+ho|W›v«¶ƒ QÚ}FÇP~Ä/ëÍn µ8^oª¡€‹\xdˆìù9~†ŸHÒï W®ú›–w„Ĩ>ˆt"°ÌÄ’jBa5gFsH"ÅøkËÅÄÜ_­ ,ѳÛþù»·Ïª®^=g„?Š"¦èóù†/´øódSñšž=V<>ÜÙ屓±GîC\IJ9Û¼þT67µ:…¸Ÿ—Zí²GÓûzØ,ªr÷‚ ìÏ99Aˆy>Îë»·^ñ»”E¡¶Ù÷ ct‘4×+j®DõU·¹¡ž~p¢Âã,Å2K ?ô@½ßûš¼Dªü_òŽh¤ö¼ì‚¹Å1jt®Žžf‡çæŠ?ì©—SEÐØy  ÿ–‘BÙy({¿šç,R·ýɤ#G:Í“˜R°ažŒ)ÜΞ¡ÚeÏ¿b3Ôqb ’9qs¢Nr%Q]Žã·Àñ®=—&Èt¬¤>ˆ"&£È¤ãâÛ‰ &½éH+ØœçŠÃ%2¥Ü,€N>ÏW›ºF>=F¸ƒž¥8‘SŽÆ%_kËcµÇD“o=mAäÔ˜§#›<³(;åùyÜ •uRY'•Ý`Ý$1P¬ïjÓõƒ¤W²ñNŒ— +ûkšTµdb>¢ò¡ ËXüŒ!/œ“u: “äÙI±¬ Zærj#:‘&6ѧ*±£ÊÝò@$†Ž•°I­Ëv÷iñ~‘¤é`’ï‘ŽŒ‹'"­u|ƒ‘ºn‡€ø±‰³Øÿ°ÀÓ‚‘.œði‰‚¹\Ó-Â(ž?|Á‡!½8¨$e'íÊH×Õ~@Žê;èö;\°ƒŠd¦Å™›åÖƒåüëDOìrns…Y|¦¢ÇK›Í%ȉޣ>ä|ã@’3<Æ .{G3µYó[ùÏÇ8ö TbÛ4åŽKùÍËzå±sR:û{êÑ¥ñEÚå(ýßêõÉ%ãëÈý*G³»ô«ÛÅUD +Jl0Ñôx’¨ý(Q$å™H¹ÿ䈬‹(¦ƒ+5¾7niî/ö‡Ö&e0?(™+ÖòéÈÓð“ˆ{7ù’ ­ÕqæN4ì¼3ØgšJ¦+›˜÷ ’h¶Ôè$í‘Î RŸß2’C0“1·˜Ö±›éP®ñUíwË6žŒ+fßÄ[ßý³Ð Òqµ/·ÕµèJß[臻z¼žâ ô¶í¶t*þäüß_1§{õ{ߔ؜DÅØöOQcQ8Øÿëbö÷ë!3' + +endstream endobj 267 0 obj<> endobj 268 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 269 0 obj<>stream +H‰ŒTÝnÚ0¾ÏSœK{R\;ŽU•Öµšºª+"¹Zµ š:4!ÈNÅ´ÇèÅžwþ +ƒ¡ +)ØŽýýùœœ}.ÌLtYEgU•ƒª‰2Rä@íÏÒ‚ÈxF UQ˜E”PJTuû¡=µŠÐÇòÓÍ Œu?ôu?‡?€ãŒTŽM)“„Q +%Ž ’ µT÷¨4$nG¯¾DÌ&ž<ŒDfé ÂSGÓîÌqSÇŠÇÕ'> âßøQ/ AòÄ÷ŠYîКšsR ©Õ¡:µóá›ÒýDUë·ílFV³ú×dI29Ý#p<ÕÓN J§6›Q£$c6Þ˜–ÖÜ•·Éd8ôµ_`騭Ä­O„ÍD4~Ïõ¯i·œ«Ñÿ¸?‘'$ËßëïÐÓŽkéÆåKÓ(}×?)¸¿}—g!¨+æ}Ó{•pºx(‚+W™Z·Ë¡í'‹0dçm8+_Dñ€ Û-¾+¤kgm`xV`Ô\ù…zPOÐáÌ••7íW;ÌÜÂ"œ6ð[…žÏIÎÒÔšÝ^˜Ü’ºVu¤º‡G´ ´Ã«Î%×([,Ñb6Wås?à%Þ`þÛ%á£q*¥M«NT7µªsûeVûO3Âü­ý +mçF®«è¯‰ÃB/ + +endstream endobj 270 0 obj<> endobj 271 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 272 0 obj<>stream +H‰ŒT]kÛ0}÷¯¸òÀªdY²]Ja[ËèJihü´²7Q>Ö8’³Ž•ý‰=ì÷îJrCV2 +©d]{tî=÷ìÓ˜ÃÜFªè¬ªRàPÍ¢œ–0ü󋬤*‘ÓTAÕD 棌1 Õ$Jüo½DäýøãÍ ŒLÛµ“v Nr*ÉxÄYƸ¢œ1ÇIIS¢7nž´ÔÅâàâ¯Õçˆ#`꓇•Ì1=IEæÒ4!wîr3—•È,®¾9òY ÏÅÀ¯}UJZ¤î~`¬¹ÓµÝÝèu÷E›ö®¶z´^Š#:Šæ  +6ã^‰G2ªMÝèN'¾û<¼ˆÑœ£t §<+‘øÕA´^¿Tý<‡XQEê +âè$œIAKR-44u,Q<üü½^a $[ Ý¢îÀ.Z´ Ãá4làIÃÖê)tm`” +Ê ©ðQ{2éLÈtõ³ÃÖ+ò‰Ñõt¹Æ,œÌpI,ÌZƒ1þ²Û©é÷ëÓ…”É.§—AxØ¡ˆ^õäX©C­ɃÃŤ›Öÿ÷øœØX ^:lzű|œ…ú•¾v%(A‹ êÐü­.¸x±—÷·³š]zn̳B8VÄC_ÝbÔÚe·l׿תw|´˜ËmèÊ2£Lõm奄¥SxNéc®ÔÍf¥Ïw)þ-…ÌS*‹ÿ$•Ç èà£NAÜßÿºG>©ƒ`4-N«àYˆÐ-W®[±³Ü¸ª¼iFvèü,tþXwÖw~ãXúv$Ô^~§ÍÞV¯´ÿ6éÐbè'Íè–&NRw¥AOæeéÍ*‚Ys²Bg†[è¸e÷Û‚÷b›€ì|7°03mCÝ™›S=«·«6þ° p=Ó¥$žGÿô¡÷S0Ì°Âû°é NêtÚ­»aø qX)œ>ÆÏ¥g{»)¨\WÑ_Mz“œ + +endstream endobj 273 0 obj<> endobj 274 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 275 0 obj<>stream +H‰ŒT]¯›8}çWÌ£Y)®ÁÀUUiû¡ª­ªF oÕ>p‰oB 8¤ÙûGö÷îxœÜ M»­" øÌ9gæøÙÛM;Ÿ¼¬“guCõCRðª?Z¨Š›dÁsõØ%‚ !4Ôm²¢%î:%_ØŸ›WïÞÁzr³k]ÿ@º*¸f›u&”È Ï„€MºªxÎìa¶Ã½ ßHàÒ¿ê÷I†€9+]`yšKÊ ±vj‹P•iÖ_yÉgò €V‘¾©4/ó°?2ÎÀöñØÏÝ¡ïì”dºNWW ØvŽ|®®Ü03¼SŠ%n¦#šÁÎvòéJ!ð]¼ÈÐE,’© +5¼^²¡ežG+בCZ¢Whc.ó +‰½t®·Íݘjž±m×6s7î {€yoa@5ÑÄ’ëÜTHKˆ+¸ŠàQ1ø½;ö¨Û°-Ü[è]ûÍ"®dÛ?/ÜÓÓc*x÷0ŸšÉò(du©@Rdq®ƒí€ÐÕ­PÒi¢;ŸC™Šùƒ£ûÊȩ©Øøp÷ä~&¢ýY_‘¼Tx5\šk?³€¼hçÙ¿ç'ÿâÓ‡çíÔ?¼ f‚8I)'¶xQÿAíÞös}¡xã +ߪ–‹öVôÍ›¿›áÐÛ» þ…é2çEùKaPOG{«hÁý?§¿¥TkôK©TKÆ¿ 6Ì·Sw˜;7þtü¯ã€ê8 ¯Ü8O®÷vÚÛÍö8²—)ïX@1ãÎCÓ÷Žþ8Yºmé +³ƒÖ Üñõ„I¢@6¹âEeT I´J]™D"£›9Ô{,ѺTá¬C3nahµ¶9ÒŸr‰è>>PóB+½Ä^¨”×Ò±c§ÍÜÌG¥‘"P2ƳÒ%1wJplHõãdú½³#åËNÍ·¥+Ž ùO~Ûßñåˆûl‡é–hj ùÍßa~+vÛéÅx,Î9i’·ÁJ‡Î"f#]éÒ,4×ý#|oúnKob¤AÀmÞÆ=ÞÑú›1ØG( ²´bÇw‰Àù{S'ÿ +0.¯y + +endstream endobj 276 0 obj<> endobj 277 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 278 0 obj<>stream +H‰„VÍnã6¾û)æH¶C‘ú ÛnZlÉŽ°—EŠL'j-)e8Ec_¡ÏÛá%¶¶ `Fœo¾gxöÛm÷zñs±8+ +!›EÊò 8þ[!ÊY"@¦L$P4 ÷ Î8ç1Õ"°"ZíßȇÛ_>}‚›¾ºªÛ¿@ƒ”Åäö&är·4È™ êqPÍêA˜5’8úGñû"D@a;)NÑ=ȘÉȸiœïÔøæÆ+‰ZüiÈGŽ|(Vrô“ GL–B’ñ)Nh“ðÜ”}Ù¨Aõš†|î‚á, 1kAÈÂ(GÎ-û0qF×]K–E9þz ·XºÅ–¤¥œ†âj¥hŒú±³Ï–âúh*Y‚ðöe–’Kˆô@o2r±×Ë/ŸiF.¨`©úífy‚§EžÊ b‘=EÿK'_cn.ö(4Wuë{D–WåÓ »¯åv‡†Ú¼%d¹2Ê¿í¶ƒ·¨z“óœ 7§xÃp£†¡ñ„…ç|…äùÚ ¤©¾œ^€œ_<I–ÏHùýÂgKøÅDf –+÷®¼-R-+ ?%6€„l)2Nɘââ_ÿØ +x[¦Œ2NE#ÜÜ| +‚NÃÙÈô‚Ë´2¥þ+óŠÏ¥S¿ó\€“mç¹]sùT6[u>ž¡»C”Û”C"YAELf¯§0œ;…Ž¹”òÝ1@Nß±„/ŸÇUÀTœþíôBùä»sFX)û3ù1Šù×Ó0Q½yŸ®Ï>L0‚0žÀO˜q'ÈQÀÊ(Ü”zš ‘ج[89G ḢQˆ%“âÎ'×D8K°l-'Àºîpp>Mó!ŽA˜ŽžWyäòrŽIæɽgŽÔ{§¡/«¡ëOe'Äác.˜É?½Ý°ÛË«y"9÷Bè„#ŒùâŽA$°éen˜þo‡÷Më£éð ÑU_?uמhçÓfœ¹‰ŒÍ°Ñã%0<(Ðê®ë5¬ëÒªð$¹OÍ°¥˜3Õ(«)õ®wRãÆt†áD×HSnfߪ4ƒ=Ò„ôJ?¢yLºÖ>´‚ªk›„”u«¡„¡¼Ûâ<̈ò7„ñLf¯7‘;†nF68r\£©Uö¦›"˜}i°C ËÈ©:ìU4Åþkg£ ƒZÃþ¡¶ªÊ¤%&ŽD #¼¶¤ÙñÀœ,™¦üî&s³Q=ú‚ Ö £¦Û£Ê%ߌæ@¢c\ÐPÓr;«]«-¦l쾓Áìüøk–I©A$¶SmjeÒ&ɪ²…;Ô+;L5Ö›•Aéö¥†¶`ݵê'<4@:6©²ÕñkYoÌ]DÜuë{µ:Þ†Vª)íNö• és¼ˆn8Ǧ¯Fc„õ¸Q©LIZû»vóÊÚy½ÕlMæ­Â†‡›©Z0Õ ëkèw[€›P»Dô»¶­‘gâÑï=z ø}¸,ÿ eô + +endstream endobj 279 0 obj<> endobj 280 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 281 0 obj<>stream +H‰œUÛnÛ8}×WÌ#U@,/ºE€Ým°Èö¡F,ô%胪Ўºº¢Ü´?Òïí#ÙªƒdE€ˆ93çœ9C¿ý{+aoƒ?ËàmY*Pî‚Œ9üó‹¸à©q•BÙöàBˆÊ:ˆü£ž‚{öÇö¯Û[ØŒÃ4ÔC ?!Œ2ž°íFŠXÈ”K!`FWÌ&Ó}1#(wF3—.ü\þHL¨|qZ%–p»2ÕÎ\m᪲$ ˯|Là¥>%ð+‚Ÿ ÏÕ)>vñ÷lk¦0E4å@Ϫ݌ÆÚãhÌïBÈTðu¦hWrVáþࣹ¡ä’µ ¹¢Ÿ‰åH…d³|òLf~ssž ”ò Ò\¬AÉ„ømª±êÌdFF1¿"ZXKb3#„ˆÿ=‰¢O UQ!5„§l4þ-u1ìSÕ L|1@°„â3°FÜ™ªŠ;l•hAõ5Õ¿ ½1%^wLÁìaðÏ>ÄóhÌoèe¦ŠºIAÂSŠ µÆ”¤•§í¬P:Ì‹Þ=ÙëÞÕc»»ö¯– {¶¡’|Ù¨Çë‹žù#JèôÄõEšdªÂŸ¹ù^u‡Ö\-Ù&…o©æyì'$^RúØßÈ ýÄM¯yh}¥|óJ<|üð6œ±Äçkâ5–g¿Ý³÷ä,[Íaj†þE“®à‘K5¹ôvýûê¸7ÐXØ5Ó4»°3Âop¶ÖïS.¤šÉ·ÎCŠá˜4±T[å\EŒäNeãsYªz cgÁ0ÒÕ”ØAðxp㥙Ykp(KÚa3ó<]k§ÎÐgñºzèº*ÄÆ`*É\Þ‚¹Ž }ûÉ×<øxz4½È¿¹Ë»‰ÝîƤ¥bÎg‘øó]-Àž9êYk•|§Ìl&þÿ¯¬å*¾3H6å9pvû×^aGï«^_ÝÈóË2–¨™Ø>b_ÐŒþÊ<¸ÛØé¢Ý•ˆÊûfB=w°©l]µ3›2ø%À.ì + +endstream endobj 282 0 obj<> endobj 283 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 284 0 obj<>stream +H‰œTMoÜ6½ëWÌ‘* F%J2 mi1²º9ÐZîz[}âÖÉéïÍ ©ÝU.ŠV#Šœyï;ù}#`?G¿5Ñ›¦É@@³‹J^WâÏyÍU²ä™‚¦RØG)OÓ´€¦⩧èžýºyûáÜÙÑíØÁ?'%/ØæN¤y*i +›8©yÆÌäLÿ`,d´G2Jiþˆ&Ì|ñ%–Yp™S™>Ô.©vJUYQÅÍŸ>à…<'ðQ€¯ê‚WÙù|FçïY3ºX1ÝÝY3ÏGkÞê.F˜ìV·ôa´Ó¿õuÍ+µÊ4ÒŽÔ¸Ÿì'Š9ÿO‚ž} $À˜)Ïó5È ?Xè-½ùIÅKPUºÆ"Š@ïN[Ýgì'96â*ÐIy)°—‰à"¯ì»5fKCI‡1.˜%ÏI’)•ó’Ýv£vð·îŽÜzšºo¸Ñé&¯&3`Æ`'‚†=ììØ­ ´æ+‚ðÈžÖŽ{ü3 ÆOzx?ª‘üÌÀPög¼fó4ú'–J±§3rPÌ„—W”g{xo`šŒ rKŽ¡,óâà'ëS_?Í7Ÿ>Æ.\·¶ÛÝ<Ç„ºS…ì×mœc܉όÝxEÖGVˆd¹œA+a#[ª_2ÛÒ+>oÂr8Ýü{F+¹âXû=ï¿ê~êÌÕÉe" 6«½ÅjP’W9aÅ«êâu¯ÏYž³2ä‹.O}¶ÀBJyna@„øþCøôñç/ÐrTà23/’¾8üž½£Ž*6·ö0¹Ã8¼â†ËE'ê0ãfÐÏ ÀÅо·úžæ2þçQb¿ˆq(!—BIävÒ*Ìaêámx3)6àŒàu9S±vì'3„wí ìF[ÐÃ7Øv;c—ÏèÉLØI!<÷dÌèe÷hpüŽ!ï'9áy~26°éa ”ÃYÔóM§y!ŽÂsäðÿ‡út—}6½Fp^|ö¿æ+ºš½Ô¹Ë-Àª¶y<Ì¥Ãt§‹ˆþ·4P8@­`\Öý +¶õÁø8ÎfKgŸÍz‡'½^ó2¯ŠÐÆ—Ì3£R}ðÁààQ““Ö§õýðß÷aìÎQqªñ¾‰¾ 0:ÈÝ+ + +endstream endobj 285 0 obj<> endobj 286 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 287 0 obj<>stream +H‰„TÛnÛ0 }÷WðQ`U[¾ (°%Ý­(4ÞSPn¢¶Þì:°Ýuý‘}ï(ɉÓY@’%Š<çÔÉ—9‡‡Îû”{'y.€C~ïÅ4M€áß.”*2¦BA^{ DZNÅ£¿‡^È„¦»ðÙÈâCÖ”S4•°]8ÜÂYYѵîu‹œC”"sŒ9f9à”‡)j<nÅcªcÇbjîq£ˆ !*’—5ªp®bœBù÷M‹»¤F#D®È+þ»SÛiµºýê® Sæ#ry™Íç:LZ`û:‡Ø-^å_k?2)^7v~2™ä6ïŠh÷1h€‚òAÑÑ%‹ãd”CŸ¦ŒluV̤¨¦E¯O_º³«‹Óe[ÝŸYX³ä98Q²9X¶g{ µ&‚Iµå{”ªËMjmÎÿõºÒÙÆgÎ]jk#%)Ö¢é«p§NùQN °ŠÎ¦n¹ÊÂ0Ã6Þá&å¾üÃû>¯.Þ©Þe8>;ÿãoÃI—ê©Iµ"ݲ-×}Ù<­m¶­j1ôæ\÷ôVˆ½HrÒ›Â.º®Y–Eï'XÎz/eÿ,‹ª´Ã][˜HòÆ”vëîvÚ!C!³m¢øYºÈ½ kƒõ O ëuÿ + )‰Ql €1ô,ˆ9Žö9 +†ÍÀuÛ`qËXfï¾±ÅÔQøÜ´N’`m§¯‚QJpxÛ#וÕÅ¡ÄïgíÄ29)±]%Á“î{Œ<›>¯Vp‡km{߀ù„¾éåôý^–Ûo¯k]({BZKõW—Ù÷ú˜cçó<÷þ 0{œý + +endstream endobj 288 0 obj<> endobj 289 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 290 0 obj<>stream +H‰œVÁnã6½û+æH0#’%A€n²h·{H°QO‹™ŽÝÚ–+É›îô{;3”,%ÞÔÝ"pLKâÌ›7ouñÓƒ‚§vö®˜]…År–I—CŒ¼Hœ´L&µ…b;‹áiË8ŽS(ªÙœ—¸ëyöYüøpóáÜ7uWWõþ†hžÉT<Ü«8‰••*Žá!š;©…ßw~ûèÐôŒ.ú­øe¦0 æäa•f˜L*MBi¶!wF¹cÊ*l¿ø$€Wæ€W¾u©Ì5íˆ7åfýؔݺÞÝíé }°ž‰4Vf`óxK1ŸÅ}Ù”[ßù¦æ –} +Še¦¹¹’*qˆû¶G ôièû°Ûø.àˆR©Ògm"3Z£2™ˆŸk¾ó ] å~¿ù +kÚ·¸^í"Œî,˪«›LÀaG<"H‚[„{ G$†h“)\13°À=ø8_nHd¥ánÄ/f8™^~É ˜°§Ü…ù)sLœ t"¤N´ûš¿¹Z%ÚÈ`r~ôÀvª8ôÓq/X#ó¬N¤5¡ÞÐÄÕs{}÷ñªj6ËkFc“˜Ü(~àŸUs}FG!ê´Ì’0T”ú­²§’}ÿW¹ÝoüåàÛ•¥Ô,}¾4¸[.ùsRÖVu& +Ü}üo'•=_0g3¡Ï·Ôg+ÚªYs¶3c¥FQ› êßµ°ªƒd1–¬ÛadôqdtQ>ŒL;î¨pQPHÜ©hü¶,sv„§ÚH£ DmeÎ&ÈÀ›±õŽé)J"ü®ƒEÙa!1Žþ»º[ÁÄ Ü- Œçd8aO[‚íТ Ü̇Üÿ:ìU¹ƒcIEd⋇nå¡Åpð¥ÜøžÇ˜V<¯ÂTbeã'³•õ-?æÔ*µÇa¾B·Î¯_ˆ#–‰UjÊÌx–(Сå¨8uÈ'ËsžÚœÌìè„" Þ‡ÞÖ{Psô:D¼Þlà«Á'°JÆ'€ç=Ž—ˆ«ï€Œ¤¾F}ë—åaÓEy0Oí¾ó ¢TŸ£Šº¢s^ŠÑ­wO°ÄÐÜU¿këæ‚ÚˆGaè¾îý)'d‰x¸ýNìˆ/MÊÍ¡iHëȈ²¨f¤ê/V½¯øð`¥N³4D>ï‚ Žrd $µ¢P™?ä)<´8¢aT.S<£'³ð–SüºiUÿsçƒ ðÈ‘'›“‰ï)®Fc„z8<éöîlè+–ç\ï¯ßl€ƒ™°  *­`¢0NùÀ¶2눠2o‹ajSA€ʬÙÎà# +¡D~Æ#£ –Ñ:1ÔWdD,b9£@¾aèæhèÃ{Ò'¿-zÃôüÑ^â™íÄ[¶>9)Þ³þ«¥‰ + +endstream endobj 291 0 obj<> endobj 292 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 293 0 obj<>stream +H‰”W]oÛV }÷¯¸Ò«úþŠ]Û Y14¨½{¸¶¯m²dHrœü=ì÷î¼²•¸I;H-éŠä!y©7¿Îµé&¿Ì'oæóPj¾žd^‘+ÿøG\xi¨¢Ì S5ßN|µ™øžïû‰š/'Sþ‰·“[çýìÃõµºi›¾Y6•úW¹ÓÌKœÙMàÇ~zï«™;-¼Ð1»Þl¦U!‰2çþ9ÿmÀ`ÈÎåW’Á½Š/ŠÉÍV|gäÛ'¯N¸ó¿(øX‚¢£þ%á§Eâåáñý˜Þ¿u>šÞ,û¦u3/p®ô²wSÄÖ´Ê)!gA¥^¦ÒÜ·&9 A"6ot«·°Ûvî4úK1æ{Y€N/ˆ „ÿ‘·ù cÉ߬ٷKs]¯\ÀuŒ‹‡™ó ÜHrÙŒ¢ óbg~gÊBó±Î¬T‰¸S§^™Õ¬%¹—æðç“')Rtògë¥zêØ©êLß—õÆMœN-LYãÇÆŽÚÃ.Åt°Ë8"ÁñÌx&Æ)·0u(Á‚)uä΃!pà ó,Ưžôˆ§ðp”+[)åÒ +•o(G±„,ïlÔ^ÌÀ:²'WrÌVâI(øð|*Á_•ŠX÷.àR AšÆ°ö´k{ˆbª÷èføJœ-#q†ž8 //ÂPjq^õ/-ž”kµhú»£ÍN­5áíTßœ…pÓÁês0ç• Æn8­sÚ¿…¦–TÔRW’ËE«û²©ɱŽO2ÚX¨¨ˆ‰Óot:Å• +=¾’›Âév ÿ_SeT+âîà‹Ë#ý_øW0÷ +•‚±J}@OÄ#YXNŽ\f´­z{èÞ}ùüvÙVëw—EEäŒÌâËe+—/³^¼'iáeV ÉïK %¶‚Ï|zÐÛ]e.ßƕĹæßÁÅá¨ÀKÌ4=Ç5 + °^1£¾|þ1´$Û¡UÞð>—â¾ppÙ–;j¡ÿ£½¯¹ ¬o­ÈœíD~-üÒjSÞ›š¤‹d¹aÕlÇïhQPX`¢Å 8BÃ[G4Zéz¥žèíIgvÚúÞºô€Ï{ê + ßž’ÄDÎeW6|£ lóõ#?$¶1ò8*š$&’Ät†$/$a‹@’†/Z²Oúß Uvª©«Gü16K˜ìsÑ'»¬1š5’­Ô£šŒæG| +I"Ú»1ÜŠÐÊÜ¢È#Ð`nʪRº:èG–ÑÎ IL¬õûž«ÉÃ@ÐèM†ÚÈ.CkÚêÇ!gÙÕÎSWÛ¦5žúÈÒiKœÛ‰Ä¨iÄøª5k#RK³æŠ•IËÙV¯ø ½|´“ˆÍ5ö¼”ÁBOý,ºLF¬©;€½£n‚Ö¡½‘êí¾êË]UÿP¼Zì©’&ŸW[*d7‘ÒȆ̈́õ5JÅ­£ï5fÈ¢vxà8­.ô¡Ä°Y•kà¶l£ËúxÄã÷Ô'½¼SC;ä èýê>a‡¶…µFÔwºSeÿºðP”vFSEfJ§{<­)Š…4êbTlöû#ÙIÊ–’²rý$ +“Ã,êÇóštx!u^׈éÞ€)O†0膘` ¹8•ø…‚0ò:P9ÐH^H©f·“}-Ī¾J7ôX +G,ºã +"ûó†XîÛÕc^„5áLs¿?°‡}ö«Ùj G›svÿî.1« ç\Ñ£ýÃHÊOûDäHVº»f_­F¥ßËV ˜!ă÷Û]÷æÆn»(ÝÛ×%æD×póHUÎ7ÞQYlÕ®5BBJ#èµ™Z#Ää-±pDGÔºm¶BY‘à㓆/X§°/årË»Z}ˆü3ïz(F8^žÇÒu.­€ÇIñ²4R¤êFS<Î +Äõ÷Ï3ä`ÝHvms_®ÌXI)(º¬ï!oåмE’£æ=_¦"ÔPÓöo7Ç)ˆÝéBî¯]Þ²ÛÖ.Ú6Á)ot(—,ãˆN +¢‹Øa¬ÊnWAÕ+æèÎѽº"îÊJ6btÌÙ•¼+Ýkšz[ˆ™lËv(kú¬¨Õ¸8¤ ´grnõßüUá«Âf*JŠh”©@Á^Ö$ ™SÉj€Ì4Lë¶géÕ|A{CŽlnøªëYÈŽS˜~µƒ2?+€¬âµ|ÏÈ7•ª ±YçÉz¢~‘„çÒ™¹•Ý•¿ûÒôÂ¥o`@¶ ªÃw¸ÏÞC®¢,x2’“ÕâÔÄ<¾m +9/ z…zdÍ»”û4Ÿü7d)¾ + +endstream endobj 294 0 obj<> endobj 295 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 296 0 obj<>stream +H‰ŒWÛŽÛ6}÷Wð‘jWŠ¨«µ(´¹4i°HP»} ‚‚¶h[­.†$¯×ýŒ>ô{;Ê’í$Mˆ%Šœ™3—3Ãg?/”Øv³Ÿ–³gËe ”Xnf©—Í…ôe^ˆ0õ‚D,«™/¶3ßó}?ËõÌ¥G8uœ}”?.^¼}+>´M߬›Rü+7õb¹ø üÈW‰§|_,7óiö½©V¦î %Šs>-™)r~ŠSP/ÂØ #TS±îuû¨U&³üØxžÐ›Ÿd±7ÎçC<ÿQ¾4½Y÷Më¤ò…._êÞ°—PqäEÑD„;±ž”…ÿqOÈw GyJ–By/|bxWÖ~Œ?óFd³Q¹ñLâ¥"™ûS£TÌÀ>èVW®í7å÷ŒË÷RQtÁ´(û_²::DŒbÑÚµy[çø\>¦òI8 + žƒMnªÔ‹ärgÀ”•¦mÉEá$^"ëÜ<‰fÃ`ç^2}>jrGdz>ë5у Ž”ŠÎô}QoXvbeŠ¶Ž«¤8€xaî —p„ŒãJxÊÂ1À``"1ÊŒ)‘¼òä„`8à æiO<ÉO6à!+s›.ÂÁh*mÞ¢"6™ÏlÅÅ€tð+Ìy›ÄEÐø`4>aã_%±î€‹†ª$‰@Úe6vÚT ¤@W,[H¶3ðXa ¼y‹Û¨¿oáK±«¦ßevb£o'úÀYØ`΀¤NÀpœ£QƒWàgŒÅ"¸ÄÒ•ºÎQQŽû7ÍG1dö˜FXqV¼ +Y~[龇¨éNœàŸ[UnžÿñæÍý9ÿ¿XŒ9„2§)tUyîgª¡%\b¿¢§3Ùíú­Q‚ˆ‡”aôb«JXÙEúi:ñPæÊŽÝó÷ï~X·åæ9™ÀYÂòæCχëöù]ЖÀ“3¶/Ââe´çÕ“®ö¥¹¤)ŸÅeÄ<™H€"G‘—M¸âq¦ÓK}ú €Û]?q}õ‡R÷Qv]`. ¯ \~÷U¡ïßý/Z T^rfÔ¯‡ôܸj×m±ï‹¦þ k†£ÃŽY(-Öº,Vt¾eA ¦q+xMl‹GS#Ï!§ëñ  ô·×ö|夜C‘D·‡¡0r*?øH4´§eÍ‘ðY/œðÄë†e ãþάԡ=gIË´øýg˜iÆl«­3x&@‚ !ÑzAăÀ•Ó~E'šº<ÁfpÀ¦x‚’½&”K\Óˆ@xÈTþWøåà@F²ÆTrÿÂð"öá'q,ÊRèò¨OD§`8VÄù‡žBÔ BÓ-h¹­5¢Ò§Ù²op¨ªšÖxâ%ÅжֹíL„[/Z³1L¹Øs^;6ÂsˆpNÛìëÉö#ÖØ-DŸ·ßxÕO-åÖÔ@Ýa[AÞ‚üêPöž,ýºšå‰ûŸ™ÆZ¨;šPèÖ ~°º&EòQêG dUrÐ@vZ56Öæc-'/6€Úö±­.êóΉWz½C2Ìc(ÙI6Lz§MÛÂe±†üÛAk(ú õ¹Zx• Æ v +l¿Ó=l©µSÀŒùºšÄœ ø–:Ù³ïÖì»bsH;”^<’Ú7õëàŽÁꊸà ÌEO¦ ÙO2Víà”1Ö_(JÙÌ´DÎ'ÓJ ¢å¸¼æ2°–Ÿ§ ®‘ŸÁ©#Âìã*ÁÊö½ÑYcbdÿßhÇqn˜e5•¨(]ùWw=6“_šh¹¡Žà혉ó‘ñ~ûð>Ìå‚Øp‰ˆB" ÞRñ <Ì8QÃäBS†ÝPÙš8Xܽ£äÃÇ;ˆ¦ó‹?ͳÃN&G<ÈÛ¦$M\¨‡EƒÚ¬ä“ÑvÛ¨ªËoXÓMÝïîDž_l@¬57Ù{;šÓÌ«OwâÍš†CæpÏ;—l»;ñðpõ©*êÝçc|.t€Î¬zDZÚÔrwyz .Ê¿4«v}K“wÈ“7Ä°:t=†Le”Ó;¾”P:†’_0¹™·À¦ÒÔ[(+l'[Öx3Íí +Z%G†#£®Ç€ÏPž9ͳÏ(éàýÔÿ#–ÕG\üÜ×–‹àØš ZGê4ºÖ¦0eÞY×Ä<9Nr)s)³úá ™PP Öµå’Öt8®ôC5X–E?˜jߟèâÀ­²›TIG•4ÉC´Ñõ Ò†¸ÛÀ½°¤NH—DQDFê ±JºGæb5½€-:{ÇÜŠß–/ȽàSðÐûLY8äΙ•¡#ôGô˾m‹Y9âë0ɺ!Uõ#¤jH%؆Á{'í¤T±uÒÊß ýÀ,­‚jn ;RÑó¢Û—$— BM‹Ò@8 ÆEÉ×*XnÎ;0: ¦‚^ˆ¤g‹璉Ãà„;úds*(à ôII§CIP& |aàùª¥ØÁý> endobj 298 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 299 0 obj<>stream +H‰”VMoÛF½ëWÌqYDî.¹$ @Û8…›ƒˆèÅè¦(™-E +$e9$¿·3;K}XvÚB€Dr¹3ïÍÌ{«÷¿-¬‡Ù/ùì}žkP¯f‰ÌRñã.¢LZ &‘ÚB¾™…°ž…2 Ãòr6w—¸k?»?/~½¹»¾»²kà;óDÆbq§Â(TVª0„E0ϤÕv¬6UšÞ1‚Âæ¿ÏÔ.9_Å ¦KQš çN(wHY…5Aþ¼2‡îŠáÛ,–©>ìW´ÿ^|ªÆª»>°2tÍX¬+†q,Æ +°ièù¨˜ãÝ}±Á˜ýÌ#d~ÅÁB™(,Þ\IeýA÷µÓ×nÑíú²ºi—RU€‹‰x,¡Ñ¡‘Väbx(ÜúP-¡FÔV´Ëêº×.•6ÅD!¥àΘc"ß$1Ðà²ÁPcÝ®ƒX ðPÕ-^¬ƒ¹°ÃxÃðçS\GÀ0 +ƒ§œ +ŠAUe2‰à'Ï‘Fœ[úÒq¦±æ'¤O*™(9 Kß!¬`´îÕ5ÕÇ0jÚ±†‡ÀÈXθä—|ÎÊOèõ½eôŸëÛ׎ò%ÊÚãPŒJ=Á]ù—Q»ÃÆ\±èqX¥–œP+™fZs3.û}ÛãJ½‚‡n|<Ä`UÛÆ©yÒ'šZ1Eýw2~œ:*¸ +,ø9—–—÷°Ù5̺Þ65ªòiÒ‚ˆ§á¬ˆæ8ìA Î_s‚fY_)W&†mç~[jŽÂ†×/wsuО +Y|™^ÖÈ4"{{P«ø bOøÃ~øxûåCÙ7«ƒ6Æ"q²ÿänËžoß– +¨h—$W‘HpnZ·“ÜÍ‹ÙC:)çü%2ÍÈ6;LˆŽ Þ¦ÀÄÀàÜP‰EMg*9 +B‰Èé+èg<“–Å7¬ªßÛ©ÛÕK¸.J6®š&â9=>NÞã ç+‹ÖyBËzµªzzvè[€¤Ä¡yï`Øò#2õêV˜ÝROuð‰/DtY‡ašO}Y?ÕË]ÑœÙòþ±nØ<·Õi|n>+ñ_HŒç¶#{/BØ×Mã†è…#³ºCŸÀ“+dA£•‰fWÉéŒôñ^Nù[yTÏôçákµ)Ü õˆY‹¿‡+ôÆÌÿu>ûgÞc¤ + +endstream endobj 300 0 obj<> endobj 301 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 302 0 obj<>stream +H‰”TËnÛ0¼ë+öH0ÇDIA mÒÂÍ!A¬[Ѓ,S‰[= RFÜé÷vIʱ‘4I I‰;3»;{òuÁáÞFŸÊè¤,p(›(£E ~‘T  +Ê.bp1ÊK¡¬£™_â­ÇèŽ|\|žÏáÆ ãP-ü†x–Ñ”,n8KW”3‹xVPAôfÔÝRîI\¸ø{ù-âPxð°J3„™R™8˜.`g›9T¢’¸üáÈ'<—Oü*ÐWEJsáîÆ… @æ}«Ç/U=¦(S +^ÐP4•³ã ÜgàŽÜT¦êô¨g ê= JÍ8¦lÆ)O +$|1A‹§¼ òæiÌû•ÞaÆ8Wœf„Á²²zë~§”íÿw± 9¡ñAã;¼ãÖ=ð A)qŽéE=Í`èD$pžÃ³Ò‰"PðI@ˆœ˜8ÁHD(• ‘ãöÚ¿z ˆˆPåÒv„T²Cm|Rg/áI¨½[» v3øgã÷œØXR…¢ýfJ(V‡³PžÂ—¦%iž€ -µåÏk{öhϯ¯ÎjÓ6çžRà+¥tdÈÑ‹òƒßÖæü~Ài!h–W8Ì×ô÷Üå®ê6­>Ýü]Ršq*Åšš¾TsDżvùúêßôÉ‚rõ¾>"C=/\=±µYoÆõпêvhÂ$4áB*ØTf\Ç +ƒÔ[Œ%I[™Ðy6þÀ`wâCû ¶ +±[£Á„ý*\ªÜ/ú©U)ûÌqŠŠB&(ú s®-8/Æ)™ì¨wÐÞ“»­a9UPWíziª±Z¶(#!{§6{¿ÌöÈGÆ #E†jðLJµŸ+Š@žz##]Wõ+gþ§SŸ×[£†éÊ6ÖÓ7ÚNçMŒ“ŒlÛöý_ÆÈ~þÝê®Bnt¤8:~ÚS$ˆÂ¾,£? ‹ +…ï + +endstream endobj 303 0 obj<> endobj 304 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 305 0 obj<>stream +H‰”TÛnœ0}ç+æÑTÂñ DQ¤´‰ª´je‘úõ°d³-—°J£ª¿ÑïíØÃî²ÝDi…6¶ç\fÆGïg½÷6óŽ²L„ìÞ‹yš€ÀÇ Â”:æÊ@V{žàBˆ²Â ÜO=z·ìlöîò®»vh‹¶‚ßà1ØìZŠPHÃ¥0óƒ”+V®†²¾+;Pvf6œÿ5ûàI ¨8¢áAG\‡¦&ìØb ‹ÊLägß,ùÈK½ àFDߤO”=ïKe°Y‘7gó9ŒFù Á$b/€SË®ó.¯Ë¡ìz?Që1©<–hW ¹ S${¾3jôLiòìS™÷뮬Ëfð4â3†ó#tÍÓ2æ¡Ð({(¡.s·ŒxšK¶v“ήÓÁÚÜ0ŒC 9ªÃÏ€'{;²#bÚ;+Á!wGÝà ’²~Õºoãã~‰l´Å¤Éè*9š:7S0š'!¾ ×fk%™²ÉÅÉczõñ¤èªûSLJÈj­-f—'–ÙéOkٯɉì î;˜Ýé+y&vQraÆZsÌ^²…اnÏż^UåñâyåQ¬x”¼ Κ¼jòPõ„ŠyîèÕÇ­T€‰?{1_U®WÉ뺾¦r8·å`X_tËÕ°l›ÿ®dßCcÑqßì +xS¹TôP-ûÚ{Ûý~íoN¹ŸCïh¨ƨsËÀlÈ„p4鉶tØuþmS=Á]i»ÈyŒnßsui†«E9¥ÈI{°A4a«zEØ_¬nIm”2È«jìpI®˜ËätÓ€~uŽýÞoðå@Ê%c-Iù.a8¸ Á0Øygï”8‡u¿D°·.¬­±®f7AÑÖµ³¼™»ÌoE€Ó:ù·›es•Þ”uŽ¸ fÔ¦ð{Œ— +zrPPzW…GÍs÷)V&ªÄxkšvp¹t34﯋ѰG—Œˆ|Ž0££ŽToË,±êhW>wŸ'èÖM³l£™÷G€ÐKÅÌ + +endstream endobj 306 0 obj<> endobj 307 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 308 0 obj<>stream +H‰ŒTËnÛ0¼ë+x¤ŠˆáC¢D#Ð6A‘i‚X§=02í¨µ$ƒ”‘æGú½]’~!‰Û€ERÚÙ™áîž~š2´pɇ:9­kŽªçIIT…(üÂ"WDr$JÂ%ª»„¢EB ¥´@u“da QOÉ=~?ýxu…ní0Í°D¿Qš•¤ÀÓ[FsÊ$a”¢iš)±Y¦{0qÿÀ.ý^Nò<®ŠÒ#Q‘û4]Ì]úÜÔgÅR¦õO>ä™Ø„U¤/UA*îã³=ù{> endobj 310 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 311 0 obj<>stream +H‰Œ”ßn›0ÆïyŠsiOÂõ0PU‘¶µš²JkÕpWí‚¥N–.@d“vO²çݱMÒLMÚ)1Âçû~ßÁ‡³/3K—|ª“³º–  ^$«Jàø ‹¬bZ‚*˜ÔP· ‡eÂç<‡zž¤a‰UÏÉ=ù8û<­í‡~Þ¯áд`9™Ý +žq¡™àf4­˜$f3˜ö‡± ýE¼ý^M +Ê`Wyö r¦2oÓFïÂ{sïJtAëGŸEx¡öañu•³Rúú@,¤ ³yÓ͆~£Ä˜ÿƒfè’ÿ£âß“ÛÆ6­Œu4Í0ìyŒÁY!°_©`"«ö2p ‹¾õÕ¬"†r¼Žq³Š›^ÀJKñQêÎÐ5ܦÿÅý‚8ª˜Fùp3"a>ÁcÀ*„«@+VfxÕLé}2ßæƒÞ\<»ÉÍõÅÜ®“A•R…<¨?„Û¹¼ÓÏÑ–gLÆu> endobj 313 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 314 0 obj<>stream +H‰”T]o›0}çWÜG3 ×Ø@TUÚÖjê*­Qà­Ú¥NÊVCTÝ/ÙïݵM“t]ºM‘à:¶Ï9÷ø˜“O%‡Í}¨¢“ªÀ¡ZG-r`øóEZP-@fTh¨lÄ`1ÊSP5QâKÜõÝ÷åÇËKXýÔ7ýü„8ɨ"å’³”qM9cPÆIA1ÛÉØ[3€pk$qpñ×êsÄPxòP© éA**SGcw渙c%:«oN|Äs¹ðU¯ Esáö{Å> endobj 316 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 317 0 obj<>stream +H‰ŒTËnÛ0¼ë+öHC‰zA€¶ Š4@غ=(2m«•Ä€¤ãäGú½]’vìÖqS)írgvöqöeÃRŸêମˆ¡^­J`øs‡´¢y¼ Iõ0XŒ2Æ2¨Û rG¼µ îÉÇÙçëk¸SÒÈVöð ¨ ™ÝÅ,eqNcÆ`FMˆx4bx +ëÉ ~¯¿1L¸?eÂÏ(O-Ìà± ‹Í,*É«°þaɧž|Ì_¸“§ŸW-{ß3Nm€{2k›q*´ 3š’FÏa¯Ä›œ—ì0VœùXwja„Òa”bÚŒÑ"F墘Æi…¼/ƒd[õ’Ì«÷m=ÌÂy 'ÇG‡œÆE̼´Îã èùà¶ÇùzI<­Êù\=7Ãc/&»øo§”• -ʤtœÆÌâÔ5¸½ù¯Ì²ŒÙN75Ã})/m)m‡¨îÑtr<Ù³ÑßMË}ÓNE„,1HB”Ñ»Žk×J ÷Ñ5GIŒkRX(98Ÿo]vã¡W7.)Ì$Qæ8AƒÐžO‚Û!gf©Ü“ÎÀÐ-WÃÀZ‹Åº·Ýï,¸Í6îZú?Ý¿@3Î-ôÏ>a³î†FHíÞWñŒ£>6=ˆ'áçÕ€lÛµ;* úÅmݯ”mRÑN¡MgVÐ +õªÓÐÊm%B+ÃvZçм•|ßÛ¤8Ù©zóÚ¨ +´5¹^òÇ9h‰w/r Ös»Müí·”8Šd/Eâ¥Ðk% é{˜7¦±KÆ1slÎv“%v=5}‡Y-P8¤€˜1–Øégþ\KÕûkißË»ý;CãTT®º?õ•¯ÈÉ-¼’«:ø-Àš´ò + +endstream endobj 318 0 obj<> endobj 319 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 320 0 obj<>stream +H‰„UQoÛ6~÷¯¸Gj˜YR”H)( +¤k1dE× ÖÀ`ŠÌØî$Ëä¥A±¿±ß»ãlËs›$@BÒ¼ï¾ï;ÞùÕ¯ «~ö¶˜½*Š43'ó þÒ"É¥Á8[(š™‚ÕLI¥T +E5›Ó£gwâzñËÍ ÜvíÐVm ÿB4w2‹[­¥­ÔJÁ"šç2~7øæÞw‡;F¸èÏâ·™FÀ˜’ó*u˜L*MÒ4œÛ…Ü*dNEÅ—@>aòÚhÅômžÊ,ñÄX'àN|ôe¿æYv‘’NøÆoy?,|í«Y¼¹àg¥›©3ô”ÑoË®lüà»>š'yÅ`˜G£—s-u’£’wc”>ª JôºÀiˆ‚K¿#šj´“‰ ^k ^ Y´`X—Œgë³e¢Ý8•‰q5r£Cî8æÜõî=ôÁ„`H,†(…[ÂCÛA;¬=aå(\%S(w’᪃s!Nü?@¸N´["š¦Ü.{>bŸæXõ8u);eÈ)u*<„ºÏ/Í$–+ðÙG©ÌE¿ké?–V¡}d¤ž7WÇ +kÅ%Ω¼9X#³ÿZi챶Šª4­ ¿“×ý›O^W]ýð†˜)âdŒ œDøx¶ßB5ÿ™D?ὋmÕñöÇOy¦y"•{„8þÈ Ö‘Ó÷_ËfWû«CŠï{ºX¦Ù‹Àõ¶¬Û•¾Ô?!„²žùôá(¦ç ýE7Œ’qö²ÄÄðcy‹}ÕmvæÝ>ß®ßiÐC Md±ßž,€PÅ¡{°)øìüsº›5“:K-wØè!²bh¡D§ö»v+áf‹­îÁS%&ÊûöoOÇx»™4_†-Íb³lßãðBëª5}TnWlQI[ŠäÕ`IKÏáÇF2OáôU¨s¦„¹¢#—a¸¡å÷O”ª9›ƒ¹ŒŒoýÙÁvªÎá«å³oJò½#žõW8Óò‘èûböŸöñï" + +endstream endobj 321 0 obj<> endobj 322 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 323 0 obj<>stream +H‰œVÛŽÛ6}÷Wð‘**†‰’A€¤YÛ`›E¬·E´2×VkI)׊þF¿·CŽdi³· 0`‘ÔpæÌᜡÞüºdëVÊÕ›²”Dò~•±"'~aLK¢2&5)Û'Ûgœó””õ*CØuZÝÒ÷ë_®®È퇾î÷ä?ÅKéúFð„ ÍçdÅ“ÔÓÞK¤·QÔ»‹þ([ p(Cp¥„'*e*ñaZŒùØÜG¥™ˆÊ?=øÁ uvF_)货 áÜÒkS¹c+@d#AMk:œ 롲ÃuåšÉyP³Œèœ?pŸ¢û›ÊV­ŒuQœ茳L™±`") •#ę̂E±çaÄ…T€ÄgŠ‰g8~Œ:€Ö˜êÀÅ +ê}xœ ˆ¢ ¼ÁÉÅ™JÁ‘Ë"ðX­XžÀ¿fJŸIäáŒðˆåùLޞܻϟÞÖvÿ.€CäJ)‹ú×lÿùݜΠÿ.ö•?¡õe·YÚŽÓG–ß”:Àµ­-¾x¾P0É4‡Ç($ŸÞsÜ"E°¹üZµ‡½¹˜QMtù`4P$C8F¼%Kd:zúæž—:ˆ"„<«°à/¼0ð¹šýÞ +2Ÿ*/ä’ù§Ïž¶ýß ¸ã =œãPïìÎœ¦þâ5\¥Å"ç|¬ç~&=ì°§ÆAðXÓ©ëLŸ)Y’5ç(Æt#ÐTAWŽ49ÁãÎ@be†¢ì6ca\–«ÿFgR© + +endstream endobj 324 0 obj<> endobj 325 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 326 0 obj<>stream +H‰”VQ£6~ϯ˜GS5>ƒÕi¥»ÞªÚž¶·ºð¶êGœ,m€È°å¤ª£¿·cºÉ*ªVZl3žùæ›ù†¼ûy#aß­>æ«wy‚„|·Jx–‚À?¿ˆ2®CP 5äõJÀ~%¸"†¼\­ýo «'öaóÓý=<Ú¶oËöÿ@°NxÌ6RDBj.…€M°ÎxÈ̱7õ7c!t6Š9wÁoù/+‰CœVq‚áAÅ\E.LM±[¸¨, ƒüw>"ðRøÁ×YÌÓÐÝ'ÄÚ9`¦è^¬©MÓß5Û‡¢ë`t62q†Fót*–¾¤'â‰=¶¨Mol¬#Lû†<‘ÈÜZreˆûÓÌÙH_¨ˆ>Š¿VR$tÔCmh‰ð—lÌ{Iø”Ç'fr=ëófŸØW-cݱõÏ&@{‰Q×ÌÐf¤y•‚ˆÍ<©hÅÓÿk®ô\ùFuÞÝí—ÏïK{ØÝzh„[)å@1÷zÓ¶ŸlÿúÕ §ƒ¿÷òÈzá×ÙŽÛ3Ë×CPÀÒ–ööJÏPŠqŠQ@.¹·˜%2os÷½¨s3¸L^¬C®ÒëäAŸs¶ƒ™^q_>_ àDñÅhð +ÁU®DÆÓô:W­¢.üäºP³®´Õ±¯ÚæMõ‰YwéncúÎë¨3çE±²7[ð’òÛ½ëâ„í¼ºÔB]ŠÔåWÈÙÒÈÔN †v@ͶjöPÓ=ÇéEý]‘Þ4r¾t¤qXX¯ì?ºT]Æþïàq¦‡C²f/´íᛡ꟫)ògÎÍ,]hö4™ Ý6]k9A#žd"Âòž¢«9ºœè7þæ}³k¡lk‡iòÓmkú¢:t£s> endobj 328 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 329 0 obj<>stream +H‰ŒTÝNÛ0¾ÏSœKgRŒ'©hbˆhîÐ.BpY·&®ì{’=/Çvh;QªÔØÍñ÷×s|ðuÎáÞ%Ÿ›ä iphIIë +~Â"¯© K*4}Âà>a”1V@Ó%YX⩧ä†Ï¿œÁ•5£éÌ +þBš•´ ó+ÎrÆåŒÁ<Íj*ˆ^º¿Õ„¯‘ÄÃ¥?šo G@Èãª(‘dAeîiúÈ]znæYI)Óæ—ŸGñ\nÂ*ÊWuA+áÏÅ\z€r¡[÷f’æĦŒVD÷zˆûñZ÷&.SNk¢W«¨q›Ô+µŠ– *öW¹®ZÛözÔÖ¥YŽ±Ì"£%Çd3Ny^£¯“à«xè»RåÉQ]M¦±XÆb_‡/0ƒl?› ®uZ †[›ðD›ŒrâRI‡Ílã³h°æjP’V9~+*ÕÆ™ÿC<„h}r!³GÔá“;º…mgãöíl' ,§"öá~ã1–¨¯5§Ú~½Ò³üýÞŠJвúˆ·×~v´ wÏ_žÈjQ0ߺïz tSCŸø?Y×Ùåz\šáÍ&Û–BÆŽ*D­7«H¿5‘V8¬Öô0þÔàbéàLxÚ4804²bl;¨ÿ“ÿ2#ÈÞb¿Wäá~»¶ç¦ï÷··Üz(£‡‹7­†åàÕw&D2Œï$³ØÊV“ldh‡;ÂÞøqÈ ¸®â­„—HŽ·Û°Š-«Š¬°ÚÓÚ‡aXz ‚Ü{ ‚ÀÂXä]b‰ ¿÷} Ñ^Dþ—®Óún +0œ²¢®_ üN¢§Mò,Àªˆ[ò + +endstream endobj 330 0 obj<> endobj 331 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 332 0 obj<>stream +H‰„TÑNÛ0}ÏWÜGgRŒ'NR!¤1ÐÄÐFEó†öRº‘¸²LšöûÞ]Û¡í(¥ªÔ\Ç×÷œ{âs>Ï8ÜÙè´ŽŽê:õ"*hUß²ŠÊDAS u1¸‹eŒåP·QâC<õݳO05zЭ~€¿'ÍÉlÊYƸ¤œ1˜ÅIES¢Vƒên•ÔåâÊÅßë/Ç‚©Q^ <ˆœŠÌÁt»pØÌ¡’"‹ëŽ|Ès±.à£@_V9-SwÞ3æ¸!_UcãDÐŒ˜˜Ñ’¨Nõa=\«N‡ð)æ´"*ÜÈ´CUÒdÉþÊд1M§elœd¨É$c´à(kÂ)Ï*lêlT¬µMGm=Sãè qI9ù†å✠ĂÈÑ!r; 0Ü#cIºWaЀKý4öúðèl£+8Y“]‚žŸ ]]#TÆ®´¢r l,¨$*,&kÕ8 ²U^² +¤ e†ÿ’ +¹ÖË}ã6ÚîöÚs=~¶'W—Ç­yXœxf¶Âq"n{ëŒ[þv:üÙ:QÀ¼ekÂrÿg <ó*£LŽWÐsÜ'Pè£ò9ç¿šnõ &/ok)Í˃ÀicÚûÆ |W-JØØûe®.×lm¿ªPÁhZÖcs¡oÈ™»0’ØÖ,WÃR÷{mÀ6È‚y<.ˆuwÛG`W!hñú“åb©æÐ…7ÎØîiâ$Ňêb´'qm.Œî¼;,zÝ×s‡õv6}Ûlñbv¤Ú Z‰Õrœu?íQ‘½½¾r|1:þÑp«`Ù;·Ú+×'ëènÔìmØsÜ 4ýz¿ÖÎ8lÛôa¶â(Ìpv²5jºA•Õf;XóØ÷KW('w®Κ…6ˆ»Äíßw]æcŽ¯zNÚV©ù¨bò‚ûΠ9¯£ *•”È + +endstream endobj 333 0 obj<> endobj 334 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 335 0 obj<>stream +H‰œUÉnÜ8½ë+êH#j—aˆ'™A&‡±‚œØjv·f´A¤¦ãÉ÷¦ŠT/v{0ƒ \šUïÕ«E¯ÿ¸°ÕÞmå½®ªT/çe!þì&)yAœó(ƒªóBØz!Ã0…ªö»E«½÷ÀÞÜÿöþ=ÜMƒê¡…à9OÙý“Pd\„!ÜûAÉ#¦F£º•š ¢71#wþ·êOO ÃÈ‚»]š#<Ä)‚évNØ!¡²<õ«¿ˆ|âÈ‹øèÀîý¬Ly‘ýû}˜:?eÒGò%3_?C–Ƽ`»J®‘“”2žCV„‹C+°j<°;9ÉN5i?HÐë•sò\ |à")‘üÛ“p‹†Qì4ü¬RðSÔåe™°Ûah•ì¡éíõº©¥iú-ìwö¬Ìu4ÌZ±p‘àq˜&È‘ž¥ã¬ÉA²]k?ˆH$X`Ü¿ +†ÙŒ³a?czt6µ²«v6Ü…€m ± 4<¥ +(SÁ¥–aæüD~K„ìÚS’ÂÄaäÏ0ò#FB?¼ Òÿ­‚ØÉø–ª‹­žšÑ4CÿBÊÏ¥:#·Œ¢ÛGX«œ[ƒu|,áÍ4tP]'±•ØšŠ¥ŽÄc*äØبŒZ*u¸¥Îc[Ð#6 +æÖ¶XÎE,r×b(hBUKÏFÍh¦GZÚ¦W þQÓ£ÙQÏÎ#µ-½UýÕ|nKl"†/%lšïHci¬î¹^Ñ)òÌE¾oÖf‡aô¾2j¢”átPS×ô²…Ñ]P× +6lÝIv_}(™V½½YSy¤Œ#mM}ª!³’u> MÙ/Ä"œê¥È2Ô-ñÇúê³Â±5©õl½ÖÊÍ”gŠì†Bnþ¤nþ8+ëàÀÈøÖ%%ëÝA”übäGU„-VÖ)­åVn›íδ µ-Cl¢päÝó“ab ·Ó0šxbòk¥aïÌÒ§v/ ò¦ma¥0ú±EÃ5¬4JÚ* +ÌØù‹¡×˜Ž]/ÝüfYñ 3ôðú¤:iÓ0ÙÄßú +Çgyð¶a–]4§óü®ò~ +0‰ö> + +endstream endobj 336 0 obj<> endobj 337 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 338 0 obj<>stream +H‰ŒTÝn›0¾ç)Î¥= ×ÆüÄQUi[£)›´V ÚMµ J”-@dm_¤Ï»cSÖ´Jì`ŸïçøóÙ—•€Më}J½³4 @@ºö¦fÀñc¡bq2aA iéqØxœqÎ#HsÏ·CÜõèÝ’«ÏË%\7uWçõ^€ú ‹ÈêZð‹˜ ÎaE}Å¢w.ïtY#‰)G¦_= îFQ‚ð #&CS:ìÄ`sƒJ’˜¦¿ ùБr,`GŽ~¬"6 Ìþ[²ª÷M®—4Fnuµ¨tC …Í3±83ŽÅ,xÆûrÖa½¸%×Y“•ºÓMKý«Ï]1Îæù‚‰P!õËÞ¶pt0pö̪{2E45;ÈPRfÈÌ—R$È‘Ã]Öê{(Æ•æ÷‰,&P¯¡{ÐÐ:;ÂvsDU#ª¡m`-*ì<Ö IIñ-¶Èè]uÍ3t5”õ}±~v‚ü¡¬•$'’ÄA’tÅ—Îâ nõUÈQÐAÇwM#Dz„¢®ÉNìÒ?Ùv¯{ó¦¾ñCßÁ´Ý?¶ÖÒˆ]?nL}EÚ]mŸ•&HK%ú¤Ýd>ö[p×pe›­ FÏCˆ´vl´Ó7ôiPvþØ^\};Ï›íúÂÒrœ¥”†™¼H?ØiÞ¸éë§ÌG*`IèRgp_Ó> endobj 340 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 341 0 obj<>stream +H‰ŒU]OÛ0}ϯ¸Î¤%n*„ÄšÒ@4oh!¸%[“TI:àð{wm'i»Ž!»¶ï=çÜ{nO¾Î9,Ûàsœ¤©é"Ð4™Ã?·P HME i0XŒ2Æ"Hó`â–øê9¸'çó/WWpÛÔ]×+xƒp¢iDæ·œ)Æcʃy8I¨ fÝ™òÁ4 ìIl¸ðGú-àP¸ä~iL2¢RÙ4¥Ï­mnf³­Ãô§¯v¯­FeÈjK‚+%‘×–Îw㎞Á¸ËµÛU…$ð;[m í…ÜÕm[lLev˜b_›;›$!íºvÿ]xNÚP¢fÆofcí9óÅO\áˆQ±@™Ç¢Ûñ½ª«Óçöìæú4oV‹3‡ÊC–RZ> endobj 343 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 344 0 obj<>stream +H‰ŒUÉnÛ0½ë+æHÃEƒ @ÛE  b¡— E¦µZ IÙ~¤ßÛ!)Ù*œ8™ÉyËÌPGßÖ½÷%óŽ²L‡lå%T¥Àðg¡¢±™PCV{ Ö£Œ±²Â ìO=y·äóâëÅ\wíÐmÁ‘Å5g!ã1åŒÁÂDo]ßé„Ù#‰ çÿʾ{ + îFQ‚ð #*CS;ìÄ`3ƒJ’ÔÏ~ò¡#Ïå6€9ú±Šh*Ìù`Gž,Ú‡®ÐçÏC—ŒqFöˆÄ48eó0܆¹%×y—×zÐ]ï!*>vZM8špÊC…”ÏFðpëœpÆ9ÍÒ©"Ú7'È3ø%¤m”’'4$ îò^/¡Üî4Ïg_И@»‚á^CïlT”s›Å-ªÚ¢ÚÖ¢ÂÉcœÔ>®bjŒÐÍнÀÐBÝ.ËÕ‹La­$9“Äw’¤ ¾u52•w'ã‡ö#|ñzÜó觨#7ø)©4ý›[Çv)·™ +öݵLb—’ƒ¡H¿iícbsÒû­Òn2f SΙ˹²ùV£í!ÄÝÝ• ß/™“§þôêò¤èªÕ©%åK) 2[È>ÙiѾSf:R‚&¡k7ƒú–âYfÑð¼ÞTúxx]T”p*ÅAU`Tp±/hÆõ¼}þêòc"%VSü¾H‹#]ZÏLZcÒ]¹ʶ9Üi|×iãµÐƒíÑ@ã¨Å¢P¸©¶ÓikE`­ CÙ¬§“›ÿÚ_8án2¼v’Bv_N»K×¢"¢\¡~v¨‹ mªxÌ«ri{ʱËuÓ·öMg`0ª“û¼GU8(ÚfU®íòÃ|S~Wi(ݹÆ&scçY°ï¦«ðF×¹cw6øŸþØÙ¹—¦ùí±û”pe‘¦Jzl«¡GÃœxïA—7k$Ãï ƒŸÔ‘â4äJº/Î3¢ + +endstream endobj 345 0 obj<> endobj 346 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 347 0 obj<>stream +H‰ŒUÛNÛ@}÷WÌãn%/{ñ%‹R/QE+ÔX}A}0fÜ:vd›?Â÷vf7«)¥‘¬}朙9ÃÉÇ…‚Õ½+¢“¢Ð  XF¹°3ø㉙“ A±Ž$¬")¤”)Uû#¾õ]³·‹÷pÕwcWu <s‘²Å•’‰T™PR‚ÇVhæ6£[߸4=cÁñïŧH! öÉÃ)Í1=˜T˜„Ò¬CîœrKÊÊrË‹D> ä•ÙøS ŸÙTÌ4½¸f‹î¾¯Ü¼á©È˜«Æ¾kç­ëWOœˆF‡ºqËDÙLN‘U¯Ê¾\»ÑõÄ: `Rä +ë+¡‹*>lù$ûbêPËÀ좽剰Ìqzƒ=W¨j†ÌbcT.&á¦Ü-Ôû'éï#×(º%Œw†PY+”òÝgµû¬D›Òú¬°Aòˆ“°5Ç[ìé׎ýŒ¬»ÛzùÅ;X/ÉxIò & °óÆËlXK˜9Ã:û°ç£ÂÅZgKõ%\<€k©BµŒz»äû¿Êæ>Ün ;­i˜  Áˆ+î¥g¡M_ òaÓùOÏM±9eTt +N÷c d˜ëgÀ²%i¬øa²ïk:SgÃùå糪o–çž[àlŒ!VlrQ¼ñaÕ‡ðå R«EžsRò—„OšÎæåzÓ¸Ó]‚¿kKs%Œþq´-䱪 !õ*Èåçÿlpè²×O­þÁy“U_oƺk_1¤>ÌðΑn „&E1"ï0¸q¬ÛÕ—45–¡( ++v2a«sïúE ¢3¡­I¦öT&p(îêð}еwAÞ'›ó8ÃÅ€fw-bkœáØ`ŠÎ=š í we¸CU×.ë•ïûðõ QŸ±†gèB¨ÉvÞé;†S§Óê°ûe|͆îW§O(1aµÝI +¿x~É­íá$Eºsëºä$³÷|§hTËþÝIs¨¢úc‘ß±ƒÛ¦`]·õl¡/Û•Ã^<ƒÂÇðÿmžob·óD¢¬>Ò0Y;ó"ú-ÀÛÁö + +endstream endobj 348 0 obj<> endobj 349 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 350 0 obj<>stream +H‰ŒT]OÛ0}ϯ¸Î¤$v]!¤m ‰1¢Ñ^ÐBëvÝš¦JÂZþÈ~ï®í$-$T©µ›ÜsÏ9÷ãä˄â‰>åÑIž àÏ#MÍ~ü!5T š +y1XDŒ2Æ2ȧQâµîÉÇÉçËK¸­«¶šV+øq¢iF&·œ¥Œ+ʃIœ*ˆÝ´¶|°5÷Ž$.þ™8 +Ÿ<œ2éAfT¦.Mrk—›¹¬dÄâü·#Ÿò\þè+“Ñ‘pñž1àžLªÇzj¿UÛ8QtD®‹¦¹³Mµzl—Õ:ÚÛò‚š¢Ôˆ=ÎðmQ¥mmÝÄIŠŒ£š£ §<5(â¼30¼ÁÊ@ìr=‹Sjˆ]ÙAÌQÔˆ ±RrMSÂà¡hì –Ã›î{ ªTshYh‚±†rîë:d5CVGÛ¥õYaƒä'%eŒO±XNØu[?A[AYÍ–ó§ (éa½$é%yl¹W¤6º|kR8Ì”8£ã Y6±Äúw–ûPÏ2ªÉ÷ðÆVÕÊ"¼Þ@݆^Pø[¬mgõ¡Ëlß/àÚ%yYÏZ…êyj†4›Êÿztî™*W wÝÁ»öØC2ÍÍà2÷Nì;íyn›³›«Ói½šŸyf6׃ùÖáºoK!˜TƒÐW4J}Tvr±+ÊÍÊŽ{4Îœñn@a£¥Ø㸆îvø¶ “B+}¨CÊ#V(ã=P7WG*‡¯£‡‹A÷‹á éæ¨)ïÉy×@Óz¹qÙ^ο®Ž~:mëçZ( ;U81œ špµþÚ?ôlHÛ.׋>rólEàè8º+ºÑ#ûàΙWé{:žÓ”q<§ý¾º³8_nÖh š16»!ÝN>=p“ïG¼[?ŠÕrõ¬ .Ö ‹K@!hJ279 +ÜEfýy ~Dý"þ 0J–Û + +endstream endobj 351 0 obj<> endobj 352 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 353 0 obj<>stream +H‰ŒTÑNÛ0}ÏWÜG{RŒ'vS!$6ÐÄÑh/h¡u»nMSÅa…Ù÷îÚN“RŠ4Ujl%>÷œsÏõÉ牀…>ÑIQ$  ˜Gšå#àøó‹4g*©Y¢ ¨"‹ˆ3ÎyÅ4ŠýOm£r>ùtuwMÝÖÓzÆšedr'xÊ…b‚s˜Ð8g 1›ÖT¦Ä}#‰ƒ£?Š/‘@ÀÄ«Lcy“©+S…ÚÚÕæ®* ZüräÓ@^ÈÀ¯}•gl”¸óž±HÀ™ÔOÍÔ|­·4VlDnJkÏWËź2ë6ð\yÃL1 jÄ_áf÷®lÊÊ´¦±4NÑ‚qãL t1L¤9j¸èüK{+“àdàuµžÑ”åÄPw‚<¨iDÐW)…f)áðXZ3ƒeÿ¥û¦ Sê9´? Øàk΄ðmí«æ}UGÛ•õUaƒä'%Å·Ø+§БæÚªz¶œ¿AñÖK’^’ÇNE*`£É7T!»Ò¡f$8M3\z»éÈqF»¤ÀšßŒ³…U½…ªôk¡ôÍñ0®CÊÕ“é¬Ýw•ñ—Žø­ñž¥ +ݺwÅrb7µ®AA,•ÈȄ͸Oƒèâ0@r-òÞU!æ¬W‘:ÝÚ³ÛëÓi³šŸyb5öÒ'{/Š~;mÂvH¡?‘p©zG$J}Ðerù\V›•ïÐp¹t +s•b¤qâû0»y{W +ž’‰Vz_†”¤PÅ Ý^h<œ´Ž^zw ¼#|¯·2ôöÂõV;m–›vY¯L¢2›vChZLX— +Iu¿kÚv¹^XØø½Ï³&Méwaêák8´'Îw±Õ>¶;žÜ£¡FkwÅÜ wi5X)!¿íóš“÷.š¡ Ac7ñßq¢f;BДë…AÛcPš’ÌISøpÙÝÐØÔÃùº,¢ ·Q‚Ü + +endstream endobj 354 0 obj<> endobj 355 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 356 0 obj<>stream +H‰”T]OÛ0}ϯ¸ö¤$vS!$6Ð`hÑh/h¡5%[“TIðGö{wm§iW`ÚT©±ãøÜsÎý8ø8°ì¢÷ytçäw‘aÙ8þü"ɘ–  “ò*â°Œ8㜧ϣØ/ñÖctCŽgÎÏáªmúfÞ¬àÐØ°”Ì®O¸ÐLp3gL»îmuk[îEý–ŠJ<¬RƒáA¥L%.Lb›»¨d"iþÝ‘Oy¡F¿ +ôu–²‰t÷oȬyhç–*–‘³’j¦Éòþsá÷]çÇ+ÿº\ÖM™ áãšÆ‚%¤D·v½ ¬™=áC@o‘ðnÝ«¢-*ÛÛ¶£q‚ÞLgF ½ˆ/’ Å Æ&£Ç2X¸Ÿ× š #ë òT Ø AÕIr¸-:»€rüÒý?Q‰² ¹ƒþÞB Ϙ>ßcÔlŒêh»°>*¬‘<â$¤¢xŠIt:ÀÖ}û }U³( xë%)/i_Ñ$`Ÿ•h?5¨£èºãU¹DË ©+Ä¥“ JrL"ùbýÉ#Üã ¨ +¿ë:(V%5sÎkGNaš~«;ø»k-߸ډ_ºï©ê²k‹¥±nüÓà â +E»¸Ít, 1ÔÄ’‘Ö +å0‡,záƒf/÷ð±;º¼8œ·«»#Ï,ÐÆŒ:²s¿óÛy¶ÛZô7$WzúŠFeörMNŸŠj½²Ó šà.óeÆêJ°°q Œ%íÚñm-xMI£Í®¥öX¡Œº¼ØS¹ßq=œf3%Þ¾“^Ò{âÒ«I7oËu_6õ+¹ãWH`XSof{ßÌŠ@ê¡ A-aƒ½G6X0’ô}Y/7wÖL|áôƒ3`Øo}xÁþ·º·¸H×Ic›µhƒ$?º©Ÿuo¥m¶Bóá+öà:ëYA[ÔK‹É‰A#hBR×%n£ÒÍ„`‰ÈäߨŸæÑo +ƒ“€ + +endstream endobj 357 0 obj<> endobj 358 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 359 0 obj<>stream +H‰”T]oÓ0}ϯ¸6R<Åiªi°‰ 6­/Yëv¦©’Œm„ß˵¤¥)±cßsÏ=÷ãèÃLÀªÞåÑQžK/£”eàøø…Θ‘ R& äUÄaqÆ9O ŸG±_¢ÕctKÞÎÞ_\ÀuSwõ¼^ÃO qÊ2»\sa˜àf4Θ$vÛÙêÎ6 ÝEýšŒJï<¬’݃J˜ÒÎM|§Î7w^ÉDÑü›#¯y¡F¿ +ôM–°‰ üÑô–Ìê‡fn©B.ç%MXFV÷Ÿ +¿o[ÿ¹Á·"–Æc¿jK&Èú¡+ëMà»SísÃR0ÞûõJ‰$x¿.š¢²mZk„Ÿ0ÎR*£G¡3Œñ´×WRË t`±YPÌ=GAž€ +ŒyBPw¥DÊ4ápW´våxÓ½Ÿ¨d†@½„îÞBtϘ>í£×lôêh;·Þ+l‘<âhRQ<Å\º8ÀnºæºªzQ.ŸC@ñëCR>$mFl ÏKÔŸºR(ÚöÆúŒ´NnMj¯·ÿ³ño Æ}>‡[p¶PÁ¦ÅSÒôõ*C~ëÛ˼¯0ß•¸JŠ_&aÇø– Ô¶!¼/|y—·™Ž•!úÒØAòTd£ÂB9Ì>™^ûÀúø±=¹º<ž7ë剧xcfÙ;Èßøí¼ Û]Mz É•#}%H•䜜=Õvm§šà.óåÁ*ÓXà8ÆÒvÝù—`ÐNÉÔ¤û(u@ ãø'¬«Ëƒ8{¯'ˆc#ÆÆ‚ßË° +>u6¤7åÖy{¥7÷ 9 Ë~ ÎlçÛZ( _ÕT &l° Ép€5#Iו›Õ`³ým6à'8ú= +1€¢m/É 8ö¿µ¾ëÎaJÝXl*ƒ¥AE$ùÞN±Ì]û½>«v™ £ª_Šu¹€ÖzVЛ•Å<Å`T“ÄõŒÁÛ¨dL‹LþúYý`wæ›b + +endstream endobj 360 0 obj<> endobj 361 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 362 0 obj<>stream +H‰”UQ›8~çWøÑTµ±Á°ªVÚk«Ó^¥vÕðõ'¥ ²Ûý#ý½7ž!Êö®wŠ”lÏ|ß7ßL^ÿ¹Rl?Á뢈™bÅ.°"Ϙ„.L.Ò˜i+â”M Ù>BJ™°bD¸„[Ášß¬ÞÞÞ²»¾»mw`?XY‘ðÕ’FªT()Ù*Œrsw]³q=‹ýÍ}¸ðKñW  `ŒÉi•XHÏt"´ñiÊm}né³òÌ„Å7Þx¥ŸàŠà§y"²Øß.øM[ºým{<7®/÷îmwjGæ#þ,B¬àçe8¦€^‚õ؇Q"RÞ… +È{!”£ HÇ_ˆâaœf"™…–rlâ6æ…8©°,Í䓺¬ù]Ù—]?„‘ W¤¯VA!#%”Éô»9\ÆS5oÛÊ}‡ji€;÷+ *^|u¬ÆÍnÇFx(AÆ0…*v{Ø8† ?…‘â㔑’iL¶Ì5ÉöñÔlB/šë ˆd@<Ø7œªB;ø (Ò$A´§&´Ü»(¹w¬weU·xnO÷À‡ÏåȆ¯ÝÉÃÌyÅ6Î_)gÑý»]˜Aä®wi<õm§Û½?Û»átø™É·¬VôRa$RY>{9Žþ¶§âC¨‘">L•‚²«©î—Òªü¹hJÿ›‹ß<ן>¼Ùö‡Ý5"#ØPC€Ï6ŠWø¸í¯~ñÔé3Ñ_pÔ3åxæý÷²9ÜÕ9š’.Gëæ,Õ"3`^°Õ¥•Ôo;r¯õ Àÿ/!>}øM7M°`ÖØó¬ùÊùÂËkþŽì9lûú8Ö]û‹n›égdû•ì t2”Þ{ Ñ,ŸÜlÉÍÝŒFÆ—ÞÍ–¼<–÷t£…^ÄEż¹C >³ÀG^ʤ. ¨ ¶AŒm@ Ÿ1ã;joý;bnJ<¶½ÌwâX7€ŒÎáû–®“ÑÁ²k!ؼÐp)4Äðlë©}5wl¾ Cftø¾(¬tõñP» +ß±Í\ƒ»[_vhiÉaÂxD!Ö8A*·+}[Ãi Jºë³îšt·^÷î,sËèÌC¨ú' ;€èÒ#)Sªåä3ÚÙœ¶%Ò> %(È=tTñr¢ÿFdÖhb±¬fBA½DM-?Ç> endobj 364 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 365 0 obj<>stream +H‰ŒTËnÛ0¼ë+x$ ˆáC¢¤ Ð6i‘æ Ö-ÈAqhÇ­-’ŒäKú½]îJ¶òj LRÜÙÙÙY}Ÿi¶ì¢/etT–†iV.¢L9SðÃERHg˜Í¤q¬ÜDŠ-#%•R)+çQŒKˆzŠnùçÙ׋ vÝ6}3oÖì7q&S>»Ö*QÚI­›‰¸†ûmï7÷¾e&ܱ<À‰»òG¤Ð`rZ¥¤g6•6 i6”; ¹UÈÊóT”?ù„Èk»ÀÑwE*s³7!×ÕºY^ÔÛ]/?¯«ûµšÜ‹XIbôR i™MñH Á*·¤Åídq*3Þ-Pç€|Gµ¾¢jŒ‘ÖM¹ÊdC•C‹ÞÈädÆ\®¦¤tJU^Wmµñ½o;'ÐcªKÉLCK¡R èÙPŠÝ÷Õ }½¨ü34Óª4àå£g+áä©;½º<™·ëÅ)r"®V#>ùP~Âí¼=}å Œ0 +\4ŠýNuvÒíïœ?W›íÚhZ\+˜³2OÀdÐþƒçõûUL™[ûŠÿ{ðÕå?œ>P!_„Êœ4ÑRÏBïæíjÛ¯šúIø›Âßš,‡þêèÙt¬Â¡*\,ɼƒÿZOŸètE.]vlÑÒ{PÈĤJy›G<š »‡°”L—È·¬óuÏžé[ÍŽçMÝ·ðôШî¯â¢ £“s¨ÓÇcþ—2ÿpJì^àñ©¹ñ› +¤Èa*Sã_Ý1 H1<¤!V9÷žÏËè”ï‰ + +endstream endobj 366 0 obj<> endobj 367 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 368 0 obj<>stream +H‰„UÛnÛ8}÷Wð‘\@,/%E€n[,²}hPë-èƒ"ÓŽ¶ºxEÙéþÈ~ogHÉVì¦E€˜C‘3眹ðÍ_kIvnõg±zSŠHRlW)Ï3"àÏ/âœEtÊ•!E»d·\‘¢ZE~ ·žWôÝúýݹú±¯ú†üOX”ò„®ï¥ˆ…4\ +AÖ,ʹ¢v?ÚöÑDáMÑûZü½’àPùàa•¤žè„ëô!vŠ±F¥™aÅ?>à¥>9ð«ßä ÏÞˆctˆ»²éwwÝþ0Þ±°v,Rð3ÚáX6ÑK5d +P®ý)W&hñ°X„>{&¹¡UVN"9‹2.©ú_V +Ž™Eq&K&®S¢®Ä2<%&Kh2 TïË¡l-s,ŠÕM`'x*!±‘ä2ÎAÖ!}Ê®š²{×mìwH©1(A‹'Kj¿ÕoÉF Z2¹ìwðaÏz`‘¤ã'„ЋòBÏ!fá#¥dªu‹aH[WCRàkÏz£ +FßykãÈ£_Xn3:>[Ûw¨*;]UÜÕ€2¡Ç€Øƒõ_÷þÿaºJ[n¦/»°ç^0 b]æ&ºÖs® +LÂö½ÿíб\¨Ú`Ly$Ë)Ëg—"•ùI?éSô¢„'ùÞ>»ÛÏŸÞVC³½õ¨d-=ºøPüáÍj¸½¨,C mN$Âo™ÌÜŸùø½l÷½™½IÜå¾Hsb4Ïb(S( sïÈ×x,±k} ÿîúçO¿é— L—tž.¯P]¤R‡T~ÀTꪡÞußý¤Ÿô/ûimG秞ñ>Z¬X槤¡<ÁÚy«Fj§ª ›[c|ÓgqÛy +f­NSà ñ`ÅPlŽ6C9ž‚ˆ±àЯ#›ÕVõ¶Æà›S¿9[aünãàÒ“wbÉÆn˹·›¯ûÑó²9ç8p†@€l8ïI?ÁY@¤÷o 9‘|‡– p µådFBâÒÓоÿ§ýåô1§éÃuèåz¹Q¥I™ØH”¹xÆT@¥í`ÿ=x<­þƒRY³ÃåÎo[GJNø`Š¶ JŠ/ŒÅQ¦ÔšˆBßCŠ·ý`Ï’y0‹b¹ŒI€d‰éƒZw;OÑHZ†Íæ`ÉcéUßBaX2µExh`ìå* Ü_«J,¢Åì¶GÛaÕùé;gC¦:Èmg–³w“Çf¡ì5 ,^HRJ»–>¤%àõ9…±=™Gæ'<ˆlë¹H1ï¾L7“‚Zs•¥ñõóuý¶Ü<Íga¼Õ0Ot˜ïŽsPEß÷ÿÛÍBÞŸ¶…Bà!>Ì _>çœÎ¯ðÛ– @”ÑÙ7w\s:ѻ˜« +ž?«´Z + +endstream endobj 369 0 obj<> endobj 370 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 371 0 obj<>stream +H‰”SÝn›0½ç)¾K3 ×?` ª"uk5u½hÔ ÝT½`Ô¡Ù mŸdÏ»Ï6 ,i5M¹ˆûü|çøìëšCÙ{Ÿ3ï,ËpÈ6^LÓþì"L© c*d;Aé1Ê‹ +¼À.ñÔ‹wO.Ö_®¯aÕ5CS4ü?ˆiDÖ+ÎBÆåŒÁÚR*ˆn½û¡;f$ÎȾy…%w«(Fz•¡¡Ù9îØp3ÃJ’ØÏ~ñ¡ÏåÀ®œ|•F4æ|0‰'u^5åí~h÷˜¿s™Òôô¬´ä£ ãû¾EË~QE?PĘçT<8CGzxÊh4Ãd“Œ1œŒBÑTÂæb¸5rOVy—ïô »ÞBœùÂM“јcl§1A²˜§\ž”èü¥_ÞÞœ]µYZ9N«äV™}È>ÙÇ¢[¥nO&ÕÁÝ;Ææ“Níž«×|×Vzñ†Æ™ƒKmRP’&!VÚÌß¿£h)$ æÏÝÞü£½£¼ÉñÛMþÀÜ,5éR»4©)Òݶ¶MýN»çS÷„ëÞZ=äPnŸumfâç¦^ [0 ;-Oym?Öº2•3ì[m_x?pÏv³Õðì0ª!/5ýßfʃǷ|§w9ö>! +ò«_`)Q›6g™R'ðù*óþ0c7Yò + +endstream endobj 372 0 obj<> endobj 373 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 374 0 obj<>stream +H‰ŒTÛRÛ0}÷Wì£Ü ]lÙffÚB[Ê ñÃqH_ðš~H¿·+ÉNÚɃVŽtöœÝ³:ø:ãp×yŸ2ï ËpÈ^LÓþl¦T 1 +²Òcpç1Ê‹ +¼À†xëÉ»&gŸÏÎಭûº¨Wðü ¦™]r2®(g f~RAtÓëòV· ÌI œ“}÷8 +›ÜEQŒéAFT†&MérÇ&73YI’øÙC>tä¹ÜØÈÑWiDaî;Æ ãa¾¬¿´Ú—äaÐU±öcªˆcò¼ +<Šh¬vp L¼)R®ןЄ´~„"k÷~º‰ ·¬|eÒp\BíFÜ8ù/Ø ¦h¸›u+FÝcÓö +§h *a»²yäd_æm^ê^·„Ø¡C§˜Ñ˜c“Ny˜¢¶—Î^¡ˆÅz@q’`¹ìŠ5K‰étœ Rv¯a±–|û } óvùˆ4ôx² ‰2aëÂ|Ü´ê¡o–4bÒLI9‘œÊ¼"Á´[9ÝW±RÒ5µ]+ƒÉ1»Äh·Kuåca· 5HWKi07rö9zêŽ/Ίvµ8¶„QÉ-²÷‡ˆ’é¢=~ÑQ{D0©6xS¤3ujÏœþÊËf¥'4Î\jÍ‘‚’4 í\…;Á_QƒN‰ݑ!å *Ù‡÷®_œÿã#•pû¶¼'Ò&’®“'¦“ŠtE»lúe]½íàÍ´;sz¶˜ Ø/DÎws#èÐØOMÝö8®ÊØSà ZKÚ‘u¾zƒQ™+ýhûAÛ›UaÙICžÁ›í0ŽÉÚ¾P;B[×#z‘#'ÜjØáÁ„e^ÙýàG$_­Öœú`Jø|8Šÿ˜ée¸Òen%ºìgwˆÃ‘’ýêÊø=QÙ(¡B¬”õ\C9XÖ]zŒ†!_Á¢¶ßZ¿ì p»r873‰ÙñݸϲZƒ^,tÑSÇè4óþ +0J7šw + +endstream endobj 375 0 obj<> endobj 376 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 377 0 obj<>stream +H‰¼UÉnÛH½ó+êØ ÀN/\ À³`à 26bÞœ(ªe1æ¢áM~$ß;ÕÕ”EË£ æ2ÐAÕKÕ{Uõºøö·{ ƒ÷.÷Þæ¹ ùÆKx–‚ÀaÆc:á*†¼ñPY²|k éÖ´g|¼›0;è§F<*&:Á"‚³ªå3”CÑ”C>Á?p°5ñ/;cî£ÿÖâI6øšÇ\rÁÂȹ2Ç"IRÒC¿lðå~¸ºýtYöõ抸8ŽZ öê@Eéá ì¯NºAW”ÐñsÏæçD“ÑÍ®6‡hR¸p56ƒXó4$½‡ Á*õsÑNE½ÌAëù›3¾·ŸþEW3…ðøÖ•ah×¼¶y1ʾÚUמWuRGéÅNz7RÔ`ÚÁ¶?b=J;n…5L;ÚÚuýè§Åž¥ õ·®ü­ºiÜM£ÑZ—q[ PºHMS´d¬­ˆ"嶠uûhè@74BFb7°2foL»P\2׌'Y: dG Î/q@¤W/ÅÂÃg…8ªº¢¹–Ñ> endobj 379 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 380 0 obj<>stream +H‰ŒTMSÛ0½ûWìQêŒ}زÍ0Ì´@[ʆøÆpp= +¤8qFrJù#ý½¬$'„d("vß¾÷vW“oS÷.ùR'“º–  ž%«Jàø ‡¬bZ‚*˜ÔP/÷ gœóê6Ióž’[òyzzq׶ú¶ïà/д`9™^ žq¡™à¦4­˜$f5˜ÅOcAúE<½«$e(OyåAåLe¾Ì"Ö.|mî«’ŠÓú—'ŸEòBmÂ)Ò×UÎJéó#ãÌÜ’Ó¹µkwÚ¬æ]×ØgZ°Œ|7̀ě·NˆR³êJ.þ¤£ ·+t€¦9Jï©`š´½ÿªH‚É»(s¥D³r›¿*„Qà؇4+@—|—”È£¾ëÆ6 ƒrM3äseqVìf*˜È*ôòlוp”QŠ÷âŠædI3äý”> endobj 382 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 383 0 obj<>stream +H‰„TËnÛ0¼ë+öHÇDIF y Mƒ"A, ‡ U¡7’åŠrþH¿·KR~ÄBø Ê$ggfguôyÊáѧEpT8³ ¥y nçT ) +Š&`ð0ÊK ¨‚È-ñÖ:¸#Ÿ¦g——pÓµ}[µ5ü…0JiB¦7œÅŒ+ʃiåT½ìuóCw ìI,\x_| 8 +Wܯ’˃L¨Œm™Æ×Nmmf«’œ‡ÅOK>öä¹Ü¸•§¯ò„fÂÞvälÞu+óE—=2±0¯•s9¾(\e»R^ôÝõ†Q‚BÛSEªÖ¾å¤NŽuÀ‰g‚&Ù6ÛéAÎЋ‘Š¦ 2¶OŠ»VÜ‘›²+ZLÅÈgâ-e4åØ»ˆSç(ü|ßz-ßÚMy“i®@ßÂrÕÓ·h¾èÛ \Ïfá{Ù5ÐvpZ>ë¡Œ¯ ]…CAјÄÆDËüV‡Ö2³lÝsâyNL(ÑOí_1è ¬ÙA²4Ͷº¸õöxmN®¯Ž«®ž8:ž«äŽmˆ$ÛlTÝÉAGÜÁ¤Úê|S¢÷9wg.^ÊfYëÉ3—»ææ $Íb÷x/n|œSçüž)ˆÞ¾|}õN¾ñnÜÿ'Е‘¾‡ç¶‡Š˜ª›/ûy»x'}b›>!|ú¦º7Ð?i¨,c³ ˆ$~-|þÜ?Æ£í¢è7.³Štú—Ÿ5üäHa]ÝðÛ«4ÌìJ— +ÅÓÜX€õ¼®¡ž?ëút~׬ܣî±”p6ðÂi˜áGde@ÿÖ‹šPar]Xqߔ NÏj]õŽîÂ#¯½9цíþè¼þ¸â¦ãlÜ<¶³Á¡0µf8:1ôÕæïà®Ôæãq«›%dèp‚–?› N_Nžx—)5‚÷ÈEðO€u{Ä + +endstream endobj 384 0 obj<> endobj 385 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 386 0 obj<>stream +H‰„TÉnÛ0½ë+æHÍEkh“´Hs°ëä º´ãVDºé—ô{;$e˵ëd"ß63š}ZrØèàCÌÊR‡rd´ÈáÏ-₦dFE +e0ØŒ2Æ(WAä–xë-x&ï—·°:Ó­º~Ae4!Ëg1ã)åŒÁ2Œ +*ˆêj¾¨„=#‰… _ÊÏG@áÈý*ÉdBeliÏYnfYI!Âò›{ñ\ÜÊËO‹„æÂÞ÷Š… ·ÛaØéÅ®éÁ‚ü雋Œ¦ùÙMgÛ®òØ»~îá0ÊÑGr|ÖÀ©xñ~Näð,·AN¨l²£“± gQ¤4ƒ4gÇr¸“óLÕP5ʨA‡QŒ™_ù4Í8²Eœò¸ÀÐîFÅ¡vÀº°!Ì[¬OF R;5ûXÕˆ'q«Àt`vC ½MkÞÎæëõÈá᥃?u+pR/ûI… ¢ë¾sÿmˆç9Ñ¡¤)Q~3:ÁXø˜Ëɲ,?˜âò¤¦×oúfþx½êõã•Jîd³"É÷/VÃÍI1ÜÁdzpyÑàQÄäþgÕôµºÚ£qæá +W×RI±l“ÇG=ÆÿÖ£z)O”ï.Ýš?þ§£Föxšíùr$ÒîÎ.%z5l{³íÚ‹ýÆ&Æy)±‹4˜W«m˜"ˆÕkÛ +7é5t-ttë5…òu«áÍ«k”ÞÕ¶-ÜNW—ÆQ l îÜJ0}¡xá¹+ƒTê‡j 4Jëjƒ}â´ãÜÚòdX×jeÀì‡7)D~ŒO~<$ú1S…šG_ÎRå]:¤H4ÁðŽFÅãå“ÆÔ/ e`×Û¡Ó½R_m nú\*S7‡|iêä¡xûÅ“j*Œ/'Qïú*´3>*Å»,=¯¡G¾/ƒß TwI + +endstream endobj 387 0 obj<> endobj 388 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 389 0 obj<>stream +H‰„TÛNÛ0¾ÏSøÒžãSì!¤ ÐÄÐDE£ÝT\„à–lMS%¶Ùóî·6YE½ˆ«8ßñ·O¾Î9ZuÑ—<:És8Ê—‘¡YŠüüBeT $ åuÄÐ*b”1– ¼Œb¿„¯^£þ<¿¸¾F³¶é›²Y£¿ˆÄ†&x>ãL1®)g ÍIœQí¶·õƒm‘p{$vpä>ÿqž<¬ôH&T*GSn㸙cÅ™$ùO'^ñ\îü*È×YBSᾊ•Xà‹ªmŸ»Åš$ ôÅΚ®ê«f´üŸ7š=Aò@i°¾Ø‚k'Ôà†pª° €S§`N¸‡À÷Áà>! USÔÑœ µ¼‰ô ²©1žc³¢-jÛÛ¶#±k§Á£†C1§\eâå‡Üw)†.!tÈ…qé0ôPtö½¸7h’" ÂbUzÔCñ[bÏ«ƒÚ; ùg¸Û6þ¹!°ŸãŽHª± â!™1éÞ ÷^ÆfÇRÏ^»óÛ›³²]/Ͻª Yr¯¿y!’t÷¢lÏÊð[“zo÷]§!âÌï¹ú]ÔÛµ=Ý¡qà2ßk†´¤©òC¯&ÓÊß³OŒHy &ÿô!ÀíÍS6(Rãù?æuœ¦¾t­jÜ•mµu\Ì gP„üÞ¼ØõOK/Þ­à¤íŸaû&lðG Î +¤ïAõ”û“ +3fËjYÁ¼¡(ä2ï gŠ *d{à´/vÓ£Úv]±²^Âkµ^£‡­ <ƒ›aB;Þ< ´¡f‚ðÚ›ó1*('(¯¹c> endobj 391 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 392 0 obj<>stream +H‰”•ÛnÛ8†ïõsIíB,%E€M[,²E£ö&腢Р7Ö7/ÒçÝ!)ÇŽƒ´(D%Î|ÿ?3ô»¿×îæä¢IÞ5Í&)i]ÿp“×T % +š>ap—0Ê+ é’,Üâ®]rMþZ¸¼„•ÝØ[øiVÒ‚¬WœåŒ+ʃušÕT=9Ýßh Â#‰—~kþI8!y¼+JL² 2÷iú˜»ô¹™ÏJêB²²¬|Èhô1}g˜ ²ßïæó«Ïï;»Ýœ¬È,y"¯^ˆ¢Ú¿èìùIÃ'‚Iõ¬÷M©G-K>}oûi«ÏöÑ8‹áêÐ5(I«[˜°‰`Ï–°ó [§òоdf»Í:„õ­ÝnpŽ|¨'h­ãL»Ý>xº•yU,»q …fŸÔÌ0Oº3£oãät#Î ò¥ ¥DîßïþýENœ©ŠX´J‡ù ¿&¯’wÅaÔÕ2ê÷ȉãÛ=†ó“>„ÿ·~~ +T<†•)ŒYA¬Ó/¾Àù~ÐzŠA›~j¹1[ãž`‡®C ÝcÀ-k–G¿béòR$šÝˆ§OŽRlš¡KÄÛž;<ðxAûa£[÷¸|$ñ”Õ>q"d{‚cSyDøb:;þkfãõH2üéƒøÓìr…eÂâì*âûÀ‰+tÉÁiÎkqœcÙ–ò†ú~j’ÿzÐK + +endstream endobj 393 0 obj<> endobj 394 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 395 0 obj<>stream +H‰ŒTÛnœ0}ç+æªÅñ DQ¤ÜT¥U”Uõ%Ê…f›öGú½Ûì%Ù¤­xÀØxæœ3gæèóŒÁB{ç¹w”çäs/!Y +»ˆ2"9ˆ„p ùÒ£°ð(¡”Æ—^h—xëÅ»÷Ïf××0í»¡+»~C&$ögSF#Ê$a”Â,3Â}µÔòQõÀÍ?Â7ႇü‹Ç0 ·ÉÝ*N0=ˆ˜ˆÈ¤YºÜ‰ÉMMV?‹ƒü»9ðLlØ•ƒ/³˜¤ÜÜwˆ¹ pï_Ö‹z(šÛ@È_«õàP¼V€azyÂò·«ÌÑ¿_YæA˜"¡.%òl€þà˜½ÆRT$Ý Kw¤`ä4ä@I)ÝÇÃbGiZôÅR ª×A¡úÇŽ% ÆŒ°(Cù.GѶŠÜ±˜v=Šʘd#‚%¨Mþ¤`Õnâq[,‰$Õäl縘€J2frIÄ^¶K"\–oE³VAŒ +ažE”0›ã‡Ù‡¡­èlE#§Æ=)<Ö˜}mCÑ`<Ž_%uÌÞ +’·°¤SìÎÉ|üÌ» BƒF£'¤¯ÜÇ("V„%Ù…¤ ˶T™01·¾²N^ôéíד²o構škþÞAþÉ~–ýéØœ +¹¥÷³}Å3ûÏÕÏb¹jÔñ&£.\f½”$ÐMèò¯Ù!8–ñ}ìB¼A‚Ð?¾~ûõ†`Ÿ%›6ÿ€ä^ùÄØæ|Ò×e_¯†ºkß1üß½8Sƒ†Ê†ÎšrmÊžú¼†Âna_ tBÙõ•ý®Û…ñé°5­^Ù}UÖóÚY»;‚ Â#t*ÝBç;Ò0æ7jáÅ'ÑM¡Ÿ¡h+¤ѵ¸ù­*eÏ«›Â¾ñ<©›¬ÝöBW˜Î‘fQÜÕêu­õTŒ3.ÜÀ:˜ »ÉÎÆÑÖµÍ/C³®Æ¦4¨aMjµW‹gCê÷U£ìBkèæ¯Äu1‡Õ{“â?›v3æîÔ²À„)­ÔÏúûÕL,K ïR)f‚‹|•{¸÷¨ô + +endstream endobj 396 0 obj<> endobj 397 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 398 0 obj<>stream +H‰ŒT]o›0}çWÜG3 ×Ä@TUÚÚjíªhÕÂ[4MuZ6(P¥ù#û½»¶IÂ’V[¸çžsï=>û<çðØŸòà,ÏpÈWAB³~Ý"Ψ *äuÀà1`”16¼"·Ä¨M° ç—··pß6}S4ü†0Jè„Ìï9‹W”3ó0ʨ fÝ›ziZöI,\ø=ÿp.¹_MLrBelÓÔ>wbs3›•d*ÌZò±'ÏåÀ­<}•Mh*l¼g¬,¹¿¾ÔÕ+Ý›Y÷çoé|"¨ÛX>¨]¬Ð0JQcrÊIœ +ã»WsDF0AÓ1ÞA 2†œÔAÑTÊÆ*¸kÁ‚ÜëVצ7mF1æžúR2špìY„Ìâ +v5hÏö³VŠ­„‚a©G$%OhLÊgØâ'ªëèááÇÍÍt6›Îç°jÚZ÷C +.únp1€ÏL×éÇp‚ˆÆ]1c‰õÊÍkõè9ô , <”ݺ +Q1ÑîþÖ<ÀæÉG?‡ˆŽ4 ]•î•e«û²y¿Ãšh_†·}ÆY_ŠãÊG§Õ: Ê‚|³É3Ò­÷ïHpL&‘Šñ›¡êØB>ôðÉžíÀåéðoº‹¯wçE[­.OaÃÈèAþÁm‹öâhb\„`Rí彡LϹ~Õõº2Óg.s×’4qüÐûðwì3°–òˆ’~?ðëÝ?f €‡@²;Þ‘7jœô»²S¤+ÚrmÇä oŒk2_éÇ7*;À_¨ˆfÕ@·6E¹²7!EhNWÕÖZfÞø˜4‹JØC«ƒíR}7G+(‚f¸p½EàfÕotëŒP#> endobj 400 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 401 0 obj<>stream +H‰„TÛnÛ8}×WÌ#Y@ /%E€Ý6-Ò`·ÁZoA±P´t¬V² ‘Ašé÷î”mÅiØ°‡àðÌ9s;û¸po“?ë䬮%¨×IÁª8~‚‘ULKP“ê!ápŸpÆ9Ï¡n“4˜øê1¹%¬Þ]]ÁÍ4º±{ø4-XNV7‚g\h&8‡M+&‰Ù93Ü™ ¤÷QÄÃÑ/õ§D   Á£•TÎTæà 1vács•T­¿zòY$/Ô X‘¾®rVÊý{¡üû[rsI3VwMÿ/9ùÐS”Il8m"£çÙJ²b‰3PBZÆ\Üî|hª“AÓ`òK”yÂR”Š©%Ë£@˜õÍÅy‘ Í +Ð%_Òù¬¯™šÁ83YšzEçQg…Àb¦‚‰¬ÂT¾Ìu|ô÷¸¥šUÄPŽ¿ó‹è¬¢³÷‹´ÒŸCñêCsÄ°»1üo)ú b©báÃa¦„úÄ,ðÉ‹¢ôé±fX2_­ýƒÝ¼}´Ÿ¯ß¶S¿¾l"K%òâBæåþ¢.NÒ\$Wúu…±âUð¹üÞ »ÞœïÑpU¨PZ±2 Mœ-ºEœJYÒWê„Býæ—Ï>_¿Òsüì8½¯Önž÷¾všØvêv®·¿ì·`J' ÞtðK5i`0Ö6÷ìδݺ£a,¨Ä¦húþ ÖãÅÁÀ U¦QÀZ E¡¯W¸Z4ÁMÓ".\>!ð¸vÍdÀ0 ¼ E’Št[×t[hÇaT˜ã¢ˆnIãš»®ïÜ> endobj 403 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 404 0 obj<>stream +H‰„T]OÛ0}ϯ¸Î¤äË!mPM MT4oÕ„BäB¶¤©’LŒ?²ß»kßІn µ•íÚ>çÜsïõÉç•„‡!øT'E¡@B± 2nrøñ“ØðTθJ¡hàBˆŠ*ˆüo=köququ˾»ªkà7„QƶZJ ™r)¬ÂÈpÅìn´í½íA¹3š9¸ð[ñ%¨<9Í’ éA'\ÇŽ¦%îÌq ÇÊLßø˜ÄK½ð3’Ÿš„çÊÝ÷ŠuêÖlF1ÏÙ"Ä»p+ÃÊ0J¸dÍW[7f¬±´Ièk“¤ŠyÓG~fÈ¢õÝq89ëB‰£Qa”ó˜ä4QßȈ£8d†ä3qð& ¦üýåaÊ3Hs1—&“ɲ/[;Ú~pÁ'씢<“˜ïHrtûr +Èû†–4v”žUÐíÏІšo˜Ù†öÅ’Nª£9¡ˆÖìÖ† ¦dØu~ܺDI6„š§ÌÒbRŒáË)þ¤È²|/^jO{ö4œß\ŸU}³9Ÿ)Õ’”Î6P©[Výù‘ÁtÃd©—ûF0sÛŒ?³øU¶»Æž¾ IApÆçÊ@ªyC‚éÂDïkHú»ËÅEÙÜy±RÄþ7 @ë#9¨ÿ-ˆ›ëwjfÒ‚e­Õöÿ”iJÙ¥KYʆª¯wcÝmß©(¹F”¦)ë𦬄ÖCù`aØÙªÞÔaŠ5Z…ʵiÓ<æëá+õ ’š8Å^+¥>'èë¾I)Ã'ªB\X<#p·ŸÊÞÂØA‹ðÒõ¿ëÌz;–õª®ÝA³ŒÖ¬Ëûº©ÇgxªÇG„i-t­þ‚:Pð‘ÆgFc€3ÏH£:„¯HcU6õ}è^š¾tþÁÆúU9þìi†­ªñ¹â“±sO…Tä¼ûom[bÌ9ëX±Ã)v–a¼(‚? §ZhÙ + +endstream endobj 405 0 obj<> endobj 406 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 407 0 obj<>stream +H‰ŒTÝn›0¾ç)Î¥™„ë0¸ª*mm4uUÕ¨á.ª&‡9 „˜²¾Èžwǘ6,ÙÖ©‘j„ù~}öqÁá© >äÁYž ௃”ê þ ‹XS%@¦T(Èë€ÁSÀ(c,¼¢a‰_íƒ%y¿¸º¹yÛôMÑTðÂ(¥ YÌ9‹W”3‹0ÒT»ëm½²-·G>柎€b ÷«$Ez •±£©=w긙c%Z‡ùW'>öâ¹|V^¾Ò Í„ûÞ+Ö€ÌgW¦ú|gºîÎö›æËU³íÛrõ½/›-8Øß“ILÓ(9hU¹–;L ŒªHFŠ¸,8Þß‘<É*² (;XƒÑÙXËI4Ê©ÉØT ZY’¹iMm{Ûvacç>]FSŽ5FœòX£ÜkOG\óï¤7vë6 ùL·èÖ‘Ó§¦”“—gþn$¾Ž¦&4Êc<Ø0¡št»fø¿ q?'](1LëF+˜ ƒ9@²4ͤÏB¾Yòž»¼¿½(Új}91'ù ŒL^  ÷X´—G­ø/tªižõaMGoöÃԻʞ¿ qæáôP°%iC‚Ëì0küí±n`q(’©)Ô¡ÿF¼¿}cG­"¦â0Âÿ®[úº¯]ÝŠtE[î×_Çt"vX +éÏZ¾);À_¨ˆÚvy²ÐílQ®ËPáà¡À92Uõ 릅;‘TÇ + ¼B«WhžyèÛ¸Ó‹×Y¸0{FàfÝïMk¡o FxNLˆ"%)·½)·P4õÎh =M=Á’˜Þ¬ÊªìŸa_ö„©-4kè7Ö Aí¼ùHÆ4“hp’™×(ö…×X˜ª\a„œ´fèjm‡'Óoý +Ï¿Äû–ŽÁžýÉMó‡‰^)¶6è9#- ò­;ÇS©Éic2=¶Yü`7ŠŠ8 + +endstream endobj 408 0 obj<> endobj 409 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 410 0 obj<>stream +H‰„TÝn›0¾ç)Î¥™„ë0¸ª*mm4uÕԨ᮪&™– BLY_dÏ»ƒMšt­‚Ä!ØßßñáäëŠÃc|Ƀ“<À!/ƒ”ê þ\kªÈ” +y0x eŒ%AäJܵ îÈçÕÅÕ,»vh‹¶†¿F)MÈjÉY̸¢œ1X…‘¦‚ØÍ`›Û×H2Â…÷ù·€# pä¾JÒx¼É„Êxäi—;WyJ'4#ÀNsš—‹ SÿXv¡¢ŠØ¾ÿÝÙÞ+yWŒêc¯+>ep·qöÃ(CçmÈ)'5p*ü÷Þã@Á9MfÐlo&kSgŽÒQ4•±¹&žLÎLg;Ø®£¹O½-FSŽŒPY¬Qþ¥§óLÑü­ôoG›ÊCÞÚ0¡šô›ÖÝ×!®ç¤å˜˜XP2Ÿ4ï!Yšf#¤—)íKþSôgÛþüæú¬èêòÜ)bN‹äN ™½È?¹Ç¢;?ÈÈíLª½ùj·fñÇ4›Úž¾ qæá´‹[ƒ’4‹!јuvpŠMÀ\¹”:Pø{{o®?èù$$Éhº?-ï·Oúö]ŽíS¤/ºj3Tíú¿'b¦Ï•BúÃ?U=à*b AÅæÑB¿±EUV8@ )BçÂÔõ3”mßýqGR+4°ƒV;hžyèë~-ÁG¸°xFබ¦³0´Ð <'&D‘’TëÁTk(Úfã 4朦žàŽ˜Á> endobj 412 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 413 0 obj<>stream +H‰„T[OÛ0~ϯ8ö¤;N!¤ ª‰¡‰Šä­B“É\È–4UTø#û½œØé…†)¶âó]ÎÅ'ß3]ð-Nò<ù"H˜Nãã±f*™°HA^Î8çÈ‹ tKŒZsò5»¸º‚YÛôMÑTðh˜° Éf‚Ç\(&8‡Œ†šEÄ®z[ßÛ¢áŒ$½Ë#GîW“$>rÂd<ðÔž<Èù@K4ÿ3Ƚ|!·nå (=ai4„;ùd6½0Õ¯ÌV¶èa€yë]HÅdºëBµ·;_¡Sâ/ÒP&Ð4 S,ºóv´ˆT1­öwF`´1Vá(Š% R¾ïB8s23­©moÛŽ†1¦üÔ'“³D`ÕBÁD¬1e—£wí£²æ©-ìÕò7Q´¥xN‘ç1ÔGÉ]Ô†ëy +OÓSÎ4iZ„°oã<›3ç}…ÇZœåAo- T·jÜwIñ¼ •¨ÈúÍè $Æ í y"ôN¨<*íÙº;¿¹>+Újqîäx™R8!dïGþÅm‹öü ."âRýϘ›zUÙÓ šàN»ÊjP’¥1ÖÛm×›â¸7‡Ó{Â¥<º?Œ½¹þ¤µF8dÉfÈ>¯œÜ´VL‘®hËU_6ËwZo?+b{kDÒQþXv€/UÄ@m»Î> endobj 415 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 416 0 obj<>stream +H‰ŒTÛnÜ6}×Wð‘, †¤¨›h›´pƒ"FVoF™²ÕìJ©:Íä{;3Ô®¶)X‘9sΙ3óê÷f!ù¥I^5aš5}Rʺb +~´°µ, ËJi +ÖÅ%•R9kº$¥%Üú’ÜñŸw¿ÞÜ°[?-S7íÙ7&ÒRæ|w«•UºZ)¶i- wó⟜gÏdÉ͉†€†’ÇU^Z|d¹Ì,æ9Ää%&W˜–keDó·¾ÎN!h u.+ƒÒ þÿ0í÷Ó“ó¢T»®Ý»ßÚn™|Dó_%tYÉúûH–°àªŠ:ÜÍY–|ZZ¾ˆ4?mP-EZió1R¾ÀkÌ`µ‘e+ÕµRß©UÈ’•:ǧW¦·­onq>ˆÔBY®"E%K •MµÔ¶YßD*æT],;²ú³ *šiUÊŠ7ŽZ‘C x½LL<¸…µ,ÌôÖuC?€ªïDjàÁfÏfuf}Ü’ÚýQq‘d’lCRF$T!¸[p/,H’š¢° 0bŠIŠ˜„V{¡dÍ1Ý)ÓòH_r:Ë»*p!xú‚HÅê!áj扞£€óš‘aŽ¸YņÊéµt[HUêCÆjÝ“-ÏùúK¸~ÿîuç÷ý5¡Š3MxøÙ‡æ'ÚvþúÂ.tè¬8‘|†_”>Z»¦3oÿióÞ]£iÃÕ会™¬,xšdë +ýf*–KsN!Ë.ƒEyÿî½°¢‚ÉQ'Ç Œë ³Ýñ7Ž :?ÌË0ÏôʹLgÍb¢Ewn Ð ÓÉe=‘f0gV—êµ Kl4JÍ£Mé£Ç¾É96W7fzçƧ]D…ct½DèŒnP›™N¸´¸Ðý÷Cß;ïÆÎá$àlYp#­‡ex–¯ÔÏiÁh(¿jÏÜêðÐ"c¾ô˜ðÿvÎÖÔÇ™ôÁÁ Áá–áŸÃ4Í‘Ï3%Út^ù4CÀÑÑ4ÊøHÿ÷(L Ü›ÆýWÖ>µê\ðöŒž¡gȪWÂDO<ã,üMÛy^_C)I…¨2Lå|¢NžÑ‘ jDH¼Ç°6νš£$Û9‡)) 󎆤åÓHÓÉO´<À´‚Y‰ènÆ+OW€Ýx¿êþ¶Iþ`îxÜ  + +endstream endobj 417 0 obj<> endobj 418 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 419 0 obj<>stream +H‰„TÛNã0}ÏWÌ£½RŒ/¹"„´,hÅ"DE£}A<„ÖÐ,IS%†Âì÷îØNihU¤x{fÎ93žƒŸS}pRE!A@q¤,Ï€ããŒ(g‰•2™@ÑÎ8ç1³ t&z­ƒò}úãü&]kÚY[Ã_ aÊb2q‘0Á9Li˜3IôÊèæNw íEl8z[ü +”.¹·â4²‹Š™ŠlžÆ'OmrnÓÁ-þXø‘‡/Ô[gyI³Lzè{C®ÛºnŸuG–’ßeW•wµî=Š÷ +ˆãd£áZbà³rÔi˜!ë– +&H ‚IÿãÖóÛ'qI’]tž ¼†²ìIƒÀ!Éø”ˆ=¹IÙ•6ºëiaòCÏ‹³T`C„åˆÿÔ§#—Âåâ~G¹r)Ç?ýqÏñ1 £aÈ_(Klß¹G|\#¥ÔHdõuä«‹ÿô〓£›«ú¥.›ò>µNH?몕©Úå§ýn½œ)‡»7Õ¦³Ðð¨_¡ÀCY?P{CAºÊÐ ea?˜µKÜÁÛ¡pú˜ré7{ŢꭶšõYàÒ@Õ;82fiÎ#dü†d;mY$€S€F,#+jÛƒhCqŽîL —S(—s¨ F…ek@¿¬ôÌè9Â/ ¾ªnîüaUvæÕ nRïI!¶R( oïͺD=17 %‹ˆ†uûTã ‹Éô¼²Yt¯áI礴7(#õ“v>NAØPwÛ™MŸ\ÛͺÖM‰I3„a?ö‡xcsòõ(rÏŠàŸ«v‰Ì + +endstream endobj 420 0 obj<> endobj 421 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 422 0 obj<>stream +H‰œ”MnÛ0…÷:Å,©bHQÖOH“´M³H Ý]06«•,C¤ãœ¤çí”-Ån ðB4Hμoæ O>O9<ëàcœ”e ÊEÑ"†?·H +šÆ 2§P6ƒç€QÆØÊY¹%ÞÚäbzys÷]kÚY[Ã/£ŒNÈôž³„ñ”rÆ`F‰ZÕ<©b{F.ü^~ 8Œ]r¿šd‰ýˆ ‰ÍÓøä™MÎlZ‚ÁÃò‡•Ÿxù\ìC¸•H‹ Íã}a<’‡o—µìÂŒ4>ÿ[vÎ Ýt·*<ùã¡ÃO‘6äȃüa”Ó„§~÷|âxšSžÕ dÐsõm9*MJ3Hs6–Æ'žë^v²QFu:ŒìÁ©§c4ãØƈSžXëHì{÷½œi6Pð”ÓŒ”ÝF| +-  #Nj­àEÖÛ¥2Kl¤ „,UßÆœö‹ 9¿ð¾j+ß@»1ë½l7uˆ&!sxÂ()ÑÊ@»‚¶ƒv± >n´ ì„C8¬XtLé¤}ËbD¯[÷]…xž +šåÿôÕÂÒó¾öCH–eùŠ»Âí\ÔœmõùÝíÙ¬«çN +s"w"ÈÑF<Éw³îü ÝîHÌDºgü+^´ó"ž¹~•ÍºV§»hœùp…sN© y‚cUÐdähþ†l»ÇBˆ(?üùâÝí;¦íÓ'Ãsò/°ñ¨^Ù¾¡+f]µ6U»zÇÒ|°´è-­ŒF+Á¬VCŤӀօyõ\YÃ`G·»±^ȉó =†Œ}Æÿ0ßn.T#Ñä9é\ŽŸú}Wc”Qt‘xˆ•h[ AVG£Áh™×šm`)_È•+xQ£æ°À“fYi˜µ¿!W.ÆLëÆÍæq£`Éxh“A@?³/²ªe˜ è§ZQ˜*åªsýŠÍJÖ_d7w»[Ù)L¯ê9T¾€7«EÛOñ.Ó¸JG¨#­sè¬×¬Uÿ\—Áo‚+˜· + +endstream endobj 423 0 obj<> endobj 424 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 425 0 obj<>stream +H‰„T]OÛ0}ϯ¸ö¤;v>Œ´±IшÄCH]è–*É +¿d¿w×vJ³vUU©qäësÏ9÷8'_f^úàsœäyòE2ÇŸ[(Í’dÊ¢ò:àðpÆ9!/ƒÐ-ñÔ[ðH.f—77p×µC[¶ü¦,&³;Á œÃŒ†šEĬS?›"[#‰…£Où·@ `äšûUœ*û1“Êö©}óÔ6ç¶-<¦ùOK_yúB~@¸•è˜e‘øà\"çû‡ËËŠâi{sQUEµ6½çò¯"•L«=å˜à*“Þ…Ç@Ø¥¤¥‚)b½ …f(üÉ«Ü¡EŠÅÙ›oõÁ¨nΞA K!Éø””ˆ½¸»¢+j3˜®§!6 §^g©Àa†‚ ¥ÑÉ+Ç_$þж¡ ÓÄPŽÿã _,}±­ó´ÂÿCñêÞ ³šô«Ö=Šõ‚ôT²áÝËH õ‰Qà’§if!½&éÚºyMGuöÖŸß~?+»jqî8y®R86do#Š³ÍFÙï˜ëJ".“ã:ýôµ«¹~/êUeN7h‚{8íæ¤!‘,SeÍÔn÷MEH¹C$ÿtäðí÷#y¹¨í}>:Mé§ye§™¾ì–«aÙ6Óî|„ö7©R…7 Ó(1fúë”k§ƒÊÝäN » ÿ½© rF:¤‘_ý)æî#Г£tG7RžnŽŒz«Y’¦o;¨-%¿mnSÒðZ¬  ÐÄÎËa0sX`åðºì¡lk¢hƆžïá†Æoßú¥¶|ÿb],«MÒä¹2 fÆ8—®ßñF7Eõµèæn÷­è ¶7Õ–+¹i­on:M]Ú“:á:‡Îà…õœ{3swÈyoŸß¸Îƒ¿ 8Éyp + +endstream endobj 426 0 obj<> endobj 427 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 428 0 obj<>stream +H‰œT[oÛ6~ׯ8Ô1¼è¶4[³>$ˆ…¾}`l:Öª‹AÉqûGö{wHJ–j¯+0øÁ´I~络ë?V^ûà·2¸*KÊmÑ"†·ˆ š +)”MÀà5`”1–@¹"·Ä[Çà™üºº½¿‡GÓ Ýº«áo£Œ&dõÈYÌxJ9c° +£‚ +¢÷ƒn^´aÏHbáÂÏåŸG@áŠûU’ÅöK&TƶNã‹g¶8³e giXþeéÇž>—'·òÒ"¡¹ð +ðî3yútûA«!Ä¿ qÕ¿WΧ<^Üôj…«mW©×ý¼7]%´ (<ŒrÒ…œ¦¤NñåD|öÚΈñŒÓ,?gæUÁ¨iŒä–”fælIŒ'^Ø£2ªÑƒ6}Åèÿµ×ÆhÆ1ÂÈj*п÷£œô”#Ͻk‹6-æÇyŠIiúêwU#¢DeÞT}ÐpÜéa§—Q&“NÈ|Ž=0 aFc´'² `ù‘£…ŒIå·Öö¯Ý¸Ø°s\ k¯ºí–z%ÑTËi‘N˹uÑ¥ÜY-†¯C›W¿ïÜwâyNúЩó?FÛ0>†0C² s;é”sê'mÞû›‡ïÖ¦ÞÞ8.̱ܱ "ɧµ¹9 ÞL¦'‘?Ôçã,Ü™»¯ªÙ×úzBãÌî‡ +H%Íc®‚Æ‹Îæß ûR”g,Ê_~póáãOx$ÏÏÊIs5¤î½.%ýÚTû¡êÚŸ´·8u¡¾ ˃i{×\OŸB;ž·ç]VU]C£|sçT$8_ss³|în÷ o`t¨¨Z8±ÔpèA¿év€F[yøæ©K,9sŸÒ¾W¯º‡u×Tí+¼¨õØš®Á²„‘z¯Û¾3Ó@àóȹÌÿÇ@LÆ“F©ÈŒtW/ý5ÎBA.½] {Æãl—Ž^–Ä2ƒÑ2r°³”‘~€zÓ ðMIm›À¶½-ž¬Û¨ÔßP­ÃØÀÐÁ‹öõn,^ZÏ|}õ¦ªZ…1’~©5…•Ö.㻯C«êÊlÜîQåu½±IÙ#÷í¶œ*-]ººàº±¡ï;Ϲ×SÿâõœÄ]ü#ÀˆÜÆ° + +endstream endobj 429 0 obj<> endobj 430 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 431 0 obj<>stream +H‰”T[OÛ0~ϯ8Τ;Î!¤ ØÆx ¢Ñ^ÐLëB·Ü”¸ÀþÈ~ïŽ/m݆¦>Ä©íóÝÎÉѧ9‡û1øPGU‡jä´,€áÏ.’’f1ˆœÆTMÀà>`”1–Bµ"»Ä[OÁ-y??»¼„ÙÐénÑÕð Â(§)™Ï8KÏ(g æaTÒ˜¨^«æN ›3‚˜rá·êKÀ±`lÁÝ*Íó)‰ÁixnÀ™%œåaõÝÐO}.v%ìÊ ÈÊ”ñ®€eKn¾žÍ6M˜Ð’ôŽÀKñiÓËNpbáͪpÒo{TF)ÍIršc§aT ¸Øé;ÇyT>‹*ø-À[ªt + +endstream endobj 432 0 obj<> endobj 433 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 434 0 obj<>stream +H‰„UÛnã6}÷WÌ#µ¨^t±‚E€m6mÓš`-ìK°ŒLÛj%Ñ•è$û#ýÞI9–Ë"@,YԜ˜Ÿý¾à°f¿–³³²À¡\ÍrZÌ៿H +š 9”íŒÁzÆ(c,…²šÅþßzœÝ‘O‹Ëëk¸í5•ià?ˆâœ¦dqËYÂxF9c°ˆâ‚ +¢·V·÷ºáÎHâÊEßË?g +®Ò—Ï%üU) W pÎ\…;òõÛå7Õ<èKÓÙ>BŽÄD± 9iŸc/xÆè<{QKz6#/çÄÝMˆâ”fXŽ£ÎN£xŽÚÅ÷ ô„¦àœ&ɤ6;h„QáØ &e4‡lΦ¤xÞª^µÚê~ˆâ™œ]ŒæsÊ“Y¥ˆç® +´x{°•¢(rÝ-õ˜؆ÿÄ0[ÝEN[Õ˜(ELº6S¸ù…ƒé!ʈ‘¨ô §˜YÀ¼Á‚X'Eá±d zOÊ~§Ï~ÃI¢"<œ’fpàá˜c0 +)Rð &L§ðAó©ÅñK[ŽÂ¢±rA†­ñŸãÁ=PFt¸íÅ^ñ±Y‡’,çųÓ܇æ8¦ùø8\Ü|ùXõÍêÂ3 +t%÷\ÈäAùÁßVýÅI8ü‚ÉìYà+Úä¤å…?sõ¤Úm£Ï÷Õ8 å +Ÿ³2Iç & {q>MwÍšÒ—ò„ ²¯ÂÍ—Ÿ$~dƒk!߯…7”Nº(C?».fd¨úzkkÓ½2òŽËä(—’ Ga“>ïD.AÁ°õ·ºªW5ÆÏFø½O"…¿ŒuÉTÖdzî¬ö„  —NèÛ³Ñù®Üžš“PaV–cÌ[³ÄõzcÃÎXPMc=~Ñëwþ¼¬cœà‹ª²8¾©#Ü!ÄtÁ‘xOçtbŽ<©ÛV/keuóƒBXÕF›Ö£%ЪÐ7»Æ†ˆ‹Ÿó$ˆ iHÕCq´öáXDnØHewèÝYhýFÀáãnµŽ<¤:J؃œ¶õçC¿_ _u«dNz¤/Ðïsœ÷‚¼µF_T¢ïCÐÞ G9';·/r2XØ(´Lu®7(Vµµz + #Ò»ó1áºÄ~ã¢ÇEÓ×·ï¾Wåìûý” + +endstream endobj 435 0 obj<> endobj 436 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 437 0 obj<>stream +H‰œT[OÛ0~ϯ8Τ;v“!$hcшăI]š-—* ”_²ß»c;½¬›6õ¡N|ü]Îwœ£OSÏ}ð1Žò<ù> endobj 439 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 440 0 obj<>stream +H‰„TÛnã6}÷WÌ#U@ )J”,ì Åvn°Ö[°ŠLÛêJ¢!*uý#ýÞ9rìØ›,J3¸ða™”"ªþòôS¢/Õ3D8QºÌx‘xâœz¶¬ÿ6·õ°1<ÊËÔ¥*x–^]Õ!8ÎûaçSŽb‰øÄ%O~P2LdQr¼}§4`Nb®Á•šç  qND†<°ûz¬{3™ÑEqŠ‚ß’”‚çkK.ÓûDœ5]úÓ‘æ%3‘Àÿù9+rZñ¯¡Ä õÝDb¸ Ï!BÉ\¤¸Føð2SÂüäœà Räyá!)'uY—w{w÷íë»fìÖw ±T2ð`W†$+Ž†f¼»5¸$Bé·3¤Z—Áçó?u¿ëÌíM +‚+C…JЊ)öjÉÓ³>‘¿l±™¾Rªß^½öíëÝ1ÇOOCúfíÕf®ÛÝÔÚá{‡¨–IF½ïi:¨‡4[DJ™g¦m=A_`‹NðhÌ øie`²hÇÇÓЛ›5uˆä\§8áâY ÖµxW±±žÐ5e-v­fv»0.`+lºX1;r¨¶-¶·¦PcãÏ>Ih¬lø6ØoÍ@Bôøþl¨.É)ù„’o¬*©ãý(ù7Ü1N¤Ùh:S›#9®èØÑ;5òM™u/¨ì||š ]ÃÁ>Á¾&T¤Êx®t6KuARI0ƒ{BÌŒQœ ´¡Â83M(}°¸ Ð™‡ÃŠ…/+¬ÛÚ¾46³óxh‡pÚ€¥ƒ—Ñkéó&1g‚Ws¥N<ç-ºo»öc;™ÐN;\g­›Ì˜bÑ%›"9»ãv-‹„r§ÝqjO¿FüRÅ.[µî§×xÝÕn ½éíxà43ÂåÒ;[ȯLÏqó~7}e+P› ëúÓÝâÒ{Þ¦×W_KŠM†ÏÕâ?QPÌl + +endstream endobj 441 0 obj<> endobj 442 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 443 0 obj<>stream +H‰ŒW[oÛ6~÷¯àËj¨URwEmɆ¬(j$~ ú Ø´£M JnÚ?²ß»s¡dÅJ³!€CQä9ß¹}çèíwZì»Å¯ëÅÛõ:Z¬w‹<, ¡àIf‘ˆó0ÊÄú°Pb¿P¡R*ëÍbIK¸õ´¸—¿Üývs#VÎövcñ–y˜Ê»•V‰ÒY¨•wÁ² #iŽ½9<'"<K|Yÿ¹Ð 0"å¼JóÿÅi'¨çÀÊsT®P­ÔZë¿~Âðu<Š •iXD(€1(AÞõ•ë¯Ì¾ê +yn¹ŽtXd³› é†U³Ý÷G09X¦a.m ÃD¢õ:Ó +0õ ÛuJçQ˜ND«³A›ã£1óHæ"+Ô“¦`ÜËU媃éë‚e®ÇNUa®!zKê¤×]yKâ1„‘!ùceŸLJÄ` 1•%.âXç°µ2ncÚ¾ÚÃ!#Žðkƒ¥–|IôVt(E™¬úP¬¿ëMÕ4ßÿOC‰ +[E\0†ëv‹œQƒ#§!YŒŠ"ƒí#=‚r0ÔÓóy”‡yœ¥àµÑÚr´Ý…š*øëˆH{ë‘‚•`l…™DÔEêQ/™?ª³–y WA0—«-ÛÒ¯Òø™3×õÁˆºÃßðávÛ‰ÝÍÒú'Ã[1‰Ú ÎBùU»פ߻qRÑÙŽ ZÿüÃ<øT}³äf\׬@ð:)R†(m³åh »ó½©ö(õƒòg9ð£ÝšŽãa8}ßÈ€¹‘ƼP8ÓŸ\‹«­ÙU§¦—ö}Ý)’XùÍgX&!Târ^=„6ã’»E÷—²;Zúßb˜´ì v2Ž™ªJZûš>‹T¹.Gèø‚”Þ?u>|¿qÍîa¤9xGN^@,ñqã>\Ð݈Tœ¶½`Ö, +òú¤^cÞ Ò´bq%1Q)²8,‘Æ@gfÔ—œ +·Š²CNŸšÇ€ÆlœIùüñ?(уQ`èÐ$^5ôœô÷ò +ã—Énãêc_ÛöuÊŒrŸ˜§–¯a–çPut´±¯x_Ø#o¸ +¥†XH þíÎÙnMk8£š‘‚ÏPî$’xEi˜¨!{;Ì›'æ˜ÑòXOùcž#uÓ çáÒXõ@ðe¬”LºÎ´ÓÀCÉÖ¼±…©¤§µ–{ŸCxasr¡yúðH.ã5Ï¿÷)œ|Âé@–'c_J¸/qjpª¼¡ãGž´!~éÈÍc¶½a(ðY”Ãü7õÉŒ*vu_"žŒ ‚o™.z.gÛWèqvsæPÕ-؇öC‰JþïdvN“ácèä‚÷¡bÉËwï‚ÉÈ÷òP7wæú±¦tØØáòþÂß­'xk[hÕתn@].«`äzçcÎW:;õ£èNôx<úí¾vm&™öé¦ØÛîêýi:äƒJ«¡)Ñ»“Û˜á+‚ç^1|y:™2ì¤úq#‚6òðÒ"tgvö<”@2Ú¶wð]‹Vq„`úŽò4™ùp>–l9ýRJ?žP0ý(88ÝCréø5U,74(ôù‰nÚ$¼s~©µÛRèœÊ~÷iIüÔAa¢f‘>wLk’­fV!ô…dV)Van›Ê/@„;µ TkT–Éë¼qÃI2Ô + >0„b ìŽÞ´SýÑX¼ÑX¼/ðÅr}±Œ +Hð"{­|®×‹Õq¯ + +endstream endobj 444 0 obj<> endobj 445 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 446 0 obj<>stream +H‰ŒSËnÛ0¼ë+x$ ˆá’%¶6Š4@cĺ9¨ +㺵¤ý‘~o—¤;v ¤•¹;;³;¼ú²²îƒOyp•ç’ÉŸ‚„g)øsA”q-‰J¸Ô$¯AÖàBˆ˜äeº«^‚ý¸ü|sC]34e³% ÓåD$@s‚,Y˜qIM;˜ê»éˆ´9ŠZ8ö ¥kî£8‰ìKÅ\E¶Oå›'¶¹°m)€dùOK?òôA½B¸È ÐYÌSiÂ=}ºšvfÖEO,Æ[á CtR¹Ö¥ÊË^µ¨˜…1OhÀGÔŠŽÊRTúàeq‚x’`‹½ 2Ê·q2Í¢SqH +œš]]Q™Át= #ýÄUðp{¡Õ“áèfŽ?h_ô­©™æ5Làs¬ðÉÊ'»Q9ZáûPb„º7,FŒ¾mÜ»f˜´gŠk„w#%Ô£À=¤Hp&é5©·º~é§w·×e·}š:.ž£Ç‚žÈ8Ý”Ýôh¨.E +¥/ëó[Ï\ÎüwQµ[3Ù¡ðp™ÛOF´âi„žÍxtàxÇj#y¥ŽäÎÝÝ^ðÅØ;Ú_Ô‹[S~k3»5Mû²Û´Ã¦©Ïº&ÜW¹PŽ^?ö¤ ˆC×>zÒ´¦+ ?j2ü°Lâþɦ'ås×™zØþ!Ýs]oj[ÆB üœù.Æ-»poªÝœÒ5Iú«Ÿ ù^]}Pú?Ööó<ø'À2…(Ð + +endstream endobj 447 0 obj<> endobj 448 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 449 0 obj<>stream +H‰|SËnÛ0¼ë+öH5#’zÓŠôP°nMŠD[j%Òéùú½]’r,4EáI™;3;ýù¸çp´ÉÛ&¹išCR²º‚a“Õ¬ K& +hæ$…c’²4Mshºd¶Xõ”|!·ûwwwp¿g:3Áo »’ådÏÓ,åãi +{º«™ êäÔü¨þŽ$Ž~m>%E »¼Ìü"s&3Ï3GòÒ“§ž–p.ióÍ˯¢|./úã.6PÔ«Å @joí³î†Åhs¶°WÚš>7Æ®u£ÑVèÕ™¿Õ­Ð%.ÕF±Fä«5úÖµ0ZhiÎ2ÒQÉ8ùqÎ= µqC8(ŠÅY@ýTÚY0±¤;/ð4ºaÔà´ Véð—¥;Ájï-þ <ÅŽÁTAò ¼y¿F&®E”Èà0N팄`]ë~7xµœè£ +«}ýxcR‚@/ž^0«ˆéÚ F}:cÊuŒV¬"F‡}L ü>4W’³oYw. |úuHc—K&9Ï`ÇÏêò=µ 7iz~ô¥Aïf™[¸99Ðà—j»áz? +Á‚œç[ã6ÙÊH‚È>×^9o’@ÿ%†7ZõðøŒÍþ²àžO +¬ºi¤NEȸ":”¬l8 y-·lòʶ´§Ó¢9Ñ0Ÿ­ƒÉøÃwºCkð† ïcÓz„/XUTrØfŠôê­`YY¿¶öµO³õÆW^ù=8¼XÐúäóä¿ûçä[&½ÿ:!€cqîÓÈ)ËEþ¡Iþ0/>)n + +endstream endobj 450 0 obj<> endobj 451 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 452 0 obj<>stream +H‰„W[oÛ6~÷¯à#µÕªî²Š¢@—vC7(bc{†‚–èX«,©¢/ö'ö°ß»s¡d5J:4hHŠ<×ï|çäåO[_ܙջÕËÝ.¾ØV©›m„ÿhenˆ0uƒDìN+OÜ­<×ó¼XìòÕš–ð꼺•o·W>ˆ]Ó7yS‰…³NÝXn?ú^äù‰ë{žØ:ëÌ ¤n{}ÚëNx'”(Îù}÷óÊ)çUœFø+ŒÝ0B='Vž¢rÕJßœÝh~Äæûá$‚Vì@’Åî&@d³ï£yýËö槷 +°þ/lHÜT$Ͼ'|~+o´»™4mC¿k„ûÒ8¡›HÍ›Wì˜ç¦>Dpí»~”ùïÐ|°`Ôí{¬ÜÃ+^êgx…M g¦¾>›7íZý÷뼫oè9½ñCŸÄJ¼2&áÓ¾/MÙÔôn<…ÃùóÝwü躬?]5§Võ徬Êþ}u²x¶ØæÝܦ ~ÂUæõ%$&“¿^F·ßÿ©Nm¥_-4½òÒtót”„¸ª¾|"8B,B#àóä¼|…éûÅt–™¥É#çÖOd= +-ï-‰4yW¶=h~‡`ª° à +ÛK#à§?‚ _jq(;Óö¬ï„ÑtXþJäU©aÍUØO6! z^„Ûò=ABØ—ø¼©ÇçÎ:„:«§;`ÇIµõÎ4Û;í˲X²ë¡‚»\Ø*i=ª]@çòNë\ÊgÇ9©k¡U‰…‰ ·Ý’yžsÊzÒ«¥f¨ +u ¨áN9ò&¨ßbND MˆqQÀÕ>Lˆ9B‡§Æ[ˆü¨óϬג\ä†Y,Áµ¬*˜¾-Ô*ôÆ‚ž1ŽÁáLׂ°õŸ‘Î ZºæÒ‡Š„ú({ÆÌ&0¨}ÓEŒIÕ€ ÁóYÇ8Üä¾@BݵeŽV±×u•ˆë-© ¸aE‡{>¬ôi„‰µæÿ Ÿ˜ad2IŽ9Æaô>–w81q#ßñöT‹”ÚÌÕ`mÁb v fõ¼Ÿ†‡(ŽÇîFada ?îË) 0˜b†¶PSç±–!Ö€öiY94àÝwúËPÚ6@‹Ùä€ê o)XîÜtŸuDÌGƒœ“BFŽ7  ûx^⟤åýnõßL‘^ã + +endstream endobj 453 0 obj<> endobj 454 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 455 0 obj<>stream +H‰ŒT]o›0}çWÜG3 Ç6†ªª´µÍÔUjª‚öRí“0ˆ0i:Mýû½ó$MšU%\ì{Ï=÷œØ“¯)……t¾dÎ$ËPÈJ‡ã$¢~&1ð9fdµC`áL !+Ï„ªjë<¢ÏéåÍ Üwmßí +þ‚ëq¢ôž’€ÐSB u½3$Ö½¨Š˜Îñ‘†sdߪ™in£úå‡ØtŸÚ6çº9Ñm¥¡›ýÒôKŸú;Ù¢$Ä1ÓÞž>šV«¼MŸöy¿‘0 :¼áaQLi˜Gô Ü'H®[ón\Õ„"éú8BÂ~œÙ<%Ÿ`N•¨Å„ó²+Kú'¨oåÅŸqén£µ{1k馮óî·Î/çE·*/ÌDãRŸr‹tjÖUkS¢ƒ·©,Œw©W]õ$LÌÊò}à뺒²jkxØÊÛÉ4¯V¯K³OCÁs/º&_}¸àáûåÇrÿ;TÑÝÅ‘¿*!  Ó‰Œø‘N$Fz €9‰Œ;4HøÎ’˜”ëç¼^¯ÄÙyÒÓä}OU…zfÓéa«+€ž»¶',0v¤*Ïv¢¼1f·¯·í8Ü~-þ€~{¤¤%”ðc ½½£ˆFCßžš+}j"$ ß+~Ã9Q´»kX`ïšlYI¨u½¤Ì& ¥]lzØ.‡P<©›¦_ +j7ÆÊlK³RjGLWæc„¡:í#KjY*¿$˼Q`Zó’R! ÄhêMS¶P´µáÕ®º&PÞÌa>¨!ÕGkóÙ»L]jf(ÛÎŽí Ìàþ~ðG4½ú·Ë‘´¡uÞåf8݈#¡”`WTϧ|µ´q=)Vƒ&yÕˆ¹®ßVý²j̪¬…VÑÖ`Ëç:sþ 0dX—R + +endstream endobj 456 0 obj<> endobj 457 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 458 0 obj<>stream +H‰¤“Ñn›0†ïyŠsi.ðl v¨ªJÛÚMÝͪ†»j41 †hÓjÚkìywŽM“h]w3E +6ØÿÿýÇÇï>/%lÆèC½+Ë$”udx±??È +®SP†§J ØD‚ !r(WQ⇸kݱ÷Ë××p3ôS¿ê[øqbxΖ7RdBj.…€eœ> endobj 460 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 461 0 obj<>stream +H‰”TÉnÛ0½ë+æHÃE¥ 0Ð6A‘Š CЃ"Ó. -ÉÔ +ÿF¿·\d;©‹…zfÞ¼y3âåÇ%…IÞ7ÉeÓ0 Ð¬ë +ˆû#¯qÉ€ ÌJh†„À&!˜R@Ó%Y0]Ö.yBï–îîà^Ovê¦~Aš \ å=%9¡%¦„À2ÍjÌÜZ9Ü5ÈýÑcäЪÑ%ì¯;ݯ¡âÑ)§Â£#çèôâ.cH-JBBé‘èѼGz¤1·?ÚaÛË«Úÿ7à”Uá7í/”áÿ¤Ä¥ÀˆÇ!Üø!”ÈtZm­šÆYñó>ÈqqY÷ñ«ÏåqpA cücÆS5¶ WfÌÖ\€² Æ•êZ# ØħD4ݼ‚)#°¾ëÆæ25&0eA€·êFÉŠÈ “fÜmhÙ·V}—`'_ ÖJÏ.ëjFË‘mÇUpaY`ZÃuIsœ£ñ€Ú˵Je>¯Ž¬ØI1¾¹g¹ž´ô" /Ƴtäüˆ*\’üMC§W‚Ö±¡Ì¦î]AZm6Rˆ&Hb‡ +“Üžè«g& § ªïUfd7+ƒÃvÜ6ÉoxW+î + +endstream endobj 462 0 obj<> endobj 463 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 464 0 obj<>stream +H‰œ”ÏNÜ0ÆïyŠ9Ú‡ÛIœ¤BH¥ ŠJTˆÍ©¨“˜ÝTù§Ä@¥ª¯Ñç­ÇÎî"Zz¨ö`{<ûÍ7?ròq#`»DçUtRUTQÎʸûùMZ2%!É™TPõ‡mÄç<ƒªŽb¿uÿzŽîÈû͇«+¸™G;Öc¿€Æ9ËÈæFð” Åç°¡qÉ$1“5ý½™AbNBPŽ~­>E J_<ì²<Å%ÉX’b>ϱ8DzDˆ‚VßÐ~ì‹ä áw¡Uf¬(à=‹îÈÆêÙ¶Ã–Æ KɵÑËcØΔ³‚˜Þ álƒ¿#¥?œ*–ƒ*øZÅÛT¡È­¡+É2~uŠœ ²Ð„)bÂáÝA^ð ÏY.òX0žçT/|{Ç3Ú´§ÏËÙçϺ7?Oë¹{8$ÄQK$"G-â.ê9\{ +)e®0…ûB>#>ši™ÌðÒç\~×ýÔ™w{µÿnÎõ\ï\Xü͘äÉ?%Þ˜÷µ¾î‚Wd©çv²í8¬”ßêFFZÊ0ÒWCÓÖÚ¢Œ{0°;T‘Á‘…ñaì Р7Ø0ÊE Ížƒt¤wÚB»À(¸ Ùâ,äîÂÊK­ÃÍ`ÕÎå÷ÔÍ1>º,zk`Û>­GÐ~…ºk¦†zç«HmÀŽÐØPï=¾æ÷‚„ +v±W=Mó89­ŒÌ­ö«¥‡<ƒpCcé`4ZïchÀ þrŸãœ`ÿºëà‹™G¼5A²A +i½C©X&Å[ ¥µ·q]ëeq*MëJ»WÚ½ñœVD+‘@ú }˜v†FÛ•ÝôèžeOë­^Kêùq.‚™?e’Œ4N]ã³ãàþI̶0…‹¿'Šè5nB”Ñ'Ç—¼¬¢ß š^`€ + +endstream endobj 465 0 obj<> endobj 466 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 467 0 obj<>stream +H‰t’ËnÛ0E÷úŠY’ 1$%QR`h› HF,tÑ  U¦]¶erÓòýÞ9ñ#p")SçÞ¹3Wïç +V>yÛ$WM£AA³LJQW ñ7y-Œ†¬Ú@³I$¬)¤”4]’Æ-~õ˜<°7ówww0sÃ8tÃþOKQ°ùLÉ\*#””0çi-4³»Ñn¾Z:ÜÉXÀñ/͇D!PGqÚe–¬Yt6$^qd™R5o¾û9ÙWÙwT€© Qé@åðÀ>[Ç+¡ØÀ±’’ÝóŸ6kñ`Ø‚g¢fývEþN)]85¢SÉ3iHåÞò!~7ÄuK|ds»&<šP˜sª„Êk,ò†8õÑíÀ ƇÀ°´ ôw“G?ýó±{ûA¾)XÌ¢.B_JmÓDí‡-ÚR쉣lA(,=gŸ¸1¬÷ö)ž'[/§d4=ð¢Õ¬> endobj 469 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 470 0 obj<>stream +H‰”’MkÜ0†ïþs”VôaKV´ %…Â’½„\W»qñ²—´”üþÞz$Ç{H)4’FÏûÎx.>ì9§äM.¬ÀÁMM lùBªHM…Û% Ž £Œ±ld!\^='äfÿþþv~˜‡zhá7¤™¦Ùï8ËW”3û43T7ήûê<Ì‘qéû1á Pñ:ÇETæ¨ÓEqâ e ,µßÑ~ís¹!B P¦ ¥@@ðÌɧjš\õ-Í$Õ¤é1ˆfÎ-ycKQ ªd+2xZ‰.-¨!Ó8„µO=N¦TRE\Ü\nxÎ"ŸQÍ—þfœ2­K°·«K‰ÌÍäbïêyºþ…ûÝ05s3ô/áäsÕžÜËUíÛÃ5`;²¸äYd¹¨}¼8×SŒV˜Â‚PÈÈÎfxnôf†™s÷£êÆÖ]¾Òþ»„%]P#Ké²âo–“ÿ´$ƒ¥àHÆ–ßbË™jߌؓµ¿oëÄ6¸8Ñ8¸öÉGFAÐaŽø« +‡ÁCcýÜÔ§6U´$•‡n©iÚœ!p@^}G~ùȘú䃌ŒS¡ÉÜþ„©®âQ½éâ}5¼ë\?GþMþ0Þ«ßb + +endstream endobj 471 0 obj<> endobj 472 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 473 0 obj<>stream +H‰œT]o›0}çWÜG3)ŽCUUZ?6uÚÚª ISµFLÊ8²lšö7ö{gû¦¤kÒ>,yÀÄ÷œ{î±Oæo3ËÁ;ͽyžÀ!¯DDEhû´Ø\Úæ̶%<à~þÕÊQ>…[áqÑ$À öŽ|X7c½jjÕû1•ÄãFØXŒë¥ì Ùe'ìÐ*Š‘õVùMÉ°ÒîÙùÆ!N_И(|9šè9C~F%7îLÊòsô• Ëi„Ž +ÍœºãÍpò뺪æ×Ýïã²oª°0‹ç‚K‹'¶è½.¿©ÅéÏ7uS´ªð“æWzDÉðöãÙ‹˜CÍ2]›¢WÏóW¦¼ìË_w;Æ€‰Ø2º#"®b¶3‡‡BNæ°ÔÕ\ü(ÚU£ŽØþÏR0ãèÑžº`¦šfÙí³?WúÀöƒ/Óö“Ñy*_=uzÜä¯Ú¹½j1ʾ^µî¶÷j4…t›×@`^3;Íæ^u0Þ*“Jh§P€o¨Ñœò¾èÜþRaF%›ô"mˆ´…L)é`—±Ë®ÒPÚPÒ¶E·€J÷ÛÈÇ4áaˆtÏi] @› Ä„´nÐëat–º«ê¥«X÷…ñÂ-;ÐVØ.3!h%üñEº#æ-„°1§Å÷¢Y›So•û½èè–”Ó§Á?'´ûgr‡x‘{¨ö^þ + +endstream endobj 474 0 obj<> endobj 475 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 476 0 obj<>stream +H‰”SMÓ0½çWÌÑ9ÄëÄNV«•€ÝE…«o‡lê´E͇’,EBü ~/ö¸¤@Š”Lä™÷Þ¼_½-9l§èµ‰®ŒÀÁ4‘¦EÌ=¤U¤¦Bi#ÛˆQÆX¦Ž ]Õ1Z“Wå›Õ +Ç~îëþß!N4ÍHùÈYʸ¢œ1(㤠‚Øa¶í³AøI<\üѼ‹¸H¢L§þ#3*SÏÓríÉ™§%\ˆØ|òòÓ ŸË£Ð€*2š‹ õkòô`ÆýPαr¢ªÙg+.ä(ªAåìW(u‚²qF 2 =~»ØyÃÉKªˆ ?× ¼‡êG‰6 ÁÍ “ýáüñ4Ì{ý`t % + +endstream endobj 477 0 obj<> endobj 478 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 479 0 obj<>stream +H‰”SÉnÛ0½ë+æHDs‘H) ´NZ¸§ r1rpÆV!K†)Ø)ŠþF¿·CÒ ê +¸ÎÛ4œ|®¬\ò±N&u-A@ýšVÀñ “¬dZ‚2Lj¨7 ‡UÂç<‡ºIÒ0ŪC² ªÙ|»aš¡ƒ_@SÃrR=žq¡™à*š–L»íæ«Ýôwñpô¹þ’”<Îr“ùAåLežgÉ'çž–©hýÍËÏ¢|¡Îa è2g…ôQ³ôdÞwvœ­—ýÊ‚GùÓº†éâ]i$Ç™8_lƒgšhw ‚ ÒˆKFS¿–ÏÑÞ•6a +Ÿë…_ŒÁÑÖñ¯¼KF3ºà§B¯Lûêy´4g%qÛ!Œ=EÝ‚8ª˜&6.nbÜ/xÄçÌ”‚j¹1ÔwÑ­P×AÝÜôǼ±o?o›]÷: B¹¯J_Ið ÙM¯Ä+¥ÑþJ0=¦j‘•æLÍËpçþm¹Ùvöæ„öß‚1²¿‘\ýSˆ +B‚c½ó±jâš]»Û¡?fø^ýéQ߇Ìc›T¶á°¶=ÂdÄî±ýǵ…eXb÷HüWc»·ÐzíЬã :£cÔ7|vྻ1în(vq@59´ã:(’xÀo–s”ê¢æØ´ðø4›> endobj 481 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 482 0 obj<>stream +H‰”’Ñn›0†ïyŠsi.p}l°¡ª"uk5eWÕ‚võ‚7c"€Y:M}=ï|ì¤éÒmÒD‡àóÿßì‹ „õ½+£‹²”€P>F†9wù"-¸–  —ÊM$` .„È ¬£Ä—®k-Ùõâý|wÛ~êë¾…Ÿ'†glq‡"¨9 +‹8)¸dv˜ìæÁnAÒÅH.¾/?Fè¥7UfRz¨Œ«”|6Áܹ [†2˯„Ÿ|T/¾ +t‘ñ\’@`–¤À®»ªí×ónØM@*¿GGYp|Ûê“S•§!ørð™ã$wQúݽäò>D:ãASpóJTœ²À!Éa#Þ CSc.Ž£©{É>Ù8ã‡Þ?»Ø¡"cÅ5³áå2LØÉ£ú‚t»š ÆäPÞ„€¨Îgsµg?æÝÊ>=ûòsÕîìóU½mgžY*4$Â܇z;; –FÓŸ9ÄMN˜æ…B~ÍíSµZ{yTûov7½Œë?ÁH¡þ £<ŒgQaÊ74eÍÆzÛ SÓw‡‘þ%4áx,l7Áþ‹í|³ýæŽ}Õ¹Ÿ%NhbM›F´ÍÛàR­à!´)Ãß«à)Wˆ©KwL<Grç‚b4Ó•ó¬ÖbÍê~>¨a“ãXAý½n‚ƒƒ©ÙXí45ÝÚ/y0LŽŽ¯gs:µ·eôK€øø„ + +endstream endobj 483 0 obj<> endobj 484 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 485 0 obj<>stream +H‰œ“ËnÛ0E÷úŠY’ 1|H¤Ú&(Ò•Q ÙY(.í¸Ð ”\(úýÞIGNÓ¦‹Â°9¶É;÷\Ž.>®ìÆä}•\T•Õ61¬,€ã+YÉ´e˜ÔPµ ‡]Âç<‡j“¤¡ÄSÇdMÞ­>ÜÞÂÒõS¿éø 45,'«¥àš ÎaEÓ’Ib‡É¶Öô{ñrô¾ú””¡y¬r“ùEåLe¾O›ßœû¶DÈœV_½ý,Új–UÐeÎ +9 ^`Mª~¢šiR7KgiFÆðF*CÎFK¿Ç!Té—³\zN#Te c=„hZ ^O~6 ˜¼˜¯<Š’³ü¥Ç3œèN—óG@šÐ §#ÜgKsV’qèÃÚy.x +mürQ^ð¨èo:Œƒ)]G@¡¼¦Ï+D5ŽÏÕq\|¿«›ƒýqµqÍv¬rV(aüY‚lÜâCÜRí·ÔH™ž›‹¬4ssŸ)î¹yªÛ¡±—Ïjÿao€Û4û›ÉÕ?ý¨à'ØQ1ßkŸ¯&ãÆí‡ißw§0ß‚8?0â4#+ÛMp|´ÍXAì7|¦G “w óåýÎÖaË—8<8ˆRú!˜µ³Y[Fiغ¾…ºû4YסÞI5è8šJqœx?4U ÇÞ÷Ú…_êC\-‹D7UòK€Q=ú + +endstream endobj 486 0 obj<> endobj 487 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 488 0 obj<>stream +H‰œSKoœ0¾ó+æhp<6ØE+¥IT¥§U×Êe•¥» h¶R•¿Ñß[?È6ª‡ŠƒÇ0|¯±Ï>nš)ù`’3c ˜‡D³²îžPd%S¤fB9$š„3Îy¦NÒPº¿ŽÉ–\n®noa=ös_÷-üšj–“ÍyÆQ1ä64-™ v˜íá‹AøI<½7Ÿt€"Ç*×™_dÎdæy‘\{rîi +EÍ7/?‹òQž B ¨2g…8°%×ûf?WíšJ¦H?RÅJ2_íª®±QÏë,P)¦Š—XA‡Ìb[Z2MŠ¬ .š:á¤[Ÿ 2ï·$â>º}#U ‹ù5¼s‹¿e<ï"RLƒ*øK{*Úûliîئ¡kGݼLÁ­›óèÓÁ#øœi—)¤È¸Ö˜ë8e”ó”X?.9]§ÕO¿} +Õ]Õ~·OõØ>¬‚pî‘P¢öHÄ}¨ÇÕG±¥ÔÊ·„H£çôÌJ}’ÂËÐsó£: ­=FûOp ¨ó¿I\þS’ ’‚"¹œ'¸"S=î‡yßwKºï}<ß›å +‰<Ÿíf8îlç`2bÝ ©àkT aŠgxô!C½«Â‹ØÜØ)’ݘä· Ÿæ– + +endstream endobj 489 0 obj<> endobj 490 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 491 0 obj<>stream +H‰œ“Ënœ0†÷<ÅYÚ <>6ØE#¥IT¥›Œ +êf”Åy.7éTªú}ÞúB'4½,*>Àá?ßÿc¯Þ‡1zSF«²€Pî#Íò ¸½|‘äL š eq8DœqÎS(«(ö¥ýêmÉMqû𛡛ºª«á;ÐX³”ä GÅs(hœ3AL?™æ£@¸Iœ}*ßEh…ªT'n‘)“‰›Ó„áÚ çn,A¡iùÉá'åEÂWÁ€ÊS–‰‹€p[òþÃí湡 S¤/&ªÈŽÆhy¦ç1ðüš¦Š¥ÉB+^ÐØ +ç$¶=Kˆ‚Æ™M¡£ÈÔ€L„OÁï+XÁ5ÃlIûâfŸóoú-*Å4¨Œ/mªÙ¦¡)ËÉØw~m©åF2Ri}›psüZyäAŸ3öŸÛ<¸Ö”wÁ-J§9'gCÛÙ¬®Ïãúëc»zÜïW7Uej3ì¦S{øv] õ~íÁ¹SB‰Ú)û¢Ö¯…–\+×âÏñ +&¹¾ ðÜ÷ÜÙ5}m®~ªý‡XRÿ‰JpùO*é©<” ™ß¹Ì«áÔO§®þ›¼'!Ã&*L;ÁùhZ«cOÌg{Z¦£ÞîWåö+ŒÝ@uÜ…Žƒñ˼qïË臺0æÝ + +endstream endobj 492 0 obj<> endobj 493 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 494 0 obj<>stream +H‰Œ“ÍŽ›0…÷<…—öç^6ŒF‘ÚdÔN73¬n¢Y¤Èù©ˆ€i*U}>oýÃ$$‘*\À>÷;çšÉ§ɺ‹>êh¢µ Hô*R¼È ØËiÁ¥ ‰âB½€¬#à]E±/í®c´ ÊÙý=yl›¾©šùCX¬xFËG„Pr %‹ .¨9ôfÿÍ´D¸5 urìY‰Ð + +ßûÐ\¹æàÚR9Óß~ð19Iø*EÆsqHÀ‚>}}6ËžIÇÔ–ýP¾tæ2 ”À‹±RpŸx–Êå°8ØXœqI†6—rçVZ<ŸWíÂ1äÙ ì Óù'!É‘9ŒÝÉÁa/hwhü½fiÇKfÂÃM0jå‚>p…vÔ1rP*'zl¢·ùXÈꥻ=vÓ_õäaµšÌšf·­×óæXÿ¾­ÚÝjêÉÁIa‚ÊIQû¡j§W–Â’BI·Ä;¦ã3 ¦…:±@á×Üý\î;sóªö_l)JŸì•2rð¡€äL8šÍfâ1ÏcAçn +’vU»=ôÛ¦"Ç›Qiêž7¦ö›Íû¯ôC6Ž×¿jY,l+àmUm–þ“tJ×akxN4\P^¹ÓÑ_¸ò‰ + +endstream endobj 495 0 obj<> endobj 496 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 497 0 obj<>stream +H‰œT]o›0}çWÜG3 Ç׌«*Ò–VS÷²ª ¾D}Ș›t"€€&“¦ýýÞùƒ&$©ö0EŠoâãsÏ9¾0ûœ#¬ûàSÌŠ‚BñHª2`æãŠXÑ”ƒ”§Plë€QÆXED®4§öÁ’|Ìwwpß5CS6ü0’4!ù=²˜aJ‘1ÈÃHQNt;èí7Ý·A,]øT| Ðr×ÜW‰Œí"*bÛgë›Kۜٶ¹ +‹V~ìå£8P¸ÊHUB3~ @K°$‹ÇUµÓù°„*òÚ{§`Ó8žpD¦Â1eë̇Qf|7!R$ åþ'ïðLg‰]&úŽÞ`t6^ÌE8)•flj,ig§o·Ö¡QŒ¤M‰ö?®¼SCÌó3*ÑÜr„”I™Aqã}¢°œ'Y½ö×eW=Ï@fO @iOë}?w8f«__[]ÏUÓëï¿'hžd§hüú‚›¿.>LÙ•Ýü,9GÙH- Øg-c,äÁ2Ssûsµm+}õÆö?A]Ú@j¦Zù‰•yT0FenÅzòc̼íãqßÛ½@ð÷g  ’Ç&#vƒr"] +ÂÓ¦”ôe÷Ò/M=NÎå!Ÿ?¼8÷.s]°ßèÚð Ñ;óà ;«z“î¡Ü¸ÍU½ö ÷=6šÞϹöÛ"ø+ÀE+#D + +endstream endobj 498 0 obj<> endobj 499 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 500 0 obj<>stream +H‰œSËnÛ0¼ë+öHDó%R +mîÅA-ôbäà¨ô£±%CRêE£ß’+ÛSôPHW$wvf¸}˜ XuÉû2•¥å2±¬Èû'º`F‚²L(w ‡UÂç<ƒ²JÒú¬C2'ïf7“ Ü·MßTÍ~M-ËÈì^pÍ…a‚s˜Ñ´`’¸}ïv®ö(àèCù1PÆâeV‡AeLéPg‡Åm(ÎCY"§å×@_#}¡N1B¦ÈX.QÏ“OŸo&uïÚ-ÍXAšJ&HõÔ!×íõéÙ‚èÀ|ÅÓ4÷š*üw ‚ÉÔvAL’sÉ UÁ i8’7¶fÁäü˜8˜A—‹‚º}Çšz®‚tT1Cþ\¡F/8âsf…?ßT0nmå-**`ž­j¼?ׇnüsZ¦Ëå¯ëªÝ.Ç‘+ÉB ’‰_¨Úñ…ÜRX¶D­(3=Wº°§êÁU¿çîûb·ßº«#Úp†iý7.’«rQ‘K¤¢ÐÜÛ`®!]Õnöý¦©'ß +ˆIòtEÂÝ 2su‡µ«=LFÜ7úµƒ 6¢a9ñt¡ë=vŒö ãýiN +4ˈáCµ^ÔñÔWCÀ¿@ i‡‰çí Ùhcä3O¨GÐ'÷ºÃÆ33¤§’iR­áÑmBEV¡,þ¹Å ÷ýHeÎǫ̀W6žûú®L^î % + +endstream endobj 501 0 obj<> endobj 502 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 503 0 obj<>stream +H‰„“ÏjÛ@ÆïzŠ=î4žÙ•v¥ m’–ô⥓ƒ#”bGB’I¡ô5ú¼Ý?Š¬Ø ÁÐì7ßïÛñâÛŠØcŸ|±ÉÂZɈهÄ@Y0t¿Pd%hÉ”©™Ý%ÈDÌ™­’4”îÔK²æŸW××ì¦k†¦j¶ì/©œ¯n3$ „ÈV"-AòºêÝ}Ý1é{÷râÎ~OÈ Ê0©I"T@—9rî×üöÇÅjØ B;{¡x]¼M€$B6WˆÔYðàªBEþuëÐEšƒá ȸOÀ!ù.ò™#C€zîî@ÆF®ñZN¢Ñ`˜.pŽ¥G¬ZäPò¾mÂóY8ÇÄ{¡@ó:¾œER'Oõ ¹;N!c +f/#')¯ùšÔ¾?¯ºíÃ2xCßìò7¾™Ÿ¿ôËØ‚¾üm»}½øºÙöõŸ“32/ŽÎЇg¦9UWuË£pB‹D¥}KÈ0Æ—¨(+ÍD…eè¹úµÙµÛúìUíYPùn§îÜÆ–qؘ†ûn…÷t1vûéð¦ï|º—wÉfÛñONÅ=¸ô{ y_uOíðÔ<—~šÉ,±+›ü`¢ò~ + +endstream endobj 504 0 obj<> endobj 505 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 506 0 obj<>stream +H‰„“Ën›@†÷<ÅYÎTbïÊv(›z·ï¸$RÏ0/궯EmŠ)~™{¼ÚtË +Ú‹;·Ê¬^(ìîÁÃ#MÍ÷~”Tùk¥%ì&$uÙËzutâçNÿ›í"ù + +endstream endobj 507 0 obj<> endobj 508 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 509 0 obj<>stream +H‰”SËNã0Ýç+îÒ^Äø‘Ø B•@ˆ‘‰ØT,2©i‹š¤JB;Òh~c¾w|íÒB‘F]ä´½÷¼ìœ\æCô­ŒNÊR‚€ò)2,Ï€»Iδe˜ÔP6‡yÄç<…²ŽbÝÖ6š’óââæîúnìên€Æ†¥¤¸<áB3Á94Ι$v=Úæ‡íAâŒ"HGËï‘p„Ò‹”š*e*A&ˆç(K„R´|FûI°/Ԟ£@ç)Ë$ÄrÿpñP­6ö¶›Y@š÷ÙE’}¶›xuDYH>]÷]Bâ£Ó8s™@0ùB9¹ëà +?¤]–ÝQ|¨C3:㯋èAãö”Ü[š²œ ëÎ?[ê¼ +2PÅ4±áËièØÑ ø93Âk,7Æ…¹ …úÐÎÙv˜üBðû¬îWOo”ã¦PÂà&qÔýä(AÉÆ4dŒÒ"ÉÍ^šç~æêgÕ¬Wöô•íÿ ÃùËØ5Õ¸¬?s$¹ú§#åyC*ô{‰ýj2Ôýr=.»vWæW1Äþõ*\’¶#l¶»¡™;"wýÇ…… :†-/*§!ÉuçV0ÚÔ‹ªõ?Ì—íœÆÛ RMªÞÂmXl_ªìàìmÞé»dGWþ +0>½óC + +endstream endobj 510 0 obj<> endobj 511 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 512 0 obj<>stream +H‰”TÍnÜ6¾ë)æH¡Ã‰”‚À@k…‹0²j.FZ…¶Uk©…¤b[}‰ú¼rvµò:Pè R¢æû™oôæç„Ç)ù©NÞÔµ õCbyU‚À+.òŠÚre Þ%Á…Ôm’Å%~uHîÙ›ëÛ[¸‡yh‡þ…4³¼`›;)r! —BÀ&Í*®˜ÛÏn·u#¨pF³P.ý\ÿ’H,¨"8­ +›‡›.¸ÎÎŽÀm–I§õï~Nô¥^JÄ 0UÁKµˆìïÙ¯¾I5/ÙjvðDâ¥R"x¹*@¢óH!¬J’¿GåiVp‹Å$ÏY0Aò4+Q  +7Å>“È †Ò˜p[Q<˃£¸co^ùc¸SŠµ6CÚ>º´à›öC¼ûyK6¡ZÃmÞ’^,/ÕÜJlt&¹°…ÝZ©CM²k8øw‡éêï®™ÿÏ»vì®"M¾“ZÚðÃíxuÁŸŽTÖ„#Q&)ÌÎÀ2¯ì,ªxæýŸÍnß»·§jÿ.lÜØ5ý·¨(¡¿KEG*‘‰&[o‚­†MíØíçnðG_óË€¨œ²q~†Ã“ó0?aÍôHæ8˜œŸ†žšøn‚­£Cúaš_u© Ði¦øør´ úHBå<7Vb:N¤ÍBB‡Á?R.Fh¶½ƒÔ0„ø÷A Œ®OqÔY‡ V OüÅ¡Fn£k(Mi¦0Úƒ§< +´9îžc´n(n‡Ó‰ftG‹²½µ³¯“¾ô6ž+ sÙVêæËfj^Tª\ ×g÷-)Çß‘,«0…u4;¶1æ¢ {FËçƒCÛàËÝ54þKl mÉ­1Å +äüT|XÜ“¬‡Oî%¹V°.XS2ÿC¨¾îvyêv¹tûr03¥¸.*C«uqa +Yøis}ñ»à¶2ù‘ñ«<¢#ÚT9*ŽÄ(n)r”ª“:’²„/$åö.ºÓÍ_'@ܱY,¹ÈmµrKÒrøÍÏ]íÐèIÛø5Ã`×|qÐ<Ⳝ5±M9CL8 þëŒPçdÅÀz¢¤:k6¤y?LS·íQ z&¯ M)˜ŸÇ¡?Ž!=Àîðcù‚9ܫܾ¯“ÿ¾T + +endstream endobj 513 0 obj<> endobj 514 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 515 0 obj<>stream +H‰”T]kÛ0}÷¯¸òÀª>lÉ.%°5at£Ô¢/¡™QÄ¶Û ÆþÆ~ït¥ÄM“R˜ý +sï¹çéúêkÉaÕG_LteŒfiZäÀÜ냴 J€ÔT(0ÛˆÁ*b”1–©¢Ä‡®jÍÉçòöîî»vh«v!N4ÍHyÏYʸ¢œ1(㤠‚ØÝ`·?ls$A¸øÉ|‹¸¾yˆ2â"3*Sì³ Í56gØ–p™Åæ'ÒO}.GªÈh.ÀsæÈcy;{±ÍñV7ç®çE+|gDAõ|×ʼn#+H3Z¡EA 8uúrªÉSwÆŒ+…Ëk ö* ¢grá‹¢TÎŽ…HMaõœ<Ø8s<ú]ë×&vÄ9écéHÙ°¹f;xÎ>£š»N8eZç`¦›ä›nªn³œxj s¹äsÉ;ŸLÃ×ߋͳýs‘+²|Ì}°«ºlçó›ïÏx%. O›˜zk}‘烻Ó|óÉeU]ÕMÎìó@‚I…@Þå`pòª›§RºÃ1“Ù¯Åv·±×G´ÿsë èø‡ ™Ž¬Ž.¤8 ãwÔ +T.ù{ºx¡?ÔUøÞ^– WdŠWD‘¾êêÝP·Íá>\3Ä8êB„K_¢¼ýÚ60¬'zÛø¨é‡7n4„û@gý¶ +Kýb{°Þ¢&ä õ²®H¥‡e¾mÝ8¹ÒamÁ9JáÀñô¬Îçef¢ Õ#B + +endstream endobj 516 0 obj<> endobj 517 0 obj<>/ProcSet[/PDF/Text]/ExtGState<>>> endobj 518 0 obj<>stream +H‰ŒTÑn›0}ç+î£= ÇÆ€¡ª*mm·uOQ@{‰ú@‰K™D@›IÓ~cß»k;!4ë¦%R| ¾çžs|oŸ2Õà}ȽEž ôKàøµA˜²8©XCÞx*3Îyyéù6Ĭ½·&ï³ë»;XöÝØ•Ý~õ‹H¶<ä"f‚sȨŸ²€èݨ›ÝC`ÎHbàè}þÅØâ.ŠTh1š:+®LqnÊ!cš3ôCG_È ÂFN@œF, Ž"1kr£iDª†˜ f_PŽ”65UL‘¶rÄ^›"BÉ¢dꌈ--69²Þ3¨#ž±D°àÞÉ<ã(Ò‰9Ç“>8¨;\ÎÅLAœð™8GcMV(†¥dØuvm)’d ’ŨÓn.œ8„Üás¦Þ´/W +]ºqÒ„4˜èWU +]l궺ÜW?VE³Ãxñ¹Ûšg‹•.»Ý/®»f·Õ£þyYöÛÇ++‚T!…2¨Ä¤/u_êv\v{Ý[¸¯Åöùœ J¦œõ¶h0éú¹ïqùWÚ¼T^7z¥›¢nÔ3¤Ún†yZþ—}Ù_¹nñ.cƒg/ÇÝ‹² [BMvñÔž¹ý^.Žh“Éÿã/ÀÁÜ7Ì­ü˜n˜4œ¹øV¦<–³#¯±¯WgRÿf†Q+OÓ„½5”}½ë®=t×Ü£TMIbú "7.™aºÒ-@cÒê=ôÆ »AtÁ@ýÛŠ—E>líC ]£®„…J +,µ&OÐVdâdc=$hЛn£dZC6ýh¹‡¤r' +»9V2И±Ñ#V˜Så‹”%iÌïþ|losï· >Ux + +endstream endobj 519 0 obj<> endobj 520 0 obj<>stream +H‰¼VyTG¯î9À à…­¨ z8G‰Ëdf”1œ3j6®ô t2—Ý= Ál˜qº¸¨QŸQð@%&Q_t5ƈ·¨5êÆcyÞÇz$òÄ$[ «ˆ>_ö]»^¿îúêW_ýª¾«@òu&Ãéµ­HrÅÚ,S¬ª§f:Éò¬vʵÓ5 ýJk¶–ðD}=u#¼·ÐUdßøZ¢ +€’3E¶²Â;ekPóL)¦©‚=íQSøÖéK,F¿r<€Ai¨?¨ØÎO96L@¿6dF›ÓJá–ŸN°e!ŸØ©)®SýÑüãh=@8(;=îçò@š•h½¯\NŽGû@O³ Ÿp±´kG]ubv!}‘ ëhÂ(æ¡oèx3¡WQ%í1¬2­òaO̯ó*¦ cý¥¾#¸D`¾T6\Š‰1oŽ‰ëŒ0*Ÿ‘ô]QѤt´,`p ÞQBƒDw}â üQÝîÎ>!Y=p +ŸY[ç :½x*ôb xhuÒ÷5C/¦lïý¹÷QQìù”'†#:)‡RQ®Ø'Ø×ààiÖAódT"Yp@Í2&¦È¡$ k IaÀ/xØ“Bë´ÛiÖÊP6Âä,äK)–&²ÝÃÓ,Gh50"¬§:ÆC5ìxÞë‰:$LRÅ%¨Ôo¿ + +žÚg÷I€È3@Ï ÜãM'ÚˆÍ7¯ºÏ¾Éœ»WW¥;r÷ŸÏœ8} ¢röÿ»¡Ÿ4¿×ÕX]ÂÕØkYr+BâþW0°…dœ\4×g'Ãy¶Z2Ɉãúßø`î×ë7ɹéðy×öbÉe¸ÃÀÉEW‹j¾±Ë7}Ú¶æØaª}×ï5+W+©íýaÝ•±Y“þÙÒg+~¬A¬¹Ù+¶)õpÍeytõÇ–…õúJcã$'SëÏ÷ë}ê‹[EöS•íYßM +2\ø²UšIûë#eû«­‰’ô$ÕŒnÙ¼d|d¾ñÈÃMÇJd®é‘Ô¶ª#»Ï5um”^²4þ$—]McV5&õØgRJ¢,ùzVZ¸J2D²á3eü´ì̱caµ¥~®ø(âúð1“ß6õ¾ÐSŸsäªþÎåeïn;«šW[ú3¼·í£ãé:ƒúú•ÍŠ9»«ã.Ϙ=?ñ„xžüͶÁ7¯Èýê©^4}‰¢#ºVzª ç/0l¿Áb(“ú¢Œ+‘øˆ|a– $ÄiP° _~eVÙ¦Ç|0 &À¸'8 }½˜ç]܈ØØß±oG*ë,ö…R û Çôb±—îz.h„8Ÿz}éýùÓ®ð÷ïïWÞ//.›þ¨ñ-Ûš}üétSÂáyÓähoE7(—OÞ¿‹ˆ|í ¼}â¯Ügӛƴ»½þ`ä­kdåZ+Ó +. íîŸ4~Ž¼u>“Éý²×ÐøuáTÙ'ÿ˜¢0|–yôŒækWÕׇ7s™C$<ôÊjÑëÛçAî{sÚÖ¶_·Ë C¼E¿ó|˜¿š‚â3µ§JÂxòiÁðbÿï$þg= +¦têIÐ1E 4è­â8"Žˆ&2+ëä….y”) xÆé JT¤ì!Ì—ã¹&2Ê…Žo°lÅ£tÀ;d è< +#]`w: +ÈØ·#‰„(ºÔkG'Û¡öɸßKÆ¡gé år&JðUB¹·? +Ùõ:ßÔ¿Z~|íÿ´è‰êQ‰©ó>Ÿu®eBNƒ}o?ÜÑ01ÈñxDµ7FNË™˜¹ÇD+½>È |ò:N…Ÿ 2IôÌz˜Ø3 ÕøŠ ò[k)^t?¹Ù1òžŸw¹õx±÷ūוeq ÂÅ°ëâ*Â}*:v‘‰¥PÈè +'N|#¦zÅ‘HÜ¿.ªâ¿ÊHÚlÓr¯h«Ç+Úl.f8ÂJ³Vgäñ^¦oCÏô\´Q£x­ùP@”N/ lǼÉdÆõ8Yˆ0‹¼¤©EŽŠŽä›1ëb£:“‰kÕ + ×FšÃRZGåˆ;JgÔF`·EKƒ‘É zûHü¯á¢5ˆQkŽÔ¹h³1Ú`Ò:‰á##9½AÐ9©s2h z“îu3‚ç5‘È¢ç~l3O XjeäÂ5QšQ:Ó@ΤÓÉzb¨8e„ë*Ò„–ÖÚ° ¤¢ËlÖçc11yVËd.Õ–ê+k²e²©)4é˜ ˜@rËläw÷Ìø©nFR<ÆAª-K°p“l85Ù)$~?iRFZSZmi)Îœ‘Ïl*ûH‘ê@ÀkÊ7¨³‚Oš·ŒOµ%Ú&&[ñØÝQJÖNØñÄæ†MþìÇN+ìdóŸP¸ÿ±2´­˜òÜU‰ÿHÚsBÚsŽ´,Sr*Î?Xõj?nˆZ5 $$Hùl5àþp9à^^F~±¬uséØÆ{dݳ}ªÄ[”Âÿwî +œRÑfkñi>¯âï%GTÍŠå>9[¹›~]ü*fôšF+k“ÏdzFô{|Qq¶¡`ÿ£zUí¦÷S—ŽmÙ–)wY3;Ȳ3vÌŸ¢T«5¼áè&Ç.—Î~ ?ª ¨l[¸â­Ú×Nù9Tq8Òà=:ãÓ;ö†êÃWžti·ïêˆU=|Í*åF¬|Tç³~‚ÊÎúã¦×›bÈfüöî—œ‚;º¸6…Êd°6û3eקVjϨÚîä,®Z{TÏíóÊî­Œ¬Ê“u¯5Õ²Ž(­^=õÁf±«B׆¼£*R9zmÏ,´`ƒ Hƒd°à“=¾gáۊ㩾®wVO‡ç›ŸÒr®sz>=-Ã’>gšeÐsÇ:6;ë}¯>nEó_)Íxíú“üåZö׫ “ŠË“?óÙÕã÷ãëûûÚ6$T=ùbÉ›Æâ++Ã7-:؃õXe95á6/Ó¯3&êf·lN2ÕGÿÍ?‹š‚Üvÿ|瘾ä^xÃ[î®êô[7ë¯ý<–Ò¥ÓÇ{žPÇZ +çD,¹1,vÚæW¼×ÙiÖ%¾Õ(.*;ŠCj‡·³÷*³×¨º+}›œ«xêC!-ÓVoIŸeK›Òâý/x3¢i¢W+grŠ³ú¤LÃó3gÂ|t”#£Í–® +Vn¢ÐÚäŸF«ÕEc:r|È_ŸKpê ‹‹;}u€ÉòÉßç¢eœÒ»m°txÚiG0VžÎÈTœŸÁÁª `µR÷B¬”7†þš#ÖÖß³Œ/:æyÚ ä•§[ØØN½Kï›=èâ·Ë´f¬ÿÖù«/¾¨Y½Ñm¼¸ý¦Ì}ïªkš¸¢,g×o;ú”ò~Q{ñsÝ«/?jßé\åF2Ú7.idÙÇå>aÓm›>2)Óª.T’-©šˆ{cŒ£. ¸,gyqÙ¡ýÿšpû­Õñ'O¿·¬ øÊOufï…› k×çíèqpJ®eí.u‹°4Y½~VÂΕ_ø¹•ö>fýûÅGŸh·”ö}´_ØôÐ'Üþi¯ªÌîûóËÌ•1îjû¢»ãc}Þíµ•_ëèr|ÌŠWß TaÍ4|ß3ËÝë ÷¾•Ó{îºñeê’C¶ù/Ì_Ÿ³ÿp—_¯éÜ®Z lÉ€¬@„]ߦ7Vê)“ÑvL{Je”eÞ~/ œWÇ#Ê–jƒáÀI ìuq>€K MD’$Ç,{Û +ðÁ·“~R]s»ì vÌ‹’DÏ!±ÐÜš>~W:Ÿ‰jzÃd8)ð!|ŒcAä”àÊpü 0H,„Bæ÷Y0K÷q´l€»C IÁ€HæÁB"WœÆjKC™öèO”L)±Ã”"À*èÕ(±¿$ÇþêKC‘K€Ì›®’Rz@¾bK °ž„Òv;œ„Û¤' bŽ´D*”>7ø…ñm,—TR +r™a"Ö¡¹ˆ Ö@GÃèi1bŠE às8AX`'‚'‘úȇð%TC-Ôc”v"þ$‹œ&gdÐX!VH¼” Ù@‡õ,²pÖ—ô!ÇÄ1Û˜sWÄ‹’Ê`&̆LX¹P +çà8O*§53ÛÀ Кyˆ©ŽCq%Ád(N’2:“e+€¼Ð‚Në…hÓbØðüeÞG›2¤+ f2žÌ#ï’eä#RLÊÈvr ¦–a˜l¶’½%ÖHr©@*Áu} Öà~虈DVÁMÔ¯? $ÃÈ÷4€2„íØ(ŠAÒ(iôtzA_¤ ƒpÔ9 +bõÈCP‰¼Up +®ÁC´ƒ—=O´Gz#1‘ D±Ü%Tþ ¡Sénz† `ªØv{ã^ÑKÜ-Þ%©TÚ)•K'þUã:ZôÀ˜3œÛ‡ë|Wá'ø7®áBº#Ö2õÍGùu¤ÃɕΧeTb˜\æ8Û•ÍõbŠ˜/î‘‚¥(Œ-3«+ãw(F“Æ¡l;ZslEÏìÁè©;Ä›ø%áÉXK&’$b#ÓÈt’Iæ¢UKÈ^rˆÔóäe© õB;ÐIÔNóè^ZAkèUËLg2™Ï¿å |ÿ.Ú—á­Ä.X#º°†—Å 0Ňxo‹+ÅJ©²XY¢T+a¬fŸÒ‡õ|S¹¦ŒªBÍS©Íêõ‡âètvuœsüÚñqŠ3e}üŒ¸w‚àQÞgÕje §F¡(‹ ¢Šw‰q~UòYH+T•Fá•$ø4¢|+MO=šâNq‹éäL Kâˆð(ëÔb%“¾‚ýF¢U<'Ât‚ߦqQHÛ¡Œˆãâqå¨zP­æ+´2Idñ§TC5\ ß]¤íðGy]ýäèHSî8¶Š,kŸzÓ!” 8W°P~í<Æb¬U)¾A:úNÃÿ*ìÀùüŽÊÕëÊ€ø‚øp[èE>‹5ž¦-â4~)Ç~ü27ò·•E´›·ÃËi“8DsÄ61ñ¼†>ág9;w¾™+ºHU²D]!xý}Îe¼qº•úÙ Ržà3ô®8@Ÿçˆò³;³&æ ¾3ÆQ¥ž¢<®žWÏ œÎš qzx!ßñ;Ó­#jÊÉ!Jÿá|„¦‰ÛüŒØB=ü-å¯üQC_¤ˆò¤ðá»·Õe ,v +§‰/ey9ª…êRxü&U#Ÿ@néVÿèxV¶•‹Ê¿¬å¾û¸cêÝk´Ö©ÇéÖ½TOWyoä&Õ ªe­¥AñºzÍÊçLvÓvØÝ·¸ŠçZo·2¸ ¾1åGGÔ~u¯úUõä¦qœšÏÑAz™~lò}ä­aÇG`Í 8{z#ÒbZ†ÕUS-N¥U 5ÒZœ§aœ’]¨-·ãäýý„¢ÈP °ÇFÌë¢MÀ?‰ õ4íÆþßG8Ó ú@üXSÜ¢OüJì=t•®*ï(^^K—ÔçÕ=ÔLs©‰s!ùaxi6æ X!í!ráô_Š]Š¸·nY¿·~8ñø€îSjéVŠÈ[Óâ]Y½¢ª²byùÃË–.Y¼há‚2OiÉü‡æ=X\4WŸãÖf®ðWÁ¬™ù3ò¦çæLsfOÍÊÌHŸ’–šâPÁTê×aÍ,›j±^_ï‘}½ ˆ¶ûaS*0yŒ©…íaÚä‘^Œìú¯‘ÞØHor$;µ*ªò”j~]3Gêtmˆ[›‚h½Niæ˜Ý~Ônï·ÛYh»Ý˜ ùgv×i&‡5¿ØÑmøÃu`ÍH÷é¾Hº§”¢éhf eæëÛ¢œ_ÍvCäû+¢‚Ò² ”Y ×ùÍYzÔÀTŠümfcSÐ_çr»CžR“}z»Iz­™]b!Ÿ-ÆLñ™©¶­G®†úµhéc`ÈIíá’ÌN½³mCÐTÚBRÆ´È­3ówÞ˜y¯ æ9¾à¾û©.ÅðÏìÑd×0öiæñ¦àýT·ü†B๢(6=#64k&ö†‚&ï…HM®D®*¶¾ˆî—˜ð&Íœ¢×êÝƦ0\S`˜´ú)÷É‚ï°u +üšÑÔÝæJ—j«{ :ŒÕO½1Ë«ÍšLñ”FÓb†NÍŽ72³îoD’4»e—­†ÕI˲ÔH_…€0µ šu¬©\~"ådt”cžc–Ù ô˜S|aÃY!ñr¾é(rêšq›úØß&cÚ☔"çm’M'ÉP=Ñ6KJÌùóeˆ¤úàSèXm÷—yJw ‰}›SÃÌG°m[¨bÌïvK÷y©³·)ëkÔî:IÞ%!S„%åL‚’·FRz”äô°ŽH~ÅQž™Vœüe;gäú»+Lžñ?È‘½¡Yohj j~#·mCˤ^Œ^ž¤Å[f®/¨¸D¼%\ŠMEPnH–`¦©á—buçPj¢ÒÆ°0áúØ7”îvÿŸ“†¬ÈYöß½iq5ÍŠ’ÉýÊIýIêe +V‹ECK«a¤O¢pF@×FØh²zÛuÍ©Ã(<Šmþp£CÖ©~—aÝ\hTÕ¹¯)êå¾æÖà°“Hëk žDIã ׆¢sA k8sm¬HbeO“=j`DúITŒ’äöõÚTÕFØýŽ!&—–À1u ‰Îiãðx¤ïeÞBõ0b]².¨ãv4L*Ô™7åùå[œŽjöà.$cý‰ös rîjèQPé—¨óÎqnG79—G¸™+B‡}®àN±¹æÜZŽá&³3Q)öÒL*£näÞnP†qßh&'!#vÐe±‚þÌUàL¨—ö#s"÷õ‚×.dÅŸÒ›ôsh“‡Jæh½ ¾‡Üµž*‘[#¸ÿñK¨ØaÌ4û´%5ƒÓ½wóbï©ø+¹%Þõñ÷2ý T@/ð—l­m³ ¦^ 99Ðu+8µÓ!@+™T‚›À«ôjýbÜ;œ¸ ü…oaÏ#£Ÿ†üè±uêäÐÜ8ß@2ÁEàsšwÀò©´Y´à.˜‹iv¾^òÎ'!ëÅ^OùžBe2P!ˆ£|Š+ù¬·2‡a™Ë4&ª¬ ú¸¿yxo*ïÀ]¤#îqé—]à)GïÁ:%ì¶FÅ9ÈÜoÃ+èO@z¯ ½àœ€2ØMB7¬Ä< ’Ï ðˆ„fXQ´°aV(oo± ÷Õ÷éik÷/Ö+xWäu`„ŽÐ~T‰²ž.…òƒÄƒª¼Ðm?ŸÕþìG<‘hàÍŽÃkðw1ªOšÔÐV)°¾cœ ½§P®õ‰`.£Ó  îáz ±!m”°\ÂJ1KíJÂfÄîfÔ~£ˆàûañBDLÚ³@°'Åm³çΤ-P„x—>½bËÏAÄ5Ò6ìJ‰O舯*êƒö™—A.‘†ø8Íiäµî Êj¬Oi¾u ÷¹S#xÙÞ¥!XCîÑáÛNÄÍ9èÐ …¨&Gi‡×úqÿYÇ*Pu¢ŽþëUÛÔuÅÏy7‰À#‹ÒMlÇ'8,,QƒíÄ&#n›ìâ',‚-š@8”i +cZBè¤ÚDÀÊV!Ö(Ž“µN +M¦µ‚Ѻ?Ê´Q ¤®Ó´? e«úDz߽~ùàc“æãß9÷ž{Þ¹ç{î}ïi™¨/Þ+ëÙد îmXC?ur Z/ª’»AcªŽEvÜg6ÞoËàSF O‹z +â}z•€À"%£èFe*ŽÞdS@rí¶¡ºóïqäg sÑ[ :H•xûmÇìíê$yñÀ}>EëÉ +Àûëx³.¦Ã¸ê®–çÉEœ£T9u +vW´cæ“ØáߢÝڮǗ뭘ßä“h´bm%ªú¤æ=øæúµ} +ßLgé À·ØÏh7G±V£4‰SãößãøŽ™DÖ¿¦?ÓkôÞû?ÀgÁèýëû7Ø¿¤êßSÈ—ÄÇŠ¦=·á¤õ{Dù”güñ¬È(4ohµ|ï,Å|‰/ᛊ?åÀ§|¸Â×ùüœl_r77q5¾ÌLì —aýW­žÏwXggceg÷ß oúšà×øø2ü>o‚n€[9‚Ú[¢LæQš²ÌBòw™—{Kþ2@òw'åtøV§°@ˆDžÓIý >ÌŸ òs|öX猜nÿ~ˆ}@=áˆr±Ë3è}dè*’/ðW*NuX mÜ_æÏÜë´Î¸×ä)Þ(¡r ‘–ÌÍŒ¼ÿ7ßÈ!yÖwŽœÎ-ª÷š’£ØïrÜŒ/F)ãWú¡ªeÿb•?ܺ—ó´_õwa>O?§œ$€¶«º z¹ŽÚÐQg‘‰gÉJ©X‡+ O°‡1*g þ;ßå»Øßíükþ’?ã¥ÚNd-†}㥥|šÏøÿ/! §0×ðÞð}ÈßãDø!]@Œ.Ôò ¨Àlº…j¿ºD¯âüø ? zt_å³ÙžÉ‚¬™çUÄu  Ý¡?ñWX¯ˆÔ3 +ç&bx»ö]~Ÿ'q¾‡Êc'vF>ï`Ÿø!]Vןæ‹üKþ­ÚãNE%Š¦fè]d`n–j` Ìh¸·x]CŘjT»T£ZÓ?Å»P ªD5€¨Õmp†×~èû¡ï‡¾_éû‰•+[©áÊh Ä3ó Þ [©.‚†Ü&¶Æ+,ÞˆØ×CŠŸMà}Š7+Þ x·íVí=ª½GµÝªí6Ú’—ÏáÅ3%›Äf¬µElõJ6 +?v¥E4 /åÓbƒ’O‰:%Ÿ„>2»Èz±^õ7 ïƒü6úRÖ‰õqŸe…w/úÍÛ™zbð!&’$5}Àà†Ò4ƒwW¡,Yø@µ ¯ðâ +|x0â!!< 7hX‡‘µ°] î.u.X¹0“ ¹rÁ³ ËãÂò¸È$\àVQE+ÐD€Tø)Ãueˆ« 3”‰åx«°›v{Å"¬†´h=TY¨õÄ -oº6J@Ø ÒFã©9™Þ\ØIÛr hºÓÀ`&wrÄ3OsknÑ 5ˆTwéˆËU¡dåʤ|¼ )ç/ªÈôî¥HS)B.EÈ¥¸ÕéžÐP:š®7™p’á@2¸A®w(«4ew˜ŠÈÿ÷Ú¤ª«-@ù/R[M z%¸¦¶%ÐÞgu…oú€ c¬Hs‘*Î"ø*B´åànÕÊ·ˆ¢¸–ž™@~yu¦·yo0¨õ"›½È[¯¬MnârŒ¸ ‹>`Hc RT*Ù@VVPbõŽƒú@Ç@½ £ ¬Fîs©5Wí©ê®ê«:]5T5Qez[kE´ˆ'ƒòòð€ÉÉ6/òfi)&¿V|Pñ}Š{̳(¬ÿ%¬_믄õ—Âz0¬?Öׇõò°žàVÏcNýºS?îÔ·:õ•N½Ê©W:õR§îÍæo#ÞQ¼Fñ +Å‹/àmqÒ/ðv²™QñìµýÈò¹-‘ÂqËa[ ñ|²·=)ÖHå›–¶]–²¤fiRÛ.¦Àmá7ÈÄNO™éw¦f“Çô„雦妓Ãd7YL¹æs–yy¾9Ãl6§™SÌš™Ì¹‰©›<°™rÓ²¤HK‘‚Í’‡ÚÌÉf[ó¿üx _ž×†k»ümvÄîo"±žý»óc‡Z­Ö1ªåkrÈK#­;wKÙÒ–àkö6_¬Öî³×w=8ë’Ãõvß0uù›‚Ã]ž6_¼ÞSï··øB#u-Ëï™î…é醗µ<ÄY‹t¶LÎU7øáA9\'ç”s ʹêg eAm¯¡|ÿw}øG£FãÿÑh´cGtGTJõvtr™(JÑÂxç«ç›§±<›{€£êŒÑh¨ƒÔšF;IzëlÖùL«ž9:·(zÿOV¾à.ÚÉ°’†FÙDÿÍvÕǶq–ñ÷}ïÃwöå|w>ûüÚŽ?ωóÕ5i’a×7J¤ ¨”u®«I€*i©:±Â@%Lí4iˆm mH¨ƒt]6 Ô®M½ôƒ"M­¶2Mˆ"QÁ†Bi'¼Ñ’eÙÛ<ï%aLìä{îçWw÷þÞçyŸßókïá5ˆ‘\} ûÐÕŠ# ¨´é¸è©cå$Œ +<òŠ€YŽ#ÙÃÆf1¢ÒÖÃÅ-ÚBi¼YÚ¢-–Ƶ&4¥f‰ëRzJÏ‚CËIî²# Û(É_`óyÉ1î·üPÙutßqU¨“ÃŽ{eÐ{Ež#ÓÈGÎ;JRÿþ{ý¯ú»º Ïá"äü‹¾‚êdúÔ€´”õ,y:¬›x…‹Úbm¡¡5k…0)i%`·n§8Q̤sù̵ILRšñn†#I£É%9|måŠ0©µ–¹_óÿ@Q4áä»”n–ð!Q4+n ã²8¢Úi\»4‡ÆôóMm¼¹PÒÚ<Щ”tct3œjxÐ0†7¬¿#4=" š†Øås$Gj¥çòŠjPÏž]»öx¨¡*Ùg|k?&øK_X÷*—Zõ飭úEÅ«S_ßÝEïm-“ƒ«l»d"G(¡ž1– Ñ +i‚l½^ |ýPÈŠtNÏáñ5¾‹Œï<vé~Œ­Iˆ‡ùnhpxƒ14Hò€€½2Bäà'²½¹¿Õn½V(°½ˆ7OÅ›/Û°/Ýšm¹…ÎÃa`Û‹‡Ì˜–qbïE–laŒ>ø¡É%¢ û 6×mß×ñµÀ¤ýªõoc!ú/[êéNs¨à5UÉL=Ýy¿W೨·×ÎÚf6kÛ;›±cQ3‹F#ÑXÄf `È’dºizoÖÎÄTˆ ]TÉF†ÜË£lZeC÷UIB{<š4Î!«uü”㗜踑ôÀ½üŒê¸ìø¶öHö½r6\Çöj¸Ýh7Kª5"a­Qk0w²Ke¾2: +¯Z£ìäî+ª?Ð^~Xí ¥ÿ<äÞ»~½ †Y4jh_ ëƒîîѳ+›Hôk;(¿©áln5Lx{8`„±ðja=ÒzwF£z(83 TŸi½Cõ°ßà~‚‰H$Ñzk‡HuHÚqÝê0hçÔè°®ß+ý:Y›òãËü8l¢ª³ŽV =Î1­Ê“üUY–bWQU²ªº®jš„«tu@Á +MJSá"êÇù N×móHs“¥9¯±ÜmÀRuÓ]ŠžJ» I­-:å.9CgËj=Dâø~w‰÷3L­îØ´Xø ·v1̸Ï Aí"Ž‚.@„ò_}žÉÙ¼v õ³©ƒ©!>°|Œ¤ªÒ¹~sWô:>sü,ÈáB­ J8úÙröÕj¯î ¼aeÏpo6wêQzÉ/}¦ªSÁþðž,õ+AáYK¥~ÄßaÝ×QˆØ /Ä›™kßB\{áD¯Ôu— ¸Ð^@ùöû(g°ýþlL•UI%sí%¤µoèT{ÙÝí[N¦Kˆ© 5mLJñ˜úp^èHgÔTÙè) † tDÊ ì¯Î®³Ë*xz‹ L=‡VÜ«-‚‡+°=n"Œê£+Ú4ö]ç^Ò§åÂÔ¢!¤&ÄX´3&¢¼˜Ïr]¹î/ú¯"+’âQ‘Ë¥uÛAÉ@ÄÁE1ë ^¾ßÁÊÁQ +&§ô8¨€q˦[(»á(N¡‘Õüï5Þ êñ­˜qݪèÌ„âq£’®·o;€¼ÓÁD50ÔÆR+fòf¨΄û¸¸á«ôzÁ„ê4iŠ½äŸŽÀoZ öT¢B¼š¾ÑbBÈhïÀAÍÕ|~CCš[l¬ü<ƒ0’ÏeÒ$4á¿Z‡1Ä]ŸúúSw?Ô×ù¿èó?ê‹Z m릅ÑÏþøÈX1\Ýüèòç×[7þýO ¥ž(oßÿ:ÖN?QÚ~ð×Êši½yá¥.—ÓÔÆ© ,Ûæ¡­¸Î/AU:~¢õö’ã×E$ÉQ':aLDyÙ?Gf‚æÈš¢øµó²D؈#‚ÏK4ÌÄ-à1¢æ¹‚t²û4dI¡ÄÜ¢V=)Ë^%RÇœtç>ƒ«’Æ¥·ôrö®²õ" Fãp¿L¢üøZVj‹ ,)Á0ékTÀ¿óZ¯º­¸²3–°p(›ÌSa2ºƒ­„é`óš]SS+þ†íÃûŠ;†SC+J*lo$Cƒ°]E¸ZÙVõK=ÈÓ$‡˜ð/ÿ£[û¶%~õàžç¨(+šn}ã?dW}lç~ß»ó·Ï>û|ö9±Ÿ}¶ã¯ÄŽcHÒœ¸‰­Ú`P¦R¡á#[Æ€ ¡TL¨€X)££ªÈ€‰BëȪ&ŒdS»‰–¢®åC´°ð±– IƒªÝ–d¿÷œÆòÇ{ï½¹WJ~Ïó{~Ïsjñ¾›Ñyk‡?.ΖHÏ®¿}埪líz¾I4˜=\æЂ«Û·¯þëÂÕ·Gn2P(À÷¶ÖÁ ÕÊÕÔä òwäi‘©u«‘~ƒ´¥n'Ó‘ßUw8ßUwŠ/zÎóç]<Ÿð×<÷øyFªä^Ÿ+À9 +€ 6q£Ý’¬tÐÕð‡ˆHö!o XMyúÞ`Й*àí½Q%~c{ŸSч•I̪fA¡}¾zº¬¡ºø¨M'-ÞúœNÏÞ+â% @É0100“» µŸÁÿKЀ×AÐI"—剙(‰¦¯6/Gx£‹Ô†UÌëËù¨Š]ŒSEHÃeüÀ£®©­ÕµawÉ^DÇ­_®fà-!’óhoJc=R‰æ×üôa¡õn•ÝÃq®W»wüqñ‰¦@™×ûí¶Ž=ëçìHq‹Cœ³nOçûÍÔojûšwß™Ÿáœœho?¹jú+O“^ÂÛæ-x¥±Öeòp•Ê3ý?›½ fÓÒOà ýHBª,Ìó tþ +ŸÊzû„ßÖmœ¼HuÚlg… $-¡hp²4%U¡ð'išÑI6ûdƒáóÊï#màFv8s tڬڱζÄï¯@ö†V©HÂóT ôö†F°Â´úàÇáh›A¤­RÈP#Gœ ñ…Ü}ÍëhÑd¨ÑQ¯&ºçá¥Æ±§=›iÃRçc~bl3*D9‡#Œizè"¾øæ“IJ=©­ÃçȺ/5</\LÇþó>©ÝðÃ15 ©O‡$àù;„çP¹º®†,å&_È÷6ˆºtü»ñ…ññ_Åßõ~"~!½„ÄnBb6åÁ°ÑÅewE®ðKè *Btª1 šü +ØQ4ÂðßT“G1—)œŠÔ (NµöÁ—K"r_;ÉyÓÆɧƒù-š`)×ûUì5z&˜ -œL6µaǘTdõsÃ!è»,}7LÛª¼vùÇŸ[1SŠœƒßÝÓñû®›7Yˆ9Óˆ„0;†[**®÷û*™,¹^çöw¼Üý-NtSi¢C ŸN¨n¨Heðªµ*ä’kCd@ +D‹#_"4rSµå™'ŒS™éÆg˜¹F} +Ü õ Ž>CÚ3\+F.©f¢p[6²¸¹a£‹q£LÔ˜àøéü<~¿ŽßÊ¿ Ÿæûä«–«Î/XÞ‚uFCPõÚå`Dj þ@Z'­«l¯^•é N\±Þ4߶:çÁôpgwU·ßãåD6„dÖ±DÍ8SMU¥`ŠÄ ɄΣ·±rzäp_Z¡iSy_WÝŠKSL¬xC¯ —&2 &q†º€jŒed¥ºN†” )oö4®Ã›Æ-]Ó 2;†šÀöÃÌ$X”ÇÒQI¦"© ÄðœÝawÚi½•µ°”>Å$TäC|LPÔ ^."Wá0©K«X²WßXp„©(nˆi´ Äà5'Gt­M8š[*ž$ž ŠÆ;„+£Ü ‡àû4AÜ:ópË–?Ÿ=ò“3“¦NÉ쿼~vèv°Î¸òöp¿7zpåªÎý-‹ç6R|ûŠOíúzËKݽ¶uigKÈîuzÌ®á·îHžØûæ/6{z2tåÅ‘aú +t¥€6¾e¢ÉàÖƒt%(½ž¦Îš¬,»D@.A@˜ «Ç"XÍaj‰Åì°sf†³ZŠÐ‰˜:zÜcòºï=bŸfhÆgŠ&< ;­›H3AÎÔ¨øØÜÆy©Tˆë£úÍEk1yÁü^’_‡KTÖ•gÓã  +#×z2áªâÈ5_w++er–¨ Gî¡ÈÈç=±Dl3Y©ÃJ<®÷+¼®Zѳáþ‹ÊÅãn.ªÐ7Ê”)î§Ü”»€UK.¨p7RŠÉ[óXìŠ>h‚•HÑm¨„§53ély…C`ŒgPE>èP•2CFc´ÂŠT.À’6V«( c"LÁúÿI5á¦6ÔYâJŽÜí…4ÿÈÝ^ ä©f #èDxÓ‰°Ãd‡EíÌe"ˆð¹@Îr&³ÿ‰ßŸß “ǤŽ úÉÚ؆(ÜæÙÓüÒåŸíßÿÙòeó —wí¾Ôg<»æ@çÚç:=Ç6n<Ö½aC7õRîÈ¢W¯î\x¤6_ÿ½æm|°­yVÃß[_Ý»¬¹£cØ°òС«]äA=À‹ÊáYjÚ`d†$ªzC.Êú(Ép +› k ÔÔZC°Ô¸s©XJ NÌ>/{ËùuøŸ‰Uº~„³D%É­Ý øŽj Ni¸¥wõeßÉ^Ì2 Œ¬Œ¢6kÌRiJ@úƒ…–±ËqŬ#z¦š«AÐÌ’âf£EÐ,–êRͲb/Ë—Ý0(©3ÔQT;!]܃!0Z_5n¡¦”ò„CK¦£Â‹U…ÂŒÀÚ¬6Jï;Ãs.ŽÑë" p¤Ò‰EC‚L”ŠÇU ›Æ8Ú` sœ÷¡´¾z\»/Ô”$‚Õ†Ç5 öZ“Ž¢êÑpÕÜò#3åkcÑ x'O¢û¿Ñ»`ÎÁEýûWÿ®vj}´cþó[çÖ—‰«'–»Œk\ù}K—¿þúžhÏIÔŸÚ×üðËö ½üb÷­žµ³vUO q¢ÃcáqîNâãóÇ·ÿ¼WU“€óèL7#2_V5Ù{ÜcÒ;Oc7hƒÝ}‹÷¿”W}lçß;ÛÜÙNîÃÎ9çóùÎ>ß߇8qB Åkù,Ä0H¡™“Ò®Óˆ˜HëhY‚Œ-tí¦•–¤Á´F[Ù€*0¦²©´¡±iRûLª´ÏŒ´B]µa³÷½8_¨š¶Øw~ßçœ(Ïó{Ÿç÷û‰±¯\†qЌ۬IV×EQgÁÉãžÔЂ±%µ¤¼±sá|²Ü¹l¾ªc½K–nÀš¦ô u¸yÃ¥øÏ}¢é€a@ûôã5‡Öà¨~8߸üý\ ß~x±Ö÷‘Ǽú ße8AŒ£3‚þ¦ÈƒÞš' †$‹˜ƒÍ.}89‰Þ@¼Û<)²è6JMÏTôÙì.PNÖ» Â:£Ç…'&ï}BÒI‘ Ä*ø¯ È‰¡Z¹V¡E,Èâp)ù¶ïwÁƒä‚ÿ~~Výœ› ʨ'€2ÁYàlê™ j/ÁåœúP"å}(‘ÄüDªþ‰@X'ë'‹|ÿÞ7ÒïŸõTj>7xžŸN…@ÏÛžïdá[%«ƒC$-·;Uüšè£öJ§Ì—…Áè ]v>5 ˜¦…áøÙ âG%¡a¬átq«6d¸†–“ý¯eð£FÃh3 #cÊšiÓ¤òùÚ\Ú”iÂCnHúxAñ²ÈsÉ­V€r@9¡7¨d$E‰IrRŠFmÓŒKÑ°$EyŽ‹r NJÓü¨Þ0n1Y%Kd³´èØF4dDE"zn6\^ +›†Tbè"à #)ÒmiJò ™d¿ÕBœcðWàrÀÝ¿vó‘1¼VbÑw®—»ÃÝç<>ûBî‘ð€$5’ÜWý`ä‡XÔÞÅ÷¸ûSü  Oö¸á÷°]9óÒŸ•?ÂÑÚ›Bþ5 +Eþý›YÛ2Jé^_íÞ€¶˜X×!¼\ãä%×*Eº´”.)ÅS6öB†¥›aǺ_s Íg +súŒTàÈÈHýèÜ ÈîseÄ,СD¾³ÃUÄ d£ÜZB0‚ñ%ØS×Ï|®|v{õ8Þ_‡™ÁÞ‡~ùëµ ð'÷.ßöú±Úï7OÃ}鹓ƒ¹×6{CNth±¡ÎÞC÷„ÕC]¥½ËQöß¿åyÔóS°Ü*íuÂ0Š ^¡Iè‹<þ¢°#;Þ- 7_Œø;c-k…µý‘þÂPäË…C±Wrþ¶VF•’T£éÌ«Zœi$Ð.Z¼Þ8æ‰ëV'é!,ºÑ žHFt©d0­Jk®µØêi»Fç°~k€j—¿ˆ+?]}W`àb€õR`ÝùÀç×Om|)»Ò±hvc±*ßÿû%AˆÄš…º܆ êôº4›±"i×nà +—¹ëÊ óH–,Úy!?ÀuŒ„¸áíÛóòö¾’±"ƒìÅãe®‰¬M7vô¬8’?ô§Ñ›e†ä/J´YÚܳÍRœ ƒ+·¾tµö·Á&‹ä¾PѤÕã/>6þ HCóûuÔ{ϠޓѨ –ßñ |‹?:>Þ4¦Œ©GßNÍŒ™ÁÀb˜V31$Yo—èWÒ—Ä稈Œçm šѨ äEà}Á›^/ú†Ë2J\äx„²â4MÄ)"e0 d•!˜hÖŽÇ¡ŠÐ&€èüvAjÎEÎ5¨ Üö7`v’!K³`ãöB»jú›™&ÈÏÐÓúb=£{|!>̾„núSY¨6iY¨3V&y% êbbæúFä#ç÷GK/„Û¢¹IèvV^i·3äUî|gèÍloFþꡧ¾YëÆ‘WaëÐ劘Z‘:¾±öÛzSl]28´~Çž>~|¿øÁ†‡¶•í5¨¶"ÍоÑ~£é5‰Liåݳ‹úDs·–5ó`6Ž®¹D½‰¸fQâfŒíÜw긅8ŒË~o;FdÄaän©žƒNó45…šâòXíâ[ÕnáÈÚºA÷É?ð}¡t¡´¡Ô>* <á… ´XV¦R¼LÊR›Ã´(-D‹ÕÞ.;ˆF:0ðb“Åq²hØ ÃfˆŒ¥ë²Ô ±è)¡B‹MµëŽn›µË6iãzÛ©” Á& ©Q–ÎH7]â•6q* {€=ÁN±V,|r÷Ñ,¥ â³u:ùû½C»ÿ¾çÔ_¬‹œQ¸vœýê³Ç­AðáCßØäÜ/ÇZü~µ6¼ïÐî—¦ wj¼ÈÑqù7ŠÐ-}nû÷Ž|ùÀÙkjöã#æa7»*æÃ'ú FÕ8FU ¾e‡"?Œý¼ðzìxr£Bp•ÕNqÕqþŒ%K“$U“âÙ’ÓXÈ” …RYÊ ’C7”jX­Fc°% ¸9"Øaµc„"‚±L;CX)çsè4L[Ét:•”¬5UÒj:¬[•z½Z‘Öôè2€0 ”ŒlÖRxÊ°,73 ¬YÓ‰EYNVädË•Ê«­™šno¡Ö,:f'ŒÈš–ûö!Ï:ƒFÑ8ò _¡c`Øÿ€á‹‡#—˜? c §’0bHRp®/Úò#ÞVÈæìègÄ!iHŽhgrB5&¢1ó ŠS‹5 Š +× 1ó»^ˆÚŒÖÂÑh$,ñŠ!D GÈðÓF à'!4<Ä@FP÷œç—=9u|ØäÔçÿ—£¼Ûi­àkm Úí6ú®KÍ Ûôª‹sŽÉßOVjíÍͺ¹™„}†Ùwï›>¶ŸÁó5v'·íñL³SÜ¿cþ +rÁ¨ç%ô²ï§¾¿2¹‹±ŠÙÊaŽpTÉo¨=U°TA?àyIñÑ‘`RÁlÊw*µW…À19_Ä/ž¦·vÉ~¿\ð‘ /ú O0_~¾ß–;láÇ^p|c»pèïæÖ»E'‰¡ŽhŒÃkØKcÖ¥;|¬§;™ÌÁ1ɹæ@Ïêm(¼¥.g†]‘óÄbe'/­ØYÏÌ…WFn'ÂþÞÆÏ~ºÿ‰Å£d`ÑVwsâæ6=kÎø~¼aCZÞû(º´¼ÍlÁŒxïò ú‚=ÂçøZ¼™,WËýµ ÚͧµíͯhÏ7§ì©æû`s¦y¬yªÌÒ V~°¼©BѺU[_iV/žlüÖ>Ñô'ôDq›¾­¸¿2“ûIí#ýzîz­³4@qiš­Û¦¹ ˆP,«x UIèí#­˜šÛ—C}9˜Ëí+ær}E©·ÜYï^è-ß6îAlxÜqÏôzØ  Åè3<†¥½‘2ºÖ¬Ø5ª1¨AÈšÎiš´¢N©°Ïèí1z3¡¨ë*Æ  êýÆÚFÃïg ;à³hçaMã¥Y¸ù¨:8XƒFéMøÐÑN»Û.Žw= h‡‹ž¹â쉛ýÇàf ‚¬ÙáušJpx…@¯µñMøØÍBü¶Ns5¾Àãr">uÒ ÷Îóø¼CË N@aÜŸ¶¬Oæ-L•¶lyO³Ø¡„—î^¸tXWNzwä ¼½ÿ)ÜŸ±o¹µ02q_Òöu1¿è@­Ç0oë7æ^’’äágü/l!4áDÔ{Q¶¶DÐpcà GK2LøøA2á‹?¸éµáéDëž$Ïÿ"-}±ôô·ɱm¤ó£ož„ï,NÝI4 ÿAÞeª÷…g›WÈMpÛé^ÇbtlÆèÐÀv»ŽCR•„$bÁ1·$pHú ‰ª$!#˜ 1‡À@»i™úŸs¨ÃM›—F°;Ƥ1«¾Ãã½ÂÂu¯ân|剥ýdŒ|Ù·ßvváâ¡ÂGˆP-~~©BÀãïsŸ:2íþ"]’Ñz°±~œïÁ?‰g¥kà¼&u¦€)™²Qß n_“ßÏsðœt~$­Ú,Ã<öUÒ´B#:ÃÒt„•BŠcm ëHϺž2$¥à˜›`©\+•ª5©ô:µ¿Lùý^J +&¢î‡ñæñŽç£œ”ȧ]Ä[ò2¦e¥M)?{ã;¶(A Š’$CÄA²ÊudIæp £U²ƒrÊPY%’z£(&êýÈ5(_0kF¡ †(Öù ³^—dYê¯É¦ NCÅ5ÇÍó¸é5m3S1íH•6§Í3æœy÷fчvTRà(DÓð4DR¢H!DI³è9;ƪŠ£ä!ö4{žýK±Âê·Ú™õaâ¸ÀÌóáÕ÷12ËËšà™Kq¬#n—¨ãä©AèÁ)\äãÉ™ôæ­É]''ýyÞòîbNZü½­ÕÄÿçÏ&]ÿöô°Þaš–M„÷òUáôÊØ⯙ƒŽð¼CÖ U²¾ ×ÂÕï:žj½ãSrC7GÑù•N`!‹ÎÝn§<— *óxŠ¿Ž§8 Çí˜Á€(ˆèwaG"c *v†¬+éê +cĦ,w˜°éÎdÓi++¥:)ç_ÙãóQlù9§î¶0V8 æ¤Lj]+Kš&KR2`Êb‚ÃÓ€µŒTJ6’I,L;$8#_Ä/íNìì„~I”!gv€¬ªÒÙ¡ìhv<;=ŸíÈÆóÈ#Gäv62ÊŽ³Óì–¢YÈ +¹žYuØŸaK6@ÜÆ9/÷à&Ž;Žï®^'[²N'ÝK:I–d‘ä'ò[@mCÒ`·é$ÿ0ž0ÍòjBIK™&ÃË 0å1c7­1 dÒd(q;ÐÉ“6m‡ºÀÔ„d ƒ –úÛ;cc‡¦Lï¬ÝÛ]Ùãûýö÷ÝÏ—¢Æ€ku£°V§ýhžnôp`» +4¬·kÊÿâÒC²Ü7B¹&ßÁððž,åF²ld»®Õý´mÒ´ú3²lU'IfÅ»LÌú>ç@‚p 2Av$ã»ÉìLJ2á„i˜憄!Ù|Zø„ý„» \”®²W9‹‡õp¼ HÆÓÜ¿·\†]Öí¶}ä é uŸí#óG ³ž¼jÚ¼dëpuð?';LLµ¹šIXëlÓØ—¦ILŒÄm¥l„‹¥Òtbù­ãÛÍu»ºù.á”Ô'3Go³û¹·\{ù}ÂQéÌ<íZ ¤¤=ìv×6a§ô†Ì4ºùF¡Yš+·:ZÙ'9&*MsT¹ªùZé G3ÛÈ1¹æÆkö2QG¡«·˜y—ÃnD ÕÉ1äEb$ÊP'2¡UîˆEîñÌZ£IÊãƒ#©AÊžt ˆµb-Tl»v¥è™‚=ñŽ£8¸Þì­èÙÞìíNj Oæ¹½ ‚$ø$ÚXá°îqÈtéíM½Ù cã\ŽŽß§½u´wÑ@™§¿§÷CÉ<€f>ßÎÕ»üÐàÞì•—Ü`í íY¾Á6ÚK½Ù¯€µ]õ8[ˆ>Å¿véoA)ì&ÀÄÈÉ"Ø‚œ¥‚„C„wÀ3¢—¿èèÏôãÊþŽëO]÷ØlÙÿîuÒt ó·NÜ‚ó°/îÌüýàqSæÌg×2q#Ý[= $Ï€’„Q1º‘”Œ£×âG—— D¼•ÞFïñxNŒ+ìÍ^O²+=<¤‰1Û<Ûd2Ï>Ø•ái‘æÅÂÈá ¤ @K8€=¥Å€ˆ¬\rë…±â×áHs)¤UxAb_´: Éu4ÐÀP2„ÏÃÛµšÔ^SƒÛ'Û¶IŒa; N×Q¡†ƒ¸K7qï}ÜuåÈ¥ÙSçÌŸöíÌ0¶¥öÎ9´.sÿ5³bbEÿ¡cÁºHǵháêúgwÓ¸/†¸Ÿ„¸£jüæqÌ~|"?Xw‹Rý3•Ï•¯,7XâÓÊ›Ë[=‹ËWä¯(Z]¹¥rìPùYõ|àãüÏÕóÅ_¨N°µå¦àê¢WŠ~x+p¸è÷ùg‚q»ÿDö6²"Çs4ÑBLÏQ ?šCÅEá@ ªõÅÈ_ZBÃ^B#^R€ÕPc1+¤/ÐGÖ bÒ™´#x?›ˆ(HÅj/N½³VÙªÀ‰€§ÀZÏu†Î…n„Œ!J$g’Å¥ì –°rM󲱜kv Õ~9u9ÅRá¯co Ž*~5P +÷l ¹Z­ðRÛ!ñ5hN·pNWÁ‚ÖÅÝ[~_v"?ô«¸­R€BtWä—Cµ×èªiŠ¨çýÐn&ðy$1¶ežú:©ßÝuqãÎÖ—¶$éhùÎÃéÌÍü gÁÁ2ý$'Ó2Ý#9Ln„«ó¦–… %;28seµ®®Â™•»ÙX²¹°"œe0Bá­y*ÓVYêOû‰ßÓäŽ$ËÕ°šœ‘Ž®n¢rã®>7x_„£Z+ä‘H&\”áhzö4 G?Ú)®=ƒŒ#ˆ>¦Ž+u@#R ©Ÿ5ón??$háêK•úab¡/ÔnmÒ°M?K9›önؼ¯dnÛs‡g<Ý2ð»KëiXõ•»wÿ¦©±ìõ?-Yòñ‘.c½B³sÁ‹6mýîÔ'§â+|å;¯õw”Ñ¥+XZò‹ÝËf>ïç=áGݸá$%³­P×ušžþ$sXm•,”]H T…Ã>…0¦J eç’…*ðQr˜ô#PFr/Nÿše~øð˜Ìg•R¥M9«Jƒ2OYª,‡j:ª|®0ÊÕEjˆgûFÈ—¡@¨üá‰^jâˆ:ªÉ[8èÔC7þ@^û”¾Ûˆæ?Í á1¡á£žj|gfþLw5^•éÐzpzh!ìÇÂ{—áÐ ¤€ô²·»¬Ò^8kC«¼—ÍʵÀ0¹i¾é½¸“oÍ%F3öæ6zw˜Íœ¤{'žå Ÿy^’}\¬LÇÖb\EÅÅeÈsæhfZíö«Ï Óñl5¢»Ã-e +«Ñ¨¤r9*ç$> ‰PÐqrChZŠ È3Uöøfžu©5m]kÝj5YåòûÈÀs”;õ'ŠãÈù +MÚSš¬Ô`=àqMFªÇ‰Ñé4ñ˜D’†‘ýrùÛkfû=y6¿Î;N®[Øñ¼æ.ô cýÈÌc7¾wz59 ³çhþaææ÷çî~V›ÑqNé'O·$ ²Heh®ÔoóýŒ=ç–†ã9бiv e<˺yŸB1:¿\%HeÕ6õœjTÕh\Ucq_(Žr ô RÚ‚–€%mg%‹ør Êtñ1Ÿ/!ù|²ä J"þÑ/µ,ÅÁÊ’[%Qˆ©!Y ºU›AÍ ƒ6[.A˜ÿ8®–Ió¥.é†d„TÕƒ[!j)¿”?Åx÷dE,öáõH çzŠé¹òýqvh 5”„¤¦´Z¹ç#é]ZzÏM‚™Ôœ3–µ/K!‡÷57ù£i— å·¢JK!Í!$'4Kö¯Ì´ÌÝv»[ĵ’ËžçßÄ›ÌøG’®Ñû¤±ÞÊÛl¼Uoï +†k÷©ú-÷An§/™É¬E‹“/A4™€ÖS|>û®9Ûçs.& vs±ƒ!U}`@šX–ÐU|U&£l]ûG2M0Ø&2‰UYÿ ꇦvlL+ÕV*ÑB ˜Q6‰©“h¥"¶þ1PGN5_ÊDÅb{Ï{þh’nªu÷¾ï=ï×ùžßû<¿_h€úBk©µÒÊÐfü˜´)4úCè/Ôyécø[âvžá0—9. ªíbPë"ÖH"–Ø‘ P‚KŒ$Î&>H0‰ýÉD¢+©jIäsÚCܬ;ê¦X÷Œû²û–»u?ãv;ÕÇ8b +"ª£*V³²ª*²“Ãþp¬T½oe‚:&2G$(ŠC’±°,†Ã2…)GÂ!h‡(šÂt$(Á‰2B%j§ c:hÐW—¡)äŠÅF»Óh÷Qø]Ü‹d¸"’á£-󂌣2–­î¬le2òDz"#[FWF6,6MŽ&÷&§’“’·’®äij7ÀîµBL“¬4Ü0U²”,+Ý’(©„7§,# Œm÷1&<Û‰ˆ†­x©ŒŠø¬ˆEƒc0b†™)æã`Î@ï´? +/G€9¢ ½)sW®ÒS':"|Mæ*ãJ¸l«ŠñâUè s7Q+¾AEÚpHÊ•29®ŸrïAþ²Aêúyõæ€â¢#SÿZCý̬{ËbÖ Äì$5A)!ERlÝ[Jƒ²BTíóc”+TªÝ>*qMŠ†Šã¸Xܬé4­Ó ã¦ô‹lô¥}7?Û÷“¨:s$ƒ½7öÏŸ}öô¹z,%†(Ÿû“ãáfÔœ‹Óé¹éÌ‹¢#pÒ&न~ë%!‹Wh9=[°|ÃÒpjen½oT*¦Öç¶ùž‘žImËý&µ?÷ûxI(i¥L©p^8¯Ïœ/ü }ž¹•/î¡;øò& +¼Pй¸Îi™~k™LA„ˆ–5-cêœÀE°)blRÀ³9ƒ5<C04#f(«Œ‚‘1²Æ`Ÿa±õ#«xºÇ¥¸©nêVgŒB!ŸËåu=•ê*j.äW2œ1ãó1ªê“$3Ï2i&¸eFYc:XOt=¡ÂN¤ß3 gnL¥Uyõil@hã`=oÊC³aµ2”${ÊCWÃàhÞ¾dârÒiÛdxh‹ bh“Åòü‚¿gÉfž³BK¡“y..Dòœ¿]‚Â'æ±æžÍH3N!¡öÑ4ÌJPÃ4»†™Pߘ†Éö3Ì'õ±…K°ð³¹½NíŽð‡ò<Ëwäy&H +.˜Ç°Ž“†Î)¸0+Öׇڄú$Ô¼4]cñdoñ"2¶È0†j›ºâ+êep +ÅkקCÑ|¼T»nerþí8vënSÖeó)ý)Ó¹EØ*ª[4‡KûnüÅøkqÇ=í rjn=¨ÉºÃÍÚ<r]š*1òÜ®öhªÓTE‹÷ôešKáT‰ú³å5S©>Sí1QSÄf"Vü‘:)Æ’^-ðÝ 2¦¿«GïêJèj·c.Î#Zv›z ÏèI=Ý1£[Ñ!`(2!áº1bb³DÍœ€¨nð´,V30,L ´ ÷Ïç‡CåYH„S4è·/ÉÅCME3/Ðý{AØû +qt-dŽÿ¤ K4JV†°Ø aq…ùp"aqìÿK¬~=–^h9úquÊ$O×Iñm¼÷?ÒÂIqÕÕ-ܼC¥ZPéÂoA°r!º +XÌh-ßMca›{[ð;Òϸw‡ä,Ñ7¨,í¢àf£tˆMRi:Gåéaz/½‹ÝÉý‚š¤Ù:ðº74¾lS‰nÐNu7s,Eó&âo§NØÅZ±,k©pËÖp”¨=–.×9ÿr åÞð­Œa<ŒÇð^|;°¬€WmŽƒR%!ë¡¡ +I65"7®l8,x°3W°•ŸØÚÝ£”Ÿø|TÄ=NТÌæ¹Rí“éF}2äÏS4}¨ Šš–ÅY-ØÔbAÔøºê¥«s—¨Ã•Ã𽩋• Ô>òu÷ÐgÚîÙºkÝ®¹UnÜIÚðÅQµ‚79®#­µ Þë¿‹éÅyr…Ëëá\ 7çyÐ3â¡=²ð½×à ¥CäM¾©QK1Z8â©óÑ¿Úñå +¶ãŽãùû3ä…˜‘\ð[±ÅoÀy·'ó0®¹÷\sïÅ;3-ùÃ70Œé÷íì«^!õ›ÌJ`ø¾­IaÅ¡ÊF©ê¨µo{ {é4J—Á9ÚüÔïøä?¯Ú/^ŒÊråB ®Úêu=’h?n­?Üv(z8EmÑAÇ»”Lˆ?Wž_RÞhû­xH9’>ÑöŽÿ¨x\9yß?Ûô`wcúþE…úqê¹Ô¯R‡ýo¤Îõ]ìû´Ï•Œ—¨#–Ò™Ö:;ãZ<)¨Ð’ ,Át¿ÏÝ;PÂW¬-x2‰<ýíuk¨—ëÝÑK÷.ôù’âNSÛHG;ŠÅ4 2«á´–׆µQí ö¦6£]Ö\š’ M=¨9Iÿ˜ó sÆyÙépÊ˺O´pÏPåÁø8î©úr¾\0—ÓÅ2Q¸³eZص\XÎ/_zQ‡€}µÕ¾@™Úm”…[®ÍN ®”+gÿ6%ƒ¡^*ÂÐÓ(Cµ³¤g¼Ï:6¥P6cèñ¶ºaÙÀ2ùMäцÝ +±ßXFo:ùÁ/_¹´brxbâñ£17òø·9xl9ç÷}ãä÷¿µëOŸÞ¾û•—Çö¼Ír“kžXî ¼‡Uº½½òÁ þÏ >òÍ'7Ž"ðýRðýF@mJâÄQ"WŽX^.mK•x{‡Džr:(ËR0Þi£±7føŠÞÞ~Âø/ßåÛÄuÇñ÷ÎÏïâ;Ûgß?Ÿí³ìÄv•m Œe@£PÄŸBP±6À íƒv¨“(tŒÛºFctë -MHp` +H)­ +k@© ZX%J6Ö:-$Ù{ç$¦6Éù|g'‘ß÷ÏïóDR1-ÏÅtÔx&Ò:At¢•'Œ¾˜<Ø–É9Ér'³–¹Ê *P—¼<^,B/¤²ÐŒâh‚ @êÅ + ãæZaÛˆ¹yµ$LYS¡Šè‚èŠèÒÐQØnýS°-Òaè2_Ô÷˜{ 7Ì´[_K X§Ã*ëÌà8ßPcª±®€+ «­ë‰ç-Ï7òÛ‚Çøw¥VÅøæV³•Šæ‡o¼tc]±xu‹ 4,d)ÂŽ’ÑÄB¬æˆ`0ö‹OòÐ8ôUkO£×uuøQ÷›+»v]Á‡þóÁOÝ>Ù1tëô•R‹ßO¨_Q{½{|Øè½r¸ˆvrŒÙÁì A‹Ã\dÆs/í †vŒ9†º*ŒˆU6‡¸±Ž%Yë£x—zF­ìge=Q¨˜]Ÿ=ûÑÆ=׳W»^×½gow÷Þ=ÝúÏÖànù}çÆk~xuS'¼RprSOOv2êÑÚ&‘“9 €sÏXÜûX¢”˜FÌ%–§‰Ó®sÜ•¢+\ÿoÞëüÝv. dˆ²à,ÿc|µ1¿Ö¿šßìßîßØHä‡oŽU=O(ºï3æ˜3Ió­I_|÷îëo,ù`ªËAy©Ôí†î¡«ÐÙù´,ä.56^ôÁW÷Ÿy4íähš*]ýg¢æøwÃOß:´óä'hï±93ºrJÎ6ÇPoxÑÖPÒdk¶‰ŸŒ_Œ[~ª3Ó7汸 šàAܯòÄa¼¡"Š úhŸjÙ÷¸µ¥ˆfDvä½Dj=:¸?vµáǶC/oØšf½ŒÙõÊÓßß·iEk¬eu¢ûqKí¯ÝfwQ‘GçY=c‹†mÈ™?ڬߌœiÌ•Ì`Ö1Dø±rSìUÄþq•º¦xyryz“ýµ.½]­O¿ªþ<}PmJ :3nƒeZAƒ™”Œ—xÊ# -ÁÆQ°ÄEÐ6™'Fh„Ñ ,Šl"“:'YE.%ß&/Ò—(ÖË;å&ù°¬?!_¯É·d½ÌebOÝgV­-¦TR}H 彸R1ÄR}÷7FÍ£ò8ð÷ßpsÌ\šþOsÐ òè*aNá“jKã›Åî${eã¾ +ä³£•A3&!ÈåÁÖÏâ!²™¢téøêÐ5f_È»®ºòøé¿fmˆ¸·^zs`àÍK[»vì8wnÇŽ.¢ó—Zc´Ï›–XE\ê…ÍŒM½Ûak+C³w¿¾q÷ùó( óQÖ ,”ÁgsÅû|¡‡,\a\oÜ wMðwÄaØBX_71´šN›ºMW}&Ÿ™öh½ídx†`ª½ ãñJ´šÔ€'QJ$’)I¥,…¾·C{5i·[H‰*ð«U©áײR|-g“%Ùli‰T5 êÕhÉ]ô&Êb&9±?gD¡äDêBŠHåá-W<5Úú8K”–¨‘ÊÇß}ô×þƒíÿ /½c$¦ÏCý‡ßkñ‡2ˆe®5Ó¾ @[.-”Ïo0¿ã¡Ï(De9¥hlx‡û[ÏèíÕ°h<$Ü÷u,ºŽ5ýßýQ2š;§ñÉeÛª— Í?ôO<>–¼¸¾zjrõRmW¥Yc©–lÄE +fü¬jð«±üêžÜT,l¼9z£€J(½ï"7¸ 4Ð!‚Ý’‹I\)—ãær˹p?æL.;õƒ8Öh#Ÿ0$›;ÀíaÇêNy¸»-`´Û,‡KÑïhâÐë [Å@†›ðø–{;DjPSiJù¾6Š÷zŃ•³®:M]bç [à,ü¹½ÚfnÖí ßÇèË—‡¿û带B,ƒ;ÿúdÈçY"ßT$¤Û^®æÑ™±iç\U‘µü{®×]DGƘ˜2Qe¢Ù‡CåÊ#jy¦–©•­+]Pv=ä"âL•zY¹œ¹©ÜÌ (ó$eR¦6T›=È”¡¬,ƒB‘YÇZ,€mðçñ?µQåøœ£{òÕ2ÏK²AqZËK*õíL*•ÎHÅ™,mÕþ#iq8¬‰ö³ÚÂé彄׻ñzYFò3®D߯PÕjEUÊ”PBJ($d3L6›‘™ÿ‘^-°M]gøœ{í{íëøuﵓëøíØyØÎÓya_1›x„u‹ ¥YišTâÑAÖiƒnÙªŽmtˆ¨Û £l™ÚmÚÈX›Àš¥la]@­4B%¨JêªJkX¨"TuÄÞîu„Uš¥{ι''ò‘¿ÿ{ü/xQ@D(€„êQÀþ¸Ói‹ç3¡x¤*F"”!Î[‘.Ž)N$M¤~o~,h©Áý(;Æ}±žå•ÇvÄèᣫV÷ýݧïÑS½W_ ¢ÄŒ^ªyŸD=(wd¥6¡F2²œž† l:Åi•«]˜”{HSn| h#d&†Ü‰¤0 ³³V¥JuÎ*ó X”.§Ò>7vÈTšÖ>gËCºß)V/}}€ò+Ÿ2?pœ5Y‰ya2SCŽ‚˜Â0d¶Æ"0“KBà¥2RÔG2Ÿ@¿{W•‚®6|àœÚ’=õéPPŠyU¥èB$‡±uyƒTeÅÙU–&Œ—)Æç–´KpgX¡‹‘Pç‰ô0þåd•ž!»ñô1ülúGKš§Ïp„ˆ¬Ó·Ó­ó´ÂÝÀ¨7Q"0*µÉ±v[·íyØoÎ6’š 'm#‰Ï³ýÔjõç!ˆF{­K³eÔB[$i©)xˆ¬¨½÷+À'DæÃóYƒ»ÚணJRT‰œ0ךëLõæUæ„yµY67š›ô|(§&çLþ`DSˆk0ÕâlgÛûÙýNm [élb›œ-¬¶\W»Záçä*¼*Õ°jÕê­ÍL¶Ü^oá/óSü ¯A¼…—yšO™xÞlòÛ‚Å*‘ßâ§ü)·ßïqûƒ5åêf•¥ŠªJ•UU•—ùkR2ÙìœlÄ©dc£œôGËw¨4Zär2˜-©•ã(Å”øh‡O¯§ÙÚšš`ÐÆMÞ\»ì©.·÷Ø)û½Ëí- ‘÷POˆ +Ýk@eÞdƒl´'QÃhÃxÝ ­/ùƒúë㰢dzmáÄÂh>ªùÈIèHR)_Vðˇ¾µu­d§ QaÅN—ÛjÖW½EÅy—£Ñ‚ÅšBÖ2—ëÁEÚÎËqŸ£µ$ˆ×‚Û¶µáæg™¶†C\æ6ÒÀÃf®Ãw]û¾2Ÿ¾p—".,¹£Vgr“A˜•à6ÁFÌ9Wéã-: xó¢5–{õr’~´{Ïšv_]÷ªí5ëדJ=±¹ªô©5)eÙ\¬nT¶o‘A=A··t7¥RMñÎ%ÕL“¿ÒÔ9wEY÷6nuw¨/‹qª|TùV¨ò:|H®½Ê\ÕQc̘ŽzE7È êè.¶‡¥žd;tùô‰ü_3ÔÏ>CÑNÏ.…°†¢ÜÀW5ÕÙ<6Ê–’l¶<ÉÏ/Ouª%™ ›RYWRS-AjY´3V§ÔhW¯cðžB^ü¤,¸|RÏ[9=çuLJX"†bQÞÑò~xIw‹¡!›íÔ✛³øÚìÓÓ=›ãC„/f)©Ì¦\e–¶ê׶ r´âß0uHÎã}Œ ÇÎìeF™qfŠ™a´Ì0¾5”4ŸÜÁܦçÚºHh†D$º£´Y†…6Ëm–˜™=cMbîHæ8î­!£Ûê^ì¨ÚˆZË»hÉOŠd°gîBXr' " :– °ÿ±ì‚ÄÇŠþƒ]´æ6ˆdD³HNŒÉ<,8šŽ mö$pX‹ŸV,¢€UÇPU%b—4kšDzú­±ôm̽……–›ýý7Ƀÿx>=ƒ­£ç±5=ó·_ܘ<ùòÔ$`M[aoUਜ¬àÌõ…ðTG¿„[¨6cL˜ÝÆýø@É3¥†¿3ç¹kì5ýõÂk2ÿät¡°/Ò}ôÍØ +e¥2—$9]~»êRþâ}–´Æ_–u#l,.3ÇmÎ8Tª©ÌgàŠ}ø% ‹<ñ ò™uX稊 “×mv5»wíui\RåW¨Î34±°ÜN'’=Uçø\-Ø옂ù|R,Ê)Ô£€z8ÇèÅõŠÌû¯0W‡¢QYW'@®H©%Ðà’ _?øîôÜ_n¾øŽB©½‹)‰>yåxßÄDß± º½oûcûÇŸ9›Î¼‘fŸrÈq%íì¿|´÷ò8`×Øý°  2¼ñÏ(œ¹;h®/&ÅWg®?~›:H?‚wt ¯9vùºÑAdz¥Ï£;¾_Úz9r¬ôw¡ÈoJ­¯ð‰âSÞSÅ´Ú?˜/´ª6l³²¬Êð#D†ç›ä(ŒæÅyäMQŸ“ÓëâSèC/ùÙ,é%o‡ÍÜ7ÃÑœ£¢Ä×ã9êé÷¼æÑŒ{¦<3Ú#•—ô,‚Úµiš@ +H.H/€:—H*}¬WÙûuˆh$ó1*ƒ #@ÄÁ"1< È/C–è®^°Ùå°2 +é +ª*…%ÀÒˆVîÛ§¤Ú}Í·ß:—žÃô_§O?>AêRAð?æÅŸ½ñÙ?eÒzÇÇ{{/_7=nú(ý P{›,4ሾ™ÛÅ“ÿ!ÿ3æ¤À*lð\Ìvoù¶ê44;²¬Ï6eá 9Ñ\´¹ ¨(XàLbŽ0ѲF, Ñdá +‚qf¸¤Ìz1Ò’åsfv†¥XG‰Þs`K 'p4И 0)2wdÑ7[>l;ܤ6N€Ê\@!¸¨‰­¾~eªÝ÷òPä¬YäÎ +¢ÉÎ;çT–u¨0¤Ú™ïs¢^}¥iÃw%3 ˜T{bïW¢ûÓn‡äyûéö‰Ÿ´t:‰Žm§Ò1ÞšKËæ›ñÌ$–­ÃwäˆIçŠßˆZÑÎuÞÚ_Õ½#\Z{Cx×þnÃ{kÿ%|ûhí=a6öéZÞ 0öÿ2^­±Q\gtî™õÎì®wÞ}ÍÌîػί½¬ñ:×°SÖ!ôi(h³…¢„‡ie•ª +Ié/LJ*(Q ý”Ú@1ŠAlB(¤T)IÕ¨Z)(Ú’T­•¶²Z¤tqïµÁu¤ª^ù»s;–ïùÎwÎÇ.ã–[²ª©Ëâˤ”.FCë寕w”GÐåËO£Ê« +ÿ:oÁ‡ƒù®Lg¯·t°3¢ímjx€*{2L÷’h{˜æ)Z4ÑÒ¥ŽèTù:è;GÛÝ »ž÷K‡BmëœÕ©M©ÑŠ­è]›A]ªã•Ô°zG»@—ù`µtòNè³”ÃÒW©ïªiŸš ¿ 6›SÁ¸†CS(êa€ïÚrÝÇ\ð K¹¹d':ä}™jQ(>`~i9žjâtcÙÒ’ƒØËÄÐ`ÙZbQÊçDßJcÕ + ¥l¾Ç™CÿRJ|â™(³wÓv™ÒÙ;™Öå{ÌÅÝêXÍw×e¬±n;‚C™(®!¨x†Ã‘Ø!‹êŠVä=øfÈ¡ ÄX($Ì“X¬î%?Ùæ²-ë{n›òi%aV ;¾ÖQ¼G*B_)ÛévúYX¤Ÿlu³8§å5ãÏ£=û&†¾¹éÆõë{ƒj„ÉÔ3GG_:þðš;×÷éýÃgè|gêR1ÍÌ–ò}ƒ¹DT62O®Üyò±´ÒKý§¯ÚmõTž.ìÒöÁoí%]ç!ì¶sˆZD½í¹ŸÆA$‹Ã—øóüþw|ƒg¿Û¾¯ýHûËí×B¿ô h#„{jaÚ‚i (œ*FQRX3ÜU/zb +¹n€ +„3¤ìgêàçž²hQ³;kTBH؉]‰Ë ;€?MÞO=œD \°ƒnP-Uš~['ù´@„qµÇâ|(ã,Š‡-Š^œ ÄÔÖÀÃE¿Þö·®º­uóst_< ¶ûoùâZÞ=¶îZ¿Œˆý¯±Ãg~â7? z !wó·Ÿß²ØŽ˜b4â¬úánX ‹·É!r_Ç÷¸‘ÞBeq%óÌy æ4 F9¿‡ Áp˜ ¦£©–@ƇõxÜÐÓ©¬Cæ÷S.pWØ®ëØé,ТŠí *Ëë²R©hCB4 8tȶ)J×HÂu ¢|· ´ÕÁ_&s¸ ™3ªƒ¤ Á9åßcÓoE°"¶*ïÿ'‡såÖãGŠ­=¿Ý$™ °2#Z”PZ7ߢ¡ü›S> ŸømÄžŸV7ì†)Œg׌¼Ñzü÷?t¿sÂjxc›¼ÞSà”tZ¦mÞÙa;b·ÛQwn”¥ä­p›¸CÙ‘™À‡^‘%ϼ@€P#TDˆ"tdXŒD1Í‹RKDñkë‘Y!ÍP훟@àÐ0ÍAWÉRň–áY…Ud`K¢‚ûH%CQ¶¬(²¬È x¢¦XTãâiÄs Rê`Ä ÉÄŠ8!Òâë`„’çE< ôH£Òqé=‰‘. œ3ÀÁjþ(Æ¡[Óµ)aÃN¬O Œ¡0ÎvçÇŸzs¼Û ƒAïâü÷ZÃ>o¾`ºp›Ì{{ˆí©90 Ü,Õ2`ñÂxö¹;'¿JpˆÄ ÔºŸñ-Ó´ÖÑ‚ŸçC­þϘE²<3Ãü#™£y'rZVßG¿¢Ðëð5휤 ÷jµ í—ÚÚ-xž…ïB:ÈUƒ1ÔìbrjV/3eu%³R]ϬW6¨Ì ¹­`'³]ݦo3·åö0ßS_О×_†§˜Ÿ©Çõóð"SWÏêÌ ¹·µëú´÷õ?k =ÒâZæµ¼>nŽçNkµkì5åÚÇàcý6üT»­‹ÙÏA((‚ +鈢:Y²´h— (×v=—þy:î¾çһܸPp¿ìB×=šsÝl.íä¨p€|¡k·—;ÈÑQÎâVsô'˜à.sÀqGYŽ °é0ËØ1?+“É‚™LÆÌ´mG ¦Ûõ™/xE•¡m…e[U,G9œt†‰óÑ„ÒÀ6tü¬CÚV5|Bƒ—ÁMJßÁÙt_¿ +nz†Z ½–á³È‰![F‘ +;¶‰„£0®š žñ:©Ã¦×Ógz¹|Éô:²8$S8˜1¢bÉDÞæÈ]'q¢ƒž®­ƒ^ï@ ’sœƒž –`œô"¬½YêU…9¬ ›‘W{úÈ0Y(ùÓ|kŠÿŒ?â7ø#þ¾?â—‘Ñ“4½Äzjß^ö )v5 ÙKàCªkcþY«ÝÕ)4j1¡I&Mã–)4k1cªµ9}‹lRFÅ/¥ÄåønvzPh‡æaXð)áM<÷ÈØâ\>oü–jµÚØØg×>»è³ï‹gCsV74ƒŒpÏЀÇ6šÎÒs%u–‡²¼X–¬Ñû·¿Vß~¦‹ñ#v™|´~p$Y·ˆé͘h6À<†n…Jó¯ðØ|–>†ëífiòŽX¢%A©,®aœe¥7ƒoK£Îhfsõ*¸*Ün8ïdÞ)^)]©Fƒ”A½¦©"ª¢TÍéŒà”S*fI°AQ XªJ’d;%ÅqJE¸PÊHB²Q¬‘‹2è¾å¨ŠúP !¯Z­”Ë•L&ÛÝ­ldKuÐ}ή«uœÑqØ°ãhá0Ki@Ó’àX”Å©{°ˆ÷'3Dz’Î9–ÝM’«“›’£I6iñ|Œ¿/€·^m8þÛ7ÌicÊp ^Ø\Õ00b5ì‚Mü+ôðn#6e ²HfÇeSøgA`Ç»ó¾;•f~=iÿÃ~õÇ6q_ñ÷=ûÛ±ãóíóär>›$µ'±MBÀ‹/$ !+Fš5MBbX +c£ÚU©´MÚ¦…ª0©›Ö2µŒ@”IëD©´ISÿšJi…²ÒN[YY©Ð¶®ÎÞ÷|vaˆ‚Ôý3Ég}Þ{÷î}ïûîù}¿ß÷Òü©¥…y±Žò_Ì»j(¿1χ(_œ/)çx Õª¢únoPC\Žçjq0§âHΊÃ8 Çp¼œR¥ sà•;úO:½vG2qjéÏÇ‘çmçߣ9–XZT-|iÚ)•òi´ZT×£à´zÄV'™­ímŸ&”´7—;Ó„’ö懒v—Ï‘&”­rkÒ$îòZ9ZwÇi¡œ×yû©¥sóœ«{¾sª…P +I½í‚B1Nâž[úýàÛZî•k%T îͼÝmÑ6çnæ—¹Âäúä5šg¢ ˆt†ì§ä§Æ÷¡æN^à/(L n¿’–{”r%)¡¬ºP)ᘓç ¶¶_Mc3rŠñžÙܯ:PRƒ û¡z!f[0²a¬]üõ±p0(/€Ÿó3~_ü_§ÉAðFµíF­Aú.÷.øêëñ`õú¯úmG®ý)´ilÀÅJ¢{èqc; RîÒÛ„dUµ¶8››šð&ßÁa‡–¦ŸÄyquc¥Õî÷u/_»:Qîñ”'Ûfú¼~»EŽ·Õª”ºÖìá–NÖ`±áê‹ß?HFš7 —à174“‘½²ÕZÂÅóq§a×C«¥hs>ìĘpb¤e‹ÝçëI®mOD1\=³Ùç³Yó‘®m%#+Ö ;Ï{CÙ9-Ò‚K‹tv#-ð6‹Å°ïNo0ÀðÒ¢q^‚R!»ÕNˬÁ=ë·¡Ô;+K¤ARq'8$‘ÞLÒ%Û¬L •ïáiM‰¿³õ¦´©ÛtÈtÄÄš|áîz^Å~H¤ 5[cÃ2ýóh6±wI<‰¦RÑHŠ“æ/µHžrßÓÈ+<>ãûÙM¢Í-Õ·u=y‹Y4útOÒ*g扱Œ:aÍ”^w¾jÃÉU‡2äzÉ«ÌEtãõ£97>¾Š>Ü@r³ ¢çÄMÒ÷ß̾å•Ü’…Ùöfö’¯Â#YÑ‘P䲋ä%]€Â5y;ˆ÷8¡Ì`]Gü-VAŒ˜üæm–]¥fÛËökL)ç“ó ÷,ä >“ƒwè3øPþ€¨<Ü + ¼>Pºœã¾ßÔ^¨C]Ã>€8ŽKþ iE+fVr«ÐÔq€Öï´ý ã€µøëf:m:žÔq>‡ øî®ÿÐØ4Ð{ ¿ûÁǶlËaë‹ýèÛ`çCY€Á3Ã8×n`ô €‡q®±gÆñ»§p®½h÷õc9Ì\Êaÿš"îüwæÍ7bEQDEQDEQDEQDEü? @/¨DüÜõ2«ñR›½ 8'/¸ÜÑëóÊ+ ‚áeUÕ5÷E¢µu±ú†Æx–75¯hY¹*õ¥VPa5tÜ¿fíºÎõ€Ý=›z7÷}ùÁ-_ùêÖþm°vꓽeÊùwwëwáÒQ2#U ++!­Ðý°¾£ð(€ƒðüP.—+䪥%A-k N³lƒ^ÍrÖ-·\úÓPòåãï ý¿¹·ËpW‹2x±`—-Œ)Ÿá]~¦o‘çtÙn&¤ËF”Wè² å.].ÍÌC4sŒúNæ¤.ˆæu™2ÃÛºl@ý_uÙ£[—M(§uý1ŽBFw†aFo¹ Ï#ú`D“»`ÆÝJ†v¼›B™ÒÔj2jÆp| ¥M?ðßT_ðL†Íød öl¦Q׉<7_#´à¯s!'%5mŽCÞ‹cv£mT/¾o1ûi>ìÅ»Qª *š¿Ô›!¼ùf”Œ6»¾À—ȨFFqæŒ67YÆ{j3¨ih¼ò÷Ô£qÔ会†Xߣ“û‡åç徑a¹kb|"ƒ*¹}bjrbj 3:1.OŽ ÆäŽÌÀ]ŒêéËäÍc{©fZîÇq-- uH’1¹mlLîÝ=’™–{‡§‡§ö µw>°æ?svÑvÎ/-ÊL-òK-ö !AÂ[¥R”˜’š›X”­Ÿ†× +E©é™Å%©E©) +™y +É©E%‰ :¿4¯hT±ÞˆO´ž > n@ÚXX¢$a? Y,:ƒnëLÊ怓2ñúh£rd5PY ,qYÕ•€+V¾L Àh Óó¹6KNH=Îô,­ìñ‚¤x~›¯œà"z±j`"ˆÞzrå÷ÿW~M`•âÐrAe4¸ ×”b0 + +endstream endobj 521 0 obj<> endobj 522 0 obj<> endobj 523 0 obj<> endobj 524 0 obj<> endobj 525 0 obj<>stream +H‰¼VyPWÝs€Ã9Ë‚­ˆ g¸…°LfÆpÎ ¨Ù¸2Ì4C's`wcÂŒuEqËx8À‰’„EÄDÄ|ð6(XjGïáŽe%À-ËQ‹´]" %R­Þp2׳IuB¾4qfì±Ï®^ºrº´låUû_Ü7¶ÜØXQDWž>‘»#î‘/¯ðg`pMí¬Yms”¤--¹ibß‹gì,ZýÍÞæ¤Ì‡æóºôûEz^ñ×2¯ô¯ó:¥ØrRÖÕÓuë»m‹Wœ•~ûîäää^(Õ+ná@^Žì/ühùÖ²¿ê´í”RÂ/§¢9xÊíäp¯CÊ™™ž¥¿ª‚^—Ÿ ÖK¥¡={µŸÜ§×»­öžøÉ +iǪéç¢Ãå…}Ýó[¼¢="fî=/¹;©…×ZõSÑ^}Z­[ýH}þ»K—:æýÄÝpµv- üº-½þÜ|8.·rîÀ7µŽƒ"(*ÛëkEN­‡85aãªÕ>£)sa$Y6‹'â>CNsé5UH3XÁ›©‡½j÷šWEø´!…ßÈJÒH`*Fc, MzLEPE¤–À”f3#Ç%Cè ´t,E!}O‘¢PÏÁ¤2Yb†:Q.´1QØØ ÆaŒX,Ž‡‘Ãqhi|sÔýggvoÜÙ \>x·¸¸8¤‚h +Ñš¡°Ž˜i’1S%¡Ê )kÏL„`¹%˜’È ±üCRÔr–s´8–h‡ãâÿ!“bªYb˜ä“_ m+â \€ZtÖÜ×W~k6}Ù·óÂéi’“Ý=í¢ãÕz^wïhhú¼¿wŒo¡ÃÞ2Ä11‘¯˜Ã0©Á€)Y  û5¡ Ád‰JµT‘&˜%U*¥ijE¢ +“+T²©"5QŽIÓä£.2)ŠÔó^-PMiøŸ¹÷‚DA l#zy,RLÔUC $AгÖ’ òˆåi«Û +(Ö÷cW‹EÔúX]_k‘UiE[ªv«¢WK׃zØV¹û'ˆ ºç´g{67sî™ÿŸùþçü£Â:ÆOd¡Ö¨4“Çò†%«WòÚpüTé­Ë©ÂU +¹AÉcWoЩõ4^©TxƒÖÂ"šªÔ©ð^¦éA¯Òjøh\aP)”ȇ D)5„mÙB¥×Çâ~¼<Ö¡Õ!QH}—¼**Z­zY­Sêõ|·T¨BfY¥{T„¸£”:Ev»¤Ôêøp•AcaÇo9-GŒŠXµ\ÇGÇꢵz¥u“8•ZÍk´Q¨Òª$µÒÊ ÐjôʘX¯’«}E£2¨¦¾àé«E©t|˜Y©÷ãõJ¥È"'ºŠu0%R©õ¨i…Ó@šÌlêí‹É)Y˜!ŒI|†9ÃâV¦c’¾3äÙ‰9@"ã<ä·:wnBZŽ‘Ïš€~aÎæü,3N%YIÈâfÍÊÉìŒ@“93Ý3¢Üδè©*¹Ÿ¨,haàÏ ó®ñ4s²Ù/9Å„Àa–T°TŒ'lØD¯–8䙸€ìþ2ÿ?f†žÙCžÿ¯B#ñ/ {þW{Þöcš?RÆ{ÊF¿Í‡É|ƒƒ¤¯fþ§þÍéÀ¢ä×ÓA©ƒ}ë‘í¯ö©oQbïŸy*ðRq£Eò¢^ÅÿJTyƒxµ£¤p/ÿýPç¡g³<æÒsõ)7Λ"6½ý¬Q|­}Ó‰Ÿšdõ»–eˆïœßs`¾Èfë¼ã¡ø)Ó7oË0™ÂÚ«f$ů®­ÔTM)¾ío.Z7§>æiúÓñâJµÖ%2§äQA{M¥ãwmζÇî…np“|”·]ÿÓmÉŽ²6=)E—Íù?œÝo¨‚ímút*…r”æ&ýÍK-õed=Or‹«îž¬×9/ÖÍÈÊœØóæØzã®ØÅ3fçKM=ÈíeñÒ©¥~ }@™ <„‚ßIø¥‚lëX +Ìž Ç3 {»çBw‹¼p‚ô®ÏêÙ™9Æì÷æGõ*ñØü…ªÑ³N‰Ö.÷–wε<èÀ×cÂOºî*üÃ¥Gèýگʗx~~ú:c"|Qð êr\½"kwÜ+Ž÷ÜQ}Ë&*½Î»>÷ñ“ÆW¹9´Î _-¡†¥)[‹¼B“]Ê+ ³ånôp»<çkmƒ}[‡\×\׿aKí¤õã·¸¥ôÛÙ”´½€FaŽRu+ÈFV@ÇâPÅòùG¥ù[e䮆¿´§!3CXcÌÎ3g¦vy‚Ýkž€ÑÑ9áÑÍ™’nÍDés±–æõ›–Ô¤3›³eRÿNê‘mX”+Êh FK¼‡ŒéìÔÚw–oóéæS¦8@2ß`píå9Ó¤.=ÇîeÇ– ß¼œádý­— `ÿ@Y@`^‚^ó›Ðüe¼Ç³rÑ̇G¼ zÛ³€€a]ý(²"l£©¸¶jùñ“¦Ì_êY~¯ÔY;*um뀦«ÃT·j8xAEΣ5w¤“/&Þù±pEóý»†‹ªäe»W·Å¬ê·$lxªÿï‚|ÿ~ÓóÉ:§¯ODŠ¸‘—+V¥¨>?2hÙãýCN»o³¿ß4;¡0]§žj”ì.ËvûxÜñCžåÇŽ($o]¼±Òxîhb¡ÜnàúÓ%N q߶èHsõðq-õ;¿üÆ€0Wi%pÜ&.†‰õ=©uêÃQ;[–RÚ—²¢%ÐãeÎ0ƒðB;w¥#†ØN ‡'9}ûGË,WkÛÖ $BSWëHš,siBýBÒÙ^üÞ‡oÈpâ­Ä W¸G¡žŒ€p™$œ¡z´0À^¸@laˆƒXk¡‘øÀT¸D€X( ,ìƒfB…F\a,DÃ2ˆËåêIp„¡‹…QÐ9 aL€-p…,è{L¸cào¬Zx ÅÄ…Ž˜ ÷¡ñùÒ`:CHÇÜò!TFÁýQðÁÜÒÎ eˆÄô¸ïLø>Æ]'3ô —C`"D@$Ì€tب‰k¼0Cíóð€ 7˜Ì3¶û»‚ûmÇDÜÓ %› ‰+ N ÈŽläüŸç£Nx\A†4 ¡–Âœu Žd0™JJè´†>b?áê…¤ +„\ÄTUP Íð„Ø?"%äSRG }¶1¼B%xÃdÐÁï!òa l„ÃP‰Ú¬¢QŒ‚Éc±Íì¿;΂=LGLóá| ×ÐnNdõ¢?0nÌb¦Œ¹Ä´¢$ÙB¤mD)¤ˆQåÏB;/U°öÁq¨@<µP7  Q“T²€l%'É¿Hu£ît<5Óõô­ w1Øw™ Ì&æs…udCÙ)l {œm°ñµy`›ÐQÞñ â…|apRøL¸"<‚¾Ðx€žið.Êõ!jòÏp +Ÿ/àïpà[hB¯bO$d4‰$zKÒH&YEV“?‘bRM¾¢"êHS-¦É´ˆ~Ak˜fs‚õfýY%;Me³Ù"Ο(n·‡ÛËíãZ¸v'›½} Ï¥ç#žßê˜Ý‘ÛqS ÂPA*¤­CÑz Œ:ÙŒ:Ù‰Þ±ÎÀY¸„Z¹Šè¾…›p n#§ÐN1qÁGB|з4d™GòÑŠÅd3)#ÇÉ RI>'—I-©#_“zr‡ÜýßUÕu…Ͻ÷½ÝWEù‹úÖ'ĸ õ7ˆŠ+ËnT¢RºË0É‚˜ªl Ä1Žµ¢OMmt:£±mœ‰©Å¨oý™mÔhG[[kšH:tÚXujª8ÄDœi”íwß² ´ÓîîÙówÎ9÷»gß²²û¬‡ žÁÇs»ù‹¼‘¿÷¾›ïåûøàäÿˆÊïðnáD¾(À{®X Š…!‹”ÑJª½Lù¾ÒŠŠ¿§œW>Tþ¨ÜTIuª£Ô‰j®ZªnWÏ«—­œSlé¶Û*Ûl›mmvÅ>Æ>˾ɾÍþ¶ý€ýº#Õ¡;ÞqœA“XËÜ]X€]¢câydm¬’%3ƒ)•»é€ò=¾XÙÏÄ'óÃr¤m¶bJ.~Io +Ƈ+»ÄÙ:Åͬ͡ˆÖ±·pÒ—Ø +—ö‰s¢ûÚ{—P¯¸†žÔ…jÍ`SÙs´˜ÿFùƒz¹¦Oä/°?+/Ø”K´›ŸQBÊL…¡¶-Œh«ØI³è¾h·p+¾«ìÂ\ÏšÇçÐ×àŸCN–ͧÐ|¶Hd°2<d"O9· ]¢‰‡ù|ú5ÛÃWŠIìu6RP/Ò^µBéŠ,UNE4XZ­b´cäÈvˆòLä;}X›HçŠ>=PjySßQ¶ŒÍà·ÅTÖÌײoØ 6 ºÂ—ð,“¿ ì?¤{ÀÐcú’Ž+»ÅÎÈ_Åá¾rtþ‰j }ŒŽf£r~š}EŸ Ÿž*è¹G”YtJ¬¢âü {ÄÑÏè(ºð1þ4ûŒ{¨Ûö¢ò9»½:…/¡§q:ˆ®\'îÓ‚È ÏÖF®Eα,Ü—ÓèK_ªùjz ýâ,:Êô±Z ùJb-¸)xŸö ?¤áxTôÐU¸§ûÐ/O£_t¡kÜÿ/Ô‹»»—>ãŒÊlûy]@~ÿbê¤iøÍHÁ]ºéU>FíNÒ6Áè¢}”­HÙB¨çìEžâJÏü¢ysçÎ.xvÖÌÓ§MýVþ”¼\÷äg&=“=QŸàÒÆûTVfFzژѩ£FŽpOIN–˜à°ÛTE`ã\ŸîifNÈTrô… ó¤®×ÂP;È25˜üCǘZȦ éÁÈ—þc¤':ÒÉœÚ\š›—«ùtͼZ¢k¬º<yg‰ÔÌnK^bÉJŽ¥$Cq¹0Có¥7–h& i>Óÿj£á •`½ð°D¯îmHÌË¥pâ0ˆÃ ™iúš0K+b–ÀÓ|…aNŽdDefê%>3C/‘!˜"ÛW[o–•|%Y.W0/×dÞzIz±9Üm !¯µióšvk­I¦CÛµpîycG‡“êBî¤z½¾¶&`ŠÚ Üc„û–˜i­·ÒT,>Òhì͆/½I“ªa´iæùòÀ`¯K~ƒXsy¶?dø±õYÅô|"שD“jÐ}ÒzY3ôb½Ñx9„É4LªhqÏÌôtâ¹!Ó§•ÝeÎÏÒƒµ%O…Sɨh9‘áÑ2†zòrÃÎÑj†S†÷ IɃ…†¸Ï’¬áR*­ˆ—“ɈôE€©­ÐI@G"ò«¡€Œ†Wa–Ych2¼!ÃY(ír¾©f;uÍxH8v½ûÞPKm¿Å–í|HR”àˆ þ˜lºÝæäÉv/1Yú̼ÜW;x•¾Æ©¡|TÀ´`a>jîrÉSÝÞá¡:(æÆò@Tר.ë8yòÝA“‡¤ç|Ì3úÛÒ³1æ‰Oé€ïI<%69ñÏpç˜Q¾ÆB“ù?¿t¹^Z^Ð|F¨¿¶¥•C´¨¿ îë—XÔ‚›J6*µHâ*ªÒ€ší×}M¡…¸aˆÑå ˆ,ŒJ­òü±ç:ÐZÌïÃøÝÏ8;ÖÚŽ=DÏûI’ØL2†Y8ë'«öíÔÌåÓe;•µBÍ–‚f@N•€Þ}ŽýÇaü< ¯ÀŒÄ¦Ä‡ÄÖ*•gea6šÃr`¬»ÿÎ|€ùwA‡@ûmïÓQÐïAo#Ÿy_$feœ±µ%¶$®cÜÂ÷JÚÁÛ¹Sæ)15ÀqÞw©EÆ`ÝA`+Æ当ؗ\¸i xPtÑó³o1.ëbÅû(ïDœäÚ‹Ø·JŽù?µ°,Æx¬q~ƒ‚V½ñGé†ÿŽ^uê2Z/|t@=ÛJÔ§ v7µ8º(gùoÖ«8ŠêŒowïvCÀÄ âæé† “ G‚swÈ?H0P 6±hÌ hfP§Ž±SÆÉg¬P!Õ¡S›Œ%ìµñZ°3òG°U3m*©e!µ8-#íï½·Cl+ítn~û{ï{ÿ~ûö{ß÷®c÷Œáç8ô{k%´nì'ÖûšPrµóùºñ݉÷u+Oˆòx,X¿lãÌ1ºí¿µÿ/PÎúºi=Ê} ×Åûü€Ÿ }˜ÙI†Ý¶¶QÈž3ZY\o t?Ñe`“¢;}!*Óúq.'!æåÃÞàˆ¸Û„5–³a6WM°€>‰žÖrè~¾–r>ðùÁ›GùÑM>7Ö—’œô×±ÌýÐó©U^ü]ìŶ±œLã¹Çg‘£zé¯nLj§µàú¤Þì§î»£üóüsÖX¿Ë<·ðøž<§ül$ߟÇGãxŒäqŽÇ€dÿ±üùxÆóÓE>E«½³½èFÛtä­×ev‡yîð'¨M¯¤6õ8µùÐý!úžÿ Ú€÷>7’S›ÜC^>-MæR¾OÈ‹‡’yÔ¡)"ž½FkD¼y•,‘G¡çOÿ ô¡¿’2¼¸2ÌÏ!?ƒè³Bä›ÃÐ}Ž íûÕKÔÊíÚÚ-Ú¶‹¸þöŽû)ωêÚ,rÑ€ûg-Bíbl§{Ÿ?€|ù2=92ïæ6®_ŸÌ íôõ‹œÿD2óooqψgèí*bX+=ï;æ{Ð%ü±^Œ}Ã]/æjuß÷Í@ìâ}mÜîþÞÛoD[£¨{9±/‹ûDûÚÅRô­Ò/ ‚pî`úéY®çñ¬È×—q?J 7.Âýào2wûλoâœ}u$߆˜Õ=Ø» }«¼\]'î8?â¾Ñ3yŽußñ"~vÑIØ£úwá“/Q4Ôãü.Ö Zÿ0ʽÕ#˜s=)î'#÷7[?➆ÿÈû×Àï)\Ï^Äö_Q9ÞiIJ!ÞÅ¡àð»sÀ_%è(*j e<Ú~并SÝÅ(?«¬£ç•.mlhÇ=òg´Fû-UJã´õȇi‡ íêR|ãh»O¥!Ô‡µYtQýý>¥KеÝ7Ž¾{±:÷“q\E©x糚C›T—þ¢}ï¿› +1nا?ùÖ"‡|“Šae. h)4àïÀëñù£˜?‡C{ŒJĸQZ“àš_¥y7=ªnCÜãzwcßFéåZGtöÓû\ã¿Ò'tðy1Nôù=Mäù’oÜ;Š'ßGq6g~çyÁƒ_7!ö=Š;K/E1ç߉®¡ßu¾&nj×÷Áö ”ç%(çÂöøô{åVØO¿-¢eQØ‹S/¡~íà.ðFô¹Œ¾×^!úì’ÄõBÔÃ@°Paï—K¾ñÆUƒ«dÛ5cú€&KÛµ»¥ó}ب·­Ü·¿x¯ù?ó¿Ég·ÊŸç/÷*ÇØœtËœüž_ÂcsWòûºƒÞÌÞ>$ßcT.ý93ÉpÅÊÑ@lž‡5CÄeE<òXÜx\lùk£Ÿ¸¿S*Å""óxˆø»Qãñþ3èi£uI]‡V„âê=±@i gç®RQ]T-«ËDÕY-i]éVÞ˜•%c™’S'”¤…'©÷P;p P©Ï:`'à¥y튺$ÆrÍæ_«µ¨×’B!µ:¶paI{ŸZ°XMçUX‹…¨êX0(90[²eIÎÍÇÂãѽhÞò†ûÄð”‰%pŽZƒ¦¬³Ï>à-àp ðAW €: è±ž£BjMlæ|½ï…kb©é%ËÂéj&®Â€*ÈåO†!U˜¶J «Š¥¤—d¼êö+ƒN(\" óˆÂ{±á’3á©Ê{T¬ "U Ò2 ø-0| èøs8HQàÇ@fÐÊ¢á\å$ÆE•cx†D9$ÊÅ¢\,ÊÙ¢œíõù 1` ÆÀLHQ„ò›†üCºÒçïÓ•ƒþƒºÒéïÔ•:®¤ùÓ<[Zø>5‚ Š`ƒ"xˈø”ìx„š€ƒ@?à~ + ‹´ +îžsɸ¥¨v@`ÐA<™è—ìÓäv?¥+AÔ‚b® ú±1Aì4·1ÑZ Ôq›Zƒ_D(eøÍÅ/¨±Ëo:9sÄvŸLN$ Ç“…c¼wûc§-|aZ7°5 +Üð¸Ç[fJÆf¶•E™Ê¥0G`ÊDøŒÛÐa¹›R”§œ–‰˜ÿ—NÁ¼ûhŒLcM‚{¨Ep7Y,Ün¿èØû1¬ÞÚ­s +¦ƒ¾ídƒÖ:Å û‚0ßgÇÚo†SØJ² >a#Ùl/¸Á±ŸBó +IË{!È”3Üáì2él:µ(Ýè›E–à©d+ÝŽyÕŠkÌ1ÿaÅ•î^óŠ]g^´ãë5/Ø™‰‚¸ÂBi晢SæÛ9§Ì× æÑô ¥šý-§Ì×ÐýÐ 1Á^» ó»Ü|Ɔ3ÁŒú#ºÅî67c*,·É½̉³½hÝhí2×ÙÛÌf õ^³É¶Í•Eq–ï˜õX— ÖØkÖbñjoáÅv¡y7_Èu:f¸@Ì ,”eVäœ7çCCYÑa3hÏ7g7óìEfn &zÅl˜2!¥,gy¡¹zôzôa=Ú GçèÑ€-Ô£_Ó£ùzô=:]Ï42Œtã6c¼1Î0 ¿¡ŠAFfÜ +Í"„²L:'¿ÆŸš(§+ü‰ž¤0C¡Ê虨Ö*µË#=å…µqÝ­ï)+¬í1–­ùú!Æv¬âÖžþµTû­ìžO–çÅÙ¸{W÷øò"쟬W}lS׿÷¾/_ûÅ~6IüœÄñs’Æ.†$ĆĭK.ÁUâ.bÆ[RMLTu8ÕÚ X+ƒv#­´Â֮ɦ†2ª ‡ù€*¨eíÕ–mÒªj0„Ê>H‹´”J;;ïÙ ¦ñÇþØõ{÷Üwîï½ë{îïžsnÆ•@‰ÎV5C¾?‰Qg°ÜxaOyƵºk +aìÙóƒò‚L¥VwMƒ.E¸?…JŸnQ[\+Ñ5ñûT=…:t·¨÷´C‰ä3S@#ã’o…ð8d<ª7óJ¢£+sÔ›Ê4o*‘Ùסmíš"*)m‹O·!R]Sü8QÛ¾jèùñx*•€%6qÝTÀ¡CÎnAšCšÝbâÈ[yœ¸ \ЀSG‘ÏÄùÔQÇc7Ö«µÅÇ4ÍÄ@Þkbz«Ñ=˜)ÜjUS“Gàn…»«G T&d~(H]À„à +0?À&dù]ˆ¿éþ7¤Û„¼x²8áŽÞpGú?”m­m½­8‘ì³ ÖÔê­yYªl_i2£È³r´|ýû²…RkukÆV ¿E )1\¯Ã Çwa¬§ÌÖgFK”3"À$¸/<ìWw–Oó1¿ ƒº¨ÐµtÕÒUFpÞ貃ÚQèRw>ì/ŸÆG +] +¨0îý¦Ðß?ê¿Wq_ÔÿVÚÖÏ_já†Ïš÷@ÿ€QúÛâp  DfqG"ÓüØ–®1IjË°žx +tuwtgêÆ(ùx<Õ_(¡Á¬Å–1Ȥ òɃLAšÀ €3ˆÞ B7ƒ¸Í h3ˆØ#«¬f>7bæsÃf{Âg3È*¤ :ƒhÎ M`ä Â:ƒƒ-öB0+ø?Œdþ±{J +…`ÆFLj|×`÷ßQß±–á– àÁTB±OˆÒ$ùœ©Hà'8d•ø Œ<Q˜ \†Îüxs3–mPæcfc¨ÚÊm¨–5ø~çPûC·5îìm& /‘ÆŸ…Qþ£|C˜†1¬¸”u^¢˜ÉX“·Ëqò|»ü ?#ÏÊ7d¡–[Ìé&n“õÐ*QŠäu(!®·0YD*a+¥DÅ*‰Kå9®ŠÐbB($©ã¼Åjä#vp„œÀQ’g'Éó̦I»$’”.ID:KEI1«Ô“íä2áÉ$9ň2ºÎRQ…z7"9N&ò(ózú¦®Î™ u¦Sb-1W´Þ°ÁÜ^¡.´C9··N5„¤Äb{ÏÅÆD²º³‹)”9=¤r¤b*ðØéŠ"jYNëz÷¡4ös~ì_äç8ÌgOgrç¹Gpû§¹+›6æ^ÇñÜaúÖÒL6då…O„/„?¢rtœÕµ£vÌìÌÁKn·;DCÖPñW„vÇÚâõjŠlá¶È¯Ð"XF{»£ÛAÓø—ÈE.0««‡ó)ƒµ7TP=ÄSüžÁ¤wãaHsžs‘]±ûæÅ—½Xó2oÒÛãå½¥A&%¥ ˜£€U'ÉOÆ+¢Aôôœ’Öûæõ¹y]Ke¯ê-Ùy= +³† ëHÇ‚ˆª5䌸j¼[¨­­®IIqi¸qÿ,nHå^û,÷ÏÜï/à'>ü×–å.©ûúvüéù_ì|—(u_ä¾Äq¹…‡?^ø¸â{ý‡roÿ,söÄÚðìà™ùЫ¬jŠ›ÐrW½ٵÙ7]2å>_r¾”ú\.ÂÅ`ÅE. Ë '²TšylÁ°!OY+±–1?Öüx–Åw†¼ 6«Xœ9Ï¢YDÀpI0]LCîIrt fnNýjöêEO÷ÝÔ'knã.$o€0ö7º+ù’b"‰’X€y7­p-Ôj««S8Œ_­ûÚÁu'÷õþú»‡|øG+SÏn[ÙsL˜hó¶N=5—{éÉÎ`éõ›Ãö‰×~Zæ2xqrᚸxáÇC!Ÿ_õCÏÒ‹¢.h‹rë¸.ÛÞʃ•oTŽøè ¯$Á"9+DáÜeJ¥âS´%$PTWù ï!±ÙѬD}I¼Õ±UÆÕ¯û†µ7Лä-þ"¹è»…+%.Íç«"ö!öiD¨ÐŽÍÌ“Öà?%ávUÀñQv»0\.ƒq%†ÆΑª & +à[Y jG8‰†Ñ%´~Äã»p@ÃÚ$©9™D`q Œ×dNŒœXqbçhµ'Èh’öP.¿g†§ª 먲F§È¯±(±%?‡ŒÌêóΨž®¿jöœ5é™nÉ‚®^eõ°ÔÝk¯ °\¨ÀYœFiœ.ìR‘¯ÖµÎHÓ +ô_ämjòs»ÉÐîܱc¹/½·ñ›øÛ¹7æ®=0ºãåùýG~þøë—æösŽì ‰'¾s·\‡,FÚú6‰Þi&Ûµíh  â Æ÷ NAø2Â= #è/|žüOæÉ´ïó\-˦Ej ðþ.ëûÀ#ä㯎/ÍýÖðiÆl;’$˜­Lºó>ó’0¶q¶IòܸU$Hæx+o/’5Z!F%M.Ì‚sõDˆ,¿·Ćy‹á™[`ÝQËð´Ñúp6®Ÿƒf}8¼¬¡œ]±‹T¢–6ÔÆ­±>eyZ܃öãÝâ *7Ø™}ÖÎ9$*Èy½´FÞ% I32õÐ2Ùg¯E‹éY³Kœ“?A·üU¾aEdÃb¨$Z8«ôkô;ËûÖ÷l¿‘…ƒÖWåèô¿È.ÿØ&Î3Ž¿Ï{¿|¾³ïl'v|1¶/qb'Äù ¦4Ih[U… FT‚®@‹‚Ó©Ý +]ж–0©j‰…?Ú5ÚØ(¿F’Â@U[~ ˆ&µ[aRP·•0dˆÆ©œ=¯lÝvνï½ïÝÉñû|ßïóylGåcª¸Íþ¦ºWl»eqƒ¼Qíß’úmÛeqµúŒó‡¤Oì“6ØÄ6Ûcê2y™Ú#¾ ‰IyŽÚ!>!ñ*ýžM’L¢b°TÙn/QÔEQž/áhÇáÞãT…rº&…ÐŽOЭ˜ÚUº+Òæ#NŸÏ°³5Œ­PÆÚ§L*TSàëèW ÒÃfCJ.ü°4sÈò¦0Œ§Aàò† &‡x›#BN)?¶ªûù“ü$Ï%x‹_Ê÷ð}ü/ò~‡âã|¯ªj£%o›óJQ ü‹Ç—°œÚžù Û§½YÔ;ÒéÞ‡KZžmþºýNFÏt£¤Yñ•7¾hßà±ò +{©Ú_ ‹?T¾³ì°Svðìù.èþ÷i§7Ýeb‚ey5h7ýªázvjá7Upg/d3`ògïÏFî?乶2~ò4!¢ú”i¥õ>H¡¨:Y6 úC²¹+é*ºVz‡î‘nJ’4F¡ŒÌ„¦ìǸ6q©²žã^¢¯H¯ÓmÒ»d7¼Cw {¤Aò ~¿4h”“aiØ6"Ÿ&gà4wš?'|&^†â?È=ú@*ߤ€LH Ø +l"r/ð¼ í%t¯ cM*P¤! 6X€aì*ÀZÚÖãmõ`55ôÃ¥| «1¢»¦SüX.žC°îºµ’ÛhžSü(OóÁäø&;øøÜ.ßÂjü—brǽŽÃ{¹¸LÓ6zæa(¿£‡‰„î$—E멅ļJš8ÂGû,Yw×Óg°ÉßSxm²ÜŠROWa£Q"ÑI:!QÒþòÖ$ßýˆï~Äwö]ÒÚ9sÔ—álôÿfêÏšš9H[ó³µ8[ûß³Hh¤—ÕGlnÂò)IZT½ )`/„õ$mÔ“ø_ž;¨'aº†ébiÞD‰1yL~<{-ûý‹Ùk(5ÞºEÜ—¢Âȃ8÷9úèrä–›¨//æíyÖ^ö +!-¤ÏpIn»®ºTO…qX*åÆ#Ê<ÇWÒè°·9ÚôNÒ ´Sí4·ÈoºúÝ{»õÃöaeØ1¬ŸUÎ;ÎêW—õ/]µßò\7¾Ñï¥^ŸILBB@QN´(ìKø,ßNç2`¸Ï-ôz‘Xw »85`N]CÒ¶B:Á¹+ K§¤Ñ<ž”8é¼U +áR(5ãÏ'˜ChŽ—`h,XŠ@ÙT91­ú/éi&'LÍSÀ”“ÒÃ&Cëx÷t‘ššò™˜_ºa¹“~<‹ðôá™+ˆpÑqÉÍZ¯·°@”¼>¯Ç,çrL5“scC=^š°-û{¨£?üèÅ«ž[÷ìÆìñhïüÖøófí»=°|Aìµìa¤ehËG·gÔ?µ?ûÙFvÔÌøÓÜÍKcEÌ ~4yMXƒŒUiëÕ…Ò"½ÍÕ^¼Æ±Z_çé™±ræN° x|;? þ²x¸øœzÊu¦øãX!©„VµÍ\\ʵʖ²$ØêoªÅßî †KÚ•GkÐ +=jZ%ïÏx>-»¼Öm1Ñ#4r×Ó ¡Ð¨ Äz4ÐCQii#DÇlÍü"b(sÓö [Ñ%ú½ÜU†LïAuòª%3êÍ06êŽ GØ´„Z̧Ôå&o†¢~Eº½#ÐŪ‚CnØ¥@όʌ—æY^eg7[÷âcD€Â/x‡ ËÒøOú/ù¹¿ÚŸòsþ¼¤üÈGÝè;ì!`y’ûCN œ.„…„À C¹/H=LwÇYÅOw狶¹s½Ýž2 +“Å2R^ÞP© OCXaÏË>…Ä,‰”o†8sá¡··6–gµw9¨Y„œbgvLÙÜ÷êó55ƒ[žCÿy"{£®hÛKÖ.^Pe¾½òçk‡ïüý¬cÍúe¦fV´­k^µ÷ãl¦ï+T4yzòß̧H)©¬u?ˆ½nl.~-ð“˜àæ91Lê8ÅÝj4·E·Û£ÃÆYãŠq%z¯\ñú¡ºî n¼z@×k§Ð’5] k mRP’)Ë^Ç°?¢±XãÓEÍ>+Õ°ÂÌe.ùÆÐgüµ Ï奚Ž£DÓ™nvÙ{'ó ÝËJƼ¹32Ï° 웡gÒ¨FˆwLÝè3½é)OËž$x‚%c/OyJ§¬±±®6ç L#fyTKKP;XtÍF%1í€(åî2m5ΆkÙñÄ­O/|⪫(Ê^wñ©÷žÜºÿÄí -îEmí]Fü «dÁ‹I/ý¦hǾ÷_N¼ðõïžh~rîüÖÅ¿Þ¶ë·WQSdÖüTö¸$µ‘yµ-©•kP?‹P?£Sê¬t›å$¢”¨¦nºMO¸ â[àîäÉ8¨£&4ó»mˆt.]sQ½ÐÉ°L£n·‹Ò0tn‚õj B” +£NÅEì¬xÈVE¢þ€|I¦dX!÷Ë“ùÚ(,Oȼ<‹`QkÒ!>äzãóî1¬¹ÈVÆÂðÁö¼-ç\ùc°ø”)§2ø™öb(JV»’IV1"³á”ó“ü.í&i|njÅ)[é¨äɯkn©%qÑ¿È. (Î3Ž¿ÏûîÞ~ÝíîÝ·wÇq ‚|©§(X7~ÅH#ˆFÂT«f’˜M›(hŒFüH ió1Ó8#Çø‘8 -;:-´Ñ ã¤Á¨3Õ©Z›)…ZÇ4Q¡Ï{‡Ö™Â°{{ÇÝÞ<Ïóÿÿ† ½¢rûüZº§}ÙµºœW«çl®yyÁªyub÷pZùä«ýï ~²àt;©Ûúâµ_Ôozn;~Ë X½-¨¾09攆ÃP஥µ¬ŽÖ± tk×D¶…ÛÉAzˆ}:î„£ôwÞŽˆ¿P-¥s(ƒ Ñ}é¢YŽÌ´¨'&šåLóåjɆiðs b.¥iP‰À»N‚ Œ™D7u[gz(’–ÍÏFΤëéù‹,^³Q»-ãdHì• xHÖˆG(ˆ¸>òÑóec}F}ktì&ã¬á[_ü³²Ÿ¸¢ªµs´>µxÖ¬fhßý‡[_†)GzÚ"µ 7~·êg/<Ç©Ó¨'Ê éä¼óAN¸’ÍpW¦Ìµæ¦¯O—~¤Nµ¦†ŸI­ÊhÎø˜Jë%#7õïÈ¿Ù÷ª^ æ¥®ó­ÉR*ÛCtOH£~‘ÕŸ+j!,’n`H5´Ø°šµAP `"F¼f6‘MÙ–‡÷¯G .9U¾2¾#Æ +ÿ¢øÂö`žt³œ}ý’C•Œª•L. +ø3á‘Q¢Bîð5ûÎ=ðuÝZˆÞ+Y»p^Ëüó_ýhmÅãW/ÀîC4çî†æºËϯÝ5|+¶§h NQ€ØpÁyg›oG”¢àÛ©lólÑÁ/›JšÊÒåÕ,o0ÕõÙµ²¼Ãl‰þ^9¦ŸU.)•%MR}&˜Ôd¦`f˜Ñ™ÑY¶ºÈó‚§IZï[Ý.½gïUöyNH'å/åoä>õ¢öwiPþAú^¾•r7ýv4­Ð»ÍGk¢+¢©Ì–­“6¼mƒÝE‹I«x™\¾H®_RÎÊÜÔsò¦ð³“̘R%C¥ Ér÷aÁE¹‹>áLö¹rÝš¼Q9±Þ²hÄk&I3Óì4–ÖœegoÆÌNºö!×3Œ0¢=êoóùM0¯+™*.~ãñäCÂ5š.ü܉Q‘¤pnáèÚÓ¸#Ãn„Æç:vøq¢Å´Vš­ÒÓüø JkÓNjCš¤uÁçN8ŽòÞ56›xM¯íò +ÞëÅZ@+VßœÈûUžÐúÛÓ0pͼ–xêö#I\Ü—9Ü·è +Ňæ\ö¨7'{CP¯Ø¡ú€˜’Ƶƪ$éÖ>’L¦'ïQßFÒš±ŒSZ’2ƒê{{vŸÞv§yeǤ<ðt¬ªéãåWoÀÓ««+ÞªhªzjS]:nB,3;ý±ÜÍE/õ”¶­yþĽ>¶êIû7¿õS«ñµ†óÏÖïhzíYÔç"B„nÌÄä;gìtŠÐ®ò]VúÝýæï?|RŠ€Ë¬ìµÜ–‘9î#;‡R—ãî¸Í–g»k„wM¨ÑÝd¼¡î‡Ê~÷~ã ÷ oè¨zÌ}Ìè%½pšžQz=g¼g|ýä‚»ßsɼè½äûÖüÖBh¦¡ë^ÍíQ-FTÇÖ½§^F0y(òe”¤H’‡g¬êÕ)˜žêVlýK}Pg†Ó§é#º`ë¿Ô©ÞE‹7©®DóÞÖ6¤èd¶Þæ"ä°–I×– ÖC¶è==ØÅž [YÎ[IÐtë33ÿ‡m*î­á/¼<|&Cô¥å–mz?ýõá«o¤N/›3}Ñø`¾Ø}ÿÎOf¼»ÞÙzÿ×´qE¤Ø)š¾¬¸‡«­{± ŒÛŽ³ÃVæ°V¥MéCu¥Å¨A F‘DK°ÄR»rZèuõJý®:Àn +ž1Â1¦Ä]qi‘XãjVÞw½/ísí“n26©–uÐvŠžb}´ ÒA&S´{`¸°*ZÞÚå²%’Ÿiem¬ƒ1Ƴ+œÂºXŽ£ €Ã‚ï’Td:ÎîjdÄCÂ¥¥U‚Jüö»d)ÐE™úÿò]õ±Mœwø}ïÎwûÎ÷Ú9Åvr!þ ¾ØA4M äJ›˜¥ñ2F4„"šI¥kˆ« ²H˜X@Ó:˜*&M¡aÁªÆ†f%Q§)@PK4Ö„±ˆHÕhÅÞïÎ!cÕ4ù|¯ýZgû~¿çy~Ï£‚AžU9xûjÇFÆ Êc¥[4k‹¡Ì_; ÆÅ<ñ\T•™(ÄEŸf&Àó~}èá ÈM@Í~mÔl‡–LŠéé˜iÔtË4câN™°¡ ‹Ÿ¯,75Ó¦S]Æiú£§ð—´¦$/¾‚xLè¦SèÜ9†Ëgr.ÿñm$gØã?¯£«IÝÁƒùx +f®šþð¯z¸r9Bì œ*Åiö*®ž‹{hñ"/ó +c×3æàÒJ#jfk+Ñ Q*ÄŸ~ïè/¾)\“Æd®<ªï^ŒÞŠš‚RÐôý¡ÒZj8 œ½Ú[UZg‰Kq{Ü/\mi–š½]Ô^Ë~¥3ÚÞú¤>{¿£Ï{B9®DÏYΈCÒw¨ðTô2º"¤ÉÇ%ß%ÿ•ât$­Âï‘ëù7c¾ÊxñXd +M’;ŽIß]åNñdäúJxn‰RíQLQti$‚­!b¾Ýf“¹*\JG°Hˆb“eØñú|Š¿Pöû q$¢¨HVUDl6ŸßQÍ®ˆÙ\a8ŒXÙVèG*!š¹©Ü–µQ#6lKQh_S¹?ë§FüØú©Ðœeƒc+4yŠ7¨¸Q=¤RjO™Íåw©Ð Zçz¤g1 ·Nð½góØ"2mðŽoÐÞÎñ„ÛPíYÒë'þÃ'Ì–ª^KÊN ’9e,bn)Heg`ñÂrŽÔ(nRƒÿË`q†Äÿ/ɘ{ͲÆ;ÆVï»'·½e¯¾Ú¾»Ç×™èöTVn^µX}%3±ÏU³°|ÓJ¯àmËL0¡ŸÙ´&ñZ÷–ê®G[©ý?"eOu|ûj¦z«½8RõôƆÀîŒ{Vã/"=x³vÖÌa@#˜^bì<«x<í¡ðIr“»ÃÝ!Ìš’þNèHÚ™væ¬p^¼Ì¥-LŸc2Ÿµ0U®z¶ÏÜ'2AWµ¹Z¬’™ +˜C"}Í©åsI„5ýâÊ‹ ‹uRlŠªÓl +‡7pÅõ À´°3á|£„¥"éù +;góÕíiÒr¿%yÛãݲƨ£^ECÚ“†¥3ôÙHK¬ÁO jïÀ’øëᲿ\©×‰yu²è®uË…ą́NØø*T§ ¾J²·´<!nNÁTö«Á¼šX´–Sb bZ,3™­|i•%.l-ÿœy®Ät–äÁzs] µ)ÑJ¬b^U°n^<¨ÅZÐ:ë*GÂÙäj¬ +µ–%b[Õ}êë Ç1õX٩ذcØyQý°ìAtÕbÎSŠç•‚!QŽ »ŠÜXr¹[ÝíàSt@³Û}¬8– ø¾ˆœ¢}šdg"‹û…rW­«ÑEƒx¾tŽŒ†qXïFØR‰Â$¬„„µ°)Ü+zà@9­¨Q 4€FÐ=Ä@CæiB#P‡ŒJ¤§ðoÎD—®vçf³Žòi2 ¯U}@ÆZÊäT‰ÑÕ)êÊ­ Nú:Xb¥ë)tó4K=¿jÍY˘B-ß3®ÖdŸ.q> .ôé:çÓæ.ß«1(ãªÑu¨±Lås ¬²" +¸ª«Ša<›BaÙåÔYgQì ¾ÞºñÙR‡óåÌ×K^^ÿ6¦>þÄ—ùÒQ®­]Û8ßÓýI|Sfò󇸴lÍò²BÕïr*«¾¸k׺ݽ±güáEáùdþâg¿óæáOû;G²“´bê…iü'í‹Fú ý v1øçü/,ã ½ƒÙËìá÷zŒ%®Š¡Eú]ú2}‰¹Nßfس_í±Qgü›Ù×-w···>ïÙ^ó8\Œ_¼Ì)Ÿ€ 6ØÆ&¸…† >cÇwœ;î#X¢)E•S$ +©Ú …TTêH:5(2„H¨ U†&G5UZâ¶V‰áë·ë#€”5•PÕÞÜýf¿ùfæÛ™o¾G>ÝM_ ”ãDÆóxeDA çã=‚GTUŸç#iLý³Bð\Ï#ãìC]ß—®{Þ÷³aDý-¹Êد¤·=#ä]ÆŽJ¯Ê?7Žú‡È[¢0àÈ>ÀJå#LØd<+÷€8  +³ü+ÙjyÝ$·f³¤€lªs<…C@Ë@MfòyBJ¢(Ìðù¨Ÿú@”˜"ϼóÔG°¹.ê¥Ã\MèkŒ)Œ*¦êÈ/º$qgæ D@+9º3Ï«x. YJÄ­9…K"fI‘ a‚ñ·²AB£ +äÒË8n‘ß2ÊœAßOõ™HLúxß®sÛ +Ž·Ôðæød|\ÿ¶ú ª£Q<9¹sÝÕ–åËïlÁÄÄUT ¡™£xÜ2VåƲώ]êȃ!lÁ|e'&+€^®lQÙ\ºHœMSÎͲȖny_Z°6©­ÿå‚¡-þùeÞºÂ5ë½0·•6Ž¿058:UÕïÉ›+Žºwu”¾NNà÷ ¢u™d”{MèYNRƒË_×ëåzý(Ï{…“ üF>äfNË9’s%Ç9Ä"¹%ÕϪ?kžeY5Y-®fO»³ÝÕæéãv¹ž½žþ¢z!ó*wÝw-k"m‡Wóêš›2žª^1Ãërkž_'?‚%ÿFr"TéA×åÖÐjk„1Ó…TQUðhq~†bdç»50TOßní²ÆåjåÚz"»©íÖ>Ö¨6L~ÊcsÀ ƒÆcÈ8g\1xÓ(18CÖ2L#ÛѸѲâËñ–ã‘éÐm:p+PÏeʧc7<)>¼Yv}ë°l/™Ü'©#è(Sˆeâ 0œ>˜\ÙÒ2/&–e©"E$wQ½$°fÝŠl¯o‡•hNκ¼çà,¸óƒ9E•=^·¢¨ƒ|úÙƒÇ>WvÔò‡1ªþž’B”7AAk¥a8y1Ôª›Ðu9¿%¶8(0›ð‹H-T‘ÐLÚHé—öŸÁOÈËÜOéaù°ã2è<CÎ÷•+ÎlpzI/ô+‡á%rN÷ÈÄŽ°7”EdIŒ +PÅ‘µÄáæ> i¹J¹ò¤B“ +EUL…*ÖÝð68·:cNêäšré å.QB­v¥AØ*Äp&¼ygÉe˜Á½M*Бú­ ÿ†u¶á†Ô¾“Tò=;F^>‚WE½i‡#Ö%Àà7o6»VªSFò¸ÈíQ’µ6W¾ã"YÊS¥gWx>Ì$çpÿœSì6îŸN~: Dß*éyiÌ7!ñ¹zÈÇ}äý?)ÜVØ î½Áþ¨° üyá7JGÿZ¸À§çÿ*=.žæNÑSŒ?@Ä~,¼(½Âñt/ÿCïóúŽïåzißïÝÇñ­Rð9ªó+ÙZïF®‘nù%t™”WqÕ”GƒÆòŽ•Ëû¼ƒŒ +<9Ê‘ûT·æ”Ça}ºC–0Ð{24Odº(b70³@×tI´bmžÓí^ÂX‹ÙflCÈÐ¥Š‘˜b‰ТÇÄ Q‡¹òÐMcd‚p$ÁþT¢‡ô}Pg*VWô1ÕHÕM}«~_ý w 2¸w_ÿ5´[ñ;èOíÄ×2qãp_~#ÝŸã‚Q~sçM<¿…äæÝ#™v¬o‚€ŽWq.æ-,;ÛØá\l¹ÌÖÒŒ5ïÏ…R‰€©‡“cohA>ß”‘<íêª{:²R!¯­x刋à µÌ2Ž'ÈÌšlºì©)Rr'Ö¸eÏæoÆý‹K¿ß‚îãKM¯œ&×>‹¿˜¨4¯º/Bª´ÿw Oƒû FÊWî?4 aïƒÊÞ» î’¯O @; à}un s)€q ë*@Îf€Çïaf>&lgf=0{?Àœ[ÓÌȯ˜¿` +PØpÅßM#4ÒH#4ÒH#4ÒHãQ8 vÞ«µ(’…à¡…>ŒaæaÊ .)ÅjÉÒú«W®Z]S»fmÀú†Æ¦ вñ‰M­›¾ö#) bX â§J`BTÃj¨…u°:±¯ú“Iä°zªìžzh‚0tAÜêIþá‹~©½þâ‚[šüë—Êä‚“pwë§îŽFðíîÌ»É;)š‚Ÿ«HÑ éo¤héž-ÂSÜ~ëä™lÍÉÝJÑ ú—Í‹±Ma!ÓR4Cº:E H÷¦h”‡½ÍлvÜ“mø4á¢:lº¢ÐH¤¸LÜÃ(î\Ì®ÃØÞis˜Ø҅㋪¶ÛÃÿáLÅŸKfâIF±m×ç<=ØV‹ÏéõJ!ˆ¿(LQ íÖ +Ñ…Ï&³eHØ£šp¾Dõ"mÈÇ~KNÚ²Z’´aÏ3øŒÃÓØÅùê_abk×ïÄUöºÖª&¾[<‰Ô¬-ø…&4ØãMØëÕc½×n·¿&ŒüÖ¸ÎjÉßg´f+jîEÚÃÛ"æ1³¹#bÖG»£ l2«¢ñX4NtF»ÍX׶"³:œ?„©ØšÌÜíÚeµô˜µÝ8®4,)Äja‘YÑÕe6unïHô˜M‘žH¼7ÒVU[·ª²² "Þî*¬ŒvµÕ&Â]Ûê›ÿý»å5³9n‹<Ž?mFÛ¿TZ3ÙÞÙ“ˆÄ#mfg·™@Ö– fC8aÌæzs}{{‘în3#]=‘¾d+ú?×ö*œ§VA%þ +îÓý&[Gw!eiý¿â*ÄwK²6ìOØ-(S=Jþ(FüÜT´ÜhÍéY¬Êá àÑb«ÿ0æCÅþ=3`© ©ã™ž¥a|v0žßæ+‡$¸ø_¸\+o=¹lâï¶?99@*Aå?¸~Lü + + +endstream endobj 526 0 obj<> endobj 527 0 obj<> endobj 528 0 obj<> endobj 529 0 obj<>stream +H‰¼VyTG¯î9ÀYnlÏ€P3ÜB\&3£ŒáœP³qeŽ:™º{@bÜ0㊺Ôk

_ö]»^¿~õ}¿úúWõ]0€(<«Ôª'…A…$·ðmÎÐFÊ>ýúãYø­G²£E_hvˆO°WÖn,f‰¹Øì›Hÿáñ¼Â|˵™‘J‚ÛpMÌ7—æ…Í. Ú€ ¤ÞtÈ¿è.mÙ‹-@÷JðcŒ*°°3«sÐÙw1™mF=Ö¸ª€­fD²Å¢ŸYè6èïh½ÂV½… ûóÑ$ö è.´1ìÓûHZº9}!M> ±í ¼ñ³#Ö3¸/ð]ƒ¾> çñ­NßåÂAcËSÊ Æ\ð:§o99q “z@w¡k¯ÌŠÆ 1>æŒÃ1~fBÉs’¡kCʆ‚¤ž‘ €6`$`Ñ;hïUÅ7˜sª©™ßUeü×î[»êœ^' O†N¬÷âµUÄ}Wz9iWзù^pð3žŽèè¥bè)äeó]¼]ÕV–¤­$+õ‡¾œHäí‘CÒ”–Ê·JµÕ!‚RNáæ=¶OA(l I)½™ÐÚòØ=M™vƒ™b +Hš!râ?8!FÃØó¼ã?M¤0N““ðÎë à¨}~ߘð‹t,ÀÐzŠ"5Ø2]cè!CÀ6]öAå<ÕäÄý8wêìá²òÅçÜöû´ýý†°¦Šb¦êðAƤÛ!ûOÞÀì“vººÒeÅ8š éÒ“¸ßœ]¹wóöIY·lÇM1ø·³ó%?TœÏ:ûhEàî gK‡».^þfÝœ…GåûÞ‘’²!ç¡ zÁ-<ÄkÇî­ö÷¬-ÿ‹ifëF™6÷Ï…©WR¢wj&g”ý¢ ŸuFºè Y#—Gvm6~pƒ©ñ­ öÁByÇ’ñÇ⣕öî΢ŽÀxÿ˜É›N]Þ,hYú·¶øwÏ=Xoª¼­;þÍ©S3îòW^Âç‡}ÙšQ¬È$MJ‚NÞUô¦Õñp ÇÅàñúúZ‰gËN^uÔY:]ðó”ù(’«¥Ã`p¯ÓüžùFGÛ–H'Ùý~ŸWÝ^òªŽíUŒì_IYHBËê-…”5ŸÐ’t1e$ ÍÆJ£¡¬=.=ƒHUËßR§ªuÓ¹B¡ÊÔ©”"Ô–G üGO&H¥ÒØqÛ‡Ž¦WGÝvæöÊM„Ê^À›%%%ÅÄ P„Ñf‰DuÄÆP¬.ÔdÊ9{6º0‚0”2/BÂñHÕ)9ÎñÒTž÷è_9…œÐN‘¢$ñbh;1O€ä"܉aàtõüª}ñ¶Ïº7ž8—DÏÉLŸ<VêWã¾sC:ÇM*ª¹£ º4X•u쾬þîµÕ>Ú}^¶´¶äŸ°kçÜ*ÍÙ:uBçõí¾KöWD][°xYnÍ´µxŽø­îÑ·®‹«qTTÏ_éÛ“]ëó ãÐlðh¾; ]QÅ\x®0ƒüÈyÐ-·<+ïVa.c`TÇüÞ(`ÙBf|dä¯ø·§”{ó]¡Þâ€nœÀ“Ïçá–’†Ëó;WÝ[ö±bÝ𯿻W~oÖT~éü€‡Mo›Ûî2ü÷g[cŽ.?Kq;¼A²¦¨­mO:úÇ#âG£|ÿÄüp>µuÒ#»Ó=$Þ¾¹qyFnž±<Åt)t½yÆÔ%â«-øB*û«ÀÐèÆ€-²ÌÓ¿KRÝ·H7qAûáMëëëÚ™ô1:EµèuíÍs/{×’îM:-bÍ +Ÿåo“リ毧a üŒFãY¢Ja´ôYÃpbÓÿï$þg =&õÚ‰QRù‹~¨V +³žaˆ("œH£Œ´AúyäèÍ”IÏR6+Q,“ºÁAÜz¡7ž­•zC17qõMÑ3¨°6«Ô zô…‹†4YlV“4í)">¾ý戣î1Û§w{…:V½Ô.¢?k—gÔ?>Ý#¬oø¼1»jù®ýšµAkž4*'^ùÍ…¨•ú“M»ó“Ý“2Kš·yÜٹѓQfï¸s`T÷¶U#®vO]V›¯ue=ŽËî1e£‡\ÞõÐ§å ¶uX…øä¦î)áÓ›&Ä&/ýbÑ…ŽiY –®Yì8kÃt/ëãñƒ6ÝLœ“5=½Çxëœ.(\rzN¥ŸO2At,ãfß±õø2¯YŸ\=©xRP}/¾ÝšØåæ\c| Qì|ùê5œcÅGUï} w1ì¿øùñp—2€ŽAD|!䪺ÂñcŸÃˆ¸¥NþH$VVö_U$E¦v“×ìpò¶ë +(†0’4KåQF=KTOàrN'.zi2¤I«‘”z«‰ X†°3Æ KSFÖ\*bì†÷H#K°6 ÁDÿy<³ËÅm&­7²\—Bm‹%-¤•%B“0¢Ép®Ïúb=eÖÌ“Öú7@èÙñ¢Wm4‘c­ +· 3G ?„Ód‘dX&y ÎF‹´8нB“…<ªGý\^L"AšÍneõˆUE–H¸^›€ÊQ”([+G¸ÂRšÊ/`¹[†4!!ös!7› ‡`PA`P¿&M„B¥ÑÉÕé¢)rFž®S«´„R­U¤ÊÕi*%!ÿ7ïUÖÔ•…Ͻï%J¤¢Vã‚gUF!aq !h4$˜E,ëD!,Š B«-Ð:Ó:¨Ô긌KÇN«ƒ0.ÔÑZQà+Vì¸Pµ3ÚQ*äÍIAt¾¯ý¦ß¼—›ûîrÎ=ËνWØí £V«ðã.²ÎÖ¨43ü9ÃL%ªWrÚ üTémìTA*…Ü ä°©7èT +ƒz>§ ˜¥T8ƒÖJ"š«Ô©ð^¦é6_¥Õp!:¹Â R(‘+5Ûº„J¯Åõ8y¨a¦V‡²ˆ:…ÔwjÀ©‚CÔª™•a!:¥^Ïui…FÐ(Ô¡V.]½"”;X©SÌÄf§–Z¤2h¬äAø-çBä(£"T-×q!¡º­^éf[džJ­æ4Zƒ(@i3’Zi#Ph5zåœP^%W»!‰FePÍí éV‹Zé¸@y°|†RïÎé•J‘UO„ŠG g©õhi… Ó@ºÌß‹ ‰)˜!Œq\’)É +«øDcœ¾=äfŒŒ˜T ‘1émàN‹^œjäRF#’Lf.ÆÈÅšp(ÎÆ$:…‹ŽMMnÀxSò[̈ÒÚÓ>Î@¤Z%PÉÝE;}³¼N˜wö/6%˜Üãñ(±¦†Í¦bl ÀÒ‹èÅ#N6ig“Ý¿BfàþÇÌÐ=`ÈsÿUiœüKžûž³†½Á¸Øs•qc¼e>c¹‰¾²ñ~~^Ò³÷‹Ó÷êt`5òËé ¸¯Ð¾›÷HÉ‹m*Å[”xÌÏÜ8©¸ÛÖâØq^Åß+Ž¨ò:q~Çœ½Ü=çÁΕ)#—Òªk‰õÑÕñ3 ǶÜÝZxüéw²k»6&‰¿­Þ³?S$ü8ÃËx(löð­EIññ­§ê"âÂòù¯ßÓœòæ*nô4­ÿ`ѵ9–<š,.Wk‡ÌJÝþ »õbyÿ¦Ÿ÷:v+àÃŽÒ÷q¿Úè¸#B†û nzž!›úØ»_q +¶ön7 + xm¥tès+Ù1²î;9‹‡«®VY}^*é"deØ~3oµåmyí‡ï7–J$ÒÃ҅ݦÛËÂ¥aÅÒ,wP€ R!Áˆÿh°NÇZfˆ†ÅØ‹­xœ•æ’QYoXqЃ%§<ÌÉ©Fóò¥F‡ÚFvQ&.±å¢%Kñ4Íé1:­ÉIg2™eÞRÏöÙ®m·h”+Ê G7kÄOœÐ#Üih--+rsøâ3f‹—c¦ÁàÔ;ó¥CºC§ÏóF/‚Èy>"9Ø®A~žÞ2/o_™WøKȹ¢ß”×/&®ï”cKÓų{ú3›@`tuA‚}YÍ%vGÂ`ãû›'Øúl—ÎûîpßÜe÷3výkÍ?ÊG°óJbîFû8O‹yX9¹à̓gþ:rYÉõI×KuöÊ–ï¹As+=Cª>ó(½8õt\Ãú?zl2ß­XüÞ»‰'¶®$¹gœëOWd^¨j·íú™=G´ŠS.wÊ×]1×_ܯ]Kì+IÚ“°A÷›ÿ Žõömxn>÷©ÇátãO…sZ³v­¾9ð\ͶëU÷&I‚î$$7ÇŽÏs¾y^…×ò­Êš}6Ž¯<óúÊ|ÿæ9ÂúHßÝÅ.cÆùäÊC·ßÿØÃjç±±7?Ÿ7ש±5ùÂ1?²~vµîìqÀ‡°5¤‚B8'[ÂDC< Ð^¬¥;–åB·'Ø”dßÊÞ¶¬~B¦9ÙøÄ:ÊÞÆò8bíÌÄ€3ßØQnZg[Ç-m¹ “a~_†Í8b„)èÑ7©šÀ(Ž8^¨Ño øBñ†p‰Œ‰ÖoÞ£‘¯Jà<¤“l3“éL;úÀTøŠŠÀÜ ?ŒÇµÈ,*aw⊈‚ ²Š‰dJùÍ|+aJo7ø3Ô; ä+ö,ôƒùI^C +'ÌBûá| ÷Éd>ÉEŽ(‚FIréIK¿3’Ì‚-p„x3÷Ù(ðg±Ï d°2! Þ†<Ø +gf/á[  Cítð…¸J¼ˆ™H–S9]F›˜t¦†¹Íka(®4”6Û¥AJqZ¡ô!ÈX2Žì¥Rª§™pf?;‚U±z6žm8´5YNðïòûøz#õØÂ>8Nd(™DÖ“6ºŒ @+Ù³ÓØ´¯D`FÍÀ6ÃN8]ÂUdd¹Œo½ÆD²,;•­âgð |ôF¹\Ѧžàþ FùÂ"XÁø 4@#ú¯‰¸éDIf‘p’LRÈJ¨€ÑpZHÿDOâýn[myÊáýø[ÈËe @NQ˜áSð}¶Áøþ†ÒÜDT AëH$‰"¥¤Œì&×=SÀüÈB[eª%ßr—¿ˆ&ˆ)ø!/-„A8îi(×*ØepþŽz}‹6ü7Xˆ„ü–¤’-¤–Ü O¨+Ä7—n¤Gé ZM›™4æ#f;ó)s5—Xjù8>ßÿ.(§b­‰¸3Y¹çÀ{ð>¢‡ÏÛÖøîÀCxlóK„Ä¥WDO®IHYGÞ%I9E’ÊR!íGRGJh"]„r\¦õ´…ÇLb¦ ¦ÅLs™©cG³þlÅ®`·°Ø„OÁj¡¸÷…¶Ï,Á–HËv”v4¿€bÐQèŽ6ñÿåÕeuÄwß»»I Çßü9ïø¸ˆ\Ò(BÀ#w LB€;dà; `¤( ƒÐBÄà”)íPul‹¶Ž‚òÊL¢Ìk§•RËŸÚŽZ:ÐbA¬¥*®¿÷]™vÚ{ïûn÷í¾}ûöíÛo—槛aÙ§ ç^ܨè¬q‘>¦O¡gáÎ|ǹE¸™7q ?ÁÛОæüD»)¥ô eI¯*ƒ²LÞ#ÇÉr9UN“ßq´erƒÜ$wÊ]²]~æšëZêjr}ϵÞsÐuÞ½Û}Ü}Éãó<âÄšš×;®qœÊE üqË»`íQ\FyÙé7\BïS£ØÌ­"Æ—ä|{ø¾jÕb?¿Ê+x2§\÷—OKr?(+D9‘x‹OR5Nñ»òŒø3ÿ(¥1Ò Ÿ˜F. C¶óLœ¾ #Àø^>/üMç"Ú+g"9)ÃXÿo¤5b!½˜ÇÙ—iŸâ'dù)máõb4‡Í;ä"ñKž*‚¼GˆR®£ +¾&þÁ‹­°çã¼JOgqÃvÉ‘îYé-ˆ’y®Ë®ËÞy¿ŒŠºNÄ[n¸µ‘®§øð4ÜŒÉô¶ü€ëäJQÍWHºÞ’?½ùLç1÷D¹K\àgi¢ëâ÷o¼!G¥Wvz‘‘¢·ÏLokÝ·äxúºk î÷·©ñíWØÛ¿àc›c:ÉG—x?ýš›q7"ºYGrî…Á¿qxžwã~Äœ0ÿ\ü˜²ù4×»r]SÉÝÜO-r<¯ä»Ó/¸vÉ=¢õÖ +ÚG èÿˆ8Fßú¾h¸Qžî'-z…G®wq£ø‚ZÒß¡Íé­ðÁÏvâûô MuAä}ÚS-_tO ©žQxf">oîœÆÙ õuÌš>í¾)“«&UN¬¸wÂøòq÷Ü]öµÒ’ÐØ»ÆÜYmŽ +#G ¿Ã_TX?lèÁƒúòôÏÍÉîçÍò¸]R0•ÄÌËPÅ–r›Ó§—jÜLb ÙkÀR†júò(ÃrØŒ¾œap.û +g8ÃîádŸQEU¥%FÌ4Ô‰¨i´ñüú8ৢfÂPWx¦»Š¤?@3ŒXAsÔPl1U³®ÙŽYQÈKådGÌHSvi ¥²sæRùæêçOaù±Ê” oh¥ŠÌhLšQ­‚’ÁXr©ª«Ç¢þ@ QZ¢8²Ä\¬È¬Vy!‡…"Î2ÊQYÎ2Ær½j5R%Gím>Zl…r—šK“ âJ&z!¬Uù.|‰Bø H|[oª_Ú±‚å†Fm{›¡öÔÇ{SúH@æŠ`e×`éÚŠePD«¯·’ÙT“Ó#Ö +Cõ3«Íf{……)²5<8PTnOŸ£¢˜a7ÆÍ€ºÏo&’Ñ;RCÈnxì`aØ(ìK)-Iùf¬™×äöï 4õÐÈa×PmC9Ykd΀(c‰Mâ&6R¡_Md/©~ Æ,µÇ°\õ‹X¶¯RëùÊô™†}pìæ•úŽ$»Fxõrm …]Å¢¶q¾mg÷UÝ4•7'ƒ×× ÷0SÕÄÉeâ²ïL™Ãé¢Ï‰ÿÓð%´ß)_ètàÿ’â¹MŠ#CÇ'•ãHÈÑT5 ¨òœwÿ êçÀèùAUù¾*ïÍŒäsW‹­8cµº¸åO&ô•Ñ]¯¡<Ž]2òsQœ%|NψmÄ…S„Ðq½g®T 3­×d1ûfL*-1‘Å&:F´7îOЮð›D[:méph‚…44Ù¶šjöXM-6ü¸ÇVqÓ¤æÒWQek´ªØ8L +`p·5ò´Îesâ§ýÌVž«eÓäeÏ  ^5ŽÕæÄæ²yyÎÛ¼ša­ªRu¡ÿ°Ê TC³ tú žµª|·­Æ·-—1êW—«Á‡Í¶kL£Æ¶ìd[ºe±iøL»]. ìÕ1«;d´¥_kõ«šØ—ÕÌ•‡‚ªS&o¯O…yûìùñv¤cÆöÆøT«:‘ Z¼ÝÀgÝ=£34FµŒ“= ¼Éß&jq¨.gÀÁ—´19cÞî1¦%m"3æsÆð+%]‹ª÷DzÝ­ež‡(€Z®÷OºNÐCÀ¿ÎA¿@~Ÿ +Àâ +Ñ!RÈ´_EýöMÃ燆é#äâá]D¾ÝŽši$ê‘ÈÍÒËȯ‡S”¡byuÑkÈ×ÐzÕÌvdöI:EÄ“¤äí…”¦Ï)Îåtµ¡Ò|2½NLE¶5®ÓI:‹ªì ªÑ7Q©µÒqÔké|ú<½‹*ë]Žr?ú;j‡Qß\A}XÅM\Šú³5‚¥W‚üýÚuÈË´Ž®¶V×|™†ÝfÚËÈ/› ÿ‚&µ=ÒGäP1»óS ýõæ>z”g`oŠŽQíO¿ÇSøAd¨3P‘žåwÀñ$²ìNäöG@ÔÑé}‹’ÜŸV¡~Ú"³øuÎAM3{ßGp~{žÀ“D=}BÙ–óX°^¦Ò u£Ô]¾r/ê‘ݼEä‰;ÅdÚ +uÀ:g芨¢…à.‡m^OŸÇé½Ä¹×ÐzZ…¬_Ÿ ¸îK°Ì%ÇÞŸËÉ8»ÃÎóð…Xý¬ól‡äî' »éçì–„¥ô98Ï迯úਪ+~î}»y›5/û‘d›—MBÈf£ò!¾„@”-$„ÝÅ`¢AŒ.õc¬„Ò"R>ÇZ-ŒÀ”]âLÙ•Qûe–Žµ©a[5SÊXE¶¿{wƒñ«Ã»û;÷ÞsνïÞsÏ9û®Ä$Xó ôÉ•Âm­V}–ÞIÝÇòØyˆã{<AáU;é7ô‹ÓžÔ§¸£yMc¼°‡¹Wh§{?ÔþáÂWÑ2Qãmœ¼Œ³©$…³fœØðî¢ÌAËðÝï¤;øQDD|œ³³ì,½Ì>•6·Ü¸•„]ǃï +ŒÁƒ'âOÒŸ£7.Ûs#™Òžã6•ö¤Ûò²M?G̉3=I·'ÃãæÓJ~§àCÈá_Ÿ`–eô0ÝOánöÛÆ°“XõÝÔC»è)Ô7ÁSE¤®Åºß•Qú5É}ëˆ!NßÄyÎ@|:i"lΰž¶ð$”£tm¦-Lç%ˆ0í§sìn¬ýAVéºD¯³¯ÐúxóNcÔ˜ÌU”Î<ÿ~¹‚^HÀ7Í(CÈP¥XAzÁû`ÙµÃv”C©Æì¤Z+¼#ÇW@c +í¥Å(¿$]Žÿ€bÈ_Ðg8ÑCÈ1úXŽò^ÚûîŒÀè/oa}wÐu˜e3 B[Dß2p:ka‹CÌ#£ï,û9ÊíìvEg…ÜdÇYŒÿŠ·Xža v†NÐnVÌn¡$ŸÍ¬ƒ]Í›^C¦ÌaÕ¼ŽU°J²UŒ³l%tï…­ö0·ÉŸÉÖP”ý‹u²¹t Y, Yºœe;â3³?³ßaÞ<Ö)ç­fS`u1ãåùx=m‡ŒÓ >—=Ï­`âsLžÀ[—=í +ñM/Lm’å@j“ìÏÈô¿QøR¾„?Ê.g“+ÅÄHú>\ÉÒ:ßÀåøùv$"^Æ!× Æ|g×ið©¼QƒkÒ;8¼eÞ"£æ¸ÄÃß"´oû¼u𳿊ù†£nGs‰²ž^ Ú… }€ ,¤++¡å O +mPV¾š;ÝH(+‡MQzùúáºc”o$¦Ül”¿¿BOz“¾dY²"Y›lOö$ÏÙ’ždIROV%ɶäÑd‚Õ.[²üxýñŽäâä’dWrµêhž©„ñ²¥xm˜JQ×¢¶’V;ZƒÀàÉ­Uº±¨nHº±¨>¥‹Žq]t `À¸d`;¸Òeä؃<¨´óvÅâ®mžÅ^àIà`¥>>HÙ ìNªåëi@â½v 7Ã}8¨RÌHû2\+߈2ˆ²ž¯?PëÐë‹— è#,ÂNÕúÑŽ}Aí€~mù€^FˆŸlÄ“ + Hn—ÍhÎåÜCý¤q· ø"´DÒb£¸_;Ù¯ý±_;Ò¯ õk]ýZg¿¶¤_«î×ì&£8¬ý3¬-k7†µ`Xk k3ÃÚô°VÖš],Ê"¤Ám] éµ’–Iª³È°FÙ 64Üö´þë¡6þ¸¢;Šíµ°?èçÚê«} Ñìñ®ÒÛ½h러ªÒU¹`лë²ë²̃< –3ÙA£^5w©æͪ٤š ªyµjúUsšjVªæTÕ,Uómn›Ó–k˱Ùm6[–Íbã6²å'R§Œ€øàÌÏrŠ*Ë"¨E¶\PNò{”3Gîç)!êla¡øá~ +ÝZ¿ÐYž`v\­å-,îQ¨«¥(Þ%ÔÔ’xc Ïî¸)²Ÿ±'¢èÅù£øîŠ$XJ°6MÆ 12‚³š¶ih²¨¶i(%Ï}Á¢ {Ž«i~ë÷¾ |ý&>¡Ž‘Î'Á6:{IÒ½†GÕ㪾OÕ©ú ª>[ÕëTýu¨NŒ2å(SŽ2å(3®šûTs‘jÞ š³U³N5'Ž*òÆêŒÄ÷z£ñ频òFCñ3¥=ØWïœ×:¯CŒðùÔ6o‰äÏoFC8J©Ç»Óz]=»Á»…ï¶õ¨üÖò‰JêQ‰œ¯ä[z^Þ%ô*D•ÖóJ=oFÏ&õöwøæµî÷ù¤NQ‡Ôé( ©£¤u|tÔSä“:>õÔwt¼W Sñ½:NÀÿyæ­ž×Ùo£–èÜžtíqÌ‘^¤µÏÙ:y”ÞU>¢«Ѹ½¼%~Uy ƒEçõ¬6+'ž– +íY¾¢õ“G-ÄöHí°µŒ¨¦¹¦Yˆ B” ¶##*Z?Ë7y”íɈœ`»ðŽ¯½aÞêÖô/¶î'±u1,ò5™÷t‘cQüá€[‡‡1 ½¹‘Qüô[ ˆë ýV$lÜû|.Ÿk*“.–*‡/Vú’J-‡™¬Š«–mH´ +Uy +y¾*¼´Ýªxù$ˇO9Ï/<ã<†j¯w~uýµ×0Õ×`ÙV|ñ…b>øÀˆíc©°Õn]C.še`Æòl{~v¶=›ÙÉbÑ,t¯=Á[‡Ù½H_ÁWÝgž†9/Œ£ÚZwaÓæÜ«;ßÄ´,+«¼¬rf]«¯Ÿ1ÝSoùü¢ßéÉuä¹”¿9 Q[+¾ˆææ9r'Y÷ˆZ;5R§-ÏY­THÚg¬9–õvÞ‡~eï”}úï+wûßö[‡•ƒŽ¿gýÅýné{Õÿ)ýÒoËñÏ,½®º¥t?:eiå3¥[ýÙ[¦k|fùœŠ™Û¨™EK‹Wþîêmâ:ãïÝùßÙç»{w—œïì8þ—Ãñù_I$®nêyí @É@ ˜`@ ´ Œ®PH—‰²M¡‚Ž¢ÁºA à +Œ24Ô¡MÕTQuc+ÍʆHj„àxßÙû£ÍÖ½÷½w–õ¾ï÷}¿ï÷´m7:¡]BögPŽPWžŽ¦“ùdÙ@úÈ!2DòÃ0ü)jhyÿ,à ,ËóNá<#Šqä©}Ç‘[­yx癢œø¼›ô¯áœ@dA ä]hVQ$èÑB†EVà +Þ(å,ÜjXê"ä†×µú„v¯s˜²ã+f(CxÆþ¦^ðŒás%œCmcc´×š4Hä +F[»Øšò`ávªk̸ð_[°Ó`|`†*Þ=Jä +2]ݘšùg Í|ln JÖrj6·Ð{à€O «Ì>ÿS•“«k-wï/$B•_d¿%=×&qÄǀ뇲ì‡üŒ£; ¢îV.ʶž•ñyyÛøõ›è{±_öKä\Õ'¿)éžXol—ñ“èúŸÆ {œ3ø˜P/9t·5O­a’s¤øl€e½yüÅñêlØŠMà½Ç91ÔÛá ›”²9ý9Òµ¤ +2ðXÎý§!Ä~èƽ¬šÆ7°£’¿c£c]•†ʳ£0ÒñY[¡M!P§åt†¶íz5Oqìx+³±3Å먾xÅ/Þ˜ZþÌÇ]ݨË0ê‚Í-傶sDê¡ ¿Ã!›½Rî•j·ÙìÈŽyj›ÛE/Ñêƒ?zñ£8Ò÷­úï®Xs1)8iÊ“uo=Ú°þµñïÝþZ»‹ˆ¬SxaÓÑuëºR_ߺiQ½è}iÙšw6NÛ¸yíøå_™©÷ ðÃAˆÿ4œÊô¹µžp,ÜH5µÌ—WJ?²î‘÷(‡"§#gõS“.‡.G~¯»§YÛ­ÕÓ”Në—OÎêNå¼ð¾çbˆ5¬ÏÚžMÓ†²¬å;òe‡þ¾nk¨ÉÕä&ßl,N²êY©A-uSêc–h +¨¨fj:`ªpWn’j“=•À‰<þûñh¶ÉÊ‘qŠY»›Îªp ;05[wŠÚÚP1˜sa—:}®z˜ºL*6‘*ÁSé¦Iub˜Z ØÄJ4ù>%̺ÿƒN¡ÆâM.Þ<®û™ L“AÄ];ªËS'pCݸ®.ÁM*UÁd¨¨ÂMåerKóÿÆ’>0oîk÷·ìîëô‹„lùÅÛw:ûŸªu°ïì—.|iõÜ·Zü² J®î{k;u8þznñǯ>“ª•üüú¿lœñ|Öî„ZÂsç,}jÝô¦‘#ò¬ù»Ö,; Œ¿¹xͪ¢aÑÌ!*Þ:!´ +®å‹·2?`[w¢‘×Óg"gR§Š‘{)ñ¥¾Éê³S/êýI»ÆÏÔ—èK£/×ê£Ï§ºÓÛ‚;¢[S¯¦÷ù~©}î+jÄn®kÔß´ï +÷Gw×Ûm[0.¤±àý¤cˆul â ¢µU©š×â“ m‘¢Sz$ê²ÆYš–&e]n-{Oø³À!ÙXßÎsÊbåˆrN±(ÚäHv1‡k¹·˜;ÂãþÀÙ8µa?À}¼»+¥PF|à%&Â%ŒKpO<@jÝåúÔêi³HòÅÏOFä€ßðÇÿ1”f·™²1Ñn9ÊÄP(5"¥D3` àÒrúpÈnÃ^ùî²çöÌß¼kÞöCÿ¸ºïìÆê‰"íkï¼×ô½²vˇ/>½dE'%õ¬<лèÜæ'΀|pÃ"Hr-‹(ô(h†1ËQ %ñ¾ŒŽi$òæëù8JX’>ÍwŠÈˆÇc8™HPŒÓ-1Oõd\µY^Ç¡°^C•5²§6šÂ°>Yû¸¦VK")ÛáP­Ï«™¿›[û¸˜ ¨"Ê„ˆñX¬Æ«É^¯&ŠN¤b^NÇãjá#™aœS’0òÉŒ“t$™„(ʆ¦R¾úzš&D0œ¡p.øëgœñW¼F:¶8FÅòTã U},aÀY×PôŸsÇT¾ƒ S?G)Ütt†y50 ÑG "+ùn~ å 2ß´ÄVÃH`Nô»¥kD/„¿÷Û@*]ðµ mmæÓ†{Ô æᙎD¾x%àyˆ³¼Å€ÿ'ãUÛÄyÆß÷üŸsÆïÙgÇßgÇgßÅ—ÄIìœ8ö‘@Hh;Ò&ýÀ--t$´e[! +c›†&˜´Ò¢‰njW(ê +T ´"6mB›¦1¦jÓ”V[¦MÊ:ììyïìÚiZ¿Ï{ç÷>Ÿßï÷üÅØú<\¤è"00•…;šî1àƒ¡øž†„ÇÌ«»Å Ž7lã§~0™âØô¯êN ­±ÚÏ©ƒÔ˜±;º“|cv%µËt<¦mk¯áýµ+ KiºÁóBäß?klãý̦êQèÚ¦++"jCÔ"¿oýDfÚºû»W·>ÐýBë®nëXÇëüiþ•s¾pQó…YÆ’=kŦh&•jóùC"2 ²†µY ¤°G3ÝQ§9mHPâ ìŒEÃ=™Œ¿MemL +:V¿ eŒHZrMHœ&pÅ(×Åsenš³p^\Á>K©QÔ…ÆQšÐ +ó´ÆfAî³ö­Ó†¯§ Ÿ£B¬R¡[ôöTíh̓IŽZ|ȱžr8ÈH,8ÒePñPñ4Vö¦9 ®m¯,Ü|·Ã[/v%¼M¥>(án$ÆNÃÞ4-æÇzúô"3-ΰëê MC¾qtzúÙ o{4?Vý–ž±AÜöÅ=úåçj³Wñ[ë¿»þÄ›µß®7òtpsù•~å«®}eªµ…¦‹é[³gùØsw½_Ø;°éKT£n,\·(æ“h[µCâÌ…£Í¹ÇƒHO^ÊîXþ!ús|Nšº…n…ªÙÛCŽj÷¤½½¨è)z³+GÐ:ÏšòLy7I[å7âÇe"K}ÒêàH|µ4T˜(4%ãI9î÷É«ã#òêâÁ iB~*þta{á›#¡·Cçü³ñ_.Åÿø]üšäCN‡ÕÜ"i&Ç>‹ÈŠC³ ©‡U(‰;«â£îf•hnŠ¡€CË=î.»§ÝV÷ÑUQu| <À Tð–ÓÃ{Oú*¸ýe=¯P½T¨³ð(=?)-VÁÀP‡JÛ­¬;‘†{ljÔYH¬[ýÓdõè6¤ÑLÎTNê¹Ï%Á^óûkØít›{¬´›×–L•ÇÒ“9Ù-Úóê– ÏxÂ{7V·?õø°œ_×,Š-{Ÿý¡Y µÚÆÿEÑoò;ø\*Ú¹þÉ•“‡Òß¿ºa£‡wxZžy±-ÒV<ùù¾Þ_Ök ž£#Yð/ˆRx§¶LVܹXÔí/F£BÎJ.©f)ëNɽæú¦Fà EI=jëZüÙþèÚؘ|˜œ YwÈß±‹oÇ*ò/ÅÄy…M¶âœ2¦<¡lUv*ûìûı3â/ðò_ÉßøÊ'ü]ù®Â _°ÎåÑ%./GS¡PÄáP(¨ +К¡t¯ +4´¿®pY:¥CˆáäH½ŸàFKä†}€œ¾åËë¼n½:¢“øý\søƒo½¾egmîxw¯z¾4´Íe¼³öë:ŸçÞë|aÏí ”ËíkzÞyä{rìØEÊã2èï]Èÿ 3®õË$Äe[ÄUôØ„œbs抠f£N”F]$Jbb’¨¤tN’&]¤›ôÌ;17¡Oîî%udø)"†aE_r&$Fj–¸ŽLws7—UW5¯ãnžâólnÛÅ}ų§ùlXÈ«ð°ÿ€|M¶¤óyµ lµ’ÍW˜¯ŸYÞ1(f3*LÏvv¤ÄŒƒuÃ\™„¯ÛmD‡à ‡üðÒ(RââhR–ÃáˆßfŽ 3&aL +ÆaÁ#‚'v ³Âü ” èu+Ì·5Çà BÙ£†•c©˜âZ€a•S6¡ x’Òµ¬Íwå™üyæ0t¡w´fg.šéÊ0H}\ÖÕ\T5‡·¨ou;sj± «áp:RŒ”#¦ˆJ’ÉbQ9NcÇÙ2;Íîf-¬JL¦b«pëp”ΑÆ…²`ü"´ûGuê.ZÍ?ô!\Z½UÊ“OOáNÒó Uà=æà£F‘9#ÒJD橹ùQŠûÖÏRŸøTd#U5?‹}˜ü3}S½„;«èÖÍFh²,ú(_N(_n(_ï†hÌfJp¨¤“›b‹šhjàx㦥näÞu‹{ñ•Í̾¬Ñ˜¬¹»‘‚{Æ(f¦ç †LªÕ7K¸C ks¿±T`zkâPCŽåk³KÖ£v]ôÓ}WjLZYû»ÑßL ΃ +îäó,ÐOƒo:åÌq¬¹:¬=4G*LÆ»ÆWl—N[>æ?–¬Cžñãñ÷âWâ–Ëø²ÄÄýJHfY'Å)Ïq“„ãœN;YÂ3¬MG¯Å²[, cÇ¢ K~ŸÇÅŠ^‡8*R Øæ3€Î39”"ºû<¨Ù“ÉD8‡Í©àÍïóªâ÷1Xµ•UX›ô+X¹qlf”µòßô¤iÏ,6p?AÙ”¨‘™i<ú2¾ç:°Äß +®jõ?z&9>ƒÝÕë錢fÅ)éTV +mVÓº´º]d-Žz’é>éA/îWÒ½²¢ê!ÖÙK F>ânð25„hü¹MÐgB/Æ_LÏè_ }) GâБ¾×ÛÐ÷n|ˆ§}æ>aO*Ñ°bF"’˜Gëä6 ‡¬^·*( +¨šIŽ[ç”EeY‰(sèX€\VôÛ/­ FBn-‘™š]ºŽ);–ÇÄNÙÝÞ-/~cÁ‡‰•¯pg-¬.rŸöCCu,陽»cëó}8ùoÂú´¤ƒ4)7ÌN +Z¬nÃ×µQ‰÷VÍ­-_’ëGí¯Û˜SÖ 8ò;‹Ÿ++¾Z”"µ-^5¼ƒî‘ê÷Ë»NVÏÒ×?óVãÖÎ{Å{žü~é£ö(ºÚBû/5ŽRÑuöØ·¡¡Z‚P†_Ž»…Z»*;«µÍL=/¯oÍEso6b2¢Ëê‚®èX/[ºnšQËÌZhëpi¼<ñÿvÍ1÷µà•Rc·µ¾yɲcÛ†PVÛÊ­ËxV°æ¦ÚixG@Ö¶rÙqòyÓL­L†À­YØu,“²&¥F’ew´>‡Û½¡ð‡üŽFEëd}\FåytH(A[$sÍç5Pö% +¶€Ýµå¬Ÿ{2~ÖkAÝŸÝÈs\õs,éç´î¿­üA‰0”(ðå#fLžß…©J •æñï +æÕ÷Т‡¨7éöDïæ³c^ŦЄ7n2[®]T°4¯‹x´½fÍã²å@4›UÖh YåôÊ:Ù>ü†õe—´»}KÊmÖï$qéè_.ù22øaÚsl){ë0 Ô¡³l³íbâã…B“g?ž Y¦©/–š6Í5 À"Úþ {N,]êì0Yðaïœ +àÈ;Sg#rû4#w"ÀŸRGõz¦.dê›ÃJ#ÜØÜÓ‘ª*pý pýeÁ]»+Tá[Y»û+ßoÅ=þ™é +¶ì…w²0µS/=Ò¿AwP>ŠØ¼¥½+N㬋H„SÌÂM „pcØ  Šj¥Á‚ßGÑD³0¡<·öa²_ÈP…ÀªüαÀT–YC€¯,=ù¦s,¬z!WwØ œFŠÐf°¥éf‘nI4X˜‚P`W°ÚˬY¾÷÷›}¦rmÀ]È•Q‡8x,*b ’ ËãBÝœü×@çº4:Ìýé üË—Ùŧ†ªöļ•¿Î|µÐ[:ÎJ?­}÷5tuõ{OBßÊ}ò¯e=$®~íÕÖ@ǯ 3\|xã"àb…/ ¢ ›D.ÚסpJ<.beð¹ø¾´ˆt<ôÐG6»|¤n˜ù£<ô”ºÏ"¾gè8æp€™Ãø:D{EcZ‰ŽÍÆc˱P¬o0 òá"ÜÛéìoQÇUÊýÝ9uQ]VÃjvLÞ@tÿm 41÷y2„A‡fº¤îZ—tTí)nÁ€âŸî¾üÍ¿YÀ£{7å’rJœzÀ7胅ønýƒEFDè³=ô!Á6ú ¬º'¥¦, YŽ’¾ÙD\Ä®P“¼hRcK°E®;L~ä@¨jAβž.U3~âÕ¼?J÷›ÕdÇõ-9ÙÙâ9ÃÃcú1’;©}K{K;§ý‚ܱï8wòkÃkdm¤W–‰..Káp(•Ì˜$ +ân©ˆôþŽxí(Ñ^®Ä$˜h–ÛÞc̆!xª`肧Ã5Â*2rÎNÑpkª ßTÊDU’D¯F‡‰‰E:”&ýÔÌåt½ ˆy<ƒ!H¦§iPc§fÌ"£U2"±$‰Óѱ±X,v3RÆ•êQ`Ú6"Z¬":¾­cɆeŒˆd ª½3µH±HÜH­q]ö_º´¶½âÒ|UvMwÌeWq-wܥè.»a—iåâöÆg"¢(ER)‘HH&¥ÃÒIéuéÒ&Yº.-K¢sß­Gj®()󨊎Õ g”eÀS¤‘3çQ¹Ž²nòÈ™¾eWüJ0‹sÙ·BˆvÛWãoõ1çÈ~ú²pfÈJCëÔWHö®Àég8\Z—Îæê¾`‚{zƒƒG¦7¨rÊé²Ð,`Í㽦‹ºªûLƒNŸj}N‘¨£¤!6ÔmÚöÜ3d?~^ܯRíùÜ2ãÌÎ7YüiVŸÍÍz?vÎãóúyç=üžþ¾sÍùíÀ•Á+…ë…ëµÛ¿µ;98ÞýÜŠsß™žzòŒ“bú>=ÑÄ?T‘~¥‰€NwÛEF…¹µÅ6Ì‚ü »×ƒºÿTV†§ ð§ ó§ óÙõàÞŸ ­‰|6d>2Ÿ ™Í¦\ÖÀE ‚ËBN‰ðW~x!k>…¬Â¦§Q^Ì?¶&R¨ÃLONÁÿ|åçÊ‚ÏUWYܳPaéO?C7®±‹ÊÂäöêõ´c¿½@3øãǵùÊþ «t×'½èxÐq'¨)°Š$D…~ÁFÌœišV€Oÿã»Úc›¸ïøïwgÇóãîüˆ“»óÙÁ±œ'ĉí$k`yCPI;˜ÇVZeI A; -‹¢¢…­Ú‹!±U]³ðˆaÓÂxtÖVZ j5­ 6tLÂíþM²ïïì¤0‰Éþåçü~¿;Ù÷ý|?€ev¦òƒí)Š<”z&E¹wQX´`V°HáeT°•ÚâøØqÏqO2†¥p$‰êp\HŠí¸Ih×KÛ%&¬ +¥q[žQ µÀ3cãxGŽÙJ~¯$McädÀŒÙ £)éZQaŽÓŠð,¥ñ3¤e—¿™ûÆô×wè–[#‰hLrŠ¢DѦ%Þáäy‡ÿV#¯p*Å¥€ S:Na³cdùhX<+|‹Rø’^D„‘ÜZf`EŸX)®ûÄ!±@|§¢è,^ƒ@2.ZéâûxŠ/ŠBØ,„»˜÷<¦ô<‘ÏEÙñ1˜6gÇØÏÙÉlŽYØsçf|쌋ÕL,tw®Ë¡Éd&,žévx‘DXK÷CÛ¢þ$"î :ê ¢ˆÿ‡žÒ̸̻…Ln]ȃ\«.¸6 ÇH­Î#}V5þ_Ç¥£Ÿx÷!”ö‘éoøåÜ*®ÕVu…_Í{š÷ÿA_šÕk +}0}C7®{Ù„iuE[A‹µ…kó šô §\q©Yßfmãš‹žÇÛ©ïšvX~­?n=Î+ú§þ¶å–õ6ÿ…kLrº=e\©g#û´gÀ³Ï9(eÆëi{™Fõø£ $fwcWœÙ½òp<ÆàÕ >ÀŒ2ï37˜/™i¦€)–…KdVCp M.¡É%«iÜEï¢Уôûô úKzš6ÒEÞ4”š™ø£ÅZŠÉ}FÙÆ,檫«*QÇ5oÔ‡­}fú‹a§èæfÞȧQZOž3OLïrRºÐÜ_Î))( \N7µöÏö¼óQï•‹žºº÷{˜9öÇÁU#Ç7úÿý×c8òÞÓS·~syjß_ºOà–ׇ?X>Dðœ?†Î¯„ΠޤÚ9Ì÷º²ç¢‡Â§Êt.òCë +ëåú²N¹3¼R^îÓo‹ýæ^qý­±„£ÊíŸüVØSµ`Å]lYÛ·ôÛƒ[¦®á›S·×¯¾RWÒ̹»kÊ~ÔÔ³“ÔX_wjE Š9©„™”Ÿ;‹=ñÙSAÕ]×°ÇÖàË5ç½ï¯D®ù?ó™l~¦Ü¤˜"•¾ºòÊH—¯ÝßYê[î_VÞSµ¾¼OÞÙ#¿ä?(òõŸò ùÏÉK.–_ŒŠ×}¸8‚•hCt~Õ_ûÜ þ÷¢†½×eŠòñ> +ɬL‹kê!¡G¡H–[_‰É©dTfQE'òpŠ"©B&,í‹Å**Œ‰0¯¤4L™æ(ååyX%Íåg©C¨ ŒÆIT=íòÅÁg—@^:!FoÂ=Ö WDò4ÌE¾8+bqG*šÁT3¨eÀ—ƒžÊÔ|€30I×ç}²r9éàŠmÀÀó–&'²dQ™˜Æð€1à{2ÈòœŸƒ™¡¡–‚7‚¹Ts$Dõœ:†x]@wÒ_à³È™éOG"ÅY#Æs&Ž /MÎyà\œ «!pý¥ª¶C€I8_y$ßç‘évÎø’ªçæŠ/¥sÌäA€Þß÷Æ룫zÓW}ÿ©{c×ÖîÝ8u•2Oµ?ŒÒ_~gýŸæ½6AV±{´¦{åîä’ç‰9zðèÁ¼@Ýßâ±¥$m kªCl’ÛÚVˆOúvz¶}âæ–¾Öí‹ö‹?/¼Ûi_'nñRMžvµÛ³T^®ö´=åéUämmÛ:÷Èå«ê•…ŸÎ¹+píêr•žƒßj’F›G[u»šwµh¡k£aº>fÓ#úq?²»­‘pb~k³$ê¬5(ƒ×œvÓ¢Òš¡ùáfEÊÐœZlíÔ먚ÁhÄ8?PjŠÝUëÕä'EŽ Þ®:šþ½¬ê8jPY¡ÑPüø½D(&”E“ãw³“Ù‰ñ´& 𞜘©'4çR@J#2ª*û¡ühˆ_Ð1dÉÑNϲQ4oz×8a é;‰Vb\›¨ÕJhšÉ7§&.g¡[Wè&Å%ë¡™íD¢6wœpç Ò^5ñlèvXÌœÛæÐE ’ÃæÐïÝö‹ËØŠšöÎçþ3¸éHµ¹À ëôÜÉawèöÃM×Ö5„O,ZŒ .-Û¯›Ÿd8Þjä®{‹y«A°ulykcWYyÜå]¿hý…mßXXI6Lìm¹˜·DÛ“/¤¶%–xXœL>{ðNa\ZV|ÂSê|Rˆµ§8Ï g¸ â©×Û+÷{C[ÑV®_ê ¢—,ƒÜ~ïþàaP´×øSÒytŸcÏs·\—„'¼#x„}¿Íꤞ¯iÉ/·x%I +¡PÐÁó’(€¹à»H,çòÿž§ˆÜÔUCŠÄ+˜E;¥í9M ‡%¤³8‰BÔ«ªÃ«tW{ƒôêàšàhðFPÌP}'°Cþ@½Šx`)‘:tÊÎË`)3p…UPªÛÚèPx\8Œë)Ve]9õs"–e+gåoˆ5²5› ‹:B¯ê(ì\?Ù룳(¾G9!=¾î,õâìb½±¥q ù0ñe’k;;C¿ùeÉÉöãÿs•¼ºã­¨œHëÖ¤dZ7ÃW}ç¸Æ®™xÖdÃðøѨœLbTN´âÊ}¾8g½æœæËpšUnµ×Pœ eÛ¨dÊÈ$2'g-j™bF‡ãmèž!5°ŽßÕoèDg˜üþÖ?®ûžïjYñcoÃêÁUxŒ®ÔÖÓµÚSä):iLæ&K“Õ]ÂNu}Þx¾º_9dªÁGè1ãXéXUïï!{èm$éAmZ¿J>¤jâ]¥NŠÅ”œUÔ5V ìRµ¢&%Æ#¢ÆÊa»3 å{­1ªª9Ž(Gª†®çPUA¨Ê BNQEQ¡‚q|Œ¸¢2%¾¨êu¸¶]0L·PȺ©ˆ›IÕ5¼Á¡ë žS† ¤SV&F~@2,£Çhㆠ²AŒ3h³*sÏAiÒsf•¬B¼L²AríÏ”½•zð"ê9í0,ÉÐ)c0ØÌ 9x–Û¬º<ˆÕs€ÒyÐi§jÂÿ,zqø¡ë~¥XÒ>À¢—·²Pz…„)!*\ MÔ ¨~’¡š@×/î<ØßMÓ˜wç>™JÒóß}kx½©Ïû7îÎõ5‘!(0-íû ¤­BÉ3.{ƒ‰àWJŸ'¼„ÎH¡Ì©Ájh;«¡XäE@Ò«¡MÜ„¥,øÒî~þ:Ð;†i æ>Y…µ¯ûk:Û,†Û ¸T„KžH24ÃnWc\ìâЗÌŸ²Ò¼R©øÿ@ˆeƒ®ä¨ÂÀ¯øte‰ 3¨ )•µ6 /-*ËAin§±óeEéÀ¥þ,¾Ïÿ8Üû?]xrßVk©Z´ñ0«ÑooÁ‡§Fpãm_Z-U”¶Ä ׸ó'~”éGV¨ï¹ž;ïq¹´D€ +Œq?pzpºFNy]É(—æ’JR§Óœc•¬Î¥µ84BƒX/Bt@9P4`€–£ÑZ¼Væïu ˜aæ +v1˜wºËúj!õ«Ó ÙŒ¯RP¼;¬âvv”ö”·Æ«Aõ.:Já"´«b.QÒE':Tk©ä¦ÝbÑˆÉ 0qA7ô¹%ü‹™u‚kš‘û™ðj“5R›ÁÞê90£Ñëx΄3éðP*ýt±àÙmb³ã^oÆ1ñNñ ”ÆOü¼ t€_=dCõPõÃq9Ê´ƒïñП¸óïEÊavÎ4fõ¹¦9Û4 Ù§©Ï6ïSòÈ,“µŒ'Œ"³¾Šˆ1vÄBRÜ›øìp}1z€æyP´WX£öN¯0Ú#FtˆAîK†ONËußÏÉpªð™¹÷ä¯ &5†o‡ ìf¶DÃòäçÁK?9£u7jìQˆ~nÔ‚"ä×Ï,òs/“´ eŒeç´º%«õ{¡¨42þ®”†FvË)¹~A‚?´ˆ/¨ùU9Ã…TiÙ°{Üi]àÈ‹•ÍÄí‹Ÿ±aõe|ä±\+«yá-}ƒ/e--tà«­9Ô»gæ/ñ?Zp„†=%†QœO 9ñx&–2Ó‚˜Øwê031×äOQÏCëN_ŠáíëGSÆQa’&$¸ÆvÉ×oo£RÒ¶ùŸ.‹ÅùKЇ•|4Á¾tßFn +GÿÚ[lôçQ²?z3Ftr±ïjg«nUb·×-da!ó-ŒwôMGI"Í(¼8tT8:»ë„R¼À-¯cèê]Õör‰qP²ÆRåË%¥\.‰ãÝ3\áLH0ð[J¥‘à¬ú¸<Ž_éB‰å6—ÔzáØ5/ã4´|À³¶-kå q{÷ ®y¹ú&¼ }QN—^){ÉLc¼pᆗL×ûÂûäT}1H¡ n·‡Be ^QØž2Ì–ãl™ó×;9Ä­7Þoÿô®—o<}$Ç-}zÓE-‘Êä6¿öø¡Ã‘g¶3¨nÝ~øâîïNlÙö×gwÿr׆›÷XÓ’´Âé‰f¤lÞè~nõß`ñ¯ìæÊ <¶fƒÆ éMþ3ða]¸émSY‡ReøaâüL‘i„H%[©Ò_УåWùÑß&OØ·hbM×–®ví«¼ÐuBý#ʸùçJø/¿Ðõ3|ŸÄoŠoT.‰Ç·qü +Å€²N1%&S(Õ–=ô”3òa@©T^¥ŠªR<ƒ'=‰ŽWrµÛ\4o'’â g{©T¥-¡vW¨*;¶Á‘DHÄ\&¦ÆUŒÆéù®Ø&Î3ü}öùçîlßÙñ9¶CÎwvœKl'NâĉCÖX4ƒl*tk”lÊJ»$ h‰ +”ŸÁ:B;@ãOÚRÛ”~–PF‘(ˆŸVӄ袃¡4–0¦¦tPbïýÎv€Mšíû~î>K–ßçyÞçqðB®PTÁ°\%|mŸ“ ƒ˜rEx÷E|0LgÓd€ Ö!~Àp N l~ƒ¯n2òu–Õ :ˆµ`3øBÆÔèà†˜ñŽÉ@ó"ê\ÖÒBp ‚’amºÈÊçU YMÊ6{\ñœ¦ªÊ®'e×T­<}uߥÝÉ®ûëŸÚÐ;G:Ô¾Iôüp^ßêE#]}Ô‰Ëç.îÙœ¼·ÿ\/Ñ(ê9=_Íúƪà WÜðYç[}ij?‚_µˆbÃ17º%`%æ¼U˜í­:‘{¬êBîGòÇc»©Ý¬ ºCå¡ŠXÅ‹w—÷ŠwÝÿ(§EïùCùC/eŽ3Öz›£AüNÜNé=rˆ%—S®4ð }n¢á̳Jp¤~«¨^SªTˆu¸«‚ë•„Òªt+7â®™"‡‰°äÂæÚD¸5ܾ¦Â®ê•×™6©u#äjoºƒ·SßãÐNUg ô”ŸEbê3äNfIL¾Š'gÈÞ #õYÖežë)õñsØRKM¬U°äh‹ ùÀ* ŽÏýI×Õïÿ¬íÚ"—Õb³ÏHÞÞÖwqÑž o}w§Öc­9&ÚzÊ㶙ô£±¡|Ú£ËÔ3cŽÝj± ßüÁ™½o\ÝÙþ1uCgRYÄ÷‡=ݳ…LG†¼Ü`Þ†ÁÅ#*)Á<`‰‘)¾ÜCF–«÷;ͱáûŒ†c*…©Ò4{ƒ÷¿lÙ¿–YØÃî°ÿ1ëuR½ÜÀ¾‚_a^•6(Æ-ÖK¿å·r_Á¡ÀïÑIóIËò ÿEéÏèS|—ù?ò?ä‚ÝÇÐ K;žvpOØ[àiìˆasL²Qü‚EÄè0ç i#;ÆšŽ§9Í./‰"#͈²‰5™LB¡œcÀ¡"„yšN`°KËçš9í}·ríÜZ®‡ëçtœ+„sqˆÁpK8Ž›±ÁІïcj(ÏM²ÀîzÚI`p×lþVKç2ì «d-SyÃÁ.þoÙÛ ¤I„UCêŒdt@mÔð!=Cm±Ø³Ò€žàn€¡f·ŠìÁÁÔè1Ù%¹©æI˜ Ä“FfÒÂø2-ÂV-â„0u¦‡ªÜŸ+8yûçç÷cãºë¯-ì  ‰¾Ý³®¹wõáä^êÎW¯¥?pþã_mÀ9»Îãki]˜¹úw/­y?9rdý¢ýŸ‚.ì]ð× !ÐäfÓ3ML›“°½DÌ«+šK?¤aÁKv«aá!;D#™xšgy“ÄH&É"ñ1úa¡‰r½Z¨±œAW¨p—p(_ç£%V2ku”ÞH1Ú:g]a“sf~Sá ÚfªÙÞ,¼àx-Uþÿ¯]#Ô?¯ÃQ­"‰$µèå¶@Î3H ²·!׋ ±CÜ&Þu¢»X áVN8Z7Z‡«¨ö[i iš&Ò4<®JÇ„5®K[±Xì™:…ìa§O¤ÆAèï øí:¨æQ—Ën¢ÒQ3[ѧ[iÓç•M^oͤm›Úòü‚8Ýí|ÇÕ]øS…Â-ÐZž2µXþ?º#Ø•¨ªMjããƒÚ=-×fçZÌ6¾49ºuðFÛ¾OVýæOsuólßö¨ïóð›¾VÉåp6¾¼õÚÁö³Ý/]ÜWiÞ²`þ¡nè7 íÍ­©B·ãSŒ®˜[½¸§íöOå +ê‘€*ŠP‘µ8¯&ïxàLàràf€Pe³ºù*›ËÍ—:KB¸Ôùt䛆¸ÍÓÂXäÝÑÜÀpÂ3¨ãžb9ÛªDg©ž¹yÞë®ÆgC ÷YLQF UÄUñ<îà1аø=”•~ÂÒ¦ñ ¨ÞÈdnjš¨«Æ>±uA^¤Wwb•^ò%„lï­Ž/æPzÓ°!ã¿}š+Û±Jµ‰v2^Ú~œLÇór?én»Úêqlï-îÜjÉÛ§ÐàÓµæ‰Ù¨ "t[»t¶™Q›ûzÃZ#·€sW’ktaà\ÕàžcIW +QðïÀ‚A˜!7ÊÉPF›—•,^qEÉÆšË}”UÞP£-­Z„¾WÞ¥tUv—ì¬9í1–{ÊJÊJ–-½T¡÷x¤2¨rþ|.(vÚ[Í`XV±.hÝSã¥LBƒõ.Ñêm6«ÅÓ”e6"ÄXæËö³‰„ˆb›Ø-ö‹”øæªEÐÌQ GI??Å\4m®ÞꢃøË8sÅS{=|IÑ&†É46Ì(l=I¸$íÖ× o²”fÃ줔ÖkžŸ«&ÙÓV% +¾ø¬+/™R>JÝBLêø×QT“º•a"zrl•¤ÆÊíy§RÐ8€ ¥dOvg¤äjq[f)ãì×-qÑÔ$¨AÀN\É Á§Õ,_8¹„¦Õ±& }F£)§|–d6¹ãˆµÌ»¥mö=‚œÍØ“,Ñþ«ïž^¸õØ’+Éõóv¾9sïÊɇ{5çw«ðx<4gZi›bãnœ˜öxèÌ®¥g7-x|wúò_Î[±'ù¨·k/Ô¶!êM`p-žŸ1_8°#\.çDÈŠQÀã´ð(u×xk"ÓééîFocd1½Øý¸+ú¿ä'óµ”.Gç×i±×áUýœ.wšÊ`iyE$½t° uè +¯h‚¥ÀçÍ8€ÚêÊH¬{½Ø‡¼ƒøݸ c_$Q‹G¦ V›-2îGcš¸F·+Á…™V¦éfú™£gÜu¾DÅ ^çÐçRK‘²Hkä +~…åÈÔ•Aâû€ëé 76Þò]ÇÛ4ì1tn'éß½ˆ´¶,ÿa¼zƒ›8®øîN¶$K·Ò­twÒéÿl)v Ö‰äj[K3x”%ZCH€”é„ÚMf€– …IJÊLÓf&´CÚí$_LIŒ‡Lò:™N?0Ó|(“N¡­i<4n“Ú¡ÓÙ}{’í$Mdß{{»{§Õ¾·¿÷û‘ÒÒºü:J@sÀÏ'Ýâ„ó©Àùù2¶…3ÿè@3!„íûvýlë‘öoì౞µ"¾ãQé‘Ý_:tjdq_ƒÄqÿï–þ£{B‘ú¿@C ¨¶—Õßïi°yÀŒ+ï¨äE"2°ÿ"’æ&9ÿ8É(46‡_·éþI¿Ôæ& +õ7¢ænsØ%[#F6C‡…ózáF’"ÔO)õ øœˆÆÄ s6{›ÛIÃí~J„KøY¨¯«T†!áP$l«ù¥¡* ;)çñ¾HÙò^£áÄô ¦ñ•1Z¤L)":L_¦7¨êa2.ü _CM!Ö¬z\ ÞÔߺ‰5λ† ¶ ¸^.Ý.ÕKwEXƒÙk‹Ö­sbÀâSpR[’«•e8ŸŽøó®ƒ]É_à œk–ŠÚpòΚ$ïµk³_¼ó¯¥ÈÍ5Ó3°ç/Ã+Ø(Û_P°ÃápÊ~9 åÜ%çƒÎhW4ï‚®PÔ ‘”ä =é1u¸‚p©pàòÃ¥Áõ`ìÁÔÞÌ^ó9ç|/ÒS¯g¯Qçfe·²›žU~™–|é‚ËRŠO¡>ŽÛP4›¸b$&„ŒX2•nöÈ+£+…ή•y³°ü„’èzYòÕ#¤ù©ÒH€®Î슌õøæèz_Š*J¥(R|(UHg2³@M³àäDÝDݧÀ—eÌjÅoý¾Õhš) +€¢µP,dØŒ»Ú»:>LRpæ_¥h'ŒaH³]¿†‰$¯È&.›Ø„æh†™cÂ%æ)΄B-VNŸI °&í•û5tžç¤÷Ìp—˸†Ï`¿†gÑjDpX˜à ÔÌŸ›XÖ‡´‰ ©0p>¯“¿ô}u —Poœ7V7r3Rjæfüó›^ŒÛ Æ—ä©Ún Žú\åy2¶U› üm…ܾ”‚:™ÒÊ%HÒà”¥7Íì|ªKË=¥ë©VÞ¾}Yƒ%0ï2ƒ¯T,@a°…é«ÊʘØùêüæ[ðõØV@­!Ž^C¹¨†­% Âõð‚ §òi8ôçÁÃ’op¯€oi ¨“}[ÎQ/PR7æ ± Ó™ÆT`~6ÏZ9)ÅÍ_ ë½æ\_áïwùø–Œ‡-Q¬³nù÷΃Àä÷–ÕÌÎðQð1Þ»AÞâFçÆš„ùÐFçFkw ì±Þ_VÐdÊß5ÉVB7‘P™jÐÒ€ –33fµdÞ£1xJc¥ñÍeh4£ Kõsãh£³6RV¹Ñ¸¡ rªÀTO9ÍMߦ¹™ß…Où4ôàÍl˜\Î&EÜlÍSÈ$ÆËêÆWï¿Âñg¤Q®œÅgs¹-Æ°sö;¸ºÓ*ïq»föüÄìñ»Åà·øî-º0ûÙ­óÀ…̺ ˜ÌÒÐIæn÷nÖ{Å¥ç¼jk~Ÿ—p@Ðà†Su/Pu¯†ÑÛ½_n /þ"°Å9"52L^ 7ˆDt»îP®£Ù³p}pb*·àKÁ{h9¿ Ð?ùÏRp~gºQ%2vQ-Ä( ¿ñ-àÏE“%®Ö¶V#ØÖéèqTCžP0 åB÷D;.½G¯_ ÏÛ¦L²’\l÷dƒùlµè)jÅx1Wì,æ‹ÕÏ€¶>¿¾ú0zýùÖyªÑþø©è嘄óùU¦‰z}cÂv8>2‹2=\a¬¯ÏYIôVªÄàC^EÙ$+ +!N9=IG‡õ@eHFÈlJd2ɤ3‘èHÄ Ž’5TDERŠ›Ìb±Ppš‰’‰úùPq``Su` ¿ßYMl¬VbvqUO7öÈrDÕ¨ªjñD<šˆdsXUµR‰˜ÀmDö<Àì<-ƧíœágÝqQ ×-r-PÕD­£#kÖ²¥J­Ôë4Û55—E&‘§U~ŒB 8T–ïÍ«¬ÛTY('jLíV«6uL88š˜ÎÎOËòiY˜–µ¦ecÙîìá¬- ÓXÀ¬UÌ®àŠ>¨ªYÕTÍAç8.á·›©Ò$ƒõÛ’7‡\þ×HŸ)ÂÿÉïÍÀÃÌ©O-¤GÝVî%hpá‡8÷ƒKk%õ\©—ÓALÞ¿w&w-éúL]Ÿ0©…´–< úzRM‰22„,$eQO+ü~›879nòÜT¹AcsW9o’ÇæþÀsSmúDÓg›Þlú +x‡†FÔâÜä¸1 ° +7U)Â['Gi‘'ú9p‹`€]¸…“\u9ÕM69n‹ýÿv-Ã<ü‚ï»ûJ(뎬ÝðýP˜&â§O؊ٮU}[ö¯ã‡øô÷mkWt®\·í kôϼ¯ÿ”5$>´ù±þju÷ýT«_à']xÎë5{ÔoÝ>K•êØÓ¸]DÐ0úàÀNÀ5øsQ¼A@ü°CÅá~Ô[ 6²M<1].ÿ6üÇð°è +áî çqox7>ŠþUx<⥡80°lÔÕHœ¤òö~ûñÄÓ=χ= `ÀIe~áÓ’‹+$üIú(¾Ù-}¤àS‹”QK\6°Á•‘S¯!ç½ ‰D`7G-A´–µ¡éîž'{~ÐsÕRC»ÔÐÔL}f¨¾Xñ‚—( Å¹ªÏçꈠÍ9N?’ ŸYöÈ¡}§·:!Xñü 7ë•öìøò÷Ž/V='›ñ>ü•/ÝKƒõÞ=ª`Gÿ‘úû˸€þ:wÝöSˆ°èí‹P…&!¦mÇz‹R»·¸ÇÀÛ’Côqãyá”ã%×KÞq׸÷M×›Þw¥¿kž@h m€ˆN*[¥-ömê×éãÝoœ¦'C¿S~c¸SRZ; PIÏÐcÚQãCúvÛpyâÈå·´øâegÍ9 òôºSr#ÁxM¼. +¢^ó€fAØ ¥Eaû¢,2/5_Cms3H»…´¹[£J 4·ÆÜÌùáxñM€Ä7Ó÷?º«¶‰óŽßÙ¾‡í³ÏçGìsîü8;¾ølâ8ð‹wÁ)ÚòÈ(TjK]yˆ2J cbÀ +¬¢ÐÒXK!(ÆJW6MÚªFXP‘²i:Ö8ûßÙ •Öźï»sÎJοç?¢Ç„1–®/~í6ÂíòKŽßx —íyçÆáÂç›Ö}¾ùýK{¶¶¾º~t¶¿ù‡Ã¯þy¬uû§›N6o8±hÛ/`¸ý¬°Éø1|oQT¯æºÌ’ ü- »qܺ€?´öÅ.$/O¢eBN®RGR·SŸ´&¥D,9éqqž4¯j9Z‡^A»c»ÔŸ¢ýÌaõêa{Ä“Ò}ìbT_\‘¤)±ç¥W¥í)W¥¬,‹Ü_¹(áqó6ÖZ@4DM+sRFIJ’(¹DQŠ"2*¨™-nM,Óæʵ¥£Q›)¢‹:M”Pâ¼$‰ðîiñ²ø‰hhÛÅNq³¸G<"’yýB;=-’¢/-‰\?Ξ9¡Qw·šmæ¾Ìb ©nµYŸ0µzˆMd‰ZMÙ¡åÈ•+YJ[ Ê_¢ê±{(9v¯W•A‡°›þLßS¨o÷Ú¢Õçj}î ÆF ,54Û råŽFÉ +Æ-rÐ75ƒîß¡uj{οåsPi}ÚÖM¹77í¢F‘ŽijüV9â4Ò™ƒ€8ø¯ÁŽËk{®zo ­X9Xf·„C…®bõìÖKÖî^l;¾¥·ð÷ã†eÛýúà`áâùI:Ì D»[2NÕ„8Jc²çÅ#…‡G7MÞ,l2-Ò¹•&ð9UX-~næ®Øà_DÑê}ÊõtL9_Þ;«|¬üÑÿ§4›‹ÁZÙŸóõ(] ®A›•.u·ò¦rJéC•O”[þÛŠí²tY½Ê 3_2ÿ%è}Õ'ñ»æw…Ò©97¾µFT} “q¦*^Q55ÞÄÌ­$IŠ¢ÍŒ%îc|qrQ`qøyé…pOµÉTP=Õ1&ÃäÕ|‚Z“Þ¥nKÿ™¹­ššÔEêFÕ€P4^©Æ„ ÊoÉÑŸà/ž;y{ÉÝ9Œ…]Úí +A¶œKÈ+¼ÓV-ÉP„Ê„s¼ïÌ1~?î0cs^Њ“N ´¥CèN Cˆ@C¢\Ê1µ\*Ìêô™Æ!,©ÍÛƒ#ACÐWƒnÊb¸XŸ°^žt&½Õ»mÙ,憋´ÖI­Ž¢{ê7ëw‰×ôD š#Z ôȵÎ&<éKàYÊØþ>¾ÊUr,ܽº»»MûAm«Õnm™ñÄø_N’ER‚­áqGÓxiœ€¾ÁÉÁ•g?;vm?fvÞ)¬íºµþøÖ³ÀKâÚA‚ÿ˜à$~x~`ÿ¦Âþ¯ï¿4¸bÝ[…‡Ç~pº@/t¯ ë‘Š/ „~;L{§ÑÅD|529ÒŠV Nª“îd£þo›UºÀa±chíØé|?é|ÿ Ó~1ßÉ~ÿ!»™?ÉÈòf§“ÇÈå/„åH1èg#/.÷{Ý.g)øí +FªÈÚµ˜Cð‡Én#­´™ÂÈå’äˆK–#Ñ<èšDÀ -ñ38 ;ïçýåf9¡DdŽÍsî 0%$Q>qå9¹LN˜9¹E&<¾íKÀèvïL‡·Ø¤»õRÝ|7ËÝ-Ú\ÑåJ=úAãèpôƒ%B`_n×/Šæ²4¸]± ´=*|±Šbú‡þOú_Lþ¦wÆŒUAÙj÷Wy'¿7¼ ¯ÕJÞèªrW04¸ÿp1õoxr‰àRNYxæT¡VKz‡Ý%—RHzŒnBÒoWiÁ'rO#\„à¦O¤¯¥¯Í4^›‹×L{Ãû6êq®|»åúUå¹–Ðï*Z*ìÊ®D[æ)âïgššfÏœ” tÅ|ë.™ UXêûñô“4{Bó©é!Ía{Yî´½Ö¢ãöê΋ӹ*åi>Ï,E4î¢?¢‡èÚH „PS>Ó¤YôHn:ZS™Ogó5ÁqÓ U'çoDiÜ™J¤ ißS>õöãØö6Í0·<Ð%û NñZ›xŠ‚…—‰®DOI‰}ŠÇåq{ë»n–gŽ üõ‹ÉãåtˆÛºµQÃ"qPxËS°ÌÒÆ`´XÚš#ÛàDâ{c¹W[´èÞ]Úƒ¥}æœNAxNI[æí 8$8зÍã­‘"u©g@ã1}Zp»Ê<Æ2Ýt‘TÑ 2™qniwԤᓌ@$íMc% w%JtÚœ§fo9´xÆT{²­ Vþåõgߨ£­—Íiˆ’ÜAN ÷R¦kìÅiO]{ùéíUœVÆñIà­šª×N«ÎÔ‡½—ÿ¹\Ó¾Žšt¥Õγ ÷ž$8YZ¢Ô†Ϫx\õW¶Ö7nXâÀÃï÷¢:Ìåâ ÎÞfq"Øœ¼˜ü*iJU×’Èl±Ô%"rÅ"ÉIsÒ *>Ç…$KªßàÍÕ«Q&±JÊõJRæŒK‘—ó^!£ DÐR³ÙÂ$–F"2SW—O2K^f’IF–5Ë·i¥@æd"/w€Æû‰}õ (¿"‰8Ó©{¿ªnäþœÂÞ*5‹«àêŽvZÚ ª†‹+7 \ënžðn4Ë ƒì°MRµ40Áh@Á%æñÍÕ à„ºÊëuÀ†¿W4€2dü4ø2%Là;n 5ã@SÄ:ü[ÔÌŠ…ÉëM϶/¼þXkã}ß9ÔÙþòNr½n‡ëtH†e¨ÀDŽ^÷ºRùtˆurn>Ö.D•–_º,í¤@j¯aŠã#¹åx ðu†<†Ý—‚å5Œí~{ÈËxL…£Bn4fäfãw«L/™V³¯™~nÚËþLæ'ù}ó}‹}/øL¼çj3µ8'ÿ—ãªÂ÷ÜÙÙÌÌþÌÙÙŸYïÎîìã¿Ú®³»©±Á#‡:– +Ì -ʃ›€‚Z!×Mm#h$(QÕÒðTA…”ðJEBJœœ¤ˆ0„P%TZˆÐ&¡Vœð@Y›sfÖ‰…•÷ž9wîÞ¹žó}ç|§Þ°í\îóº×éÉÛ]%»¥­3DBSÊô—fí*&UÓQTKQÔJµê°¼ÅX~2À´¬+Å„a¸}n%Z©øyÕþ2Sóy†q÷UÆø^Ä-œA´ Ï¨œšlw«â&7(‡å#ÐÊsañÅY(oö–˜-1=>ƒ-º;ïGG?[&ºGú^þ¿ßÿ å` PW^Š|a×»<`äÆ›AyøÇ÷”kÜ ”é÷Ó™iÿíÐù│ªP-ù1y‘Q‡}çÄ7—ôqXLŒ1ž #“ +Mèy;Ä8¾ÿ"«eX‰—ÌR¦”-³çÒ±y1o>Ÿ˜OÎëQUKfPù“¨\À¿BÑ­ÕYW”¢ ©Q‰CW”l(lüG7Å€“2-TuÆ+mYøMaê`šªŠQ¶ ç~Q÷Ó^ÌœJS•*˜j~æõ{X¿JÁ¡Ç9pßô +…Æ”é›ÇÌãfÄ<$R0’MñÔ”è½ðèilV©aêÌ-¶1-`íé´1Ê““A^†Æz‘”ÁÜÜ 6AXgBq(01jü¥ƒnp«R7„îX6Ô©pÿ?õÚ™'‚Žc¢ Æ\÷•uøõ«ÁÄd0ñ¸” *u7?×Áh^ßØ?K<—žò|ÝÆð$€ÙZ^Í©œŸã{°ù‡Ùaé/üC®¨L‘¶e”œš—ßÏÀ›ÞâÐÏÿ \xŽç—¹ddž•Î„ÚUMÿ ¯q5*GBzߥ½÷Xi«™#qK’ø@Þ¶6`1;Çl;œ;9ÛÊq)g³!’†"_Ëe³‘x¿_éñ+®·üDÔOÄ£A@õ6h„›Ê{…z#ïÙMÌA ³ Û÷ ÏѧÀ+6»ˆ3|¢Œr"å%²´:‹«cYÈ-ƒ´˜q"CiG¦1%¶ÐÛ »”o’ñ,Ü]rY• ŸOÉWdE¾À£\.žþ)BãßA@WowVI£\ËÛÄGv=;l\zdO>ÞT ©J1)È`6™\jßnU†*ßú-ÚÜàQ/"Ý”Aéb0Lžf{j²‘)ã@Í-‚lá…ÏšÁgx¶”œgÙ³s‹[Ðw‘z&áQPÝ›>Ç+v%S ;lMH ûÊÛט¡Ókðu§zNá`±ñ& ¶—Mⶇ8Cƒía24ØF)CC–RnŽú†, 6I'Ü[ +Ô–CöÊÜŽm±^wÍ–q+Ó3ihÈîòF›Væh:ùøƒÀ§ã‡ |:ùx¤À§£‘ |´èß<ƒÉ‚ÑÉÐPjC ÷«·{Ÿ ;6çn“`û&µwÜ¥6ìHÁý“ÒË_›Yÿ€ÒO|¾ÐùiªûwCñ©Bºâ^ ¦ú€:mØÊóynuþÉßØÊõ¯c·Š\Ÿá—½'”*ëcÃl„²‡E¿‰a1"FÅÃbLìÐMÍÔ3É¢îhB”2N±ä85§Ñjìn|³úËÆrõ²q¹ù‘ñ'öASbÞëMRYK¯7EhZ„ŠØëMï4ðÛº€²gfã#ﻦWñ|ÙÏ|nÔŸöwí•ŸÌ|±ïôÞ] òBf_ÿÂô®3/6®×Ú;µ;ý·Y6c6À˜1k¢ÚªÔD³­ŠëWü”oú¶­ê¾ðXµ†¿}¢î×üÁ¡¡–?ãú3Ñ8-NÓã_£‡k}áeVc 8á™8µ&Ó9X#ƒŒYÁSŠ(‡‹f³ìS0±ÔZ›Æ[?ŸYke™­ |•qeÌ\¨Ø»§±`K‚Ýaõ¡è´;mêGÚÈêÎík‚\º&z¯¹ñ35‰²q•¸NžŽŸÆç ª÷MùRP÷"?vª·Ëã³d-Ùz ;ƒßêÆGaãsdºØC‘@-Ìy”™—¨_ [1uª«EÎõum’âÕ¢$ç.© +|+Õ>|+Œƒ†JY·§D_2;9j9tM¢DØí–€@1‰¬hE×zD/ŒîÞÌÅC}o’>H&Æk9k¼& ´¨6 ôFMŒ}PÕíò «u ”ЕÞû‰öI–¡0Â}jiýñìÕõן*èú‚e&µ>xwýfEÄÝ.ß¾úwøÒþBÚ­\-(qÓ®ÃÊú1Ùˆ¥I*90¾þ‰%S$óÑ,ÂlÔˆuY¸~Å]HÉDO 9é¬[Î"'[ð´Wa34KOë=J=j–å1Ù“hEXʲZ÷ÔIŸ2ôÊõz/ù‰Òn}¬4ƇGÆš­n¡ÕT™CW(Íê9(ôäPû„…wdxp{oî=TšM™)Ët€Y(‹Z–åÔ{-Ü6Å,3ÅÔá”OZ©§NZËðË¥–ß‹Æ+× ¢õü#æJj‚½‚\a `/,@ìáQöËÁö—ˆs‹Ÿ¿Ý Ú¥Aº@øʈ*Û8‰Ýñ{òH ‹ÙÜ` ‘€ú'RIŒSaN!Æ<qYö6 2Ú )wï’ |\DþZ¸›6n7ù2ö\°ã“àrùÄßìBÚ°í«¡¹î3¿«Ïl¢ •Œv¢ß n¬õ¥P>NäãÛ†¿ãóÓÝÈ'{¢‘W¢††ÑQT[ÅÌü¢ þêi‚ƒÙàq$ËmïIg ILD IË…Q±ö‰#pDüŽ¿£Sîá;¹NEœä'õ÷ø{ú¸!]WÿHw ã$?ÇW¸$HNéÂÒu‘öTu¯äž†ÓÓƒBÈXbÑm8ª‡;IÉI$tœ<›Ô¤ÿ±_ý1m\wüswÆÇcÆÛ`r`J &6l aÁ„”bš¶i(sÀ€[#c'KÚj“ªRiš6E›¦llÕF£%•”¨[–ýʶl˦iíú×´M*[Q¦LJ·v¡Úïûî’N]5Ú¦©öùóÞç}ïû}ïÝ}ß}ß÷ n¹Àµœ3 š.pŸ=‡é—B–eþóò%™—ê.¬°VÖ·á’]Ë.ÁuÛ.1Ç`‘-ü‹–%Ë%˲Å`¹ÈÏ­Ÿ¯Ü©æÌ,]¾1¶Â’Z%k+7®ß äç*CºVYë\ñ±Ò Ï^¦“Oÿ’ƒ`­š4Yr7(àQCOKKlv¶,-U8^dVð,MX+\T¢%*Ù+iiËy½&ŠjM»¨õÛ°DDöR Ruèè¨ê˜X( Ó‡ÀçÔj™Î7Ìè¹Cöjaë}ò­D®º‹P¢XÝÆU‹FýðÖføœ|s•owÝ¥–Rƒo ² ­­:Dw©¹Tð ‡ú×>–©rYMEò•~þÇé­N«I*ù98nmý¦¡Óðg´s5áÔ2ÞlçM>K Ñ¾¿5ÛZ`ðBv¯=Ôx‹ÆoÚ/Õ¿\Âð~ïËÉ9_.` E1èu:õÁPÈã­·y½õ$ò°´Ûá´;åÖ† ·*®}YÁxMM£)QjmI4’û/p×΋R @uØ*ƒ ©¾Þ!‰bBr8$§ó»Ü5H¿ô­&¶=v\û'BÝ!Ç¢×}ÇþÆ9¾N.à;öGÆd•¾Íè±eµ”Y…®ë]×)v®ºÖ\5f^0û}f3Ì4Í*ÔBÝ'M´L,{µY*í•ÚÞHbû¸EÁh·Óöõý—†‚­uÛèR7µ57Q0’º‘ÿZM±EŒvls‹EÕ®}cFâõN±È­,ŽÚ¾0j·¹=­C¦È–vî­eî:gÍú—K¬ m/JûÖÇ#å.nF.ñ‘ P²Ðq§ ?ÄÍŒ?Cî®4_?±{äÏøúMaQõg |XòJ!§×ò MrÐÛèÁ”}A\p,xBgñCü?ñ½ßøì@.j°5Ô6d½§ŠU\ø!Ü*“[­wïÖó]äÖïq«ØÉ¿ÃÿêIç¿ä×ïÖ{ ¹Øs+úÙ„SÝ÷WjŸÚíîÖý驹ÍÓï)6o:·¢÷`ATØt®)¢:·²vùUeU÷¹ª¹™’r®`,2“s£e®õ²ìk'o™Ë«CÛú æ\[…¡ â¦É¹Ò¹?Žâ,ŠQ[ªžs8¬Ï™LÆ,Š«³?»ÎÙ³u»ÂåR¶KÆ…W…7„¿ +9AœµŸ>®=k﮽K’’Höon*¯Ù|Zcõ¿á—kýÑ–ÚÀ7íaÄå:©ò{š"þšæÈ¿ÖØü5Ü#’:þBëF„ÏE€ñ·Px¾r EM:Ö5ÿà¶P?–¯%O¥„²9ÀY¥¡â€Û¡¡†ì='ïeÀ·h$[ÿE ÍSÀšGÈ ¡íu`×I »¯¾?ºÑ°‡Æî^öz}_ú¾ô[(1Ø ]ÔðÐ,°8p8Ø Œe€ñ×€C4öd0EÏ“ÇÝâ™Cyä‘Gyä‘Gyä‘ÇGàÁ©gXÆ8Áˆ;þ„Û2¬¥6{0ÉÚ­¨jï«Ûæ­‡¯þ@SsËCmè؉NÕâãèÙÛ»ïþ¾ú#zpxdô¡ý?òèÇŽ=ŽOèý¾|çyüÇ~,Qé…B¬ÊíØ6°ùïF/ú0„Q<ŒG1ŽžÆ³x^©ËåÈ‚iÒ5÷à~ôcûUÍžÜÐ̽y§ uËÏë¾¹»¹$÷öj˜q®[ß°ˆ“ÔÚéSÜ¢ÎTòˆwéÜHü1‹á°•cXŸü/uΡN¸¢sfá- hnêÜ€vC«ÎÄ“:§ùéíÅ☢·6Aµ‚S„Q̨<Šf ]KA7µÒÄY#yBÕPH’${?±U»Çž›3S0Bw’ÈnêÌ“¬jm¼ftÐÕDëAc-ªtY$©&›išCFµ¦þæ i¦rRC–Z &«®QçËf3Ií§¨NÓJRHgêžD!iœæ ‘3êØld…ÚLgB•°÷µÑf3š%‰6«yøGÎŧbqå”2:W¢©ÙT†DJw*=—JÇ2‰Ô¬2—œð+=±LìJÖ™2’Jf™d^é›%»æŽŽ¦íT´ø•=ɤ2œ˜žÉÌ+Ãñùxúp|²»/ÒÛ³××ʦñô@üÈÐÈö¾L,™˜ˆŽ~è{šð´2šŽMÆŸŠ¥ŸTRS8q%ŸNÌgâéø¤’˜U&âéLŒÕ©ìl†ºš÷äWr"1{°¾÷®k Py„"éˆj5Mw’êÚ¾;›í¤ÅÞB’îLÐÓþìþ¾R©…³TtÑsP4–Éóhã¿R¸‹"®¶ÿóW§&~¨·t®J…jhÿÆЗN³ú•_œ–s‡×^5N2MÛÕØÿÏÌ—çf + +endstream endobj 530 0 obj<>stream +H‰¼V{T׿3û—ç–7*Ž¢d™å-Ä°Ùw ÏÝ•e`’}ÀÌ¢IÊ® b}`@#”(¨A%MO°ZEñQ±>ÐÆG9>Mƒ¶R1±w ˆèñ¤´Î=sæÜïûÝo~÷~¯ €#(<©Ð¨f粧#¡¤·ÖM°Ôq^÷z(ËÐuùÿâmõ`‹¤S_ÄbÕwŸ…úlúròs­%òåøôÀyS®¡$§öbË=©$ä䑺ì#(Ф„öÂó À®ùÓ×ÀùÔ<#»ˆ²/‚óVDjƒY¯C í;XÝ€ëgFÝ¢|‡×»p½Äc&‘Üø–m ±næ›î> §Ï§Éükå­Ý¶¸ÄC24¸/pÛ¿®`èq[‡[Ý>Ž›Q¦,{ä€Ø õV7+}„"áˆÛ m‡5¨@ðL¡(@ˆðkŠðëÕx*.yN2a³Oé34R@`€ XøÎ⎵Çw>Ü9ßî§sú +ŸVýE°¿Þê|·¢±¸iDyÇ*"¾«ô»³×ûKëã\gÜáO…tt„wòÒù6.¶*KÒ&’%ºt®ûxiÙªKö?ºÖù~£sESyüHÖÖ˜{>‚¿»ƒkÒùê56û)ÆÒ–•Løœ}rÂþî‡kìlÖg>†^ü0WPü}Åå´îÁõ^ûfu[ŽÊ¯ö_½þí–¥+Nʾ9E©Üê…ò`P½àäµ{ßW…ï-ß\ö§ìEÛ¤…´ø‹é¨G&žxCêµGæYú³&pÉbå²F& îß©_|›©q[ã=iñ +Y×ê™]§"C]…½‡-^‘a ;Ì’ÞœÜ&h¯úÛ±Èw/=ªŠÌ^sO{úÛsçºþÀ¯½†Z¯–ûÝ‘Òpª ›ˆ‰Á­¼ø&ÕóPEÅàÉç u§ö=¼êñK´Ú‰ÏSæÃH²l$&á‡æþÌ7Zºa±d’-6ÓïxÕî%¯Jðà +ßÑ•”‘Ä4¬Î˜O™r1 IQzS›Í,ŠK‡ÑÉ)X¢Jö¶*Q¥‡Éäò¸TmœB‚ùéý£"°±ÿŠÃ(‚ ÂÇÆaøHZš_uÿÙ™Ý+w+†oAAAz³1Ö3C±fº$X*ãì™éü ,«S“9AŽP¢VÁqŽ$‚`9€vx.Ó~á ä2L3‡€I>åÅж"NÊE¨AÀùêÛ¹•â–/¶9>Cz´·¿S²õÏ‚:ïßÖßÚœ²ð¯]ãÛhÏ3q.²>¯àŽØ“•7ÅŸ&e­oØ5A"8Ûpu¢÷…¯¶ñŒÊS..tV]û¦GäžLÚÇùŠŽ=­«ôG["*ã»ZkçúfªO=jqj>S$Ê/÷ÕíYvêЕæÓù_ od5ÿC,º­¤O,kŽwT#ø¯«=°Ré)LìÚ! ]ššœ€Wd5lDÝ>öé ˜]Ps_ã}Í!.íÔCiÃ77>ùñäÀeiU]ñ?ñþ=Wª»ëUQ½·ZÝVª¹¹|ÕÚÌšy›Ñ ñÛÓún‰«¶TT—׺ e×Ë2Üò;ÜžìÄi|{\$´…W °áÙâ)œã+q΃v™eJ¤l ±A< Á¡ˆûy,›ÏÌ þÿ•²‰.|[\ˆ OQàvœÀ‰Ïç¡Âö’†Ëóz7 úÞÝmëR2sôeÊìk~ãé ç®÷´£+¨ôo¼üB›Øk«×»®{‡|÷Å4= æg(Ï•ÀC‰g Ê,ø¿“øŸ%ô,í9+ÿ)¯úAd§)ºßκIÿ¢ØúòÕk2ÇŠ«ßwŹ‹áèÅχڔxì"â q®ZÀ+?ü9Œˆ[jåûBñ¤zÿÒÿª"ÉS5›¬¼6‹•×ªÍ£LOÒ,•Céu,‰QCË9d¸è¥É’&MzR‚éLÙÅ2X!a Æ°4¥g %"¦0ë=RÏb¬Y‚±y$6zÏìrq›Jëô,×¥`ÛbI#ib1?ÈÄ_i2€ës˜®HGtYŽÉXk£ÀtìLÑ«6ͱŽ 4B3‡Á?ÒdA!É°LìXœ™Aèp¬{%˜4,*zTû¹¬ˆ„‚$s¡‰ÕAVY,ázm,G!¢t âòKh*7ånDTTø æ0Lf0`jÁÀ‚ÀÀ~Mfaò8µV¦JÍ‘©Õ²d­*Nƒ)Ty¢L•§ø7ïUÅ•†ÿ×݃€F…ÚpÑVãÁ!™D¢18 ­Œ3ãL#ˆb™aQa —¨pDÄ­5žHÖñD]Œ‘È®â±ʬx€G@³–Y¸Ñ`¢îýgAt«’ÚÔΛ×Ýïøÿ÷ßÿ¿÷X¹:¤×AF¥ Sâ9ÆÇÞ2[­TO›Èò¡®çXÍTüTê­ì”S• +9ϱØÔó:¥‚WÍfõáÁÓ9Ïò ‰ý,N§Ä{™º×|¥FÍjur¯TpH‡ Â85b[–Pêõá¸+çC5:”ž[H}·¬2L«RvÉÌEjuœ^Ïöh…FP+Tá!.=½ö(w§S„b³[KŽªäÕò©ø-gµr”Q®’ëXm¸N«ÑsÞÖE"”*«ÖðöÁœÕH*ÎJ Ð¨õÜÌp^)Wy#‰ZÉ+guÑt «A­tlˆÓû°zŽ³·è‰P±òáp–J–V˜0 $£ËLƾXLHLÅ aˆg“MÉX ñúÎ@§adÄ¥cÙ2‘Þ +îŒØEé6õ±ˆƒdSg`ç›p(ÞÊ$6•??=¥3¦”$kÌØgt¦}œHµH ”ûØï Èõÿ5aÞÝ¿È”`òIH4âp˜%•ÐŒ™r†#ÖXí_<â˜ÉS'3©ø2û?f†ÞÙCžý¯Jãäßöìïö¬%ìyÃ"ÖKÆŽõ—÷`ß ô“¾˜ ØßœØW§‹‘_NÅmôò)y±MIñå4öWî +¬Ô©×ÖâÖu^Åÿ+Ž¨òkNëÜVîeï uz&uäbª®1ñzì9cèV§-N—Û·ûùŽ¬qן’nÛ}`¹½ÍöL?áÈ-QïH6CÚk¯EÇG®/¯U×y95ûšò7.hœù8éñ;N5*ËôôOZÍíj¾}æÜïÓoîV°d\øs³[i´ÌÌøâ¦çC!dÓÿ{÷+NÁll;BI$P¼â¤Ôõ¹•ìhYïœÁÃUO«¿¬Ï>/ÖCÈÈ™Á1µsb¼‡f2ÎNÒ6®õ%ï5}€l–”/öÎõ˜ R  ødAï%øÆþE_FüJ†´’7sGX0Фîži)醴¥‹ oõ9à1+rGe4Í—DD–ýõüãÖ¶uû1+nͽ0º4:$‘ñ8¥F•7ùƒÃÚòƒÜòú»¤æñ9ÛB’ÞŸý÷ßUK.}øõá…k Ξxçõ¶kîîôtŸ'qõ¯hÿøÎx[÷"ù7•_íiãÜ~ Ý?ö¸qô©6uû)탳Â’¥Ã7”µ®Ö•˜©0ÌPÊóØÈÌÔDì +°ø}ÅéŠí²aR÷N7;=÷&Ÿ’Ž¬6¤-1¥,ìÆAÿ—p€±Ñ90²‡21Éš‡’ãIšÕcdZ“ÎdJ“ùK};g{©5½"Q®PpZ EoK´¿=¡O¨SÐ^V¾Ã{ÐÉjú#?·å<ïÞ7³¥.½aÓÿy£AÔ<‘ÈY¯@¾þ2?ÿi`ÔK¨ù÷͉úÊÃÅk'„Žn ŽÙÑןfé7ƒoºŒŒj ÷ݘÏ7óùÑ͇¶D®Ÿx§ðÉö¶›oFÅUNZòÍÌû¹ï=ôH¨^>Ä÷àœ§uî{rŠÊWÇ뚪2kÃ>óÊ|ft.{£tw£ãŒëÇ69ç™Hû¿b}.”×ÙŠŒp9–Úb—Ru`̳…Þ©Gÿ±`Þñ]eëÕï ‚o]Ù˜ZsR´g~µ×Ð*É–FGŒ­˜òèâ¹<¦ìÌ#׬K¥ùÉ›'q1 ÷¶þ8ãÖ²mÎTÕíŸ Žs&yÚn;®­j¹q"e·y’Ć}cEºäÔîóóemQO±u†š'W¦§ÞÛâ»Õù¶¯{Oî;¡üÐ=¿8¨uð<Àaê©H¶Jü°éÞù¦cÁH9J$T?;Š’P °lu.X¶–G˜)Ù„1ÅŠí°ÙC%L"Š¢e”¹‹u#¸á{(CÄæ®z[øÀ:þºÐ@]ÅÉõ]µóg„z2Û–²öâs6ÖÕ°šäWkï&؃Ïe›À ÈÞ"‘°<°¿ ¼`ayŠ-G¨ÃñzñÌ À[çž"lŸ!Ù”;5 (¨gFAÞîGº2HyL¿‡ü‹ƒ@• ƒU°ÍÖ[¬„Ñ0’ 6À'd!&‹M`N¸6'–‹_@,Ž†cä­e²ÅH©Ç¬³Ž&†9ßñ­°R4‰a¬]¤?N¡O1†@ Á\ø²S{Â2¢pS<Œü½@ŽœrpÕ p +¾†Hi`FK@ â0ñKñôƒw‘¶ÐX“d*ÙG9Óègè]˜†Ôs1#&`L +,ûQʇğŒ'!TMP…Ôiz“Í|€žÉãÁA<É2ƒèÉ>r‘\Dk-¥³t6zÞ3.*ˆF}ס§¾°JÝ_41É&“bROnQghž eî‹F1,ê:¢½†Ã˜Œxôo%TA5RßÂ]Qv?„ú™)•AûÓZzE¯§ËéKLS)ø ­â*±D¬¯ˆ×ÅÈÏFÀ8˜–æ!–£ç6@)r­…«ðˆŒ$Á$™˜ÉfRJJRC®zÚGÐ›è£ a¦0…Là ìŽ ENœ-¶£~q° +m;a"îrk&ÓˆŠÌ$Q$9æ“5¤‚œ&ßS 5—ú”M¿O/£—Ó…t3ŠYÆ\–dÑÂ&¡Z”Š©(qxe®0¦£¤óp¯J†Å™(sÚÜŒ’¯²–µ¨Á\ó38Žviï¡Ø‘×È@âN¤XÉ»¨U$I#&!eä6¹K~¢Jâ…ÛšJ@–Pg¨êÍÓûéºn`œ˜0&QXÁTJ@â`3Ùö«_šÚv|Ô±U !Zì'º‰CÄiâAñ´Ø$¶bä²à¸TcLeÁzDÍ1ôÔh¯úØ(®#>ïíîÙøóŒñçÅ°Çræü Ǹø|‡©ù°MîUë/b"DQ•&‚ +Å4UÅI”D)¤…†QyçPéA"¢´§PÚIJ”’¦I(NTB[ìÛþvïlp+µý§;{ïæͼyóffg~ ¼€»¾N‚ )°·<6ƒyÙ +¶ží€¦¿]d/ŽÁrN²à4à,û»í_f×Øuv‡Áx¹—WCâõ|ÿ6àgøyž2%—¤AŸuRtº]Ú)ýg¸(}.Ý–sä|Ù+/”{äçäò9ùùŽ²TY¡<îÈsô;žIEŽ»ñÄ +rA>ësÿgAã?çoðJxDüÿ»Ùmz“ÕÓu6 ++ß ØAÃâö,éÇl{ŽæïÁسtˆK/³KüIê‡÷WѧhïeUl¿ÑðYþ +}ˈÃ_>çKÇqÓÅ—âl ý}Áž¢›8‹Î èav‘jØ.Ö@›x9iô(‹ÃÂð(~™)ëo¶b¯¼—Â÷²›´”ÿÈ–¹ŸuÐ!V{‹³ut’ÿAþª|V‚—–bt+w°­°Íƒ\¦£ü Øn~¶^±Þ{~ò ¤žER€µc·Ù$Êc»aí_‡gî†<'è•Ø+d¾jÿ>æs`ç{éûoˆfÐqóizuÂO± :HÐré¯r¾Ãr™4y¢“®˜-ô"–SºJôÛƒ¸ÑHï²B:`n2çÁãfr~—z©]yP™ŠhÜÁ7ѹ´CŽ«Ž:Ç\S¶)ÝJ«Ò¤”Ê\¥\q+%J®’!ߔߗ/ȯÉGäïÀw«ä9KºŠø•öK{¤oH+¤%Rl%#ÿ;ÿ ÿ3ÿ=¿ÂÏòcü & å{æ›æ~³Ù\d.0ó‰Ä­ÄùÄÏ{O'ú[úèë#ïüf$:òöåèįsì­Ä|¾e®5—›_Âߦ˜Ï›‹—Ù38£‡Fá_¿B\}÷rº #Âùy#sR‚nÑ hèøC4{œtZãh£U¸o/<óÉ”5ö ÖEOÂ]MÆ` 4¾w²ž8¢ôL|i_§—ÍÃR;ÖˆÚÎr”¿ÍÔÄK4Qf3¾OMô![LŸNÑ©Ñ`·ÇQì:ä8F·/JwüM­io[ÝÚÒ¼jå×–-Y¼¨naí5 æÏ»ÿ+sçTWUVøf—ÏšéõÌЦ»ÕiSËîs•–LÉŸœçÌÍÉÎÊ̘”žæPd‰3ªj!]^]È^­±±Òêk tÜCÐ… +Rhâ¡êö0uâH?Fnø—‘þäHÿøHæT먮²B jªˆ7hjŒ­m ªA‹¨â†¯°qÙkw²Ñq»1C ÷6¨‚éjP„ë5‚zÖ‹ff´@OFeE32fEÚ–(+ZÌl„k£œÒ³!•(Õ‚¢Dk°D’'ØÑ-š[ÂÁ—Û©¬,Ð¥u +ÒêE®ÏB{áˆ4{u£uÚ£F+Îý1'u꾬n­»c}XHk<ömEÛþX|·‹Å'Â;ïåº$#X¼Qµº†±S‡ZÂ÷rÝV‰` Ìåžn„°u¿¥Åâjb‰o%y¨-hQôGT1I«×zGt\H©!¨u«{°´Ô?d^£Ò j´…5·XâÒ" ÷E§Ñºõ•¿Z2‘SYuæ%µÍÉM!YÙ÷"=ã<³‡[XSë¸:™%‘¶ f Ô.’„5¤ÆjzjÈèªÁ0<†Y¢×°QL +膳֢[ó…âqjªq‹píÚÏ&R:R‡Ç 'ýÌÒ²…1 >Ÿ˜=Û²‹´.2.¶ûó++‹ñ¸¶Å©âê£æ0¦Ej«¡s·ÛºÕ=1?u¢#úZÂɾJ®AòWû"‚ëç짠ÝâôqƧëÌ÷>ˆD"Ý;þæ: 󃽵‚þvO’ß´ZkjYVƒ†žÒmSÛ„^’_3ÎKa"?–\<…q—dsa‰ëÇ[p–=x¶%wÇÒÒaŠ6…©!áÔ“m$Ãíþ'ÅÌak–ýwwZJLQë›Ø_8¡?A¼,C‚À²—7µ­5ŒŒ‰¢¯ô‰,˜äUˆlȱñ|Ï`aN»O9º$w¼µælÿÖ厨aU´ÍFd©+®®Ípw‘é½Z­b¯•k·Ùö¢Qä)fκ‘ºU_¶†ex¬ísí6Ý#œ‘gã…žÁ’B)Yìø XRùÍaÝÕ±<ÏzO{X8lõº­0šÒWŽ½…Ó~“˶ÁoÅ*^xidGÒ3ÝÉi÷e6̶tt£Ú9ÚÞc¼Œ…f‘¹bX³´¨ÉX˜¼<, +K„2\V^fª-+8²§j¢¸9 ÄM%×Ï9½¥ Kö‡÷äs~÷œ{Îï{Þï½Ò¿D[ÿí¾ùW8'kätñ÷ò….ì¼,ħ)ad üƒê·h.7лxÈ¥”a4Œì m©¥T“ÅøYÊŒVX0ß•¢š•ò{þ{Oˆ:ÿ ø©O;”|.‰‰Y%SÄãþE•+¥UMR“L]LŠØ¥Ÿˆ‰âçücT3ƒex˜¿®©r6=ßMê9QB›Ëe˜¶ïç4x˜1© RsW ´Û§§zµÏžŠ<ßcE¡mgz¹"ƒ?"3 KEªÿGZ4¶SÏ”,¦§™ì±÷ý>Âøù—ü~f¡ÉÖ€þŽC÷NÂâþ~¦ÑžBÖþFêOe¯Q?bê3§ÿ¦ÎBÿ/b¦=_QæÇöD8äÿ‹Uÿe(ËŪ@þš°Gîá@ͦŸ{”Ð1qLž1±‚óo©x^ºÂ§çy¬šþâ~Hë’_gÄÝb—8.N‰ÝbhŒôÎ’ùb¾ü cë²ÆjDßíwSnmb6ÜêyÄïCÖçkü›ÝôG‰<;%V©r¹›ïÐy\/ƒ}²úä.h—çe§4ÿ–¿#‰Pøÿ|›)¹OÍ”op别âÏ2säUA­+6œ”Ýò°\Æ*x['3£ÃWj`ßc͘ëEñÔÈ›&°sèó+´÷ÂÂ/ ¯ŠOØouA~£ÜV»|R¶5u`‹,ÿ§KvÈ—íÎܧ¿’ß•ÛÄ[â¬|Ižµí4O¹ú'[äÜ›}MÚ÷D×í¬Ü,'ìÜ:#ã¡GÙO_Å»£lrl›Åð¨ÜA0×÷ØF¯NÖÙtkߤߦ­æ¢?¶/sXÙæšÏ›«™ýØÌ© òf›uÁnÚÂlžgÜsX»d#'vŽõö`6v°¦<éQ«Yö³ +Nò®{Qn’§8}3T>wµb'ÉÍilC5«c¸ÎÂkâMñ¦l–ͼ!ßâ2Ž÷ÝQ'·²oËD‹#>2 /q²üGVÝ2ÚÉQ0+eM0~¦·•„ì„<Ë|ý€,sžrnʘ\ „; &_€"9Ó|¥\©·ðͲ×ß+¿/_µO×ÆŠÄÇòøÛÍ“±Qé‘P)ï…¢äûóó2êÝq;’ïŒäÛáóò©7Ç(ŠF‚mCÒÿmÊÊlÖr?˜³0ƒv)+È0ôb¾àò9§§ñ&+£ÍÆ—x]<êwëëGÜy¥½Ñ»ôu¾:c6Î"v  *¡BšˆèkñŒÌÒcÔ»¿ß-Ž1wbmØ,é#ÕKKCÑ\2Ê ÌÃ^HÅï5Ñ10USP»†Â5Ñ C&Åg»Vå£ø’GJ£KÌœam_`ØŸö…Àî +lK`[ØšÀ.ìüÀÎ li`§vr`ólÈÚ«ñš1:{•kЭÐZTßšƒ.8ÝÐ x¸b=äè+ÖÃeÊ_¦üeëáò¨œtÁ!è†^}9ž1>èïˆ0¶ +R¨ÕA­juP«ƒAœ a(TAOx2À'Õ îW€PâlC D uTJëSªžÏÔþ…Z_rƒ8gÂã´}P÷ã«ßúêÇW?µû©ÝOí~ëk$¥u]\¯ +ÕoÄË9y$oU(+:]—㾜5SN‡ÊéDX/dº‰oCb"Эª+t§mHÏSµbv.ic]]líýýº.ŠÏB'_—ॄUXBót!©BR…6U@ª€TÍ,!. f!v¶@Ť™Äp|Â—ìŠ Çó&7÷•–þFç©eb®-’wäŠÒ†èX=‘vN¤õ…:G €âaN|z©­–ÿFEpS]S½Sß«š¬ÖÝê:K.¤ïÂNÁNl(ž»0tLFÕcÌ‚`e2Ú™ U&ãËTA´BÄ  A7ôêÌ#_?>rTŽÌè|]õˆ!ÕY¦Ây²3u(Uu¦ ¥¨N=¤U§RêDÚ‰tJ+K«OóÒÚÓRCéeéõé^z{zj™*Ó•ªR§„sÃùáÂpq¸"5;7;/;?»0»8»"­>ºV­gëÕ%!Õ%åñ~ ‰6õ.yau‘¸„8J4·Ú»6☽ë">dïºmiS§Í¦³oÖ3%{a´Í·uÕEÕdÕÂê*(}AhuAí³¹Ùj€'f˜¸"P)j@uØ2ûÔ;â(\­ÞQëÙX!u>>3+½¡Î«Z›>G8K8C8Mèa@³,gl¯NÓöÓÂ-ÊÈo€VˆA7¤2:gè[—:GìG Lù3¢N€æYœ!×øª'–b§Ú!¶«Ã(íT[al‡gÙ@;ÕÓ°ž-6§6ÀFØdsø½Q||¨ðlN#¬…u°žÕVÃCÃCÃCóžÕðÐðÐðÐ𬆇†‡†‡†g5<4<4<4<«ñ ’x+lƒíð¬Í6Ã3°Åæ´ÂØ›lN4C x6§ÖÂ:0þ]ëßÅ¿‹ÿ®õïâßÅ¿‹×úwñïâßÅ¿ký»øwñïâßUÞá7ê#à"à"àZÇ +8888VÀAÀAÀAÀ±Žpppplü;øwðïXÿ ë?ÿþøOXÿ ü'ðŸÀÂúOà?ÿþÖÿ ü'ðŸ°þøOà?ÿ„õ¿S­a!í‡,®êIX«á)û¼à ø¶Íy¾+`¥ÍyƒåPgsjàXµvê׈u謶::::žÕñÐñÐñÐñ¬Ž‡Ž‡Ž‡Žguh ‡öóþ=P +Ú òÈ.h74 ðÈhJB;ydÚÖµ»b$$¤>N2@2@2@28ÉÉÉÉà$$$$ƒ“ N2@2°"ƒs¢à´ƒ! µƒÆ¡ý¼o”‚öB£<² Ú C#<²‚’ÐN„¶AÛ¡¯óûn‚¬ä 34040404ÎÐÀÐÀÐÀÐ8CCCCã 3,0þÊgX`X`X`Xœaaaaq††††Åc‡è;ÂAêE•ÜDµÜ@Õœ@mÌ¡FŽ£VÆP3;PqTH*%‚ŠiF]4¡>Q' ¨—ª"€êPQ%~TK­01÷cÌqr³+ˆYßÀìO`Žs˜ëqÌy sßÆ1ÓÌ8‚™7c~M˜g#æÛ€y‡0»f©b¶~aP÷Ôû|Ì÷*tzZ ­‚rÔ«·âÉè&4Å¡Ô 5@!(ù¡ZˆTVâ «¼LÒ»ª„NÏx™;ÏíQn_çön7sç¶]¯ê—Ï÷Ëßî—§úå=ýr²_~¼_nï—ߧ·IŸê+Òò›iùå´¼;-÷¥åî´Ü•–ÛÒòº´¬¡í§ŸÑÊí1n¿Ë,Þ™ýœÛ¹á6­ŸÛZÉÊÄ•£ÿΪX÷µ¬jÀ]ɪ£p§²j‹ïúQí”øèÛYuÑŸeÕ¸‰¬Ú +·?«®†ëΪ=p]gÕfß 5g§z©ï²zÀ÷'µÏgªm¾,–õç]žjØ7®®ôåÃ;ò®‡¹s¾Nõ´¯)iÌG¶/w-wertA_+f>3)1Ó,fÂbf¥˜©3ubÆ'fVˆn©\R¤eR‰T$I’S²K‚D$wnéc½‘àÜíT˜sÚ™µó¶"0 K* ¤¤…N<&tÎ ëÍ嶄ì¦ óÂ>’õ›×ƒ9ZôäNÓì¦fy‚$¶u‡§«¦g0a>¹s('tšGz~¦g€Ÿ^èMšõ¼™£í5…¶Žv{¡}íx¡ü¤¹>œÈ‰Kæ†pÂtõôõ$ÎLጲm(G—X襳¼ghPê{éµæ—^z-™$•³ÑêhùƲ¶Ç{bR¾{Tßm2vÿ‹z‰ï]Ñ}kE_PdñÄ ‚™wÅLLÌàBäƒÕ+Ìc‰Á!siVh$pÕý»‡„¨Ðë]62—ZðÌ ÑØ‹{æ°È/óPœQä¡6£…<by$ô?ya#Ëk`.Ÿàyûòæãj¬w^UïäÄyNüþœ¹ûsæxÎ\!Ç–ÏQïÉY¾¨RCÞ Á+ׇ#ØUåÜÑ+ØŒá¶pxu3 ?7ŒmÃfTà«ùk¼¶L,\îõÂÅ‹}Ñhß­7™½øëÆʆ¶•=íæOÚV5µ1Ùv4—llzL+Ç|;àÆ„èšR„UvÄ[_ô®;²‡Š †ëâu/×ý¾èwÅb¢h;™$ãu£¡£äzH,(A¥N ] +\ +^ª»’dìÒ™ÑæõÒöÖãêù’l;RDÔ–£Ÿœ¡6ÍÑÏÎ:ƒu¤*'ŸUâµûTw¼…û̓Üg·µºr–s”ÄE©D^Ä«J—{ÖIÞwN˜Ð]ÞIDoèj!…s¿i½E.—O:ŠG,Oý"ý>Éß­Ãì.Å6*×Ø-û_¶«¶‰ëŒ¿wvìøÏÙw—8öãŸß}ŽcŸãàø.ÍÆ3­ +%hP1$…*J»Ñ–ò'TÝÆXSJ»VeÒ +HÓ¦­d©+VÁšT¤mh˜4:‰i•VkªD·i‚dïÝÙ´åô¾ïÝù{ï.ߟßï{µ•wFÙý=Ò ºT‚Ô½¡’ÔÔ%0¹K’~n#FžX‡smÆAå ú¥±¾,œ¬!†‹Ëç‹Êü‘—v[3ª » ž†¿bJñîzBÜ=xàé£ ¿§/.íü㾋ÿþÊÔö+áÊŠ§nÀ¹#å»u~DŒ—{Ë3?½æØK_ÄâPŒîZ?)t^xC—toXKHEi«ô‚tTš“l=s¢ï`¯ÐubLwX(ÁRøÎ 8Š‹s8+wŽødAŒ} QË©‹¨ÎëÄçgÓß@E~§ÖÀƒ)±J-ÐÀ)IáëΤ„ª7–2ÊlʧÉ8ÃÑÛ ½"aêÞNC¿\nàåe®Í5bsØ\#6׈­Ñn7¦Ú2ÒÔ¾KS™V€.™=·*—íå²Y3’HRy`€T›4ºV3JÆì’6ɧ æü•‹°lZðär>«j|>rßÀsðÄ£þLÅxªUs‡ß;«ûLÐÊ ©¼KS1ÕÄ1X…Ƕãã‘Ž˜ìˆõO ®ê±½EÁ)8¶ÁÍÉÍ©íÅoÂg}Ï$÷?dgC.…?‡¸q6ÒÉŽdÁâ¼h!‚IÉpм6Å£yVy6xvz^ôØ<çˆ1`vâÀY+!Ü} `Èìâ…Y/“da¶?=;°ã”ØRÙdÄ•wÊZ“µÆmj‰y9ßÉe”´BØ|B‚“"èÌûEÀ*Atå:DØd@qÿ~X“@ šX‚È'øBHcà h`± 5{°A•qÌúg:ÒrU:Ųn»/õZõkoí™?¿kU&ûSqhãþ·g¿¿çÔë°}zìÖ#W©þ´ªùýšèO««g¾uhú7¦í¨ˆbvy¯º¢ -o¾zúŽ§pÿB¸4Žp©ˆà/:÷alÎ=—°¬=™_yìqaðØÓׯXxXJ£t®–¤ãÓãO½<÷bXA9={2ßÜ´itל×aäá äa•ø¥ÞáMD·T Z=ÓÑra¾îÔ]–`w(‰b(ýh C«fó’Î(}¹þ‚ª‚a÷ƒ¿Ýh`úw»¬ÿ‹«d5%\ UÏ?μÈIdÒ²$¦Ð¿/´ÅÃUF• @Ë@MX%lÞn v|‹§V¡N¼ ó²«GæW»·;Ò­t[»awÅ뀔#êø†Ãâ¨Ãº3£³…L‰4N’È $ñ$¢jVÕÕÕêFµQëÐ? ã‚€|‡³¥³#Ëœdˆ£Ìy†XÅ@†->n&̮ɚ‘1øú'>U°ªÎz5õ¥. ¿‰N7Œ«‰F#&QÒÚäíÚ¤‘a÷0F×Ðâ_ºÃ« M¤544ôûåë! ;i  d +tdô`MLÃ~ë!›ªØTŦ*6U±™Šì›¦¦B !^á +á1,Úñ' mmj{SÛšº ÓnM¢Ø:‚E‹~] s²¹ÙÜlî@š;0hÅÖ,ÂXü¿“Ò0:fžC|ôR»Ú¢Ú<¦bt¾ØÄ9Ç +pšpaœ¿»¬ªé£Ñ(fˉÄ üŒ7RJ—¨¨ZŠxá®î4èä’ÑÞšD±ðÛQ­R_¸¾ÚߧU?ñ+}þÇPM #®¸ˆ¸¢ ×ë¡õä–Ìeò²ðqb¾}Þ3/}’qØ=v!a»š±vÔ?Õ¹žD^%—“ëèígÚgÉÙ„Ýá¸oø, ôóY—'/`3„-y JƒËÊàýȨªK®KÜàèÔœP:íÀ½b®/«dZµâf` ÍÃ:( “_1¯ÊY `7Gpá‡óŠÎuç5e•²AÙ©üCYTlŠÒ#3u84Ó“ôZ‚uhѽ„œ´:˜°?ö +¦4×Ið3@¬dÖMÒ\¥s™ ¤ élr™:1— í8ø—I“»V6P­4îóÍ””Šn¹|Ï%É8š ÀD½ê ÛàdsQQ!È|ÂL³/QX¡• FaõדïÞöÜ–}g`ì‰7¾”J¦Jé8פƒ¯¬y~½>qþwßyæ÷FÏ0QÕ$øìBcEQ‰ï˜?4}å‡7y^¨TUµ.ïV&”Î õ‰é÷~\xÍHŠL\E9D¸:pU·Ö7ì?°ìàÐ1~Zy+;ݺ2Ç_Ö>Q¾PÜ"?˜«æÆr{ùçr6 8*e%?š½ÅßTìß­í«Î~»òzß;ÿå»jc£8ÎðÌÞ}gßÇž¿vö|¶oÖgû¾Ìí®×ßÙŠÁ¸$V IÛÈ)jÒDI‚Q‰¶Q%-Rùa§ µ r@Š@Q›¦ÔW‚1‰*~4 (HE ¡`©‘RD€V*¶ûά?ÎUº}föÝÙÛÝ™yŸçyóão-ÝÎÎÐ tšÞ¦e>£Â^G_£çé§vœ»ÂìLΪÒX"g´Bv\Û—ý¹îÉj'µ©Â‰ž YÆa{{WÅiËê·Ð›ô·¦Ç_ð÷øW»˜žÍ¢) Z•S­"åsÝ«œ-´LWKSpÏï”4wÍUpTÆ#¼dþ‡2t£ÌºàQ‚ÿC@¨¶P‘)é"ÿ£"ìbQÓùCR"™Jg@I²úšše:PúÙy +:dþ¨wQS@ößY|é%£~ªôYPe))™hkm‰7k¼Ç)qK¨±OgY¤eb¶Xbbƒ+-U+ÀÖ'yõ÷ûÕj(ŸOÊ‚Õv'í¥ß¦ÏÑ—é:NM?¦—è—ô?Ô¢õÔ¦ŸP7¥ZO,+Ã!Ï¡'f¯‡S€<‡Ö»Á*pÈsèaý[à ÏÁî)˜n¦kywNZª•H)—Ïû|^/šÄwXÑ:LG©»œbxç#ý”«CŽ7öhŠˆ‚oWÀ‹êü56«VmJUo~ +£R«6‰S|>‹STÉÝYÔ³,û£¬ ñ°ô*«üAÞÖ±£c¸ÃÕyt-E(&ô±"¬ËXV×<ô•c£¸& +IioÏ%ï:â¹¾ÀæC;¯; +àô4W)ÝJw)Ýïv¯à·:ý /%ªºç‹ ÈÁÂKgœjâ8Ò@L*£â«' Ã•šzѹËE>bJ࣠+ƒ,÷hºPT©¸TäóÄÛù©âòfò–OT¯Åˆ_ž€ÝPªIેàO2¡Èõ[cb"ÛàßÓ žÞÎ!Ã!«hLŸw®v:PWÔy`HåçÃ,¯œÆ%B—Á&Ô2÷‘F K fd×.«žJôÑ 7‡±ã>ç×Õ)ów4s6mïo +øÑ’Tâ.Ž@‘1boâl¹nvô飽]\§Ø7W?¿¹æ”W‹¢)Äõ<îV ÙœµçfOçoùˆ}{†žnBxî3àÎàNÓuö("2‘ˆ¨)3LÃïç: ’ë„îûìMpT·Uš4/š.R©Ö*õª;¢Ôª)¥EuWéœi-ÐÐY$=€Â$¤Ë$F9OÊGЈ¾ÇØc@ô}Æ>ó=ôžþ®ñ®ùú@¿BnùûƳæO`À/ŒýæÛƯ̿Ÿš”¿’KêgÆç¦g‘#Cr)s.Ðç;'Z;/Û Øî¶ÆÓ—ûޠÇ¥¼¶ô— ¬&‡J£ó|Fil!êkìSucƒ  ²b´ >—VÎií-ÐJ—X”¨5„¨ +27šØŒÁ0“Á“Á“0‰ÕÐMëlKç(JoAëÃô†H¬ñ*V±TÉ€Ûn²Jœñ&ÚÚ8QÁ^™ØÖ)š-N3à4½NSMqí:‹· œŽå&5äIòù=™&·Hy ‰“­ä58C.oœXà#øi9µäÙUg-²Ìî:›µ³RVxõØ0È¥„¨LcTRfIàEü“-`+Œ܆Ü"˜K•Clíz+ÄRk$„›BÙR;>:ìÙPšW„@`ÀeéÁ¡Â 0RAÛ¹ÈMˆ8<&ÏÜ)Ø·nM‡ÁÏSUuïLËn÷î‚óv¿tf÷ +²Ð'³#ð)ÊzØ·H@Z0‚ªqÈ,ˆF›{m"h‚®±u€8Árí +„'”¨â@æyF\jðóK~~ À$•UÐÐ9ˆ¿âÃîkÍáwŠÉHƒc[Î@óÕ®‚±C9åá{ÊÕŒ[Îùz“½zÍÊ.{Óß°‚«®m²Wu®Y ñ›}¶Ý×ÿå„KŸ_âϪÍ̘ô,ðKj•çîå\»sMzbæ˜ç9÷i`˜>{\˜ž¼(–ÊÕS]-(¥v¢œê®G5ª´’t§Ö’Í©‡È`ê)òLêó„¯$ÓO•f-ó×aª5Ç[ZÛ‰$¥ÒŒ–ç w0u‹^vÿ _4.¡ÏÝ=«æ´áÑh¬©±!ZQ®W4ö”JÅr)ž³ŠJjT•œJª#¦shXÅêäÜ$‹ä:U-E,E³(U¤d*Ås4’—E‰’AåÉK&·%v$†®Ë¬D$sŸŒ‰À\0£Ãj’uY’Y5±F¸ö§Ù "e†Òàö7Ë"g¸ã\8AKr/ §??Ð}Hž‹ù°êÜ•c•Ç­ ñ8Bb-R¯ðÊõÿ:±ò ÔÜkÒ ëÓŒo|"vé7J<×Åó_'›^ûñNÿtüåÿþù‡uííï ^ÈŸÛõðÆí°R °Rÿ†•jG«¤2ö½ÖÆ•½4þÝø—QÖeô[GÌ'L¢ue6dͼú_¾«>¶‰óß{þþÌÙ±Ï_‰ï컜?Î;ŽíÄ!ν@KF€ÒòP¦MUb¶©Ð²‘teUAjV-e¥ÒèºnŒ,ªZÑ®]7µ0u[ËÐ?">”YAZGÕB’ýÞ³ ºI¾÷ãÞ÷õî÷üžßó¤v(Ï^-¼Áÿ‘·:egÊ”m‘åTQY ô)+„•òá{¡ÔXêýTÃöÔÓ)ºÕŒ8HËIâ¦DAà¾Þ3¾u7°Ýù?´Í½Ñ·‡{¹´òc=È!Ž¨8èǵw‘þ¥±t6÷nu ãùä¸Ç”ß(o*:ÏÏ+üÂ<_!쮤ÒËS©t+)Y­­fG*ЩÁ()Ë„…ÚÿCÿÒ±ÖlN›ÃsH=ðç#˜ð® >\*¯“Gd ÇÈËÿÎzêÐ{»ÉÐôEÈ]ÀéV¨¸¬ó¶bª^¨2-—+Z¯sáD…©h¦¨ RE&l(k¼{6—)mZŸ)ÃÙö¶vÚXæD” C“uˆ¨½9-RaŽbºkà%N€ Ðõ½€‰õ±¾¬¯ªýé ±£PÃã³÷z€3g§†ËsûZ/(=}?øòç>8°à[s·=2‰NÏÌÜ…ÍOö¯ûY©8PÜÊpJ.þ0$.u­‡ò´¸tpéô—1¿Ø’kµBKù¡Ñt¾ƒÄ @M¨Ç8¯¹ÉÕÃÎ{"´CØYznžµg|öbÌ51ö†œ%hkÒ—tñhp¢=–@ȣР‰ëÕ­Üvñ…À¾î±öÞkÿ´áïí—J_†®—œ%ò”v8$Ï,Íñ‡ôÞN?˜‘ÆÏÚBÕ¦+ï‡KÛ’w‰ã¨„ù|,¢S•%ÊZeHyF¹¨Ó +RNI4¶.µ®³ê¬ãHwTg¦È^«+vŸ…Ó" G+ØŸÅ`>³q_ ÷2ÈpÌȳé, Rz[ I4 LÜãÕJÆ:<ŒGðn¬ÇÁo8 Dá(ôŸƒÓd>§w;Ô™Ñ Åœ¤(#À*ìgÂ|˜ž +_ ÓáeÌOuÆ/þê õ~Ù + X®ñF诿wº¤p£R†²4¡VÕJ.¨Ù¾bµÎ 2lËÙL*rU ·abYð•,K ˆŠI±Vºz×G/\”¸M©V|¬ž%¬HΣ9¡ˆ?],¦Å¶ ³ù¹UO­ê,”â»^ûõC×ÿ°äÉŽx ¦HM,ëjþþ¢¡Ñ|{ÍùåðwŸþçòWr¼[íÝÙ§J ‡q¬(Íg‚ܪƒƒÏžĘڻ¿Oõ¹6²RnS§´Ðãìé;ðð¾5[ºÔ2àÏ-ÀŸšƒ#XÂAìÃ~ <§fàlØÚü6ÍÕ«´šDvèªÖ4!5·éöJpáòÁÂèP¢Ie¤¬wTJ‡¦*í÷råígoQf}-î€&Ù¹ Ó¹£¡ M$^x7GÄÝïX‡¸Â„šÚí$k0Üiº‚\'éE1Ä2Á`ÐÒ1híFo£3È€Ø`'ÚãÇZˆª: € ó9˜ ­S»AÝuwwW‰êk(G‹µ”w\ÕAžþÓN]4^yê¡ ½¤‘ÇÔÍ ·‰ä”F;»ÑL™ N=xúEƒ4³t˜ˆ&™DÙ‚ ú€sµýöuB¸Ì%*ÝÝo /S{‰ˆöJú7ö“ÒoìU{¦_ÆÖDAÊ Ì^ÐïÓý‚JP®ù¤eZ‘¡À„‚šTe5URvnsZ ¼—ÁüŽñþ¯Æ ãõ‚™¢H5l…h¤O¸×Õ˜IvD)ô“J$;rv·•|ût˜Ï1@À#VÚY›BKR(•Jx°’Íy6¸™HØ”°ŽäP.¢·9¨qzåñÈÚ(ŠÖ-n4Xô=™§—c·‰0gâM¢&;Õ±j­‘û§+5Í= ±©”Uµ†ƒ`WV« A S³pêNys¥¼¹ê)ŠÇ`“U³†ájÏ„j½WëÖÝß±´®"ó/R°È"L`$_¥€œØžõ‘Z5Uç…‚:@tÚš×ã#Ê´CÇõÿ~Ñ¡éÊà£K†Öì)„EX\t¿õ±@¢ví±oÿpug(»²ïdo&‘8üÑóž¶Ö.Ñ1§5(ùoàÐ33«I<Ñ¿‹7»#]YŠžœ½¨Þà‚TL¢Ý¸Í@[,V»nÌüžù²ù+‹ž£;'2Ršæíi‘—&¥ÉäMãM~VtˆØÒ JÚ·‡ˆ­¶œ6óà „õÑŽ[%3EÈÀÝèñ²¾;¥ Uul­õ»p¯ƒwÄFÀOéÃT4¢75Xã«#õP&F‹&6-5˜Ðg&dÒží²ª&°a-@ð+±ÇG±<›aO³Ù)v–5½Ì"¶¾ Èë·i8˪ö«b°D ‡hxc”CIsœ«©ÛÑÑQª÷Á­˜‘b6»d ú˜­E ì +„’ë«‚HëD:*¬ÒèÖ*AÐ 80”׆EŠy…»T +ºÃº ‡¾ùÓ%Cñ™K——í[xèAÃ…nùÑÑ‘£÷î}ÅàšÉµµÍüãÌû3ÿI&²‰ö|"ž]u_ò\¯ÆnxnV³Ç"Ð÷×&÷ïÆø »ÑfGî^wlÀ}ÞsVœô\MÁ€ÃNŒkw0&„Bã(rGÁL÷jK,ì˜löú[ïÿўï½3rf…, >µã8'—ïÕmÔÞ'úzüî¡PæõÇ·ì +2ý=ÉûJkÖï¸|q×4SlàI¾%5hÀ'¹¨Íü0áF’}ÁLÝ.–oÞé; Ž¢{SÐÿ_º«6Šã ïìïé»Û[ß­oï±{ç½[¿îißí3Þ±06¤v-ÊÔš'©!-M㔊¤… +5 +¥RµIH›JA¶ˆ‚ ¤FZ0HÅ–Šxˆ¸ Ä¸MŸûÏî6Üigfggö1ÿ|ÿ÷}B§m·…+#è+3[};hÑJ å¢¶÷ѧÐ$¢‘?#‘ñ‹L7óf+øÇ æ3ÇX>$Ò¾¶sŸîI„f4ïjþ>ʘOïCÌF Æ}b>©ÒÉJlL¤*PDÝ0Ùµ`‚ªkì¥5vÀWu©(!»íA|…#oØ øŠxગ{¾ô\+©Y™Ò‚[žÓñU¶_Æ%R'†,»ì7«n®£ª =W¨ø’4xýê÷%îÿ$hª«®Aö3ç‘œÉÌ5&’ ÐESmí~@WˆŠ¢ßbÛ(;ê980‚žŸÄA!»™~ÖsÆtÉ4îço˜nznòŸÓ÷LŸ³_zþ#~!¹™:L4»Å³Å×ï~M‰û¥?‹oIÿåí!s‰Á^…PRMSÖ¢*¾";h9o¡ïZà⎰åT´¹B@¸ @ï0Š|8GaVR(F0'RÈEuSç(Ãä nD`„Ðd„Ðd$™Æ.ŒÒ}ÃÔûèÜàˆ´LQë2©áùR4;iGv¿ÝR©{Ê°”˶•ÑeØáÊ–ñ±Îgµ¬LèùÙ0ŸÇ§Õ¤ ÒÉ î¯w€èï)µ80QÈ7Œxª´^Z­%V­‡«ŠLü¯Þ"Æšõt€zU2§ç>N(WÄ8`>†š '‰6GD“æ:ðó{þ^M”›[¾<þÞí|C¨nª +Ý7S¸Œ”ó/þ­~y*üÏÔ-OH£ =ßÍxšâUÁX;â>G®µõ]Ï­ÜüüÚ5kÖÂŽ„]ø7PAê£1p§F쎚¬œ=V„ØIk¢šðè‡ q¬kbÚQj·iP_H²Fß^O±×&t¦h…î¦ ôqz Dõv8‚Ç›¥¶’Ðë¡hzÍ0zÁ¨ÂÞë {Òž>ÁÇֿ¥eÞæÇ™ÙiPDÝ*MÚötz¥Ü#4®Ö O#f† f¶ƒ”3oL•¸/_.ìœm-*[ô™*;õ2¬ÍJXõl§öävª¼(Ú¦Øk:}0ØF!òU~7ƒA02ØSª„™4ÓǼÉž/~ ìU 6+_'Úõ:ûˆ¯ÑÒÀ<½BP¯RT 2¼Kµ6bnÂ…Þ1½z/~"4&œˆŸ }·°DÌŒø%UmàˆWʲ[Å­É—Ä—’ûÄ}É!q(9!N$mËDÄB+d´Õ™m £Ë Áb(¨lnÑâ†Æ¦|¾ùzèÁl¯/‡Ëålu.¼Fqp¸á`á`ÈXW‰¦×â(ƒò~dŽLæÈp§a*‘„©ÃÍL^åP97”G®ü›ù‰üdþn¾äœÚ0¨ãëJ/Q²yÜږͶw@kù +h­|Z=«¡øvo6Ï·+zZÓÛzkW¬Z;²5òctj‡}¶NeÆ #!’Ùdb>eÕZ†4uí€ʘ­uÁR»ŽXÊÞæ2#Ôå*‡È‘ùBŽ|¡öøuê#šáq°­ø+ +°[ÕJKƒW#Zz¸jãcëÔ)ˆð¤ÆÓc”™d¦€®†óTö…,Ž`¬4° +!*$X̼½<„BÈàsøCH¥lòÀZBÜzþUÈ2ƒ«„"CŠ +ìb[2¤PT'ŶèïMÕjþ‹¼Œ#/@jÌÕO@=†Z;€z‘Wl处,àÖÞ­y-óçó§žÝº°®©{"—X[“{®q{ÇÜÒÒõQ…TŠåÔ¦$E—e0~¬Kiimm½·)‹ÇãµKz~ZÈ5%ô+©(Ë/-lÒN’±D»ÖÖiQáê!Ã=C^0™8Ó!ÄuB¤±Ûè¢s"ÖéÒ}lÉ•UÕ5µ‰d2õ¤c\N• ¬–‡.Uª &˜^x/J‡o*©çIÀ)È€?b7º–3×1ŒËVÎhZ¬ ¬d¯± Ô­ï['­—Yý‹Š`;“ÉÁS(Å/êþ¾.Ó [¨^k`>¯jpjzJqq¡âÞ‚­…€NôX.ˆ¢¦l¥¼òùîÅóá{£á•î£ý«IhÔES:žYÕ]žQºnu)i_·´¥éôÀú× +»îÏ®v±jñºÂ.—ÐDzD—*šú(·)ðô_ð†cô¨é¢í²ó +{Á{Ñw¿Þpþ›þÂä8ÍŸÐìTÙ5ïuþvÀxÅw1x‹¾aºn»í¼Åš7ûúƒo—²¾cÿ“ã]—y ý”é{¶gœýìfÎ䉔šý£!ÚÚFQ ¦&)#u‚¾,§¿yL´¤-Û,Ëô„@(M‘C5?„~áG¼Ž=Pá²*,)¼$PóPC­´/òQ%Ë läÌÙÊZwç®Âì/÷ÎQ»_Û³~v¶cãïö?ùó_œDGžÿÇ®—¯þpçÔ«{n¿¸éÉmÃ?è;täÔÂjãë°>2•Eã85+NWÌVÏ&¦ÓÓY“)`“飑ӑñêK‰›Õ×&1ÀÈ©@X6²‰A°/iŒØ ¦µ\[ÐýƒŠÇJö³~4ïóñ¾_èä}Ôîhm$ä¿Ç¿2ûLu‘(ˆ%g%Ye)Æáž° +3ápx2l<Faÿ¢Àü~ž§äÏ€ÙTÁÀ»)†…sºÑ1£“ÛôcÝè¨y¼ù:q9Ì5u«ƒ÷!úÏ4˜RæS5ÿ馇"¦ÇÿUÓ#Ôg«j©Z®k„JÕKPT‰µ"ÊFêŠÖ‡äPÝߤ319-×IÆL,%ÁÂ?à~Øx2LÄ’Z©$„ë¿v]3@jFMC&µ¹•´JàÎ’¤H?™$E‘<ˆ]ê}ªWqJ­ =(+«¹VÛMèéD^ïRp§¤z¦»Ë>ñwTõþkÿ§»Zc㸪ð½³Þ×ìzîÌìØëgfgf½¶×›}ØÞub³ÎŽ›¬]» mIÑâ Z ŠìT¢Qµ Iü£-­J}@‰ª‚ˆ($óؤ …©Bm¨DiˆB2É·v8÷ÎnØÑžsçÞ™Ù{Îù¾ïœ}™S6=;þÒ#ý'žúÎ/¦–O²Z„®Éõ* vf—ÿZ¹²w/y`ûã£Ã[_ypsp“f[ï=g`ü¢ËL8ÅÕ +è[€xq¿¶ó~Ù“Èù¯ffd_OŸN_J_õ¿Ë_í¼îÿ[çbðÓŒÄc¯Ûë÷ö´g{2C‰ÁŒ¯…fÇd€@g †GûbëÐ†Ä òdP¬¥=ŸÌ Íví¼…nãÇxÙpý™`6P‚ZS4¢f徧³ü)#\ïýKß­ŒËãlKØÕò¨.ém1ƒ‘,—6 öYj‚•Ûósé®_õAZñ}yÞqlµ§×YOWçƶÑsæÙúȨ³žÝ=Dï¾à¸y;°1Ÿ…‡×µ¡R_õÔÛ~µ-×Wpy¾ÂMØ¥lZÉfÓ.s7Zš.Ý(¹Hi´ÄEK¸dÇâ¹’Ý“/]íï/xÂvs*Þ+B¾Í›.dMμªòm¦°Q…Ûqj`K’‚¦4)Έϋ'Å·ÅyÑ#ªÃÞ·¸AË´p;쀮m‰vÝÙnW7Ýs¿ËuGî}ÎÑÂåÍ‹P€€‹ Æ]SåkI L#f…tòÛâoKe¹W÷&WöH½àaº2ù\ž¢Éª"›¯/n¤fš5›¨±è†‚o©z£êMö~0ÈR ͳ3΋DŠõŒ8#ÅXÅÁH«êͪ7fÂ`#…Ë!j©)Q“ü_Ÿíª* Škàn¤Gc4Û<ÔæiÁtÑéî®u=y`ez0qS•´ Õ\›ºvžŒvßxglçž/?ýáö£EbÉY¨x—Ùÿг[âùüOþµm[ùÉw†¾[™BÇzÑX_Ïý m“àD²fMü…vìŠêõBq¤4RLtµ'Ö66µ«ª¬Ž ïÚ=ühó–º66¥Ó´A-^¨ûZ‹Þ8ñ™|ÊŽÆ‘Ùkÿ'nS +LFp$¢¤ <œrA¶µñj:Þ‚Ÿ¶jzR %«Œ);”Iå¤ò¶2¯ÜTx&éÄŒâV"© ã<ªqlÁaÙ-â?“e©7ù‘ôÛ¼têÉ% ^F£¢âê\ -°†¸[C”6¡ñ ÿ—n*£0/•?­ù;üål{CC¸\sÇÄtkíŸ1:—?úÊÅ‹ §F*1:ã..?v_ƒ¹Q-$ÛµÌè÷âKtñ<];Ï` #vê%×3ÀR>Ûâ-!TôÛ`xAvò;O wBX/<Ùú~Ÿ|L<G©×„ÍŠ²!ú°­á^Z-î­z»Ñn°;d˶d‡í&{­Ù$x7e9’Ÿ×‡M=ñxy“Š|Þÿ‰n>Ë2¡ºm‚&ñ <]3°ëjÒ|‹ó#5SY/I32ŽÊXŽt¬–õ×™Ö¡qi-/‚Ü)È+ÌsÚp[Õó ç)1U…|ž¥{Û]!˜ZXW;¯S©H¿ô…c÷?v(udCq`Ãs;«Œ±4@•y¦½ýÁÏö<€Ù–/½:Ðßiã×j¬Ÿº÷`ÿ-üÆY"¢'#ºÇJ@ÈýaÙöã‘yOÃäÇŽZ0- Õv†šž_9ó+·4ß Q5r§õax šïIT$dI¢åœËÐØÉð!$7«*4l9-Ë@]08k‰7E¯ÍÒìMØúYdÑKìñÑyÑC¸)h Í “È ¿{l.6 @¡áQ—Êj@myA°!íºœÎKîõ¥“nYðMlÐtw†jÊ”Æ Àõ †?Ï0Ð`€m¥L8U‚ØÒS#rž«õ+臘¤8ƒf`–Fanb^igþüd&ã!«¥@µckeÐûúŸ·†»Š#8B“ÁZ>~uù¸F‡84 ýݶ[ø²ísTÄÒwÑQ½:¬HB§òSÈŠ”‹³÷ßG&Ðãä ©#vª+‡¨yHû¦~Ðkn2C'€!3VF`&É÷Ékä<:M.O®è³Âá·º;ƒÓBB<¢Ò_×ÏjWÐeíºÎË?BØ d€l%äú9¹nŒäÉ4š&/‡Ð=|"_+¶jax›Ö¸˜ºôÌM[¬5MLgv¿{¿{æÜ;çž™sæÌtLb’ªôµQ¢ŠJê¯_ %s3kfU!´Þn…12U#ò2ªFÔì«…Z^ÞÒYCžÑëû銞¹—:^UÝ°ö3 +µÖ;¿6SÀ}ÑÙX™r_ôÓ'^“‚<{ѼºÌ«_ÊÅ\Ó‚ÏUÃýözÎ’íóŽ¼)õ¹ioZ<8ÐèöY|%oá68W6sÒöÀÁòŽRjJ-ì¥ýE¹|KÖÂ’«ES…”&Ïœµ»¹‹ìæ VËÕ¬)ká83 +3Ühì~;kwxù”yM+¶''sݤU.57ÕjÓá«S?¨r¤bŒþT‚Ym…¬ö!£¼‘ª½,m7@õ—U+ lf3lV›™=\â\ +ÔûZö{/ÚRY¹5ÃúNÔ_èË\XÀÜvûRã®b¾Ö]Q×Ñ]ÝÆDË…Ì@…'¹Àæfúâ'ê*\µéÙù®ÔxÛ: +@fù`DõÁÁFßIçý|Öœ¿,ߵĴÔRTrð0;/u[ò…ªøþ¬Ü¢ùqÎîô͸àÁ=Ð@ïÅäÍ ôbV‹Òé}«åç´Gô}kéW÷»º¸à¯œàPœÐ´»ŠÓk]ªZS§Ë‹2ž +Õ ©£ÒŒ˜ö¾.ŠÄïÙk\9w ä!ØøB6÷232(MYcÖÌËÖKþ †¾có™Ë¸ÄŽ÷ó;øžãÏØÎÓoóÍ{R['ikТ’öK;[Áh–¯šÅÞ¾›šp.v–YØíÓŒ»•Zè*±YíÌ':ÁÌy8«9°¨&Ùüôà÷kH¿d„€ïkÈù`.l748ÖcÀ¢C@a@ƒp(Þ,sþÈ–w®-€{¨¼sá»ñ~Úsõ VjY /‘í F²»¹ h}ù1:¾Ñ°æŠ†Îï »ÚX±(Nͪ¼¡}#â`x@N ¡aQJq)I"¡E’G$9œŒJqa$6àZÃÉðe1¡WŠ*’„Чy•55~5U^¡)z¢CÃÉ„Ð#&Dyiéìjoë(o‘Få¨(¯÷v÷zš¥X$zª3šè´’ÃqWXÞ)Hƒk² ‹CÑDR”ň ¢œ +½4OÒR ïÿþî¤ìÞ®æÌò¹O4ÖS»—òh¯:kˆÎÄÔ§úÉæxèý¡X¡‡þÃ9ÏALR¦ÌœVJMIÓ(ûæÒýÞD©úp^;eXí}ÏþçJªt + +endstream endobj 531 0 obj<> endobj 532 0 obj<> endobj 533 0 obj<>/FontDescriptor 532 0 R/DW 1000>> endobj 534 0 obj<>stream +H‰ÜW Tgž/"€ÙL© ï™éµ$¬8ÐR«ƒ°å-îÌ'L8ÐØí”tÕƒŽP©A*&OÄR1Ý$ W(”é:e’ 7…ÅŽ¯®à;•I£Ð‘(¼&C1V-ýYüß߀k]ß3gp–k1<÷r¦Ë…œ’€»æY"±Ä¸“»«–¿×ß{âym[^û±èð]§y½7âþŠg^ýZÿòÛÉhþîQÙÎêÆ¡7ggú‘Óg~Ðu8óQX]æÔ*v—ØèŸé +lÊ­<œyæ¸3/æËÊ­ ião܉®×¯š´ÖRÚ8>eÅô†M1g:½Ä§b×0Y0©_K äç¿v>gÌÉ%Eg¶<ØVØÉé\>:7dKÄðË +ðòg¢Œ&¯66ù×–<Ø»_¸÷„~Õ O£òð†ÏÎËŠ9Á—œbv)§v–WÿeBÅÝÇýÓ¾õX²ÆÏ’ùŒ'[ÑT¾î2Û±6|¶aÉëüÜÕ›dG/¯ ŽZ\¾ði–ç;O>…ùÛ G 3ùÊõyÅí ŽäÌyåMÉe¡w„Óþÿ’x›tÚãxð?§ñb§ü7îôߢøâ|x¿8Ô—žðxb6 +wÚp +uUÿ"¥Á(, SºÎp»¡~qEJÅ…ÿ©Ä^±±‚+mny^öqòY,®òÆiî»ÕõfN¾õ¤Ó¤TïãÛÐ6ÄÔ‰½.ß³«óž0#S·èÔ­{E‰müÖÅû¦>ßSÒÚ^ÕPŒ%úYN­ÜÁÐo<ød]܃âÍ™›Îã×>¬›¹öçRÍï‰gwíf2X¿’ÐÖi«~ÿ)ñÅ©"G„1dp˜°=$àÅ|‚ý8lДm¥¹2ψG]º²»êú¢Úßµ“GÇyUï8¿è|ÀÒ&Ö5¯P=÷;Õ§)Ÿ˜˜|z”þaPóÁ¡ñâШ–5Wÿ46åû6kJþµFt£oIKq[üœš'ËÃ¥O +o_Üq#CîH‹æ n¯MpøÖ°˜ &Ó¯0»Ê6wGëÆ[¶êÆ<·/c&Lhïœú›#J{þ2#v«wšƒhíÙTÁ‰ƒô<£… ͸“ +ywJŽBGHcPôeJÒbT´,V;u3Þÿ“&£I=F£ + +$ùЄ†“Ý ;°$(»³0R‘®¥×°;`,<["¢óZ’ªK¢s9F:Mèñ#K"r +.ˆ%…Å@’ ˆAarÚIH¡—‡Þ`!² a·ü()õ¢í¹f†V*@ýiÁSÀ›h Í°ô(»Mê‡úô…‡ϲÚmYÒÁh ­a zÝ+ G»³Ûí‹yþæáƒ×«ÈÍðF Þ“éf0†Š“C7gýýfÀÁçÖ"¹š÷ÄžÛ"¨Ýså´ù¯².ì­¶ªNü­ìgûàá1‡µòÖñ/¶‡£«£2gíÙ2#4gUãÕ‚ï9×~h¯z\ÏÿͦÏæ9®þdŸ¢žm÷Õ(œÅ/ÄNûèõ–q>üPÁí ¯Á’ØŒs9ÇBujª·U§VMPeŽvÝñ’éw›•â¥;Ú–wdmÞx0LÝò`Ù]Ö¢{q[~Úš>—c5Þ]$(u®=Ї<Àûåðƒ7›—æÙŸ½k½.ø[~άŸ–oËæmð´ËÔYúþáã}neBÒZwÆe]|2õè|kjÿí£=`!ots.¢nιîè¼-`3QåÓ¯¾l6‹É©A]e´Ä`»JÐ9%~EU;¡è2¯¼?ê¸-þG¾{½é¿PHn³~¢A46ƒñœ=¢ô—_ï—]Ó£ц›‹Bòܱ¨›ÓãMÝì¨RV2ÌLQ2.2ò_Æz7kŸËÍjЙ ˜p'Ed&…¢»`èdÃIºjœx6îÄm&\ ¶,@P$È#!Œ$å$L”¥Gæ§ã& +Pv Ì8è=„—~ézIwLÝak¢p+n£ÀpÈ$Œi’4@*Aá"ùÂb0Zh&¯zëÝ0Pq¼7m4žf­[¡ˆp±ÏÍÃIŠû*ÎîäAè à«1(Yl4 £vHy>iö<e€¬ô^ ‚!±#ÐѼ ­â…N"ÇLÑMRóš;ä ÐÐþ‘°'ãY PjtrLÅ›(×hä*¦Ô‚$L«H•ciÊ$ W%õééXÛ°„G£U˜*%èÆ)A†V ÔÉðÓv»Ã’1…\§PÔê4˜B—: h3Ç+: SÓ&<½RƒÁ?Vª>xL­é¹B‡)”Ð:HSªt6½¦ÕfÀõþÁ|•‡EqdñWUÝ38¨(ˆmEÔ°ƒ(‘c8ä”Q—áF¹fÑTÏQÄs F! Š&ŠuÅÕã¢&¢‚˪I¼ Óyƒ.»û}û×~Ûozfªº^ÕûýÞQÕ‚Sà<¿´EÖe¤² àéãïíùÖfÅÿ…R)ô B|]¼]õ³ôôÊÐnE€‹6»Púnžó|õênøßIðwB]½ÿÀ?¥bBç"ó=½½_¿y2gE'IÞŠN?_¥bn ïéä=U|=çy½Õé2ÖQ®N>Nî +¥­ T(dzœúýB?‡«Gy+‘i5æ~<ºLÕ;£c±,DFñêx}XEÅDF(ß$‚SfFX2&,2õ;ƒ;E›)$.UaÄ«“„°H!\":'Q% +ªððä„7¥NˆëÌYÊ›íG`¤ê-ðt²•í³×LþoÒ¼«?V­¶Ž‰’¯9ª¯$·æ\#×H C×{õ/DJvXK °ªðøJgÎÈ%XíðÓëÜ£?©móޛؔ´hƒeÝA¡=¶ú‹t·ôÝ%«N®”x˜G6,¶y1×!geå“AÓR›òjì {ì8 ÓdÊšÙSÅ\«8pŸüÜÃÛ6áçúÆÕ®êÑùÝ\rwë£V.Ô=Nþ]1‹?Vž>1ÕÕa÷ºÜ×Yë§ZÛ¶œ6Õñ䯿h-ì´Üh¬Á#º<ù°ü›Ã`_‰ÁR(ÏÞ5‡åúYêÃìÞÝX8ý.ÝË Û´ü³»F/•þ0hXZΘàGVYaÃR“)7­Äk¶YÍßAŽÖ¾C¥ï…[Å/·íï‹^¥ïú2aDfÙ¡gÊ;Ÿ¶lÔ6æ”ÔŽZeÔÜv;fAè¤"/zÚãÓ÷g¯àp[ç·ójkÓ¹æØq×N…~U¿ôZ‘•Êú|f£ê^aÐeÓXcÏÐ{$Î3Ñä“]¡&wX4®ªOªMV*BJNCKá‹ÁyÍÚ™òeë.žL¿?éÆxëR-¹‚§º†_Hì´ävÐÙšêÿû÷Wj +§5¹<õÚmAVΟݲ , +í¨Áò¡ïÆ©awCJ0L»ŸðvFúW;9¾kÈí¦Lš¼ð_´­° ßµ½²ÊÕ[Äç&ÛŒ¼zÕ"}u\HÜçô”ïîßXñÒt Waðõ†ÌZ1ÚÏ1ñiÆͨ)Òå‘ŠAýÍá#ñÕ©¢2›â”–ª4ñÈÌÌïm{òX“X2°¬Yæ† £²&®T{ù¾ÕÔ61;$íΠѼlk¡£67Äró­}OåÆÎ¥«Gûeõñøõ¤ýN‡¯?mômÙXž¨/j„»D +€à‹øIØ´|óËö@5îËxžP"•P^ +½.u¼f· í"¿QçF&’:M÷S~1ØñÞ0ïál+˜ˆwÞÞ÷tÁâ#~9Xè–‰7­Œpð—oï7— +,a ØÀ¨ƒv8MÆ?œ¯@8, ÂûØŸÇá ÜWˆ +f$±6ÂXX {`:g&V7<00‚Á0f5HÀ¢a7¹ žà…s8€;ä@~ÏÅþçd>! ƒÅ¸úVØ §á/ð Ãmá:ºþ¹ø¸`E ‡t8·yg~˜@!‚2¨…ûÄ–ì'mì±X%6ˆÿ@-°{Áꛡǂ‹Ô‚íÍÄtñâyŽÖ—#êZ8‹k=# "áô KÓ½ãÅrä¡/ڌ֣8!_H‚8ò:¼&}P´T ÐpÝ@qHa$V¸ñh_ V¼Õ ›E”ÀQx@> KÉ%ò˜ö£ZÃûK}¥¾}j:¾ÝÅg¸F_…Ö·劚›a lGÍR\ëO(íÐAì‰q$ž$€ä“õäyAÇÓïékÖŸ± ,˜…² ÖÌ^ð~ºº+¢¿˜Š\ä\†žtAœó`¬€Dø2@ƒÖå¡ {å(Èg Ê7p Àxˆ1Ç#F‡"Gq ³ÉH~O¢I"ÙAŽ‘jršœ%mä LíétêGh4]A“h­ •´†Þ£¿ •3˜‚%²X9«cçÙUÖÄ7‡Sq1\2·•«à¾åÚ¹'œŽÞÅ–Wñ{:öê¼t!âXÑA 7‰(ãˆf,X!ôj8î(шj¬DICîÖ!¢í°¹Ó³w ªákŒÒ:ôo=\&Äw šá9¼DrôøLÉ(ò>±C~gw”…角A4$!Ï•¤ +å ¹‰(uˆ0ˆÓ%4…fÐMtÝIOÐ3ô:zBdôÄPæμØ|–°$¶}Ì>a»Y «fgX=G¹œ?—À­å +¸½ÜQî×ÈÝä弟‹RÁWñ§ø‰±Ä\2Y¢”TK%i­:øÎA%TõÎ}’MJøŒ´2Žih]@ éu¢å.+ôÀL|î¶?£…ï‘«t*™ÏÂÉBäOK¢HìbÃÙ^6øx¢dþ$”Üø•ÿT|.ýœQ>—u—´–B]ÞQ&“þ $ûéAŒ˜L˜ 6œ\§Ó¹Ä’ÚÐéR ŽR ›Îfak?»‹f* ŒH¨ØoäW[p×þÏîjw-_…±e £‹äزll.¾©¶dI† ìØ–¡Z ­.µŠ= Ðq))-CIEð(“Èô2Ít˜4q2í‘ŽœI©ßÚ—<1ãvJ \Ú‡R2B§)FýÏJ6vÊ´}ìLWúÎ?ÿ¿ç²g÷cÜ?·po soã3áù£ôV·Èÿ}NB7¹ô¤Þ5h\”¬ç.‘Ý‹§Ïÿ0÷RÍ} °X¾èãü¸âöäf¸kð.>ù»p®q7`>5úÎù÷Þ7ðI³s¥¸ŸÂø™ôöôtÉÓÕÙÑÞ¶më–Ö–ÍÍMîFWCýsuNÇ&u£]±m¨]o­©¶TU®«Xk.7­)+-)6É’hð­ƒj_T¡Î(œêÎn&«1TÄV(¢TAUßjªDu7eµ§=~ÁÓ›÷ô.{“â»Q ª +ý( *Y²o(‚üù€ª)ô¾Î÷ë¼àÔ…RìvŒP‚–±€BIT Ò¾ãc©`4€ýeŠ~ÕŸ4º!c,F¶9Z¥NfHU7Ñ®*Ø™á@.ŪhÒj5ÀJ ¼#¥ƒC‘`Àj·kîFJü 5NAí¥k\º øõ4TôSIO£Œ³ÛsJ¦q>õZÖñ¨«dTˆP>¦±å.Ì Uß¼cy*bçfäìJ«•O-ã +S©³ +}k(²Òjg­¦aË9ú¢©>LýŽb(¬`6îŒ¡ä ¦TØ°»Êß_R 2MôB‹Ô^u,u(ŠsS“¢0S©á©ËÕ^¥zµÅݘ1•ç6S¶¦À””®d’Ë6ÓÝ^YÂ*RŸÇA•„‚•DT¼§vÖ$Û!•hG7¼4‚Qtgdœù£)S'Ó³xjp˜T%õà +Pïÿeµ&VЈÓgÀX¶N–×Ú—xêrц¶D$?Î)ÖØ­ËÛÜdzœO4)Hpø`Ç6¦u6ãðÛíl‚Ïe½GžŠäeâÖYð6»4ÊE™e~ɲn³œZ²,‡GU\ÉWðüXGeçò©rmp¬“’ÊcNæí¡°ÚQ‚©halC#«¤¼½}ÙVàèZ„·rŽ³òºåeg&DJ¨àÀ¿¨/êѬ$ãªÔ5D飦èÎ|«íöÿ2(›û„EéäiX¡LÚéZ-w­’W•W’â±`ÁÉ…Fö¥RÆ•6`ƒ&?éÆvï“÷7ÉGõa\y]>ÂS•]Ÿ¾Ú!fàŽá +ć0 +Câ ì;`':Ñ6‚p£íu´9ÐÿH¾Îuär¨ß…øш#D¡!v#¾…â:à}Ä9Œõ°xFùóa¼á7PaØ ‘š…»P#܆:Ñ +;…렢Ήù·J`y‡á$THµ,&÷g”w‹ôù+Öð28…¡c» g k߶vC=ôŠ0ßm¨Ä~~&þ‰BºË@äÀÿûÁ:¦}üCbìó‚ vð»ðþ®ƒ›û)ø‘ѾÑ"üïÉÏ!ÏêoC^C:Ž>ëBûOÖ:È +û‘6c¿ûùßÁuò¸„tý· +`-ù\Ïë!8[³Ç +DæD‘lFú7Ä#y/ÔKw!„ý¿¸Dù-pžðã…1Âøƒ˜ÇÇÿƘaË%Ü®s2äÎã½+âœó“àƱùŠt—|Çj@Lj!ígÀþÚmˆ®: WˆQŒö0Ê»ÄaH0H6hÅØ&Ì5ÂÖÚ6c: +õï.Ô¯S¬³ÇÕ·/î‚Œqñf¯,ã!¾o<Äï’Ks 㻹ü:ɽøysî Þ̽˜§ "ÿb,¹ë}ëÀÌÕáÏÉ9a‚TâîøªÞ¾ ·=zÛÌZ®y¶ÙfËrM³o1Ò8[[d“·øV­¥ÎlóÔ1¹ÊÛu¸Þvs¦Úv ñ^]«íUO«í4¢qeæW7So›¨›øúÄ÷&Î +mPY‰³l.—½Yrû—{*Š*ŠÚÒYòko‡”þ•”¾,¥¿&¥G¥ô—¥tŸ”Þ.¥›¤´KJ;¤ô&©B6Ë&¹L.‘²,‹² s2ÈÙÜM¯‹mþ +ÑĈ(°VÐyÇZ¶ÑñIÀ™Ã¯;º–q¡p/mw…²Rn˜¶¹BTÜÉ2­¡–r¯f ŒD²$ÇTg¬ìÔžBrgÎ[ TÓHˆÎ' W裰š%F|PÔ^BÍ!ôZ òx¥ÇÜ]ÞÑxF-´®§—ŵò + N}6rŒ}|‘£—%ÛÓ†Q›Öµi¦MëZK-½ +GèL­F[“«ÕÈeßUï öUƒID”ž;>f¡§âŠ’ñ^-¼ 8£ñÄ£±$½ª&Ô«”ŒïÄ3Ì'˜Ù§2p"8Éœð&³>¯/¨ÆÚ x¦azUºï/¥›ƒÿ׳$κl`¦Ÿ‘qš™XÆi–qšeðèƒãá^ŒddèÕððÑée®ØˆSµÚµÞJÓd·>o]vË+Ö ï@1žÅ%ø^WŠ`&·Ïíc&\0ÌTÆ^ù +&Ë+]vëä‚É„êrµ\Ç\_¸^fX‚ã¬d.7Ïš5ÛZ];g8vá×ncœ´.ïQJ Î $x0Š†Ïs5E’ P-×·[\¦‡žþEπ鑧ߴèÏ¢‡¡e³½Ü^îÀ×6CKK] a¤§à˜“£”™žQR¬”ZœZT–šâìéãáì§\™›”Ÿã‚ŸËÂPÉPÀÊÆÈ ¤VqC˜íËÏÄ%PU + Î@^ "â™` +@‘ ~= Ë,žH¡Iúp—)0erJájŠbž@bŸ!ƒ%0èBYF`QG Ž Ô“tC XWмb .b(’)@7x2ø0xi?m =• ¹ I`Û|öƒT§íͺ¯ˆ€ZJd!©óÐ.VpjdbúßÈzËÊÉ350> endobj 536 0 obj<>stream +H‰TP=o„0 Ýó+<¶êUºˆånaè‡ +ížK TœÈ„ß„ÂUlËÏ~zÏ–çöÒ’O ß8Ø žã¶W=A¥Áy›önËv2d&wëœpjiP×B¾çáœx…»¾¯Nêä+;dOc†õÇgFº%Æoœ(hp8y~6ñÅLò—ù‡ökDÐ[_íêÁáE64"ÔJžš£ ¹ÿóƒuì—aqlk¥u#òöŽ^¹êfÄ.ÌÙãvúf¤Xð„·ïÄ‹Z ñ#À‰¾jö + +endstream endobj 537 0 obj<> endobj 538 0 obj<> endobj 539 0 obj<> endobj 540 0 obj<> endobj 541 0 obj<> endobj 542 0 obj<> endobj 543 0 obj<> endobj 544 0 obj<> endobj 545 0 obj<> endobj 546 0 obj<> endobj 547 0 obj<> endobj 548 0 obj<> endobj 549 0 obj<>stream +H‰´W[oÛ8~Ÿ_¡—¦“PwkQHœ¸ÍL“xbg;‹nd‰Šµu$C–›z~ý’yHKvÒf¶yÏõ;¾}û‹ãœžÍÆWW£ð²ÊêœNÓºqŠtµ¡‚¶ZÕOó&­6ë´¡U¶C¤m[OëMÙ–uu9MÊ»Ø6[E¼«Û´•OoêJÎË*/«çô#-Z~0NWï›t7mê‚Ip~½¨[ç}ZVŽGþñF2ܽ?Wô ûá\]Ž#7‰¢ß¼˜Æ×ÿþCqÝŸÌNœOtáŒkfCîü‡„döévÊ>]ç›'îlž#¶ªêöòqAóI]µÓzU²œ^6MÝzý¸NÛrQ®Êv÷‘~£+Ç= €ÒÐÍævñ_šµ,óôaƒ ]\ ^ãºúF›öê‘ŸÎë«*§ß™Ù@ž¦›Í|ÙÔÛ‡åïÓË÷›¾ÜPæäïž y\/æeö•¶špA‹t»jï(ÓÒ°d\U-­ZuÞ±´ÌìócÁF®ê¦³tÃ’>k&ÿaÇs™~£‚z_eË´z ¹RÏ—ÛÇE•–+)M†+P-[œ2[o×Oúüb6¾£ëºi™]€ ?žíªvIÛ2;¯WyÅÂÉâNH'¨lÙ¥OiS±+Xo•ó˜;¿¹ü—á5}¬›ã’`Æ?þXg_/ÊM[®V´™¦Múˆ$\§ßgÛņ¶Ó¬e—„:nïcù7UßN¯·ËY³¡Ì–ðGŠ]µ “º¹¨³«ª¨5 CmxL×;a´M`×{áCº*ZVj‚¨ êíôJY³Odi]38´3Ú¶"t |Ö¦M+ÌŽu10Ó'ZDA›É¶Êx…þÓ³õzµãäûñ]Zåçï»cÐ((ÊjpU?l1r9”À ê_ßtÝè)ÝmTœÏ û §Ü0h4}„³ª-ÏVeº2eáèJ¨ŸªMú¸^QL6ð.Î4ß|·¦¼‹eÛE™™LwtS¯¶Ü{Ö»ˆ%€®Û¥„^¯Ø%‹É’aYà˜H(‹Ž|Ø2–6Ì“ñ¼c‡îÛ_Ö|º–yQ\gãYghÉÿ–O Fùs’fmÝ8ä$ Ýɇ™pbã|vñ÷Å9ý×Þã}÷Î +@¿\7ü!¹Üty2lúœaéS™³Txa¤>ÐòaÙê³?·)oèŽOúe¿¦`P>‡ñ‰¨GutŠÓïÐ¥¯#löÉDÐ Mh2ê0]=KÎt•?ˆíúÿ”ƒ†ÿ<&û-H^×U= ID…ª£T<’®l˜ú>ÂdŸÐaL² +09_Íç“ô»¦ÖCøGgŒ +_…§³¿`£Óôbò—›Ž—4ûjú=g|ýc¡e»äÅä¶ZíLêM=oÊÇóú»Ø0õtg$I˜××4/Söí¶(Øv> S‰šÏùþ„²Ëv<š‚ µš, x µ·Ûv½m»µSmàoL*Û3sñ¶@”;úÀv³fw“>² ˶]ÿóôôééé$ã]ù¤nÞèp¥ë5ÛN﫯ÃÍ/ÝV»ÉšRl˜*Ç¿Ooœ·“ËÉÄ'¡ï“ˆŽR/Iʾ¬UºIšF õC’!M£8ôü»O/ ¼…OFÄ'yìšÊÏÌ'‹$ôcz~‡ÞˆË%„×*Gk°ÿˆÿŽÂQEÅˆË +Š0‹<—É +&“ÉØy³ÏEPÄE''dòcÚñ¯ÇæÉa¼aqXo$éÛg#&2f?øYÄ?ÙYp:³ƒÉ1þØ—ò8=ìx¢ ûÎïr ñ˜¿Œ•ý( “HøFÑ(N|agçÛA_]òÐ')ã)=± ãŒÇÝ ë˜ñYXda2"£Nv'ó]—ãÉÝY—cæ'ó-&_Q"}e¶‚_œÆÏbäÄ…&¦ŸÊoý.$?M.cD;}*vF.ô=ÎÃu`y^,eÑN–°ÑÓ|B_&íµ|â¼ÂæLÚi†|ÝÓåÊ3WÊI´ØF¡7—±õt\„–M1ö¡_âGÄ +ûú%Oœ>»Úǃ¸Í¤m‘>ë³ÝĶ)[Ä)CyôäýPÇXø–ÀZFLL=Òò¯´'ÈPüiqÇCòQ ±‘‰ÖçA¨}»"™¨ !?6åsì@‚ȹ-ø™ñ3j9ï¾ ú¢¿`Y‚– œ"_Œ>3"¤ˆºO8ÇrÀY&㹈º3~7öÍX( ¢ØúÕ?ÆSi;¡ÿñï_WÈ_Œr'e‹|ÆR‡§éÜþ"³ú-µJ œK‹Ð̽³×l#çTëº$ïBßBæ’ê8áá9}Íž½{˜Ìå/Û/Á0Ź̩ Á?`ú‚QëÃCõú^RóBVbö¬>œ‹~¸…zŠlà÷´_ +[XøˆpˆãlôÕê‘Óù¹ê‘}3/EB=Xàz3ìTê—õ¦fã¥ìwjªç»šïyõ*ÎCOá“´#Žúõ¨ù†|½@ܱ÷,˜¡ôçâ‰gçz¤LÊøQbàÜŽÿ³ö‘øjÝv'ÄÌ/Ôñ+ãòE#b¼û°L5ÿc”ìçÕíjVõN„Ø#ÄbÎ#UÓ7/TMÃõt2qŸ0v‚ûí¿Ú7Q¿ÄÕÐLArŒýfñÂÒ•#,Æéðv@ïˆ)ÌåäWúûĨM¼;(,ÀÜ€¾Šv¥¿'Ïð¡ÏfžwxËXulìå ’kÅÂè5}r§0“¸fŽõâgäXõJöÞ-Â_k×U÷sЇAFÐÓ£=É~éÞ0„5cW€š…¾üµ¦ü+´†ÖŒWûEªmP¼vó¥¾Â’ ýär65Þ¸_†¤‚ùDíQöL8øVÉë¿pí¹r6àý} îÁ‡ÂG2Ž½Ïh¯S>Cßω®i4Cí7ƒŠ›gúƒn÷ÀÔ¥b9ðV8è×@œ„þwŠÂ>àé5Þ¾åô8»ä»BíM^þõ…8yyO€;xöÇÇ°äÍ¡øÁn«Öl߆Þ}øÝ{wØ{’Üs•ô¹†m9Z#Êš ²ÏÌîoÕÞÂx(ÌÛž<«7e`ÙbóÞÌôGÍnŒ_\ƒPû…¦÷Î=åv‰=Ñs ùÉïÛ'µtã~ {>ÀGððC|q[ ÁÞá"Ý®ærá­€lP}yø0~}t&q;ø®8#\{Çòâj{q¬é|ß÷—Ô¶Ú‘Ð{Qäz¾-°z‚Œ“Êu~àîPÿÀ|РFòîs¨6DþЙÚa¬}#–³ˆÿsyÜ ;öŽöa…õ6»6]TëWó³½0½ÍN\ßxæîõQ~'%fß…< Ù;´k(ùÇæUÒƒõžY£ö\È/Ì&ûmaÛlÍ`±WÄdo>ôùµ§âh½#(A¾¢oÀ2ÄÚEvÉY£öl`4ù¹xùų®°öO_Ç¿ùpl°ž—îêÝó‚¾a¼‡vˆ;ÄÅ—ÿدDËP=öµkv®oô?Ú« ·mÞý +½ •i‰–€À·=¥E‚´‡ ;f +nÄny},™³œ¥(9ŽÑCGÉÝÙ!ñºÈû˜·:1£çŒo4+b^‘ ø9qýíF<ÉÅ{´3ÏäPg9ʿΓw+öØ2¿ý·íÿŽ4vs¤ÐØ÷´ø­úžçÞ¯}žªÃ\îõ¶¡|0Ç)¾öy·vê¬î’6àŸ%sÜß#»ØqžžÃ;Ð\Q:cý¾&`ÛÕ°ÎÅ[ ú†S°[åÊ'8è Ia|”÷O³“¸OëЗ|S3ò?ô°Ô¬ÔóGâIԔϳ¨ëúGüú+á¦Çž¡ûòlª¼§0:Gä×­E~QߊF“¶µ¿Õ~ä‰ç¸=.#wц!_¤÷ùj±ŽqIxjÇyÚ½GíxöÄZ O>Â[Åï¥ãØ¥Ƹ}gÕ=…8Ûå \|Ïâ­×²Ïè¶ô`Ì›’bKñ˜u{úùgË«ÚaÍ*wUÊ«×GTk©¸P†øtM¸~u}×ó묗UÈ]žó=¤Ð1É=Ò÷‹Iùñ!]ý€Ÿ”ýçÔ»Uèuñ®ˆ›qeÏ\ˆgÊ·#/ò’)måó̈'ý¥3Í:Íuõ]r׳ÚãG5:O;Óœµ<ç¼ÿ`-_Su8Êaö8‰ÃÑÚcê©n]›“û5A=ëhö§|0ðE>E.>Vkìb1Y,²­Û­›í®ÙlÜëËòuù{;¹¼Ü¿ýòõçÛ>oþîšç?Ùý´Ê³ýßCûæûò—»mÞ\v_˜é…1UVÖå…™Û¿áËþƒµû×<ºÉ»4JÏ¢ + +endstream endobj 550 0 obj<> endobj 551 0 obj<> endobj 552 0 obj<> endobj 553 0 obj<> endobj 554 0 obj<> endobj 555 0 obj<> endobj 556 0 obj<> endobj 557 0 obj<> endobj 558 0 obj<> endobj 559 0 obj<> endobj 560 0 obj<> endobj 561 0 obj<> endobj 562 0 obj<> endobj 563 0 obj<> endobj 564 0 obj<> endobj 565 0 obj<> endobj 566 0 obj<> endobj 567 0 obj<> endobj 568 0 obj<> endobj 569 0 obj<> endobj 570 0 obj<> endobj 571 0 obj<>/Type/Filespec>> endobj 572 0 obj<>stream + + + + + + + + +RGA Ascii ProtocolMatt Stephens + + + + + + + + + + + + + + + + + + + + + + + + +endstream endobj 573 0 obj<> endobj xref +0 574 +0000000000 65535 f +0000215388 00000 n +0000215909 00000 n +0000216030 00000 n +0000216151 00000 n +0000216272 00000 n +0000216393 00000 n +0000216514 00000 n +0000216635 00000 n +0000216756 00000 n +0000216877 00000 n +0000216999 00000 n +0000217121 00000 n +0000217243 00000 n +0000217365 00000 n +0000217487 00000 n +0000217609 00000 n +0000217731 00000 n +0000217853 00000 n +0000217975 00000 n +0000218097 00000 n +0000218219 00000 n +0000218341 00000 n +0000218463 00000 n +0000218585 00000 n +0000218707 00000 n +0000218829 00000 n +0000218951 00000 n +0000219073 00000 n +0000219195 00000 n +0000219317 00000 n +0000219439 00000 n +0000219561 00000 n +0000219683 00000 n +0000219805 00000 n +0000219927 00000 n +0000220049 00000 n +0000220171 00000 n +0000220293 00000 n +0000220415 00000 n +0000220537 00000 n +0000220659 00000 n +0000220781 00000 n +0000220903 00000 n +0000221025 00000 n +0000221147 00000 n +0000221269 00000 n +0000221391 00000 n +0000221513 00000 n +0000221635 00000 n +0000221757 00000 n +0000221879 00000 n +0000222001 00000 n +0000222123 00000 n +0000222245 00000 n +0000222367 00000 n +0000222488 00000 n +0000222608 00000 n +0000222728 00000 n +0000222826 00000 n +0000225753 00000 n +0000226294 00000 n +0000226416 00000 n +0000226538 00000 n +0000226660 00000 n +0000226782 00000 n +0000226904 00000 n +0000227026 00000 n +0000227148 00000 n +0000227270 00000 n +0000227392 00000 n +0000227514 00000 n +0000227636 00000 n +0000227758 00000 n +0000227880 00000 n +0000228002 00000 n +0000228124 00000 n +0000228246 00000 n +0000228368 00000 n +0000228490 00000 n +0000228612 00000 n +0000228734 00000 n +0000228856 00000 n +0000228978 00000 n +0000229100 00000 n +0000229222 00000 n +0000229344 00000 n +0000229466 00000 n +0000229588 00000 n +0000229710 00000 n +0000229832 00000 n +0000229954 00000 n +0000230076 00000 n +0000230198 00000 n +0000230320 00000 n +0000230442 00000 n +0000230564 00000 n +0000230686 00000 n +0000230808 00000 n +0000230930 00000 n +0000231052 00000 n +0000231175 00000 n +0000231298 00000 n +0000231421 00000 n +0000231544 00000 n +0000231667 00000 n +0000231790 00000 n +0000231913 00000 n +0000232036 00000 n +0000232159 00000 n +0000232282 00000 n +0000232405 00000 n +0000232528 00000 n +0000232651 00000 n +0000232774 00000 n +0000232896 00000 n +0000233017 00000 n +0000233116 00000 n +0000236462 00000 n +0000236595 00000 n +0000236745 00000 n +0000240491 00000 n +0000240624 00000 n +0000240775 00000 n +0000243830 00000 n +0000255848 00000 n +0000256127 00000 n +0000256260 00000 n +0000256384 00000 n +0000257981 00000 n +0000258114 00000 n +0000258226 00000 n +0000259319 00000 n +0000259452 00000 n +0000259564 00000 n +0000260875 00000 n +0000261008 00000 n +0000261133 00000 n +0000263415 00000 n +0000263548 00000 n +0000263648 00000 n +0000264546 00000 n +0000264679 00000 n +0000264791 00000 n +0000265616 00000 n +0000265749 00000 n +0000265861 00000 n +0000266901 00000 n +0000267034 00000 n +0000267146 00000 n +0000267996 00000 n +0000268129 00000 n +0000268241 00000 n +0000269313 00000 n +0000269446 00000 n +0000269558 00000 n +0000270788 00000 n +0000270921 00000 n +0000271033 00000 n +0000273033 00000 n +0000273166 00000 n +0000273253 00000 n +0000274155 00000 n +0000274288 00000 n +0000274400 00000 n +0000276456 00000 n +0000276589 00000 n +0000276676 00000 n +0000278597 00000 n +0000278730 00000 n +0000278842 00000 n +0000280387 00000 n +0000280520 00000 n +0000280645 00000 n +0000282335 00000 n +0000282468 00000 n +0000282593 00000 n +0000283701 00000 n +0000283834 00000 n +0000283959 00000 n +0000285922 00000 n +0000286055 00000 n +0000286180 00000 n +0000287283 00000 n +0000287416 00000 n +0000287541 00000 n +0000288592 00000 n +0000288725 00000 n +0000288850 00000 n +0000289792 00000 n +0000289925 00000 n +0000290050 00000 n +0000291422 00000 n +0000291555 00000 n +0000291679 00000 n +0000293554 00000 n +0000293687 00000 n +0000293786 00000 n +0000294921 00000 n +0000295054 00000 n +0000295178 00000 n +0000296641 00000 n +0000296774 00000 n +0000296886 00000 n +0000297893 00000 n +0000298026 00000 n +0000298138 00000 n +0000298977 00000 n +0000299110 00000 n +0000299222 00000 n +0000301763 00000 n +0000301896 00000 n +0000301983 00000 n +0000302431 00000 n +0000302564 00000 n +0000302689 00000 n +0000305297 00000 n +0000305430 00000 n +0000305517 00000 n +0000306026 00000 n +0000306159 00000 n +0000306284 00000 n +0000308818 00000 n +0000308951 00000 n +0000309063 00000 n +0000311588 00000 n +0000311721 00000 n +0000311833 00000 n +0000312635 00000 n +0000312768 00000 n +0000312880 00000 n +0000313577 00000 n +0000313710 00000 n +0000313822 00000 n +0000314921 00000 n +0000315054 00000 n +0000315166 00000 n +0000316004 00000 n +0000316137 00000 n +0000316249 00000 n +0000317054 00000 n +0000317187 00000 n +0000317299 00000 n +0000318227 00000 n +0000318360 00000 n +0000318472 00000 n +0000319229 00000 n +0000319362 00000 n +0000319474 00000 n +0000320447 00000 n +0000320580 00000 n +0000320692 00000 n +0000321670 00000 n +0000321803 00000 n +0000321915 00000 n +0000322935 00000 n +0000323068 00000 n +0000323180 00000 n +0000324185 00000 n +0000324318 00000 n +0000324430 00000 n +0000325259 00000 n +0000325392 00000 n +0000325504 00000 n +0000326498 00000 n +0000326631 00000 n +0000326756 00000 n +0000328134 00000 n +0000328267 00000 n +0000328379 00000 n +0000329057 00000 n +0000329190 00000 n +0000329302 00000 n +0000330099 00000 n +0000330232 00000 n +0000330344 00000 n +0000331223 00000 n +0000331356 00000 n +0000331468 00000 n +0000332699 00000 n +0000332832 00000 n +0000332944 00000 n +0000333888 00000 n +0000334021 00000 n +0000334133 00000 n +0000335066 00000 n +0000335199 00000 n +0000335324 00000 n +0000336185 00000 n +0000336318 00000 n +0000336443 00000 n +0000337574 00000 n +0000337707 00000 n +0000337819 00000 n +0000339610 00000 n +0000339743 00000 n +0000339868 00000 n +0000341981 00000 n +0000342114 00000 n +0000342226 00000 n +0000343353 00000 n +0000343486 00000 n +0000343598 00000 n +0000344393 00000 n +0000344526 00000 n +0000344638 00000 n +0000345502 00000 n +0000345635 00000 n +0000345747 00000 n +0000346512 00000 n +0000346645 00000 n +0000346757 00000 n +0000347452 00000 n +0000347585 00000 n +0000347697 00000 n +0000348436 00000 n +0000348569 00000 n +0000348681 00000 n +0000349538 00000 n +0000349671 00000 n +0000349783 00000 n +0000350726 00000 n +0000350859 00000 n +0000350971 00000 n +0000352021 00000 n +0000352154 00000 n +0000352266 00000 n +0000353313 00000 n +0000353446 00000 n +0000353558 00000 n +0000354275 00000 n +0000354408 00000 n +0000354520 00000 n +0000355304 00000 n +0000355437 00000 n +0000355536 00000 n +0000356564 00000 n +0000356697 00000 n +0000356809 00000 n +0000357642 00000 n +0000357775 00000 n +0000357887 00000 n +0000358739 00000 n +0000358872 00000 n +0000358984 00000 n +0000359800 00000 n +0000359933 00000 n +0000360045 00000 n +0000360932 00000 n +0000361065 00000 n +0000361190 00000 n +0000362003 00000 n +0000362136 00000 n +0000362261 00000 n +0000363060 00000 n +0000363193 00000 n +0000363318 00000 n +0000364143 00000 n +0000364276 00000 n +0000364401 00000 n +0000365227 00000 n +0000365360 00000 n +0000365485 00000 n +0000366502 00000 n +0000366635 00000 n +0000366760 00000 n +0000367569 00000 n +0000367702 00000 n +0000367827 00000 n +0000368970 00000 n +0000369103 00000 n +0000369228 00000 n +0000369951 00000 n +0000370084 00000 n +0000370209 00000 n +0000371041 00000 n +0000371174 00000 n +0000371312 00000 n +0000372268 00000 n +0000372401 00000 n +0000372526 00000 n +0000373304 00000 n +0000373437 00000 n +0000373562 00000 n +0000374343 00000 n +0000374476 00000 n +0000374601 00000 n +0000375375 00000 n +0000375508 00000 n +0000375633 00000 n +0000376467 00000 n +0000376600 00000 n +0000376725 00000 n +0000377668 00000 n +0000377801 00000 n +0000377926 00000 n +0000378798 00000 n +0000378931 00000 n +0000379056 00000 n +0000379915 00000 n +0000380048 00000 n +0000380173 00000 n +0000381023 00000 n +0000381156 00000 n +0000381281 00000 n +0000382073 00000 n +0000382206 00000 n +0000382331 00000 n +0000383118 00000 n +0000383251 00000 n +0000383376 00000 n +0000384122 00000 n +0000384255 00000 n +0000384380 00000 n +0000385161 00000 n +0000385294 00000 n +0000385419 00000 n +0000386369 00000 n +0000386502 00000 n +0000386627 00000 n +0000387444 00000 n +0000387577 00000 n +0000387702 00000 n +0000388545 00000 n +0000388678 00000 n +0000388803 00000 n +0000389573 00000 n +0000389706 00000 n +0000389831 00000 n +0000390751 00000 n +0000390884 00000 n +0000391009 00000 n +0000391874 00000 n +0000392007 00000 n +0000392132 00000 n +0000393155 00000 n +0000393288 00000 n +0000393413 00000 n +0000394204 00000 n +0000394337 00000 n +0000394462 00000 n +0000395384 00000 n +0000395517 00000 n +0000395642 00000 n +0000397533 00000 n +0000397666 00000 n +0000397791 00000 n +0000398434 00000 n +0000398567 00000 n +0000398666 00000 n +0000399356 00000 n +0000399489 00000 n +0000399601 00000 n +0000401477 00000 n +0000401610 00000 n +0000401722 00000 n +0000402494 00000 n +0000402627 00000 n +0000402739 00000 n +0000403461 00000 n +0000403594 00000 n +0000403706 00000 n +0000404385 00000 n +0000404518 00000 n +0000404630 00000 n +0000405383 00000 n +0000405516 00000 n +0000405615 00000 n +0000406264 00000 n +0000406397 00000 n +0000406509 00000 n +0000407063 00000 n +0000407196 00000 n +0000407308 00000 n +0000408008 00000 n +0000408141 00000 n +0000408253 00000 n +0000408878 00000 n +0000409011 00000 n +0000409123 00000 n +0000409772 00000 n +0000409905 00000 n +0000410017 00000 n +0000410628 00000 n +0000410761 00000 n +0000410873 00000 n +0000411468 00000 n +0000411601 00000 n +0000411713 00000 n +0000412270 00000 n +0000412403 00000 n +0000412515 00000 n +0000413078 00000 n +0000413211 00000 n +0000413323 00000 n +0000413898 00000 n +0000414031 00000 n +0000414143 00000 n +0000414751 00000 n +0000414884 00000 n +0000414996 00000 n +0000415628 00000 n +0000415761 00000 n +0000415873 00000 n +0000416424 00000 n +0000416557 00000 n +0000416669 00000 n +0000417281 00000 n +0000417414 00000 n +0000417526 00000 n +0000418096 00000 n +0000418229 00000 n +0000418341 00000 n +0000419266 00000 n +0000419399 00000 n +0000419511 00000 n +0000420156 00000 n +0000420289 00000 n +0000420401 00000 n +0000421129 00000 n +0000421393 00000 n +0000453742 00000 n +0000454000 00000 n +0000454553 00000 n +0000454821 00000 n +0000455226 00000 n +0000475756 00000 n +0000476234 00000 n +0000476508 00000 n +0000477002 00000 n +0000508898 00000 n +0000533744 00000 n +0000533931 00000 n +0000534162 00000 n +0000534348 00000 n +0000541424 00000 n +0000541557 00000 n +0000541845 00000 n +0000541902 00000 n +0000542047 00000 n +0000542175 00000 n +0000542329 00000 n +0000542476 00000 n +0000542631 00000 n +0000542786 00000 n +0000542923 00000 n +0000543144 00000 n +0000543365 00000 n +0000543576 00000 n +0000543737 00000 n +0000547356 00000 n +0000547393 00000 n +0000547418 00000 n +0000547482 00000 n +0000547625 00000 n +0000547764 00000 n +0000547906 00000 n +0000548048 00000 n +0000548190 00000 n +0000548332 00000 n +0000548474 00000 n +0000548616 00000 n +0000548758 00000 n +0000548900 00000 n +0000549042 00000 n +0000549136 00000 n +0000549278 00000 n +0000549420 00000 n +0000549562 00000 n +0000549671 00000 n +0000549706 00000 n +0000549931 00000 n +0000550015 00000 n +0000553631 00000 n +trailer +<> +startxref +116 +%%EOF diff --git a/numass-control/msp/docs/commands.htm b/numass-control/msp/docs/commands.htm new file mode 100644 index 00000000..837a3ba7 --- /dev/null +++ b/numass-control/msp/docs/commands.htm @@ -0,0 +1,489 @@ + + + + + + + + + + + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

 

+
+

MKSRGA  Multi

+

Protocol_Revision 1.1

+

Min_Compatibility 1.1

+
+

ÐÑинхронное Ñообщение о подключении к прибору по tcp-ip

+
+

 

+
+

«command»     ERROR

+

Number            + 200  

+

Description      «err + description»*

+
+

Ð’ Ñлучае ошибки возвращаетÑÑ Ñто + Ñообщение

+
+

Sensors

+
+

Sensors OK  

+

State     SerialNumber            Name

+

Ready   LM70-00197021  “Chamber Aâ€

+
+

Выдает вÑе ÑенÑоры, которые могут быть иÑпользованы

+
+

Select "SerialNumber"

+
+

Select OK

+

SerialNumber  LM70-00197021

+

State         Ready 

+
+

Выбираем ÑенÑор, Ñ ÐºÐ¾Ñ‚Ð¾Ñ€Ñ‹Ð¼ будем работать

+
+

Control "AppName" "Version"

+
+

Control OK

+

SerialNumber  LM70-00197021

+
+

Получаем контроль над ÑенÑором

+
+

FilamentControl         

+

"On/Off"

+
+

FilamentControl OK

+

State On

+

 

+
+

Включение нити накала

+
+

FilamentStatus 1    ON/OFF/WARM-UP/COOL- DOWN

+

Trip                        None

+

Drive                       Off

+

EmissionTripState   OK

+

ExternalTripState    OK

+

RVCTripState          OK

+
+

ÐÑинхронное Ñообщение о любом изменении ÑоÑтоÑÐ½Ð¸Ñ Ð½Ð¸Ñ‚Ð¸ накала

+

ПоÑледовательноÑÑ‚ÑŒ : WARM-UP -> OK -> ON или COOL-DOWN -> OK -> OFF

+
+

AddPeakJump "MeasurementName" "FilterMode" + "0..8" "0" "0" "0"

+
+

AddPeakJump OK

+

Name                   PeakJump1

+

FilterMode           + PeakCenter/PeakMax/PeakAverage

+

Accuracy                 5

+

EGainIndex             0

+

SourceIndex            0

+

DetectorIndex          0

+
+

Создаем режим Ð¸Ð·Ð¼ÐµÑ€ÐµÐ½Ð¸Ñ PeakJump

+

0..8 — точноÑÑ‚ÑŒ измерений: 0 — Ð¼ÐµÐ½ÑŒÑˆÐ°Ñ Ñ‚Ð¾Ñ‡Ð½Ð¾ÑÑ‚ÑŒ, но Ð±Ð¾Ð»ÑŒÑˆÐ°Ñ + ÑкороÑÑ‚ÑŒ, 8 — наоборот

+

Ð’Ñе оÑтальное полагать 0

+
+

MeasurementAddMass "Mass"

+
+

MeasurementAddMass  OK

+

Mass  10

+
+

ДобавлÑем маÑÑÑ‹ в PeakJump, которые хотим измерить.

+

Чтобы добавить новую маÑÑу, нужно повторно вызвать Ñту команду.

+
+

MeasurementChangeMass

+

"MassIndex" "NewMass"

+
+

MeasurementChangeMass OK + MassIndex                          0  

+

NewMass                           6  +

+
+

ЗаменÑет маÑÑу Ñ Ð¸Ð½Ð´ÐµÐºÑом " + MassIndex" на новое значение

+

(индекÑÐ°Ñ†Ð¸Ñ Ñ Ð½ÑƒÐ»Ñ)

+
+

MeasurementSelect "Analog1"

+
+

MeasurementSelect      OK

+

Measurement                + Analog1

+
+

ОпределÑет Measurement по его + имени, который будет иÑпользоватьÑÑ Ð² дальнейшем Ð´Ð»Ñ MeasurementXXXX команд

+
+

MeasurementRemoveAll

+
+

MeasurementRemoveAll OK

+
+

УдалÑет вÑе Measurements из ÑпиÑка + Ñканера

+

 

+
+

MeasurementRemove + "Barchart1"

+
+

MeasurementRemove   OK   + Measurement                Barchart1

+
+

УдалÑет Measurement Ñ Ð´Ð°Ð½Ð½Ñ‹Ð¼ + именем из ÑпиÑка Ñканера

+
+

ScanAdd "MeasurementName"

+
+

ScanAdd OK

+

Measurement PeakJump1

+
+

ДобавлÑем Ñозданное измерение Ñканеру.

+

Сканер ÐЕ должен быть запущен.

+
+

ScanStart "NumScans"

+
+

ScanStart OK

+
+

ЗапуÑкаем Ñканер.

+

Далее - аÑинхронные ÑÐ¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ Ð¿Ð¾Ñле каждого из NumScans + ÑканированиÑ.

+
+

StartingScan  1 16858 0

+

 

+
+

Сообщение о том, что закончилоÑÑŒ некоторое измерение, указано + Ð²Ñ€ÐµÐ¼Ñ Ñ Ð¼Ð¾Ð¼ÐµÐ½Ñ‚Ð° первого Ð¸Ð·Ð¼ÐµÑ€ÐµÐ½Ð¸Ñ Ð¸ оÑтавшееÑÑ ÐºÐ¾Ð»Ð¸Ñ‡ÐµÑтво измерений до + перезапуÑка

+
+

StartingMeasurement PeakJump1

+
+

Сообщение о том, какой Measurment Ñканера запущен (Ñканер может + иметь неÑколько режимов измерений)

+
+

ZeroReading 5.5 1.01e-8

+
+

Ðулевое значени Ð´Ð°Ð²Ð»ÐµÐ½Ð¸Ñ ( 5.5 - Ñто MassPosition)

+

ИзменÑетÑÑ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð¾Ð¹:   MeasurementZeroMass "ZeroMass"

+
+

MassReading 1 2.9383e-5

+
+

Парциальное давление газа заданной маÑÑÑ‹ в формате маÑÑа, + давление (выводитÑÑ Ð²ÐµÑÑŒ ÑпиÑок маÑÑ)

+

Далее Ñканер оÑтаетÑÑ Ð·Ð°Ð¿ÑƒÑ‰ÐµÐ½Ð½Ñ‹Ð¼ в режиме ожиданиÑ

+
+

ScanResume "NumScans"

+
+

ScanResume  OK

+
+

Продолжает работу Ñканера Ð´Ð»Ñ Ð¿Ð¾Ð²Ñ‚Ð¾Ñ€Ð½Ð¾Ð³Ð¾ ÑÑ‡Ð¸Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð´Ð°Ð½Ð½Ñ‹Ñ… Ñ + поÑледующей цепочкой аналогичных Ñообщений

+
+

ScanRestart "NumScans"

+
+

ScanRestart  OK

+
+

ПерезапуÑкает Ñканер Ñ Ñамого + начала Ð´Ð»Ñ "NumScans" измерений

+

(полезно в Ñлучае Ñбоев + ÑканированиÑ, чтобы заново не переопределÑÑ‚ÑŒ параметры ÑенÑора)

+
+

ScanStop

+
+

ScanStop OK

+
+

Выключает Ñканер и ÑбраÑывает Ñ Ð½ÐµÐ³Ð¾ вÑе имеющиеÑÑ Measurements

+

Ð”Ð»Ñ Ñледующих измерений нужно Ñнова ScanAdd + "MeasurementName"

+
+

Release

+
+

Release OK

+
+

ТерÑем контроль над ÑенÑором

+
+ +

* номера и опиÑÐ°Ð½Ð¸Ñ Ð¾ÑˆÐ¸Ð±Ð¾Ðº:

+ +

200 – Ð½ÐµÐºÐ¾Ñ€Ñ€ÐµÐºÑ‚Ð½Ð°Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð°

+ +

201 — неверное количеÑтво + параметров в команде

+ +

202 – ошибочно переданный параметр (Parameter 1 'State' could not be interpreted as on/off)

+ +

203 – ошибка дейÑтвиÑ, подразумевающего корректное выполнение какого либо другого дейÑтвиÑ  (No sensor selected/Must be in control + of sensor/Not scanning)

+ +

204 – ошибка в параметрах, ÑвÑзанных Ñ Measurement (Measurement with this + name already exists/Bad SourceIndex/Invalid mass value)

+ +

300 – ошибка выбора ÑенÑора + (неверный Ñерийный номер ÑенÑора)

+ +

 

+ + + + + + diff --git a/numass-control/msp/src/main/kotlin/inr/numass/control/msp/MspApp.kt b/numass-control/msp/src/main/kotlin/inr/numass/control/msp/MspApp.kt new file mode 100644 index 00000000..8f923ddf --- /dev/null +++ b/numass-control/msp/src/main/kotlin/inr/numass/control/msp/MspApp.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.control.msp + +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaUtils +import inr.numass.control.NumassControlApplication +import javafx.stage.Stage + +/** + * @author darksnake + */ +class MspApp : NumassControlApplication() { + + override val deviceFactory = MspDeviceFactory() + + + override fun setupStage(stage: Stage, device: MspDevice) { + stage.title = "Numass mass-spectrometer view" + stage.minHeight = 400.0 + stage.minWidth = 600.0 + } + + override fun getDeviceMeta(config: Meta): Meta { + return MetaUtils.findNode(config,"device"){it.getString("name") == "numass.msp"}.orElseThrow{RuntimeException("Mass-spectrometer configuration not found")} + } + +} diff --git a/numass-control/msp/src/main/kotlin/inr/numass/control/msp/MspDevice.kt b/numass-control/msp/src/main/kotlin/inr/numass/control/msp/MspDevice.kt new file mode 100644 index 00000000..1dfef147 --- /dev/null +++ b/numass-control/msp/src/main/kotlin/inr/numass/control/msp/MspDevice.kt @@ -0,0 +1,413 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.control.msp + +import hep.dataforge.connections.NamedValueListener +import hep.dataforge.connections.RoleDef +import hep.dataforge.connections.RoleDefs +import hep.dataforge.context.Context +import hep.dataforge.control.collectors.RegularPointCollector +import hep.dataforge.control.connections.Roles +import hep.dataforge.control.devices.PortSensor +import hep.dataforge.control.devices.notifyError +import hep.dataforge.control.ports.GenericPortController +import hep.dataforge.control.ports.Port +import hep.dataforge.control.ports.PortFactory +import hep.dataforge.description.ValueDef +import hep.dataforge.exceptions.ControlException +import hep.dataforge.exceptions.MeasurementException +import hep.dataforge.exceptions.PortException +import hep.dataforge.meta.Meta +import hep.dataforge.states.StateDef +import hep.dataforge.states.StateDefs +import hep.dataforge.states.valueState +import hep.dataforge.storage.StorageConnection +import hep.dataforge.tables.TableFormatBuilder +import hep.dataforge.tables.ValuesListener +import hep.dataforge.useMeta +import hep.dataforge.values.ValueType +import inr.numass.control.DeviceView +import inr.numass.control.NumassStorageConnection +import inr.numass.control.msp.MspDevice.Companion.SELECTED_STATE +import java.time.Duration +import java.util.* + +/** + * @author Alexander Nozik + */ +@RoleDefs( + RoleDef(name = Roles.STORAGE_ROLE, objectType = StorageConnection::class), + RoleDef(name = Roles.VIEW_ROLE) +) +@StateDefs( + StateDef(value = ValueDef(key = "controlled", info = "Connection with the device itself"), writable = true), + StateDef(ValueDef(key = SELECTED_STATE)), + StateDef(value = ValueDef(key = "storing", info = "Define if this device is currently writes to storage"), writable = true), + StateDef(value = ValueDef(key = "filament", info = "The number of filament in use"), writable = true), + StateDef(value = ValueDef(key = "filamentOn", info = "Mass-spectrometer filament on"), writable = true), + StateDef(ValueDef(key = "filamentStatus", info = "Filament status")), + StateDef(ValueDef(key = "peakJump.zero", type = [ValueType.NUMBER], info = "Peak jump zero reading")) +) +@DeviceView(MspDisplay::class) +class MspDevice(context: Context, meta: Meta) : PortSensor(context, meta) { + +// private var measurementDelegate: Consumer? = null + + //val selected: Boolean by valueState("selected").booleanDelegate + + val controlled = valueState(CONTROLLED_STATE) { value -> + runOnDeviceThread { + val res = control(value.boolean) + updateState(CONTROLLED_STATE, res) + } + } + + val filament = valueState("filament") { value -> + selectFilament(value.int) + } + + val filamentOn = valueState("filamentOn") { value -> + setFilamentOn(value.boolean) + } + + var peakJumpZero: Double by valueState("peakJump.zero").doubleDelegate + + private val averagingDuration: Duration = Duration.parse(meta.getString("averagingDuration", "PT30S")) + + private var storageHelper: NumassStorageConnection? = null + + private val collector = RegularPointCollector(averagingDuration) { res -> + notifyResult(res) + forEachConnection(ValuesListener::class.java) { + it.accept(res) + } + } + + override fun buildConnection(meta: Meta): GenericPortController { + logger.info("Connecting to port {}", meta) + val port: Port = PortFactory.build(meta) + return GenericPortController(context, port, "\r\r").also { + it.weakOnPhrase({ it.startsWith("FilamentStatus") }, this) { + val response = MspResponse(it) + val status = response[0, 2] + updateState("filamentOn", status == "ON") + updateState("filamentStatus", status) + } + logger.info("Connected to MKS mass-spectrometer on {}", it.port) + } + } + + override fun init() { + super.init() + meta.useMeta("peakJump"){ + updateState(MEASUREMENT_META_STATE, it) + } + } + + @Throws(ControlException::class) + override fun shutdown() { + if (controlled.booleanValue) { + setFilamentOn(false) + } + controlled.set(false) + super.shutdown() + } + + override val type: String + get() = MSP_DEVICE_TYPE + + /** + * Startup MSP: get available sensors, select sensor and control. + * + * @param on + * @return + * @throws hep.dataforge.exceptions.ControlException + */ + private fun control(on: Boolean): Boolean { + if (on != this.controlled.booleanValue) { + val sensorName: String + if (on) { + logger.info("Starting initialization sequence") + //ensure device is connected + connected.setAndWait(true) + var response = commandAndWait("Sensors") + if (response.isOK) { + sensorName = response[2, 1] + } else { + notifyError(response.errorDescription, null) + return false + } + //PENDING определеить в конфиге номер прибора + + response = commandAndWait("Select", sensorName) + if (response.isOK) { + updateState("selected", true) + } else { + notifyError(response.errorDescription, null) + return false + } + + response = commandAndWait("Control", "inr.numass.msp", "1.1") + if (response.isOK) { + controlled.update(true) + } else { + notifyError(response.errorDescription, null) + return false + } + // connected = true; + updateState(PortSensor.CONNECTED_STATE, true) + return true + } else { + logger.info("Releasing device") + return !commandAndWait("Release").isOK + } + } else { + return on + } + } + + /** + * Send request to the msp + * + * @param command + * @param parameters + * @throws PortException + */ + @Throws(PortException::class) + private fun command(command: String, vararg parameters: Any) { + send(buildCommand(command, *parameters)) + } + + /** + * A helper method to builder msp command string + * + * @param command + * @param parameters + * @return + */ + private fun buildCommand(command: String, vararg parameters: Any): String { + val builder = StringBuilder(command) + for (par in parameters) { + builder.append(String.format(" \"%s\"", par.toString())) + } + builder.append("\n") + return builder.toString() + } + + /** + * Send specific command and wait for its results (the result must begin + * with command name) + * + * @param commandName + * @param parameters + * @return + * @throws PortException + */ + @Throws(PortException::class) + private fun commandAndWait(commandName: String, vararg parameters: Any): MspResponse { + val command = buildCommand(commandName, *parameters) + if (debug) { + logger.info("SEND: $command") + } + val response = connection.sendAndWait(command, TIMEOUT) { str: String -> str.trim { it <= ' ' }.startsWith(commandName) } + if (debug) { + logger.info("RECEIVE:\n$response") + } + return MspResponse(response) + } + + @Throws(PortException::class) + private fun selectFilament(filament: Int) { + runOnDeviceThread { + val response = commandAndWait("FilamentSelect", filament) + if (response.isOK) { + this.filament.update(response[1, 1]) + } else { + logger.error("Failed to set filament with error: {}", response.errorDescription) + } + } + } + + /** + * Turn filament on or off + * + * @param filamentOn + * @return + * @throws hep.dataforge.exceptions.PortException + */ + @Throws(PortException::class) + private fun setFilamentOn(filamentOn: Boolean): Boolean { + return if (filamentOn) { + commandAndWait("FilamentControl", "On").isOK + } else { + commandAndWait("FilamentControl", "Off").isOK + } + } + + override fun stopMeasurement() { + runOnDeviceThread { + stopPeakJump() + } + super.stopMeasurement() + } + + override fun startMeasurement(oldMeta: Meta?, newMeta: Meta) { + if (oldMeta != null) { + stopMeasurement() + } + if (newMeta.getString("type", "peakJump") == "peakJump") { + runOnDeviceThread { + startPeakJump(newMeta) + } + } else { + throw MeasurementException("Unknown measurement type") + } + } + + private fun startPeakJump(meta: Meta) { + notifyMeasurementState(MeasurementState.IN_PROGRESS) + val measurementName = "peakJump" + val filterMode = meta.getString("filterMode", "PeakAverage") + val accuracy = meta.getInt("accuracy", 5) + //PENDING вÑтавить оÑтальные параметры? + sendAndWait("MeasurementRemoveAll", Duration.ofMillis(200)) + +// val peakMap: MutableMap = LinkedHashMap() + + val builder = TableFormatBuilder().addTime("timestamp") + + if (commandAndWait("AddPeakJump", measurementName, filterMode, accuracy, 0, 0, 0).isOK) { +// peakMap.clear() + for (peak in meta.getMetaList("peak")) { +// peakMap[peak.getInt("mass")] = peak.getString("name", peak.getString("mass")) + if (!commandAndWait("MeasurementAddMass", peak.getString("mass")).isOK) { + throw ControlException("Can't add mass to measurement measurement for msp") + } + builder.addNumber(peak.getString("name", peak.getString("mass"))) + } + } else { + throw ControlException("Can't create measurement for msp") + } + + storageHelper = NumassStorageConnection("msp") { builder.build() } + connect(storageHelper) + + connection.onAnyPhrase(this) { + val response = MspResponse(it) + when (response.commandName) { + "MassReading" -> { + val mass = java.lang.Double.parseDouble(response[0, 1]) + val value = java.lang.Double.parseDouble(response[0, 2]) / 100.0 + val massName = Integer.toString(Math.floor(mass + 0.5).toInt()) + collector.put(massName, value) + forEachConnection(Roles.VIEW_ROLE, NamedValueListener::class.java) { listener -> listener.pushValue(massName, value) } + } + "ZeroReading" -> { + updateState("peakJump.zero", java.lang.Double.parseDouble(response[0, 2]) / 100.0) + } + "StartingScan" -> { + val numScans = Integer.parseInt(response[0, 3]) + + if (numScans == 0) { + try { + command("ScanResume", 10) + //FIXME обработать ошибку ÑвÑзи + } catch (ex: PortException) { + notifyError("Failed to resume scan", ex) + } + + } + } + } + } + + if (!filamentOn.booleanValue) { + notifyError("Can't start measurement. Filament is not turned on.") + } + if (!commandAndWait("ScanAdd", measurementName).isOK) { + notifyError("Failed to add scan") + } + + if (!commandAndWait("ScanStart", 2).isOK) { + notifyError("Failed to start scan") + } + } + + private fun stopPeakJump() { + collector.stop() + val stop = commandAndWait("ScanStop").isOK + //Reset loaders in connections + storageHelper?.let { disconnect(it) } + notifyMeasurementState(MeasurementState.STOPPED) + } + + + /** + * The MKS response as two-dimensional array of strings + */ + class MspResponse(response: String) { + + private val data = ArrayList>() + + val commandName: String + get() = this[0, 0] + + val isOK: Boolean + get() = "OK" == this[0, 1] + + init { + val rx = "[^\"\\s]+|\"(\\\\.|[^\\\\\"])*\"" + val scanner = Scanner(response.trim { it <= ' ' }) + + while (scanner.hasNextLine()) { + val line = ArrayList() + var next: String? = scanner.findWithinHorizon(rx, 0) + while (next != null) { + line.add(next) + next = scanner.findInLine(rx) + } + data.add(line) + } + } + + fun errorCode(): Int { + return if (isOK) { + -1 + } else { + Integer.parseInt(get(1, 1)) + } + } + + val errorDescription: String + get() { + return if (isOK) { + throw RuntimeException("Not a error") + } else { + get(2, 1) + } + } + + operator fun get(lineNo: Int, columnNo: Int): String = data[lineNo][columnNo] + } + + companion object { + const val MSP_DEVICE_TYPE = "numass.msp" + const val CONTROLLED_STATE = "controlled" + const val SELECTED_STATE = "selected" + + private val TIMEOUT = Duration.ofMillis(200) + } +} diff --git a/numass-control/msp/src/main/kotlin/inr/numass/control/msp/MspDeviceFactory.kt b/numass-control/msp/src/main/kotlin/inr/numass/control/msp/MspDeviceFactory.kt new file mode 100644 index 00000000..390d9982 --- /dev/null +++ b/numass-control/msp/src/main/kotlin/inr/numass/control/msp/MspDeviceFactory.kt @@ -0,0 +1,16 @@ +package inr.numass.control.msp + +import hep.dataforge.context.Context +import hep.dataforge.control.devices.DeviceFactory +import hep.dataforge.meta.Meta + +/** + * Created by darksnake on 09-May-17. + */ +class MspDeviceFactory : DeviceFactory { + override val type = MspDevice.MSP_DEVICE_TYPE + + override fun build(context: Context, config: Meta): MspDevice { + return MspDevice(context, config) + } +} diff --git a/numass-control/msp/src/main/kotlin/inr/numass/control/msp/MspDisplay.kt b/numass-control/msp/src/main/kotlin/inr/numass/control/msp/MspDisplay.kt new file mode 100644 index 00000000..7cadb5ed --- /dev/null +++ b/numass-control/msp/src/main/kotlin/inr/numass/control/msp/MspDisplay.kt @@ -0,0 +1,217 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.control.msp + +import hep.dataforge.connections.NamedValueListener +import hep.dataforge.control.devices.PortSensor +import hep.dataforge.control.devices.Sensor +import hep.dataforge.fx.asBooleanProperty +import hep.dataforge.fx.bindWindow +import hep.dataforge.fx.fragments.LogFragment +import hep.dataforge.fx.plots.PlotContainer +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.plots.PlotGroup +import hep.dataforge.plots.PlotUtils +import hep.dataforge.plots.data.TimePlot +import hep.dataforge.plots.data.TimePlot.Companion.setMaxItems +import hep.dataforge.plots.data.TimePlot.Companion.setPrefItems +import hep.dataforge.plots.jfreechart.JFreeChartFrame +import hep.dataforge.states.ValueState +import hep.dataforge.values.Value +import inr.numass.control.DeviceDisplayFX +import inr.numass.control.deviceStateIndicator +import inr.numass.control.deviceStateToggle +import inr.numass.control.switch +import javafx.beans.property.SimpleObjectProperty +import javafx.collections.FXCollections +import javafx.collections.MapChangeListener +import javafx.geometry.Insets +import javafx.geometry.Orientation +import javafx.scene.Parent +import javafx.scene.control.Alert +import javafx.scene.layout.Priority +import javafx.scene.layout.VBox +import javafx.scene.paint.Paint +import tornadofx.* + +/** + * FXML Controller class + + * @author darksnake + */ +class MspDisplay() : DeviceDisplayFX(), NamedValueListener { + + private val table = FXCollections.observableHashMap() + + override fun getBoardView(): Parent { + return VBox().apply { + this += super.getBoardView() + } + } + + override fun buildView(device: MspDevice): View { + return MspView() + } + + override fun pushValue(valueName: String, value: Value) { + table[valueName] = value + } + + + inner class MspView : View("Numass mass-spectrometer measurement") { + private val plotFrameMeta: Meta = device.meta.getMeta("plotConfig", device.meta) + + private val plotFrame by lazy { + val basePlotConfig = MetaBuilder("plotFrame") + .setNode(MetaBuilder("yAxis") + .setValue("type", "log") + .setValue("title", "partial pressure") + .setValue("units", "mbar") + ) + .setValue("xAxis.type", "time") + + + JFreeChartFrame().apply { configure(basePlotConfig) }.apply { + PlotUtils.setXAxis(this, "timestamp", "", "time") + configure(plotFrameMeta) + } + } + val plottables = PlotGroup.typed("peakJump").apply { + setMaxItems(this, 1000) + setPrefItems(this, 400) + + if (plotFrameMeta.hasMeta("peakJump.peak")) { + for (peakMeta in plotFrameMeta.getMetaList("peakJump.peak")) { + val mass = peakMeta.getString("mass") + get(mass) ?: TimePlot(mass, mass).also { + it.configureValue("titleBase", peakMeta.getString("title", mass)) + add(it) + }.configure(peakMeta) + } + } else { + showError("No peaks defined in config") + throw RuntimeException() + } + } + +// private val logWindow = FragmentWindow(LogFragment().apply { +// addLogHandler(device.logger) +// }) + + private val filamentProperty = SimpleObjectProperty(this, "filament", 1).apply { + addListener { _, oldValue, newValue -> + if (newValue != oldValue) { + runAsync { + device.filament.set(newValue) + } + } + } + } + + override val root = borderpane { + minHeight = 400.0 + minWidth = 600.0 + top { + toolbar { + deviceStateToggle(this@MspDisplay, MspDevice.CONTROLLED_STATE, "Connect") + combobox(filamentProperty, listOf(1, 2)) { + cellFormat { + text = "Filament $it" + } + disableProperty().bind(booleanStateProperty(PortSensor.CONNECTED_STATE).not()) + } + switch { + padding = Insets(5.0, 0.0, 0.0, 0.0) + disableProperty().bind(device.controlled.asBooleanProperty().not()) + device.filamentOn.asBooleanProperty().bindBidirectional(selectedProperty()) + } + deviceStateIndicator(this@MspDisplay, "filamentStatus", false) { + when (it.string) { + "ON" -> Paint.valueOf("red") + "OFF" -> Paint.valueOf("blue") + "WARM-UP", "COOL-DOWN" -> Paint.valueOf("yellow") + else -> Paint.valueOf("grey") + + } + } + + togglebutton("Measure") { + isSelected = false + disableProperty().bind(booleanStateProperty(PortSensor.CONNECTED_STATE).not()) + device.measuring.asBooleanProperty().bindBidirectional(selectedProperty()) + } + togglebutton("Store") { + isSelected = false + disableProperty().bind(booleanStateProperty(Sensor.MEASURING_STATE).not()) + device.states.getState("storing")?.asBooleanProperty()?.bindBidirectional(selectedProperty()) + } + separator(Orientation.VERTICAL) + pane { + hgrow = Priority.ALWAYS + } + separator(Orientation.VERTICAL) + + togglebutton("Log") { + isSelected = false + + LogFragment().apply { + addLogHandler(device.logger) + bindWindow(this@togglebutton, selectedProperty()) + } + } + } + } + center = PlotContainer(plotFrame).root + } + + init { + table.addListener { change: MapChangeListener.Change -> + if (change.wasAdded()) { + val pl = plottables[change.key] as TimePlot? + val value = change.valueAdded + if (pl != null) { + if (value.double > 0) { + pl.put(value) + } else { + pl.put(Value.NULL) + } + val titleBase = pl.config.getString("titleBase") + val title = String.format("%s (%.4g)", titleBase, value.double) + pl.configureValue("title", title) + } + } + } + } + + +// override fun evaluateDeviceException(device: Device, message: String, exception: Throwable) { +// Platform.runLater { +// logFragment!!.appendLine("ERROR: " + message) +// showError(message) +// } +// } + + private fun showError(message: String) { + val alert = Alert(Alert.AlertType.ERROR) + alert.title = "Error!" + alert.headerText = null + alert.contentText = message + + alert.showAndWait() + } + } +} diff --git a/numass-control/msp/src/main/resources/META-INF/services/hep.dataforge.control.devices.DeviceFactory b/numass-control/msp/src/main/resources/META-INF/services/hep.dataforge.control.devices.DeviceFactory new file mode 100644 index 00000000..e6595c0a --- /dev/null +++ b/numass-control/msp/src/main/resources/META-INF/services/hep.dataforge.control.devices.DeviceFactory @@ -0,0 +1 @@ +inr.numass.control.msp.MspDeviceFactory \ No newline at end of file diff --git a/numass-control/msp/src/main/resources/config/devices.xml b/numass-control/msp/src/main/resources/config/devices.xml new file mode 100644 index 00000000..48541f74 --- /dev/null +++ b/numass-control/msp/src/main/resources/config/devices.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/numass-control/msp/src/main/resources/fxml/MspView.fxml b/numass-control/msp/src/main/resources/fxml/MspView.fxml new file mode 100644 index 00000000..3bb4f584 --- /dev/null +++ b/numass-control/msp/src/main/resources/fxml/MspView.fxml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
diff --git a/numass-control/msp/src/test/java/inr/numass/control/msp/TestScanner.java b/numass-control/msp/src/test/java/inr/numass/control/msp/TestScanner.java new file mode 100644 index 00000000..42b1a3f2 --- /dev/null +++ b/numass-control/msp/src/test/java/inr/numass/control/msp/TestScanner.java @@ -0,0 +1,38 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.control.msp; + +/** + * + * @author Alexander Nozik + */ +public class TestScanner { + + /** + * @param args the command line arguments + */ + public static void main(String[] args) { + MspDevice.MspResponse response = new MspDevice.MspResponse( + "FilamentStatus 1 ON\n" + + "Trip None \n" + + "Drive Off\n" + + "EmissionTripState OK\n" + + "ExternalTripState OK \n" + + "RVCTripState OK"); + System.out.println(response.get(2, 1)); + } + +} diff --git a/numass-control/src/main/kotlin/inr/numass/control/DeviceDisplayFX.kt b/numass-control/src/main/kotlin/inr/numass/control/DeviceDisplayFX.kt new file mode 100644 index 00000000..9d5f6b94 --- /dev/null +++ b/numass-control/src/main/kotlin/inr/numass/control/DeviceDisplayFX.kt @@ -0,0 +1,128 @@ +package inr.numass.control + +import hep.dataforge.connections.Connection +import hep.dataforge.control.connections.Roles +import hep.dataforge.control.devices.Device +import hep.dataforge.control.devices.PortSensor +import hep.dataforge.control.devices.Sensor +import hep.dataforge.exceptions.NameNotFoundException +import hep.dataforge.fx.asBooleanProperty +import hep.dataforge.fx.asProperty +import hep.dataforge.fx.bindWindow +import hep.dataforge.states.ValueState +import hep.dataforge.values.Value +import javafx.beans.property.BooleanProperty +import javafx.beans.property.ObjectProperty +import javafx.beans.property.SimpleObjectProperty +import javafx.geometry.Pos +import javafx.scene.Parent +import javafx.scene.layout.HBox +import javafx.scene.layout.Priority +import tornadofx.* +import kotlin.reflect.KClass +import kotlin.reflect.full.createInstance + + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class DeviceView(val value: KClass>) + +/** + * Get existing view connection or create a new one + */ +fun Device.getDisplay(): DeviceDisplayFX<*> { + val type = (this::class.annotations.find { it is DeviceView } as DeviceView?)?.value ?: DefaultDisplay::class + return optConnection(Roles.VIEW_ROLE, DeviceDisplayFX::class.java).orElseGet { + type.createInstance().also { + connect(it, Roles.VIEW_ROLE); + } + } +} + + +/** + * + * An FX View to represent the device + * Created by darksnake on 14-May-17. + */ +abstract class DeviceDisplayFX : Component(), Connection { + + private val deviceProperty = SimpleObjectProperty(this, "device", null) + val device: D by deviceProperty + + // private val viewProperty = SimpleObjectProperty(this, "view", null) + val view: UIComponent? by lazy { + buildView(device) + } + + override fun isOpen(): Boolean = this.deviceProperty.get() != null + + override fun open(obj: Any) { + if (!isOpen) { + @Suppress("UNCHECKED_CAST") + deviceProperty.set(obj as D) + } else { + log.warning("Connection already opened") + } + + } + + override fun close() { + if (isOpen) { + view?.close() + deviceProperty.set(null) + } + } + + protected abstract fun buildView(device: D): UIComponent?; + + fun valueStateProperty(stateName: String): ObjectProperty { + val state: ValueState = device.states.filterIsInstance(ValueState::class.java).find { it.name == stateName } + ?: throw NameNotFoundException("State with name $stateName not found") + return state.asProperty() + } + + fun booleanStateProperty(stateName: String): BooleanProperty { + val state: ValueState = device.states.filterIsInstance(ValueState::class.java).find { it.name == stateName } + ?: throw NameNotFoundException("State with name $stateName not found") + return state.asBooleanProperty() + } + + open fun getBoardView(): Parent { + return HBox().apply { + alignment = Pos.CENTER_LEFT + vgrow = Priority.ALWAYS; + deviceStateIndicator(this@DeviceDisplayFX, Device.INITIALIZED_STATE) + if(device is PortSensor) { + deviceStateIndicator(this@DeviceDisplayFX, PortSensor.CONNECTED_STATE) + } + if(device is Sensor) { + deviceStateIndicator(this@DeviceDisplayFX, Sensor.MEASURING_STATE) + } + if(device.stateNames.contains("storing")) { + deviceStateIndicator(this@DeviceDisplayFX, "storing") + } + pane { + hgrow = Priority.ALWAYS + } + togglebutton("View") { + isSelected = false + if (view == null) { + isDisable = true + } + view?.bindWindow(this,selectedProperty()) + } + } + } +} + + +/** + * Default display shows only board pane and nothing else + */ +class DefaultDisplay : DeviceDisplayFX() { + + override fun buildView(device: Device): UIComponent? = null +} + diff --git a/numass-control/src/main/kotlin/inr/numass/control/FXExtensions.kt b/numass-control/src/main/kotlin/inr/numass/control/FXExtensions.kt new file mode 100644 index 00000000..c3e6bad9 --- /dev/null +++ b/numass-control/src/main/kotlin/inr/numass/control/FXExtensions.kt @@ -0,0 +1,151 @@ +package inr.numass.control + +import hep.dataforge.configure +import hep.dataforge.fx.plots.PlotContainer +import hep.dataforge.meta.KMetaBuilder +import hep.dataforge.plots.PlotFrame +import hep.dataforge.plots.Plottable +import hep.dataforge.plots.jfreechart.JFreeChartFrame +import hep.dataforge.values.Value +import javafx.beans.value.ObservableValue +import javafx.event.EventTarget +import javafx.geometry.Orientation +import javafx.scene.Node +import javafx.scene.layout.BorderPane +import javafx.scene.paint.Color +import javafx.scene.paint.Paint +import javafx.scene.shape.Circle +import javafx.scene.shape.StrokeType +import org.controlsfx.control.ToggleSwitch +import tornadofx.* + + +/** + * A pin like indicator fx node + */ +class Indicator(radius: Double = 10.0) : Circle(radius, Color.GRAY) { + private var binding: ObservableValue<*>? = null; + + init { + stroke = Color.BLACK; + strokeType = StrokeType.INSIDE; + } + + /** + * bind this indicator color to given observable + */ + fun bind(observable: ObservableValue, transform: (T) -> Paint) { + if (binding != null) { + throw RuntimeException("Indicator already bound"); + } else { + binding = observable; + fill = transform(observable.value) + observable.addListener { _, _, value -> + fill = transform(value); + } + } + } + + /** + * bind indicator to the boolean value using default colours + */ + fun bind(booleanValue: ObservableValue) { + bind(booleanValue) { + when { + it == null -> Color.GRAY + it -> Color.GREEN + else -> Color.RED + } + } + } + + fun unbind() { + this.binding = null; + neutralize(); + } + + /** + * return indicator to the neutral state but do not unbind + */ + fun neutralize() { + fill = Color.GRAY; + } +} + +fun EventTarget.indicator(radius: Double = 10.0, op: (Indicator.() -> Unit) = {}): Indicator = opcr(this, Indicator(radius), op) + +fun Indicator.bind(connection: DeviceDisplayFX<*>, state: String, transform: ((Value) -> Paint)? = null) { + tooltip(state) + if (transform != null) { + bind(connection.valueStateProperty(state), transform); + } else { + bind(connection.valueStateProperty(state)) { + when { + it.isNull -> Color.GRAY + it.boolean -> Color.GREEN + else -> Color.RED + } + } + } +} + +/** + * State name + indicator + */ +fun EventTarget.deviceStateIndicator(connection: DeviceDisplayFX<*>, state: String, showName: Boolean = true, transform: ((Value) -> Paint)? = null) { + if (connection.device.stateNames.contains(state)) { + if (showName) { + text("${state.toUpperCase()}: ") + } + indicator { + bind(connection, state, transform); + } + separator(Orientation.VERTICAL) + } else { + throw RuntimeException("Device does not support state $state"); + } +} + +/** + * A togglebutton + indicator for boolean state + */ +fun Node.deviceStateToggle(connection: DeviceDisplayFX<*>, state: String, title: String = state) { + if (connection.device.stateNames.contains(state)) { + togglebutton(title) { + isSelected = false + selectedProperty().addListener { _, oldValue, newValue -> + if (oldValue != newValue) { + connection.device.states[state] = newValue + } + } + connection.valueStateProperty(state).onChange { + runLater { + isSelected = it?.boolean ?: false + } + } + } + deviceStateIndicator(connection, state, false) + } else { + throw RuntimeException("Device does not support state $state"); + } +} + +fun EventTarget.switch(text: String = "", op: (ToggleSwitch.() -> Unit) = {}): ToggleSwitch { + val switch = ToggleSwitch(text) + return opcr(this, switch, op) +} + +/** + * Add frame + */ +fun BorderPane.plot(plottable: Plottable, metaTransform: (KMetaBuilder.() -> Unit) = {}): PlotFrame { + val frame = JFreeChartFrame().configure(metaTransform) + frame.add(plottable) + center = PlotContainer(frame).root + return frame; +} + +//val ValueState.property: ObjectProperty +// get() { +// ret +// } \ No newline at end of file diff --git a/numass-control/src/main/kotlin/inr/numass/control/NumassControlApplication.kt b/numass-control/src/main/kotlin/inr/numass/control/NumassControlApplication.kt new file mode 100644 index 00000000..a97b5449 --- /dev/null +++ b/numass-control/src/main/kotlin/inr/numass/control/NumassControlApplication.kt @@ -0,0 +1,81 @@ +package inr.numass.control + +import ch.qos.logback.classic.Level +import hep.dataforge.control.devices.Device +import hep.dataforge.control.devices.DeviceFactory +import hep.dataforge.exceptions.ControlException +import hep.dataforge.meta.Meta +import hep.dataforge.optional +import javafx.scene.Scene +import javafx.stage.Stage +import org.slf4j.LoggerFactory +import tornadofx.* +import java.util.* + +/** + * Created by darksnake on 14-May-17. + */ +abstract class NumassControlApplication : App() { + private var device: D? = null + + override fun start(stage: Stage) { + Locale.setDefault(Locale.US)// чтобы отделение деÑÑтичных знаков было точкой + val rootLogger = LoggerFactory.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME) as ch.qos.logback.classic.Logger + rootLogger.level = Level.INFO + + device = setupDevice().also { + val controller = it.getDisplay() + val scene = Scene(controller.view?.root ?: controller.getBoardView()) + stage.scene = scene + + stage.show() + setupStage(stage, it) + setDFStageIcon(stage) + } + } + + /** + * Get a device factory for given device + + * @return + */ + protected abstract val deviceFactory: DeviceFactory + + protected abstract fun setupStage(stage: Stage, device: D) + + + abstract fun getDeviceMeta(config: Meta): Meta + + private fun setupDevice(): D { + val config = getConfig(this).optional.orElseGet { readResourceMeta("config/devices.xml") } + + val ctx = setupContext(config) + val deviceConfig = getDeviceMeta(config) + + + try { + @Suppress("UNCHECKED_CAST") + val d = deviceFactory.build(ctx, deviceConfig) as D + d.init() + d.connectStorage(config) + + return d + } catch (e: ControlException) { + throw RuntimeException("Failed to build device", e) + } + + } + + override fun stop() { + try { + device?.shutdown() + } catch (ex: Exception) { + LoggerFactory.getLogger(javaClass).error("Failed to properly shutdown application", ex); + device?.context?.close() + } finally { + super.stop() + } + } + + +} diff --git a/numass-control/src/main/kotlin/inr/numass/control/NumassControlUtils.kt b/numass-control/src/main/kotlin/inr/numass/control/NumassControlUtils.kt new file mode 100644 index 00000000..9d1c80e6 --- /dev/null +++ b/numass-control/src/main/kotlin/inr/numass/control/NumassControlUtils.kt @@ -0,0 +1,117 @@ +package inr.numass.control + +import hep.dataforge.context.Context +import hep.dataforge.context.Global +import hep.dataforge.context.launch +import hep.dataforge.control.connections.Roles +import hep.dataforge.control.devices.Device +import hep.dataforge.fx.dfIcon +import hep.dataforge.io.MetaFileReader +import hep.dataforge.io.XMLMetaReader +import hep.dataforge.meta.Meta +import hep.dataforge.nullable +import hep.dataforge.storage.MutableStorage +import hep.dataforge.storage.StorageConnection +import hep.dataforge.storage.StorageManager +import hep.dataforge.storage.createShelf +import javafx.application.Application +import javafx.stage.Stage +import org.slf4j.LoggerFactory +import java.nio.file.Files +import java.nio.file.Paths + + +/** + * Created by darksnake on 08-May-17. + */ +const val DEFAULT_CONFIG_LOCATION = "./numass-control.xml" +//val STORING_STATE = "storing" +//val dfIcon: Image = Image(Global::class.java.getResourceAsStream("/img/df.png")) + +fun getRunName(config: Meta): String { + return if (config.hasValue("numass.run")) { + config.getString("numass.run") + } else if (config.hasMeta("numass.server")) { + TODO("Not implemented") + } else { + "" + } +} + +/** + * Create a single or multiple storage connections for a device + * @param device + * * + * @param config + */ +fun Device.connectStorage(config: Meta) { + //TODO add on reset listener + if (config.hasMeta("storage") && acceptsRole(Roles.STORAGE_ROLE)) { + val numassRun = getRunName(config) + val manager = context.getOrLoad(StorageManager::class.java) + + config.getMetaList("storage").forEach { node -> + logger.info("Creating storage for device with getMeta: {}", node) + //building storage in a separate thread + launch { + var storage = manager.create(node) as MutableStorage + if (!numassRun.isEmpty()) { + try { + storage = storage.createShelf(numassRun) + } catch (e: Exception) { + logger.error("Failed to build shelf", e) + } + } + connect(StorageConnection { storage }, Roles.STORAGE_ROLE) + } + } + } +} + +fun readResourceMeta(path: String): Meta { + val resource = Global.getResource(path) + if (resource != null) { + return XMLMetaReader().read(resource.stream) + } else { + throw RuntimeException("Resource $path not found") + } +} + + +fun getConfig(app: Application): Meta? { + val debugConfig = app.parameters.named["config.resource"] + if (debugConfig != null) { + return readResourceMeta(debugConfig) + } + + var configFileName: String? = app.parameters.named["config"] + val logger = LoggerFactory.getLogger(app.javaClass) + if (configFileName == null) { + logger.info("Configuration path not defined. Loading configuration from {}", DEFAULT_CONFIG_LOCATION) + configFileName = DEFAULT_CONFIG_LOCATION + } + val configFile = Paths.get(configFileName) + + return if (Files.exists(configFile)) { + MetaFileReader.read(configFile) + } else { + logger.warn("Configuration file not found") + null + } +} + + +fun findDeviceMeta(config: Meta, criterion: (Meta) -> Boolean): Meta? { + return config.getMetaList("device").stream().filter(criterion).findFirst().nullable +} + +fun setupContext(meta: Meta): Context { + val ctx = Global.getContext("NUMASS-CONTROL") + ctx.plugins.load(StorageManager::class.java) + return ctx +} + +fun setDFStageIcon(stage: Stage) { + stage.icons.add(dfIcon) +} + diff --git a/numass-control/src/main/kotlin/inr/numass/control/NumassStorageConnection.kt b/numass-control/src/main/kotlin/inr/numass/control/NumassStorageConnection.kt new file mode 100644 index 00000000..617bbd26 --- /dev/null +++ b/numass-control/src/main/kotlin/inr/numass/control/NumassStorageConnection.kt @@ -0,0 +1,59 @@ +package inr.numass.control + +import hep.dataforge.control.connections.DeviceConnection +import hep.dataforge.control.connections.Roles +import hep.dataforge.control.devices.Device +import hep.dataforge.nullable +import hep.dataforge.storage.Storage +import hep.dataforge.storage.StorageConnection +import hep.dataforge.storage.tables.MutableTableLoader +import hep.dataforge.storage.tables.createTable +import hep.dataforge.tables.TableFormat +import hep.dataforge.tables.ValuesListener +import hep.dataforge.utils.DateTimeUtils +import hep.dataforge.values.Values +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.util.* + +class NumassStorageConnection(private val loaderName: String? = null, private val formatBuilder: (Device) -> TableFormat) : DeviceConnection(), ValuesListener { + private val loaderMap = HashMap() + + + @Synchronized + override fun accept(point: Values) { + if (device.states.optBoolean("storing").nullable == true) { + val format = formatBuilder(device) + val suffix = DateTimeUtils.fileSuffix() + val loaderName = "${loaderName ?: device.name}_$suffix" + device.forEachConnection(Roles.STORAGE_ROLE, StorageConnection::class.java) { connection -> + try { + //create a loader instance for each connected storage + val pl = loaderMap.getOrPut(connection.storage) { + connection.storage.createTable(loaderName, format) + } + + connection.context.launch(Dispatchers.IO) { + pl.append(point) + } + } catch (ex: Exception) { + device.logger.error("Push to loader failed", ex) + } + } + } + } + + fun reset() = close() + + @Synchronized + override fun close() { + loaderMap.values.forEach { it -> + try { + it.close() + } catch (ex: Exception) { + device.logger.error("Failed to close Loader", ex) + } + } + loaderMap.clear() + } +} \ No newline at end of file diff --git a/numass-control/src/main/kotlin/inr/numass/control/StorageHelper.kt b/numass-control/src/main/kotlin/inr/numass/control/StorageHelper.kt new file mode 100644 index 00000000..209ad0c4 --- /dev/null +++ b/numass-control/src/main/kotlin/inr/numass/control/StorageHelper.kt @@ -0,0 +1,46 @@ +package inr.numass.control + +import hep.dataforge.control.devices.AbstractDevice +import hep.dataforge.nullable +import hep.dataforge.storage.StorageConnection +import hep.dataforge.storage.tables.TableLoader + +import hep.dataforge.values.Values +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.util.* + +/** + * A helper to store points in multiple loaders + * Created by darksnake on 16-May-17. + */ +@Deprecated("To be replaced by connection") +class StorageHelper(private val device: AbstractDevice, private val loaderFactory: (StorageConnection) -> TableLoader) : AutoCloseable { + private val loaderMap = HashMap() + + fun push(point: Values) { + if (device.states.optBoolean("storing").nullable == true) { + device.forEachConnection("storage", StorageConnection::class.java) { connection -> + try { + val pl = loaderMap.computeIfAbsent(connection, loaderFactory).mutable() + device.context.launch(Dispatchers.IO) { + pl.append(point) + } + } catch (ex: Exception) { + device.logger.error("Push to loader failed", ex) + } + } + } + } + + + override fun close() { + loaderMap.values.forEach { it -> + try { + it.close() + } catch (ex: Exception) { + device.logger.error("Failed to close Loader", ex) + } + } + } +} diff --git a/numass-control/src/main/resources/img/df.png b/numass-control/src/main/resources/img/df.png new file mode 100644 index 0000000000000000000000000000000000000000..076e26a2ed849cbaae060d15adb6148a3b56a3c3 GIT binary patch literal 54732 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

s%ww{03OY|N=RqVm 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%0A!(<~w@))CgMB=C}tiVTR$`5(tbM}G`~7pY%g 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 literal 0 HcmV?d00001 diff --git a/numass-control/vac/build.gradle b/numass-control/vac/build.gradle new file mode 100644 index 00000000..b9467448 --- /dev/null +++ b/numass-control/vac/build.gradle @@ -0,0 +1,20 @@ +apply plugin: 'application' + +version = "0.6.0" + +if (!hasProperty('mainClass')) { + ext.mainClass = 'inr.numass.control.readvac.ReadVac' +} +mainClassName = mainClass + +dependencies { + compile project(':numass-control') +} + +task testDevice(dependsOn: classes, type: JavaExec) { + main mainClass + args = ["--config.resource=config-test/devices.xml"] + classpath = sourceSets.main.runtimeClasspath + description "Start application in debug mode with default virtual port" + group "test" +} \ No newline at end of file diff --git a/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/CM32Device.kt b/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/CM32Device.kt new file mode 100644 index 00000000..af00b83b --- /dev/null +++ b/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/CM32Device.kt @@ -0,0 +1,60 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package inr.numass.control.readvac + +import hep.dataforge.context.Context +import hep.dataforge.control.devices.PortSensor +import hep.dataforge.control.ports.ComPort +import hep.dataforge.control.ports.GenericPortController +import hep.dataforge.control.ports.Port +import hep.dataforge.control.ports.PortFactory +import hep.dataforge.meta.Meta +import inr.numass.control.DeviceView + +/** + * @author Alexander Nozik + */ +@DeviceView(VacDisplay::class) +class CM32Device(context: Context, meta: Meta) : PortSensor(context, meta) { + + override fun buildConnection(meta: Meta): GenericPortController { + val portName = meta.getString("name") + logger.info("Connecting to port {}", portName) + val port: Port = if (portName.startsWith("com")) { + ComPort.create(portName, 2400, 8, 1, 0) + } else { + PortFactory.build(meta) + } + return GenericPortController(context, port) { it.endsWith("T--\r") } + } + + + override fun startMeasurement(oldMeta: Meta?, newMeta: Meta) { + measurement { + val answer = sendAndWait("MES R PM 1\r\n") + + if (answer.isEmpty()) { + updateState(PortSensor.CONNECTED_STATE, false) + notifyError("No signal") + } else if (!answer.contains("PM1:mbar")) { + updateState(PortSensor.CONNECTED_STATE, false) + notifyError("Wrong answer: $answer") + } else if (answer.substring(14, 17) == "OFF") { + updateState(PortSensor.CONNECTED_STATE, true) + notifyError("Off") + } else { + updateState(PortSensor.CONNECTED_STATE, true) + notifyResult(answer.substring(14, 17) + answer.substring(19, 23)) + } + } + } + + override val type: String + get() { + return meta.getString("type", "numass.vac.cm32") + } + +} diff --git a/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/ConsoleVac.kt b/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/ConsoleVac.kt new file mode 100644 index 00000000..0f883bf3 --- /dev/null +++ b/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/ConsoleVac.kt @@ -0,0 +1,62 @@ +package inr.numass.control.readvac + +import hep.dataforge.control.devices.Sensor +import hep.dataforge.control.devices.Sensor.Companion.RESULT_VALUE +import kotlinx.coroutines.runBlocking +import org.apache.commons.cli.DefaultParser +import org.apache.commons.cli.HelpFormatter +import org.apache.commons.cli.Options +import java.time.Duration +import java.time.Instant + +/** + * A console based application to test vacuum readings + * Created by darksnake on 06-Dec-16. + */ +object ConsoleVac { + private fun Sensor.read(): Double { + this.measure() + return runBlocking { resultState.read().getDouble(RESULT_VALUE)} + } + + @Throws(Exception::class) + @JvmStatic + fun main(args: Array) { + val options = Options() + + options.addOption("c", "class", true, "A short or long class name for vacuumeter device") + options.addOption("n", "name", true, "A device name") + options.addOption("p", "port", true, "Port name in dataforge-control notation") + options.addOption("d", "delay", true, "A delay between measurements in Duration notation") + + if (args.isEmpty()) { + HelpFormatter().printHelp("vac-console", options) + return + } + + val parser = DefaultParser() + val cli = parser.parse(options, args) + + var className: String = cli.getOptionValue("c") ?: throw RuntimeException("Vacuumeter class not defined") + + if (!className.contains(".")) { + className = "inr.numass.readvac.devices.$className" + } + + val name = cli.getOptionValue("n", "sensor") + val port = cli.getOptionValue("p", "com::/dev/ttyUSB0") + val delay = Duration.parse(cli.getOptionValue("d", "PT1M")) + + val sensor = Class.forName(className) + .getConstructor(String::class.java).newInstance(port) as Sensor + try { + sensor.init() + while (true) { + System.out.printf("(%s) %s -> %g%n", Instant.now().toString(), name, sensor.read()) + Thread.sleep(delay.toMillis()) + } + } finally { + sensor.shutdown() + } + } +} diff --git a/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/MKSBaratronDevice.kt b/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/MKSBaratronDevice.kt new file mode 100644 index 00000000..bfdae799 --- /dev/null +++ b/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/MKSBaratronDevice.kt @@ -0,0 +1,56 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package inr.numass.control.readvac + +import hep.dataforge.context.Context +import hep.dataforge.control.devices.PortSensor +import hep.dataforge.control.ports.GenericPortController +import hep.dataforge.control.ports.Port +import hep.dataforge.control.ports.PortFactory +import hep.dataforge.description.ValueDef +import hep.dataforge.meta.Meta +import hep.dataforge.states.StateDef +import hep.dataforge.states.valueState +import hep.dataforge.values.ValueType +import inr.numass.control.DeviceView + +/** + * @author Alexander Nozik + */ +@ValueDef(key = "channel") +@DeviceView(VacDisplay::class) +@StateDef(value = ValueDef(key = "channel", type = [ValueType.NUMBER], def = "2"), writable = true) +class MKSBaratronDevice(context: Context, meta: Meta) : PortSensor(context, meta) { + + var channel by valueState("channel").intDelegate + + override val type: String get() = meta.getString("type", "numass.vac.baratron") + + override fun buildConnection(meta: Meta): GenericPortController { + val port: Port = PortFactory.build(meta) + logger.info("Connecting to port {}", port.name) + return GenericPortController(context, port) { it.endsWith("\r") } + } + + override fun startMeasurement(oldMeta: Meta?, newMeta: Meta) { + measurement { + val answer = sendAndWait("AV$channel\r") + if (answer.isEmpty()) { + // invalidateState("connection"); + updateState(PortSensor.CONNECTED_STATE, false) + notifyError("No connection") + } else { + updateState(PortSensor.CONNECTED_STATE, true) + } + val res = java.lang.Double.parseDouble(answer) + if (res <= 0) { + notifyError("Non positive") + } else { + notifyResult(res) + } + } + } +} diff --git a/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/MKSVacDevice.kt b/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/MKSVacDevice.kt new file mode 100644 index 00000000..be219a14 --- /dev/null +++ b/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/MKSVacDevice.kt @@ -0,0 +1,123 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package inr.numass.control.readvac + +import hep.dataforge.context.Context +import hep.dataforge.control.devices.PortSensor +import hep.dataforge.control.ports.GenericPortController +import hep.dataforge.control.ports.Port +import hep.dataforge.control.ports.PortFactory +import hep.dataforge.description.ValueDef +import hep.dataforge.description.ValueDefs +import hep.dataforge.exceptions.ControlException +import hep.dataforge.meta.Meta +import hep.dataforge.states.StateDef +import hep.dataforge.states.StateDefs +import hep.dataforge.states.valueState +import hep.dataforge.values.ValueType.BOOLEAN +import inr.numass.control.DeviceView +import java.lang.Double.parseDouble +import java.util.regex.Pattern + +/** + * @author Alexander Nozik + */ +@ValueDefs( + ValueDef(key = "address", def = "253"), + ValueDef(key = "channel", def = "5"), + ValueDef(key = "powerButton", type = arrayOf(BOOLEAN), def = "true") +) +@StateDefs( + StateDef(value = ValueDef(key = "power", info = "Device powered up"), writable = true) +// StateDef(value = ValueDef(name = "channel", info = "Measurement channel", type = arrayOf(NUMBER), def = "5"), writable = true) +) +@DeviceView(VacDisplay::class) +class MKSVacDevice(context: Context, meta: Meta) : PortSensor(context, meta) { + + private val deviceAddress: String = meta.getString("address", "253") + + var power by valueState("power", getter = { talk("FP?") == "ON" }) { old, value -> + if (old != value) { + setPowerOn(value.boolean) + } + }.booleanDelegate + + + @Throws(ControlException::class) + private fun talk(requestContent: String): String? { + val answer = sendAndWait(String.format("@%s%s;FF", deviceAddress, requestContent)) + + val match = Pattern.compile("@" + deviceAddress + "ACK(.*);FF").matcher(answer) + return if (match.matches()) { + match.group(1) + } else { + throw ControlException(answer) + } + } + + override fun buildConnection(meta: Meta): GenericPortController { + val port: Port = PortFactory.build(meta) + logger.info("Connecting to port {}", port.name) + return GenericPortController(context, port) { it.endsWith(";FF") } + } + + + @Throws(ControlException::class) + override fun shutdown() { + if (connected.booleanValue) { + power = false + } + super.shutdown() + } + + private fun setPowerOn(powerOn: Boolean) { + if (powerOn != power) { + if (powerOn) { + // String ans = talkMKS(p1Port, "@253ENC!OFF;FF"); + // if (!ans.equals("OFF")) { + // LoggerFactory.getLogger(getClass()).warn("The @253ENC!OFF;FF command is not working"); + // } + val ans = talk("FP!ON") + if (ans == "ON") { + updateState("power", true) + } else { + this.notifyError("Failed to set power state") + } + } else { + val ans = talk("FP!OFF") + if (ans == "OFF") { + updateState("power", false) + } else { + this.notifyError("Failed to set power state") + } + } + } + } + + override val type: String + get() = meta.getString("type", "numass.vac.mks") + + override fun startMeasurement(oldMeta: Meta?, newMeta: Meta) { + measurement { + // if (getState("power").booleanValue()) { + val channel = meta.getInt("channel", 5) + val answer = talk("PR$channel?") + if (answer == null || answer.isEmpty()) { + updateState(PortSensor.CONNECTED_STATE, false) + notifyError("No connection") + } + val res = parseDouble(answer) + if (res <= 0) { + updateState("power", false) + notifyError("No power") + } else { + message = "OK" + notifyResult(res) + } + } + } + +} diff --git a/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/MeradatVacDevice.kt b/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/MeradatVacDevice.kt new file mode 100644 index 00000000..ff1428d0 --- /dev/null +++ b/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/MeradatVacDevice.kt @@ -0,0 +1,94 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package inr.numass.control.readvac + +import hep.dataforge.context.Context +import hep.dataforge.control.devices.PortSensor +import hep.dataforge.control.ports.GenericPortController +import hep.dataforge.control.ports.Port +import hep.dataforge.control.ports.PortFactory +import hep.dataforge.description.ValueDef +import hep.dataforge.meta.Meta +import hep.dataforge.states.StateDef +import hep.dataforge.states.valueState +import hep.dataforge.values.ValueType.NUMBER +import java.math.BigDecimal +import java.math.BigInteger +import java.math.RoundingMode +import java.util.regex.Pattern + +/** + * @author Alexander Nozik + */ +@StateDef(value = ValueDef(key = "address", type = [NUMBER], def = "1", info = "A modbus address"), writable = true) +class MeradatVacDevice(context: Context, meta: Meta) : PortSensor(context, meta) { + + var address by valueState("address").intDelegate + + override fun buildConnection(meta: Meta): GenericPortController { + val port: Port = PortFactory.build(meta) + logger.info("Connecting to port {}", port.name) + + return GenericPortController(context, port) { it.endsWith("\r\n") } + } + + override val type: String + get() { + return meta.getString("type", "numass.vac.vit") + } + + override fun startMeasurement(oldMeta: Meta?, newMeta: Meta) { + measurement{ + val requestBase: String = String.format(":%02d", address) + val dataStr = requestBase.substring(1) + REQUEST + val query = requestBase + REQUEST + calculateLRC(dataStr) + "\r\n" // ":010300000002FA\r\n"; + val response: Pattern = Pattern.compile(requestBase + "0304(\\w{4})(\\w{4})..\r\n") + + val answer = sendAndWait(query) { phrase -> phrase.startsWith(requestBase) } + + if (answer.isEmpty()) { + updateState(PortSensor.CONNECTED_STATE, false) + notifyError("No signal") + } else { + val match = response.matcher(answer) + + if (match.matches()) { + val base = Integer.parseInt(match.group(1), 16).toDouble() / 10.0 + var exp = Integer.parseInt(match.group(2), 16) + if (exp > 32766) { + exp -= 65536 + } + var res = BigDecimal.valueOf(base * Math.pow(10.0, exp.toDouble())) + res = res.setScale(4, RoundingMode.CEILING) + updateState(PortSensor.CONNECTED_STATE, true) + notifyResult(res) + } else { + updateState(PortSensor.CONNECTED_STATE, false) + notifyError("Wrong answer: $answer") + } + } + } + } + + companion object { + private const val REQUEST = "0300000002" + + fun calculateLRC(inputString: String): String { + /* + * String is Hex String, need to convert in ASCII. + */ + val bytes = BigInteger(inputString, 16).toByteArray() + val checksum = bytes.sumBy { it.toInt() } + var value = Integer.toHexString(-checksum) + value = value.substring(value.length - 2).toUpperCase() + if (value.length < 2) { + value = "0$value" + } + + return value + } + } +} diff --git a/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/ReadVac.kt b/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/ReadVac.kt new file mode 100644 index 00000000..871a1e21 --- /dev/null +++ b/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/ReadVac.kt @@ -0,0 +1,34 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package inr.numass.control.readvac + +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaUtils +import inr.numass.control.NumassControlApplication +import javafx.stage.Stage + +/** + * @author Alexander Nozik + */ +class ReadVac : NumassControlApplication() { + + override val deviceFactory = VacDeviceFactory() + + override fun setupStage(stage: Stage, device: VacCollectorDevice) { + stage.title = "Numass vacuum measurements" + } + + override fun getDeviceMeta(config: Meta): Meta { + return MetaUtils.findNode(config, "device") { + it.getString("type") == "numass:vac" + }.orElseThrow { + RuntimeException("Vacuum measurement configuration not found") + } + } +} + + + diff --git a/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/ThyroContVacDevice.kt b/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/ThyroContVacDevice.kt new file mode 100644 index 00000000..0bca72be --- /dev/null +++ b/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/ThyroContVacDevice.kt @@ -0,0 +1,66 @@ +package inr.numass.control.readvac + +import hep.dataforge.context.Context +import hep.dataforge.control.devices.PortSensor +import hep.dataforge.control.ports.GenericPortController +import hep.dataforge.control.ports.Port +import hep.dataforge.control.ports.PortFactory +import hep.dataforge.meta.Meta +import inr.numass.control.DeviceView + +//@ValueDef(key = "address") +@DeviceView(VacDisplay::class) +//@StateDef(value = ValueDef(key = "address", type = [ValueType.STRING], def = "001")) +class ThyroContVacDevice(context: Context, meta: Meta) : PortSensor(context, meta) { + //val address by valueState("address").stringDelegate + val address = "001" + + override val type: String get() = meta.getString("type", "numass.vac.thyrocont") + + override fun buildConnection(meta: Meta): GenericPortController { + val port: Port = PortFactory.build(meta) + logger.info("Connecting to port {}", port.name) + return GenericPortController(context, port) { it.endsWith("\r") } + } + + private fun String.checksum(): Char = (sumBy { it.toInt() } % 64 + 64).toChar() + + private fun wrap(str: String): String = buildString { + append(str) + append(str.checksum()) + append('\r') + } + + override fun startMeasurement(oldMeta: Meta?, newMeta: Meta) { + measurement { + val request = wrap("0010MV00") + val answer = sendAndWait(request) + if (answer.isEmpty()) { + updateState(CONNECTED_STATE, false) + notifyError("No connection") + } else { + updateState(CONNECTED_STATE, true) + } + try { + + val address = answer.substring(0..2) + //if wrong answer + if (address != this.address) { + logger.warn("Expected response for address ${this.address}, bur received for $address") + notifyError("Wrong response address") + return@measurement + } + val dataSize = answer.substring(6..7).toInt() + val data = answer.substring(8, 8 + dataSize).toDouble() + if (data <= 0) { + notifyError("Non positive") + } else { + notifyResult(data) + } + } catch (ex: Exception) { + logger.error("Parsing error", ex) + notifyError("Parse error") + } + } + } +} \ No newline at end of file diff --git a/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/VacCollectorDevice.kt b/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/VacCollectorDevice.kt new file mode 100644 index 00000000..41d287ad --- /dev/null +++ b/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/VacCollectorDevice.kt @@ -0,0 +1,152 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package inr.numass.control.readvac + +import hep.dataforge.connections.Connection +import hep.dataforge.connections.RoleDef +import hep.dataforge.context.Context +import hep.dataforge.context.launch +import hep.dataforge.control.collectors.RegularPointCollector +import hep.dataforge.control.connections.Roles +import hep.dataforge.control.devices.Device +import hep.dataforge.control.devices.DeviceHub +import hep.dataforge.control.devices.DeviceListener +import hep.dataforge.control.devices.PortSensor.Companion.CONNECTED_STATE +import hep.dataforge.control.devices.Sensor +import hep.dataforge.description.ValueDef +import hep.dataforge.exceptions.ControlException +import hep.dataforge.meta.Meta +import hep.dataforge.names.Name +import hep.dataforge.states.StateDef +import hep.dataforge.storage.StorageConnection +import hep.dataforge.storage.tables.TableLoader +import hep.dataforge.storage.tables.createTable +import hep.dataforge.tables.TableFormatBuilder +import hep.dataforge.utils.DateTimeUtils +import hep.dataforge.values.Value +import hep.dataforge.values.ValueMap +import hep.dataforge.values.ValueType +import hep.dataforge.values.Values +import inr.numass.control.DeviceView +import inr.numass.control.StorageHelper +import kotlinx.coroutines.time.delay +import java.time.Duration +import java.time.Instant +import java.util.* + +/** + * @author [Alexander Nozik](mailto:altavir@gmail.com) + */ +@RoleDef(name = Roles.STORAGE_ROLE, objectType = StorageConnection::class, info = "Storage for acquired points") +@StateDef(value = ValueDef(key = "storing", info = "Define if this device is currently writes to storage"), writable = true) +@DeviceView(VacCollectorDisplay::class) +class VacCollectorDevice(context: Context, meta: Meta, val sensors: Collection) : Sensor(context, meta), DeviceHub { + + private val helper = StorageHelper(this, this::buildLoader) + + private val collector = object : DeviceListener { + val averagingDuration: Duration = Duration.parse(meta.getString("averagingDuration", "PT30S")) + + private val collector = RegularPointCollector(averagingDuration) { + notifyResult(it) + } + + override fun notifyStateChanged(device: Device, name: String, state: Any) { + if (name == MEASUREMENT_RESULT_STATE) { + collector.put(device.name, (state as Meta).getValue(RESULT_VALUE)) + } + } + } + + + override fun optDevice(name: Name): Optional = + Optional.ofNullable(sensors.find { it.name == name.unescaped }) + + override val deviceNames: List + get() = sensors.map { Name.ofSingle(it.name) } + + + override fun init() { + for (s in sensors) { + s.init() + s.connect(collector, Roles.DEVICE_LISTENER_ROLE) + } + super.init() + } + + override val type: String + get() = "numass.vac.collector" + + + @Throws(ControlException::class) + override fun shutdown() { + super.shutdown() + helper.close() + for (sensor in sensors) { + sensor.shutdown() + } + } + + private fun buildLoader(connection: StorageConnection): TableLoader { + val format = TableFormatBuilder().setType("timestamp", ValueType.TIME) + sensors.forEach { s -> format.setType(s.name, ValueType.NUMBER) } + + val suffix = DateTimeUtils.fileSuffix() + + return connection.storage.createTable("vactms_$suffix", format.build()) + //LoaderFactory.buildPointLoader(connection.storage, "vactms_$suffix", "", "timestamp", format.build()) + } + + override fun connectAll(connection: Connection, vararg roles: String) { + connect(connection, *roles) + this.sensors.forEach { it.connect(connection, *roles) } + } + + override fun connectAll(context: Context, meta: Meta) { + this.connectionHelper.connect(context, meta) + this.sensors.forEach { it.connectionHelper.connect(context, meta) } + } + + + private fun notifyResult(values: Values, timestamp: Instant = Instant.now()) { + super.notifyResult(values, timestamp) + helper.push(values) + } + + override fun stopMeasurement() { + super.stopMeasurement() + notifyResult(terminator()) + } + + override fun startMeasurement(oldMeta: Meta?, newMeta: Meta) { + oldMeta?.let { + stopMeasurement() + } + + val interval = Duration.ofSeconds(meta.getInt("delay", 5).toLong()) + + job = launch { + while (true) { + notifyMeasurementState(MeasurementState.IN_PROGRESS) + sensors.forEach { sensor -> + if (sensor.states.getBoolean(CONNECTED_STATE, false)) { + sensor.measure() + } + } + notifyMeasurementState(MeasurementState.WAITING) + delay(interval) + } + } + } + + private fun terminator(): Values { + val p = ValueMap.Builder() + deviceNames.forEach { n -> p.putValue(n.unescaped, Value.NULL) } + return p.build() + } + + +} diff --git a/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/VacCollectorDisplay.kt b/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/VacCollectorDisplay.kt new file mode 100644 index 00000000..a0c9effe --- /dev/null +++ b/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/VacCollectorDisplay.kt @@ -0,0 +1,140 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package inr.numass.control.readvac + +import hep.dataforge.control.connections.Roles +import hep.dataforge.control.devices.Device +import hep.dataforge.control.devices.DeviceListener +import hep.dataforge.control.devices.Sensor +import hep.dataforge.fx.bindWindow +import hep.dataforge.fx.fragments.LogFragment +import hep.dataforge.meta.Meta +import hep.dataforge.plots.PlotGroup +import hep.dataforge.plots.data.TimePlot +import hep.dataforge.values.Value +import inr.numass.control.DeviceDisplayFX +import inr.numass.control.deviceStateToggle +import inr.numass.control.plot +import javafx.collections.FXCollections +import javafx.collections.MapChangeListener +import javafx.geometry.Orientation +import javafx.scene.control.ScrollPane +import javafx.scene.layout.Priority +import tornadofx.* + +/** + * A view controller for Vac collector + + * @author [Alexander Nozik](mailto:altavir@gmail.com) + */ +class VacCollectorDisplay : DeviceDisplayFX() { + + private val table = FXCollections.observableHashMap() + + private val sensorConnection = object : DeviceListener { + + override fun notifyStateChanged(device: Device, name: String, state: Any) { + if (name == Sensor.MEASUREMENT_RESULT_STATE) { + table[device.name] = (state as Meta).getDouble(Sensor.RESULT_VALUE) + } + } + } + + private val viewList = FXCollections.observableArrayList(); + + override fun buildView(device: VacCollectorDevice): UIComponent { + return VacCollectorView(); + } + + override fun open(obj: Any) { + super.open(obj) + device.sensors.forEach { sensor -> + val view = VacDisplay() + sensor.connect(view, Roles.VIEW_ROLE, Roles.DEVICE_LISTENER_ROLE) + sensor.connect(sensorConnection, Roles.DEVICE_LISTENER_ROLE) + viewList.add(view) + } + } + + inner class VacCollectorView : Fragment("Numass vacuum view") { + + private val plottables = PlotGroup.typed("vac").apply { + viewList.forEach { + val plot = TimePlot(it.getTitle(), it.device.name) + plot.configure(it.device.meta) + add(plot) + } + configureValue("thickness", 3) + } + +// private val logWindow = FragmentWindow(LogFragment().apply { +// addLogHandler(device.logger) +// }) + + override val root = borderpane { + top { + toolbar { + deviceStateToggle(this@VacCollectorDisplay, Sensor.MEASURING_STATE, "Measure") + deviceStateToggle(this@VacCollectorDisplay, "storing", "Store") + pane { + hgrow = Priority.ALWAYS + } + separator(Orientation.VERTICAL) + togglebutton("Log") { + isSelected = false + LogFragment().apply { + addLogHandler(device.logger) + bindWindow(this@togglebutton, selectedProperty()) + } + } + } + } + plot(plottables) { + "xAxis.type" to "time" + node("yAxis") { + "type" to "log" + "title" to "presure" + "units" to "mbar" + } + } + right { + scrollpane { + hbarPolicy = ScrollPane.ScrollBarPolicy.NEVER + vbox { + viewList.forEach { + it.view?.let { + add(it) + separator(Orientation.HORIZONTAL) + } + } + } + } +// listview(viewList) { +// cellFormat { +// graphic = it.fxNode +// } +// } + } + } + + + init { + table.addListener { change: MapChangeListener.Change -> + if (change.wasAdded()) { + val pl = plottables[change.key] + val value = change.valueAdded + (pl as? TimePlot)?.let { + if (value > 0) { + it.put(Value.of(value)) + } else { + it.put(Value.NULL) + } + } + } + } + } + } +} diff --git a/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/VacDeviceFactory.kt b/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/VacDeviceFactory.kt new file mode 100644 index 00000000..ff0fbc41 --- /dev/null +++ b/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/VacDeviceFactory.kt @@ -0,0 +1,39 @@ +package inr.numass.control.readvac + +import hep.dataforge.context.Context +import hep.dataforge.control.devices.DeviceFactory +import hep.dataforge.control.devices.Sensor +import hep.dataforge.meta.Meta +import java.util.stream.Collectors + +/** + * A factory for vacuum measurements collector + * Created by darksnake on 16-May-17. + */ +class VacDeviceFactory : DeviceFactory { + override val type: String = "numass.vac" + + private fun buildSensor(context: Context, sensorConfig: Meta): Sensor { + return when (sensorConfig.getString("sensorType", "")) { + "mks" -> MKSVacDevice(context, sensorConfig) + "CM32" -> CM32Device(context, sensorConfig) + "meradat" -> MeradatVacDevice(context, sensorConfig) + "baratron" -> MKSBaratronDevice(context, sensorConfig) + "ThyroCont" -> ThyroContVacDevice(context,sensorConfig) +// VIRTUAL_SENSOR_TYPE -> VirtualDevice.randomDoubleSensor(context, sensorConfig) + else -> throw RuntimeException("Unknown vacuum sensor type") + } + } + + override fun build(context: Context, config: Meta): VacCollectorDevice { + val sensors = config.getMetaList("sensor").stream() + .map { sensorConfig -> buildSensor(context, sensorConfig) } + .collect(Collectors.toList()) + + return VacCollectorDevice(context, config, sensors) + } + +// override fun buildView(device: Device): DeviceDisplayFX { +// return VacCollectorDisplay(); +// } +} diff --git a/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/VacDisplay.kt b/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/VacDisplay.kt new file mode 100644 index 00000000..6ce8c9e5 --- /dev/null +++ b/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/VacDisplay.kt @@ -0,0 +1,185 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package inr.numass.control.readvac + +import hep.dataforge.control.devices.PortSensor.Companion.CONNECTED_STATE +import hep.dataforge.control.devices.Sensor +import inr.numass.control.DeviceDisplayFX +import inr.numass.control.switch +import javafx.application.Platform +import javafx.beans.property.SimpleObjectProperty +import javafx.beans.property.SimpleStringProperty +import javafx.geometry.Insets +import javafx.geometry.Orientation +import javafx.geometry.Pos +import javafx.scene.layout.Priority +import javafx.scene.paint.Color +import javafx.scene.text.FontWeight +import tornadofx.* +import java.text.DecimalFormat +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter + +/** + * @author [Alexander Nozik](mailto:altavir@gmail.com) + */ +open class VacDisplay : DeviceDisplayFX() { + + val statusProperty = SimpleStringProperty("") + var status: String by statusProperty + + val valueProperty = SimpleStringProperty("---") + var value: String by valueProperty + + val timeProperty = SimpleObjectProperty() + var time: Instant by timeProperty + + + override fun buildView(device: Sensor): View { + return VacView(); + } + + fun message(message: String) { + Platform.runLater { + status = message + } + } + + private fun onResult(res: Any, time: Instant) { + val result = Number::class.java.cast(res).toDouble() + val resString = FORMAT.format(result) + Platform.runLater { + value = resString + this.time = time + status = "OK: " + TIME_FORMAT.format(LocalDateTime.ofInstant(time, ZoneOffset.UTC)); + } + } + +// override fun notifyMetaStateChanged(device: Device, name: String, state: Meta) { +// super.notifyMetaStateChanged(device, name, state) +// +// when (name) { +// Sensor.MEASUREMENT_RESULT_STATE -> { +// if(state.getBoolean(Sensor.RESULT_SUCCESS)) { +// val res by state.value(Sensor.RESULT_VALUE) +// val time by state.getTime(Sensor.RESULT_TIMESTAMP) +// onResult(res, time) +// } else{ +// Platform.runLater { +// value = "Err" +// } +// } +// } +// Sensor.MEASUREMENT_ERROR_STATE -> { +// val message by state.getString("message") +// message(message) +// } +// } +// } +// +// override fun notifyStateChanged(device: Device, name: String, state: Any?) { +// super.notifyStateChanged(device, name, state) +// } + + + + + fun getTitle(): String { + return device.meta.getString("title", device.name); + } + + inner class VacView : View("Numass vacuumeter ${getTitle()}") { + + override val root = borderpane { + minWidth = 90.0 + style { + + } + top { + borderpane { + center { + label(device.name) { + style { + fontSize = 18.pt + fontWeight = FontWeight.BOLD + } + } + style { + backgroundColor = multi(Color.LAVENDER) + } + } + right { + switch { + booleanStateProperty(CONNECTED_STATE).bindBidirectional(selectedProperty()) + selectedProperty().addListener { _, _, newValue -> + if (!newValue) { + value = "---" + } + } + } + } + } + } + center { + vbox { + separator(Orientation.HORIZONTAL) + borderpane { + left { + label { + padding = Insets(5.0, 5.0, 5.0, 5.0) + prefHeight = 60.0 + alignment = Pos.CENTER_RIGHT + textProperty().bind(valueProperty) + device.meta.optValue("color").ifPresent { colorValue -> textFill = Color.valueOf(colorValue.string) } + style { + fontSize = 24.pt + fontWeight = FontWeight.BOLD + } + } + } + right { + label { + padding = Insets(5.0, 5.0, 5.0, 5.0) + prefHeight = 60.0 + prefWidth = 75.0 + alignment = Pos.CENTER_LEFT + text = device.meta.getString("units", "mbar") + style { + fontSize = 24.pt + } + } + } + } + if (device.stateNames.contains("power")) { + separator(Orientation.HORIZONTAL) + pane { + minHeight = 30.0 + vgrow = Priority.ALWAYS + switch("Power") { + alignment = Pos.CENTER + booleanStateProperty("power").bindBidirectional(selectedProperty()) + } + } + } + separator(Orientation.HORIZONTAL) + } + } + bottom { + label { + padding = Insets(5.0, 5.0, 5.0, 5.0) + textProperty().bind(statusProperty) + } + } + } + } + + companion object { + private val FORMAT = DecimalFormat("0.###E0") + private val TIME_FORMAT = DateTimeFormatter.ISO_LOCAL_TIME + } +} diff --git a/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/test.kt b/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/test.kt new file mode 100644 index 00000000..13cc7d3a --- /dev/null +++ b/numass-control/vac/src/main/kotlin/inr/numass/control/readvac/test.kt @@ -0,0 +1,20 @@ +package inr.numass.control.readvac + +import hep.dataforge.context.Global +import hep.dataforge.meta.buildMeta +import kotlinx.coroutines.delay + +suspend fun main() { + val meta = buildMeta { + "name" to "PSP" + "port" to "tcp::192.168.111.32:4001" + "sensorType" to "ThyroCont" + } + val device = ThyroContVacDevice(Global, meta) + device.measure() + device.connected.set(true) + delay(400) + println(device.result) + device.connected.set(false) + +} \ No newline at end of file diff --git a/numass-control/vac/src/main/resources/META-INF/services/hep.dataforge.control.devices.DeviceFactory b/numass-control/vac/src/main/resources/META-INF/services/hep.dataforge.control.devices.DeviceFactory new file mode 100644 index 00000000..f36271a1 --- /dev/null +++ b/numass-control/vac/src/main/resources/META-INF/services/hep.dataforge.control.devices.DeviceFactory @@ -0,0 +1 @@ +inr.numass.control.readvac.VacDeviceFactory \ No newline at end of file diff --git a/numass-control/vac/src/main/resources/config-test/devices.xml b/numass-control/vac/src/main/resources/config-test/devices.xml new file mode 100644 index 00000000..937c6f94 --- /dev/null +++ b/numass-control/vac/src/main/resources/config-test/devices.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/numass-control/vac/src/main/resources/config/devices.xml b/numass-control/vac/src/main/resources/config/devices.xml new file mode 100644 index 00000000..4f2405d3 --- /dev/null +++ b/numass-control/vac/src/main/resources/config/devices.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/numass-control/vac/src/test/java/inr/numass/control/readvac/TestMain.java b/numass-control/vac/src/test/java/inr/numass/control/readvac/TestMain.java new file mode 100644 index 00000000..0f6fc0ad --- /dev/null +++ b/numass-control/vac/src/test/java/inr/numass/control/readvac/TestMain.java @@ -0,0 +1,45 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.control.readvac; + +/** + * + * @author Darksnake + */ +public class TestMain { + + /** + * @param args the command line arguments + * @throws java.lang.Exception + */ + public static void main(String[] args) throws Exception { +// File serverDir = new File("d:\\temp\\test\\remote\\"); +// NetworkListner listner = new NetworkListner(new FileDataServer(serverDir, null), null); +// listner.start(); +// Annotation config = new XMLAnnotationParser().fromStream(null, TestMain.class.getResourceAsStream("testConfig.xml")); +// Main.runConfig(config); +// +// Runtime.getRuntime().addShutdownHook(new Thread(() -> { +// try { +// listner.close(); +// } catch (IOException ex) { +// LoggerFactory.getLogger("test").error(null, ex); +// } +// })); + + } + +} diff --git a/numass-control/vac/src/test/java/inr/numass/control/readvac/TestRemote.java b/numass-control/vac/src/test/java/inr/numass/control/readvac/TestRemote.java new file mode 100644 index 00000000..81c17b6b --- /dev/null +++ b/numass-control/vac/src/test/java/inr/numass/control/readvac/TestRemote.java @@ -0,0 +1,35 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.control.readvac; + +/** + * + * @author Darksnake + */ +public class TestRemote { + + /** + * @param args the command line arguments + * @throws java.lang.Exception + */ + public static void main(String[] args) throws Exception { +// Annotation config = new XMLAnnotationParser().fromStream(null, TestRemote.class.getResourceAsStream("testConfig.xml")); +// Main.runConfig(config); +// System.exit(0); + + } + +} diff --git a/numass-control/vac/src/test/java/inr/numass/control/readvac/TimeShiftTest.java b/numass-control/vac/src/test/java/inr/numass/control/readvac/TimeShiftTest.java new file mode 100644 index 00000000..db8e890a --- /dev/null +++ b/numass-control/vac/src/test/java/inr/numass/control/readvac/TimeShiftTest.java @@ -0,0 +1,46 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.control.readvac; + +import hep.dataforge.utils.DateTimeUtils; + +import java.time.*; + +/** + * + * @author Darksnake + */ +public class TimeShiftTest { + + /** + * @param args the command line arguments + */ + public static void main(String[] args) { + Instant now = DateTimeUtils.now(); + System.out.println(now.toString()); + LocalDateTime ldt = LocalDateTime.now(); + System.out.println(ldt.toString()); + System.out.println(ldt.toInstant(ZoneOffset.ofHours(1)).toString()); + ZonedDateTime zdt = ZonedDateTime.now(); + System.out.println(zdt.toString()); + System.out.println(zdt.toInstant()); + + System.out.println(ZoneId.systemDefault().getRules().toString()); + + + } + +} diff --git a/numass-core/build.gradle b/numass-core/build.gradle new file mode 100644 index 00000000..430c0aff --- /dev/null +++ b/numass-core/build.gradle @@ -0,0 +1,12 @@ +description = "A bse package with minimal dependencies for numass" + + +dependencies { + compile project(":numass-core:numass-data-api") + compile project(":numass-core:numass-data-proto") + compile project(":dataforge-storage") + compile project(":dataforge-core:dataforge-json") + + // https://mvnrepository.com/artifact/com.github.robtimus/sftp-fs + compile group: 'com.github.robtimus', name: 'sftp-fs', version: '1.1.3' +} \ No newline at end of file diff --git a/numass-core/docs/server_commands.md b/numass-core/docs/server_commands.md new file mode 100644 index 00000000..efcdc519 --- /dev/null +++ b/numass-core/docs/server_commands.md @@ -0,0 +1,189 @@ + +# Команды Ñервера Numass # + +### ÐÐ´Ñ€ÐµÑ Ñервера ### + +ÐÐ´Ñ€ÐµÑ Ñервера в локальной Ñети по-умолчанию `192.168.111.1`, Ð°Ð´Ñ€ÐµÑ Ñервера в Ñети инÑтитута `172.20.75.178` + +### Хранение данных ### + +Хранение данных оÑущеÑтвлÑетÑÑ Ð² файловом репозитории `/home/trdat/numass-repo`. СчитаетÑÑ, что вÑе фалы Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ð¸Ñ Ñ Ð½Ð°Ð·Ð²Ð°Ð½Ð¸Ñми, начинающимиÑÑ Ñ `@` ÑвлÑÑŽÑ‚ÑÑ ÑиÑтемными. + +### Формат ÑÐ¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ ### + +Сообщение Ñодержит иерархичеÑки организованный текÑÑ‚ в формате JSON (meta), а также может опционально Ñодержать данные в бинарном формате (data). + +## Команды ## + +Сообщение Ñодержит два полÑ, определÑющих его Ñодержание: поле типа ÑÐ¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ `type` и поле типа дейÑÑ‚Ð²Ð¸Ñ `action`. +ПоддерживаютÑÑ Ñледующие типы команд: + + - `numass.storage` - Загрузка и получение данных из Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ð¸Ñ + - `numass.state` - Получение или изменение ÑоÑтоÑÐ½Ð¸Ñ Ñ‚ÐµÐºÑƒÑ‰ÐµÐ³Ð¾ ÑеанÑа (напрÑжение, токи, и Ñ‚. д.) + - `numass.event` - Отправка ÑÐ¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ Ð¿Ð¾ текущему ÑеанÑу + - `numass.control` - Отправка управлÑющей команды Ð´Ð»Ñ Ð¾Ð±Ð¾Ñ€ÑƒÐ´Ð¾Ð²Ð°Ð½Ð¸Ñ + - `numass.run` - получение или изменение параметров текущего ÑеанÑа + +#### numass.storage #### +Обращение к репозиторию текущего ÑеанÑа. + +#### numass.state #### +Считывание или изменение ÑоÑтоÑÐ½Ð¸Ñ Ð´Ð»Ñ Ñ‚ÐµÐºÑƒÑ‰ÐµÐ³Ð¾ ÑеанÑа. ДоÑтупны Ñледующие дейÑтвиÑ: + + - `get` - Ñчитать ÑоÑтоÑние (или ÑоÑтоÑниÑ) - *Not tested* + *ЗапроÑ:* + ``` + { + "type": "numass.state", + "action": "get", + "name": [ + "", + "", + ... + ] + } + ``` + + *Ответ*: + ``` + { + "type": "numass.state.get.response", + "state": [ + { + "name": "", + "value": + }, + { + "name": "", + "value": + }, + ... + ] + } + ``` + Ð’ запроÑе вмеÑто маÑÑива может ÑтоÑÑ‚ÑŒ проÑÑ‚Ð°Ñ Ñтрока `state: "<>"`. Ð’ Ñтом Ñлучае в ответе вмеÑто маÑÑива будет один JSON объект. + + - `set` - уÑтановить ÑоÑтоÑние (или ÑоÑтоÑниÑ) - *Not tested* + *ЗапроÑ:* + ``` + { + "type": "numass.state", + "action": "set", + "state": [ + { + "name": "", + "value": + }, + { + "name": "", + "value": + }, + ... + ] + } + ``` + + или + + ``` + { + "type": "numass.state", + "action": "set", + "name": "", + "value": + } + ``` + + + *Ответ*: + ``` + { + type: "numass.state.get.response", + state: [ + { + "name": "", + "value": + }, + { + "name": "", + "value": + }, + ... + ] + } + ``` + ЕÑли запрашивалоÑÑŒ изменение единичного ÑоÑтоÑниÑ, то возвращаетÑÑ ÐµÐ´Ð¸Ð½Ð¸Ñ‡Ð½Ñ‹Ð¹ JSON объект. + +#### numass.event #### +*Not implemented* +#### numass.control #### +*Not implemented* +#### numass.run #### +Считываие или изменение текущего ÑеанÑа. ДоÑтупны Ñледующие дейÑтвиÑ: + + - `get` - Считать конфигурацию текущего ÑеанÑа + *ЗапроÑ:* + + ``` + { + "type": "numass.run", + "action": "get", + } + ``` + + *Ответ:* + + ``` + { + "type": "numass.run.response", + "run": { + "path": "", + "meta": { + + } + } + } + ``` + + + - `start` - Ðачать новый ÑÐµÐ°Ð½Ñ Ð¸ обозначить его как текущий + + *ЗапроÑ:* + + ``` + { + "type": "numass.run", + "action": "start", + "path: "", + "meta": { + + } + } + ``` + + *Ответ:* + То же, что и в `get`. + + - `reset` - СброÑить наÑтройки текущего ÑеанÑа + Эквивалентно `get` Ñ Ð¿ÑƒÑтым путем или `default` в качеÑтве пути. Дополнительных аргументов нет + +## Протокол dataforge-envelope ## + +Ð”Ð»Ñ Ð¾Ð±Ð¼ÐµÐ½Ð° ÑообщениÑми может иÑпользоватьÑÑ Ð¿Ñ€Ð¾Ñ‚Ð¾ÐºÐ¾Ð» dataforge-envelope (типа message). Ð’ Ñтой вариации протокола запрещено автоматичеÑкое определение длинны метаданных и данных, в качеÑтве метаданных иÑпользуетÑÑ JSON в кодировке UTF-8. + +ИÑпользуютÑÑ Ñледующие атрибуты конверта: + + version = 1; + type = 33; + metaType = 1; + metaEncoding = 0; + + Пакет Ñ `dataType = 0xffffffff` ÑчитаетÑÑ Ñ‚ÐµÑ€Ð¼Ð¸Ð½Ð¸Ñ€ÑƒÑŽÑ‰Ð¸Ð¼ пакетом, закрывающим Ñоединение. + +Порт Ñервера Ð´Ð»Ñ Ñ€Ð°Ð±Ð¾Ñ‚Ñ‹ по Ñтому протоколу по умолчанию `8335`. + +## Протокол Http ## + +Пока не реализован. + +> Written with [StackEdit](https://stackedit.io/). \ No newline at end of file diff --git a/numass-core/numass-data-api/build.gradle.kts b/numass-core/numass-data-api/build.gradle.kts new file mode 100644 index 00000000..2963d76f --- /dev/null +++ b/numass-core/numass-data-api/build.gradle.kts @@ -0,0 +1,19 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + idea + kotlin("jvm") +} + + +repositories { + mavenCentral() +} + +dependencies { + api(project(":dataforge-core")) +} + +tasks.withType { + kotlinOptions.jvmTarget = "1.8" +} \ No newline at end of file diff --git a/numass-core/numass-data-api/src/main/kotlin/inr/numass/data/api/MetaBlock.kt b/numass-core/numass-data-api/src/main/kotlin/inr/numass/data/api/MetaBlock.kt new file mode 100644 index 00000000..ef42390d --- /dev/null +++ b/numass-core/numass-data-api/src/main/kotlin/inr/numass/data/api/MetaBlock.kt @@ -0,0 +1,39 @@ +package inr.numass.data.api + +import java.time.Duration +import java.time.Instant +import java.util.stream.Stream + +interface ParentBlock : NumassBlock { + val blocks: List + + /** + * If true, the sub-blocks a considered to be isSequential, if not, the sub-blocks are parallel + */ + val isSequential: Boolean + get() = true +} + +/** + * A block constructed from a set of other blocks. Internal blocks are not necessary subsequent. Blocks are automatically sorted. + * Created by darksnake on 16.07.2017. + */ +class MetaBlock(override val blocks: List) : ParentBlock { + + override val startTime: Instant + get() = blocks.first().startTime + + override val length: Duration + get() = Duration.ofNanos(blocks.stream().mapToLong { block -> block.length.toNanos() }.sum()) + + /** + * A stream of events, sorted by block time but not sorted by event time + */ + override val events: Stream + get() = blocks.sortedBy { it.startTime }.stream().flatMap { it.events } + + override val frames: Stream + get() = blocks.sortedBy { it.startTime }.stream().flatMap { it.frames } + + +} diff --git a/numass-core/numass-data-api/src/main/kotlin/inr/numass/data/api/NumassBlock.kt b/numass-core/numass-data-api/src/main/kotlin/inr/numass/data/api/NumassBlock.kt new file mode 100644 index 00000000..79e62d9b --- /dev/null +++ b/numass-core/numass-data-api/src/main/kotlin/inr/numass/data/api/NumassBlock.kt @@ -0,0 +1,116 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 inr.numass.data.api + +import java.io.Serializable +import java.time.Duration +import java.time.Instant +import java.util.stream.Stream + +open class OrphanNumassEvent(val amplitude: Short, val timeOffset: Long) : Serializable, Comparable { + operator fun component1() = amplitude + operator fun component2() = timeOffset + + override fun compareTo(other: OrphanNumassEvent): Int { + return this.timeOffset.compareTo(other.timeOffset) + } + + override fun toString(): String { + return "[$amplitude, $timeOffset]" + } + + +} + +/** + * A single numass event with given amplitude and time. + * + * @author Darksnake + * @property amp the amplitude of the event + * @property timeOffset time in nanoseconds relative to block start + * @property owner an owner block for this event + * + */ +class NumassEvent(amplitude: Short, timeOffset: Long, val owner: NumassBlock) : OrphanNumassEvent(amplitude, timeOffset), Serializable { + + val channel: Int + get() = owner.channel + + val time: Instant + get() = owner.startTime.plusNanos(timeOffset) + +} + + +/** + * A single continuous measurement block. The block can contain both isolated events and signal frames + * + * + * Created by darksnake on 06-Jul-17. + */ +interface NumassBlock { + + /** + * The absolute start time of the block + */ + val startTime: Instant + + /** + * The length of the block + */ + val length: Duration + + /** + * Stream of isolated events. Could be empty + */ + val events: Stream + + /** + * Stream of frames. Could be empty + */ + val frames: Stream + + val channel: Int get() = 0 +} + +fun OrphanNumassEvent.adopt(parent: NumassBlock): NumassEvent { + return NumassEvent(this.amplitude, this.timeOffset, parent) +} + +/** + * A simple in-memory implementation of block of events. No frames are allowed + * Created by darksnake on 08.07.2017. + */ +class SimpleBlock( + override val startTime: Instant, + override val length: Duration, + rawEvents: Iterable +) : NumassBlock, Serializable { + + private val eventList by lazy { rawEvents.map { it.adopt(this) } } + + override val frames: Stream get() = Stream.empty() + + override val events: Stream + get() = eventList.stream() + + companion object { + suspend fun produce(startTime: Instant, length: Duration, producer: suspend () -> Iterable): SimpleBlock { + return SimpleBlock(startTime, length, producer()) + } + } +} \ No newline at end of file diff --git a/numass-core/numass-data-api/src/main/kotlin/inr/numass/data/api/NumassFrame.kt b/numass-core/numass-data-api/src/main/kotlin/inr/numass/data/api/NumassFrame.kt new file mode 100644 index 00000000..db031452 --- /dev/null +++ b/numass-core/numass-data-api/src/main/kotlin/inr/numass/data/api/NumassFrame.kt @@ -0,0 +1,28 @@ +package inr.numass.data.api + +import java.nio.ShortBuffer +import java.time.Duration +import java.time.Instant + +/** + * The continuous frame of digital detector data + * Created by darksnake on 06-Jul-17. + */ +class NumassFrame( + /** + * The absolute start time of the frame + */ + val time: Instant, + /** + * The time interval per tick + */ + val tickSize: Duration, + /** + * The buffered signal shape in ticks + */ + val signal: ShortBuffer +) { + + val length: Duration + get() = tickSize.multipliedBy(signal.capacity().toLong()) +} diff --git a/numass-core/numass-data-api/src/main/kotlin/inr/numass/data/api/NumassPoint.kt b/numass-core/numass-data-api/src/main/kotlin/inr/numass/data/api/NumassPoint.kt new file mode 100644 index 00000000..a9021ace --- /dev/null +++ b/numass-core/numass-data-api/src/main/kotlin/inr/numass/data/api/NumassPoint.kt @@ -0,0 +1,135 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 inr.numass.data.api + +import hep.dataforge.meta.Metoid +import hep.dataforge.providers.Provider +import hep.dataforge.providers.Provides +import java.time.Duration +import java.time.Instant +import java.util.stream.Stream + +/** + * Created by darksnake on 06-Jul-17. + */ +interface NumassPoint : Metoid, ParentBlock, Provider { + + + override val blocks: List + + /** + * Provides block with given number (starting with 0) + */ + @Provides(NUMASS_BLOCK_TARGET) + operator fun get(index: Int): NumassBlock? { + return blocks[index] + } + + /** + * Provides all blocks in given channel + */ + @Provides(NUMASS_CHANNEL_TARGET) + fun channel(index: Int): NumassBlock? { + return channels[index] + } + + /** + * Distinct map of channel number to corresponding grouping block + */ + val channels: Map + get() = blocks.toList().groupBy { it.channel }.mapValues { entry -> + if (entry.value.size == 1) { + entry.value.first() + } else { + MetaBlock(entry.value) + } + } + + + /** + * Get the voltage setting for the point + * + * @return + */ + val voltage: Double + get() = meta.getDouble(HV_KEY, 0.0) + + /** + * Get the index for this point in the set + * @return + */ + val index: Int + get() = meta.getInt(INDEX_KEY, -1) + + /** + * Get the first block if it exists. Throw runtime exception otherwise. + * + * @return + */ + val firstBlock: NumassBlock + get() = blocks.firstOrNull() ?: throw RuntimeException("The point is empty") + + /** + * Get the starting time from meta or from first block + * + * @return + */ + override val startTime: Instant + get() = meta.optValue(START_TIME_KEY).map { it.time }.orElseGet { firstBlock.startTime } + + /** + * Get the length key of meta or calculate length as a sum of block lengths. The latter could be a bit slow + * + * @return + */ + override val length: Duration + get() = Duration.ofNanos(blocks.stream().filter { it.channel == 0 }.mapToLong { it -> it.length.toNanos() }.sum()) + + /** + * Get all events it all blocks as a single sequence + * + * + * Some performance analysis of different stream concatenation approaches is given here: https://www.techempower.com/blog/2016/10/19/efficient-multiple-stream-concatenation-in-java/ + * + * + * @return + */ + override val events: Stream + get() = blocks.stream().flatMap { it.events } + + /** + * Get all frames in all blocks as a single sequence + * + * @return + */ + override val frames: Stream + get() = blocks.stream().flatMap { it.frames } + + + override val isSequential: Boolean + get() = channels.size == 1 + + companion object { + const val NUMASS_BLOCK_TARGET = "block" + const val NUMASS_CHANNEL_TARGET = "channel" + + const val START_TIME_KEY = "start" + const val LENGTH_KEY = "length" + const val HV_KEY = "voltage" + const val INDEX_KEY = "index" + } +} diff --git a/numass-core/numass-data-api/src/main/kotlin/inr/numass/data/api/NumassSet.kt b/numass-core/numass-data-api/src/main/kotlin/inr/numass/data/api/NumassSet.kt new file mode 100644 index 00000000..05a5ddde --- /dev/null +++ b/numass-core/numass-data-api/src/main/kotlin/inr/numass/data/api/NumassSet.kt @@ -0,0 +1,88 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package inr.numass.data.api + +import hep.dataforge.Named +import hep.dataforge.meta.Metoid +import hep.dataforge.optional +import hep.dataforge.providers.Provider +import hep.dataforge.providers.Provides +import hep.dataforge.providers.ProvidesNames +import hep.dataforge.tables.Table +import java.time.Instant +import java.util.* + +/** + * A single set of numass points previously called file. + * + * @author [Alexander Nozik](mailto:altavir@gmail.com) + */ +interface NumassSet : Named, Metoid, Iterable, Provider { + + val points: List + + /** + * Get the first point if it exists. Throw runtime exception otherwise. + * + * @return + */ + val firstPoint: NumassPoint + get() = points.firstOrNull() ?: throw RuntimeException("The set is empty") + + /** + * Get the starting time from meta or from first point + * + * @return + */ + val startTime: Instant + get() = meta.optValue(NumassPoint.START_TIME_KEY).map { it.time }.orElseGet { firstPoint.startTime } + + suspend fun getHvData(): Table? + + override fun iterator(): Iterator { + return points.iterator() + } + + /** + * Find first point with given voltage + * + * @param voltage + * @return + */ + fun optPoint(voltage: Double): Optional { + return points.firstOrNull { it -> it.voltage == voltage }.optional + } + + /** + * List all points with given voltage + * + * @param voltage + * @return + */ + fun getPoints(voltage: Double): List { + return points.filter { it -> it.voltage == voltage }.toList() + } + + @Provides(NUMASS_POINT_PROVIDER_KEY) + fun optPoint(voltage: String): Optional { + return optPoint(java.lang.Double.parseDouble(voltage)) + } + + @JvmDefault + override fun getDefaultTarget(): String { + return NUMASS_POINT_PROVIDER_KEY + } + + @ProvidesNames(NUMASS_POINT_PROVIDER_KEY) + fun listPoints(): List { + return points.map { it -> java.lang.Double.toString(it.voltage) } + } + + companion object { + const val DESCRIPTION_KEY = "info" + const val NUMASS_POINT_PROVIDER_KEY = "point" + } +} diff --git a/numass-core/numass-data-api/src/main/kotlin/inr/numass/data/api/SignalProcessor.kt b/numass-core/numass-data-api/src/main/kotlin/inr/numass/data/api/SignalProcessor.kt new file mode 100644 index 00000000..e02ae7ea --- /dev/null +++ b/numass-core/numass-data-api/src/main/kotlin/inr/numass/data/api/SignalProcessor.kt @@ -0,0 +1,11 @@ +package inr.numass.data.api + +import java.util.stream.Stream + +/** + * An ancestor to numass frame analyzers + * Created by darksnake on 07.07.2017. + */ +interface SignalProcessor { + fun process(parent: NumassBlock, frame: NumassFrame): Stream +} diff --git a/numass-core/numass-data-api/src/main/kotlin/inr/numass/data/api/SimpleNumassPoint.kt b/numass-core/numass-data-api/src/main/kotlin/inr/numass/data/api/SimpleNumassPoint.kt new file mode 100644 index 00000000..aee698dd --- /dev/null +++ b/numass-core/numass-data-api/src/main/kotlin/inr/numass/data/api/SimpleNumassPoint.kt @@ -0,0 +1,30 @@ +package inr.numass.data.api + +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaHolder +import hep.dataforge.meta.buildMeta + +/** + * A simple static implementation of NumassPoint + * Created by darksnake on 08.07.2017. + */ +class SimpleNumassPoint(override val blocks: List, meta: Meta, override val isSequential: Boolean = true) : + MetaHolder(meta), NumassPoint { + + init { + if (blocks.isEmpty()) { + throw IllegalArgumentException("No blocks in collection") + } + } + + companion object { + fun build(blocks: Collection, voltage: Double? = null, index: Int? = null): SimpleNumassPoint { + val meta = buildMeta("point") { + NumassPoint.HV_KEY to voltage + NumassPoint.INDEX_KEY to index + } + return SimpleNumassPoint(blocks.sortedBy { it.startTime }, meta.build()) + } + } + +} diff --git a/numass-core/numass-data-proto/build.gradle.kts b/numass-core/numass-data-proto/build.gradle.kts new file mode 100644 index 00000000..03f95fde --- /dev/null +++ b/numass-core/numass-data-proto/build.gradle.kts @@ -0,0 +1,46 @@ +import com.google.protobuf.gradle.protobuf +import com.google.protobuf.gradle.protoc +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + idea + kotlin("jvm") + id("com.google.protobuf") version "0.8.8" +} + + +repositories { + mavenCentral() +} + +dependencies { + implementation("com.google.protobuf:protobuf-java:3.6.1") + api(project(":numass-core:numass-data-api")) + api(project(":dataforge-storage")) +} + +tasks.withType { + kotlinOptions.jvmTarget = "1.8" + dependsOn(":numass-core:numass-data-proto:generateProto") +} + +//sourceSets { +// create("proto") { +// proto { +// srcDir("src/main/proto") +// } +// } +//} + +protobuf { + // Configure the protoc executable + protoc { + // Download from repositories + artifact = "com.google.protobuf:protoc:3.6.1" + } + generatedFilesBaseDir = "$projectDir/gen" +} + +//tasks.getByName("clean").doLast{ +// delete(protobuf.protobuf.generatedFilesBaseDir) +//} \ No newline at end of file diff --git a/numass-core/numass-data-proto/gen/main/java/inr/numass/data/NumassProto.java b/numass-core/numass-data-proto/gen/main/java/inr/numass/data/NumassProto.java new file mode 100644 index 00000000..8919b9e3 --- /dev/null +++ b/numass-core/numass-data-proto/gen/main/java/inr/numass/data/NumassProto.java @@ -0,0 +1,4868 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: inr/numas/numass-proto.proto + +package inr.numass.data; + +public final class NumassProto { + private NumassProto() {} + public static void registerAllExtensions( + com.google.protobuf.ExtensionRegistryLite registry) { + } + + public static void registerAllExtensions( + com.google.protobuf.ExtensionRegistry registry) { + registerAllExtensions( + (com.google.protobuf.ExtensionRegistryLite) registry); + } + public interface PointOrBuilder extends + // @@protoc_insertion_point(interface_extends:inr.numass.data.Point) + com.google.protobuf.MessageOrBuilder { + + /** + *

+     * Array of measuring channels
+     * 
+ * + * repeated .inr.numass.data.Point.Channel channels = 1; + */ + java.util.List + getChannelsList(); + /** + *
+     * Array of measuring channels
+     * 
+ * + * repeated .inr.numass.data.Point.Channel channels = 1; + */ + inr.numass.data.NumassProto.Point.Channel getChannels(int index); + /** + *
+     * Array of measuring channels
+     * 
+ * + * repeated .inr.numass.data.Point.Channel channels = 1; + */ + int getChannelsCount(); + /** + *
+     * Array of measuring channels
+     * 
+ * + * repeated .inr.numass.data.Point.Channel channels = 1; + */ + java.util.List + getChannelsOrBuilderList(); + /** + *
+     * Array of measuring channels
+     * 
+ * + * repeated .inr.numass.data.Point.Channel channels = 1; + */ + inr.numass.data.NumassProto.Point.ChannelOrBuilder getChannelsOrBuilder( + int index); + } + /** + * Protobuf type {@code inr.numass.data.Point} + */ + public static final class Point extends + com.google.protobuf.GeneratedMessageV3 implements + // @@protoc_insertion_point(message_implements:inr.numass.data.Point) + PointOrBuilder { + private static final long serialVersionUID = 0L; + // Use Point.newBuilder() to construct. + private Point(com.google.protobuf.GeneratedMessageV3.Builder builder) { + super(builder); + } + private Point() { + channels_ = java.util.Collections.emptyList(); + } + + @java.lang.Override + public final com.google.protobuf.UnknownFieldSet + getUnknownFields() { + return this.unknownFields; + } + private Point( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + this(); + if (extensionRegistry == null) { + throw new java.lang.NullPointerException(); + } + int mutable_bitField0_ = 0; + com.google.protobuf.UnknownFieldSet.Builder unknownFields = + com.google.protobuf.UnknownFieldSet.newBuilder(); + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + case 10: { + if (!((mutable_bitField0_ & 0x00000001) == 0x00000001)) { + channels_ = new java.util.ArrayList(); + mutable_bitField0_ |= 0x00000001; + } + channels_.add( + input.readMessage(inr.numass.data.NumassProto.Point.Channel.parser(), extensionRegistry)); + break; + } + default: { + if (!parseUnknownFieldProto3( + input, unknownFields, extensionRegistry, tag)) { + done = true; + } + break; + } + } + } + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(this); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException( + e).setUnfinishedMessage(this); + } finally { + if (((mutable_bitField0_ & 0x00000001) == 0x00000001)) { + channels_ = java.util.Collections.unmodifiableList(channels_); + } + this.unknownFields = unknownFields.build(); + makeExtensionsImmutable(); + } + } + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return inr.numass.data.NumassProto.internal_static_inr_numass_data_Point_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable + internalGetFieldAccessorTable() { + return inr.numass.data.NumassProto.internal_static_inr_numass_data_Point_fieldAccessorTable + .ensureFieldAccessorsInitialized( + inr.numass.data.NumassProto.Point.class, inr.numass.data.NumassProto.Point.Builder.class); + } + + public interface ChannelOrBuilder extends + // @@protoc_insertion_point(interface_extends:inr.numass.data.Point.Channel) + com.google.protobuf.MessageOrBuilder { + + /** + *
+       * The number of measuring channel
+       * 
+ * + * uint64 id = 1; + */ + long getId(); + + /** + *
+       * Blocks
+       * 
+ * + * repeated .inr.numass.data.Point.Channel.Block blocks = 2; + */ + java.util.List + getBlocksList(); + /** + *
+       * Blocks
+       * 
+ * + * repeated .inr.numass.data.Point.Channel.Block blocks = 2; + */ + inr.numass.data.NumassProto.Point.Channel.Block getBlocks(int index); + /** + *
+       * Blocks
+       * 
+ * + * repeated .inr.numass.data.Point.Channel.Block blocks = 2; + */ + int getBlocksCount(); + /** + *
+       * Blocks
+       * 
+ * + * repeated .inr.numass.data.Point.Channel.Block blocks = 2; + */ + java.util.List + getBlocksOrBuilderList(); + /** + *
+       * Blocks
+       * 
+ * + * repeated .inr.numass.data.Point.Channel.Block blocks = 2; + */ + inr.numass.data.NumassProto.Point.Channel.BlockOrBuilder getBlocksOrBuilder( + int index); + } + /** + *
+     * A single channel for multichannel detector readout
+     * 
+ * + * Protobuf type {@code inr.numass.data.Point.Channel} + */ + public static final class Channel extends + com.google.protobuf.GeneratedMessageV3 implements + // @@protoc_insertion_point(message_implements:inr.numass.data.Point.Channel) + ChannelOrBuilder { + private static final long serialVersionUID = 0L; + // Use Channel.newBuilder() to construct. + private Channel(com.google.protobuf.GeneratedMessageV3.Builder builder) { + super(builder); + } + private Channel() { + id_ = 0L; + blocks_ = java.util.Collections.emptyList(); + } + + @java.lang.Override + public final com.google.protobuf.UnknownFieldSet + getUnknownFields() { + return this.unknownFields; + } + private Channel( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + this(); + if (extensionRegistry == null) { + throw new java.lang.NullPointerException(); + } + int mutable_bitField0_ = 0; + com.google.protobuf.UnknownFieldSet.Builder unknownFields = + com.google.protobuf.UnknownFieldSet.newBuilder(); + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + case 8: { + + id_ = input.readUInt64(); + break; + } + case 18: { + if (!((mutable_bitField0_ & 0x00000002) == 0x00000002)) { + blocks_ = new java.util.ArrayList(); + mutable_bitField0_ |= 0x00000002; + } + blocks_.add( + input.readMessage(inr.numass.data.NumassProto.Point.Channel.Block.parser(), extensionRegistry)); + break; + } + default: { + if (!parseUnknownFieldProto3( + input, unknownFields, extensionRegistry, tag)) { + done = true; + } + break; + } + } + } + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(this); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException( + e).setUnfinishedMessage(this); + } finally { + if (((mutable_bitField0_ & 0x00000002) == 0x00000002)) { + blocks_ = java.util.Collections.unmodifiableList(blocks_); + } + this.unknownFields = unknownFields.build(); + makeExtensionsImmutable(); + } + } + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return inr.numass.data.NumassProto.internal_static_inr_numass_data_Point_Channel_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable + internalGetFieldAccessorTable() { + return inr.numass.data.NumassProto.internal_static_inr_numass_data_Point_Channel_fieldAccessorTable + .ensureFieldAccessorsInitialized( + inr.numass.data.NumassProto.Point.Channel.class, inr.numass.data.NumassProto.Point.Channel.Builder.class); + } + + public interface BlockOrBuilder extends + // @@protoc_insertion_point(interface_extends:inr.numass.data.Point.Channel.Block) + com.google.protobuf.MessageOrBuilder { + + /** + *
+         * Block start in epoch nanos
+         * 
+ * + * uint64 time = 1; + */ + long getTime(); + + /** + *
+         * Frames array
+         * 
+ * + * repeated .inr.numass.data.Point.Channel.Block.Frame frames = 2; + */ + java.util.List + getFramesList(); + /** + *
+         * Frames array
+         * 
+ * + * repeated .inr.numass.data.Point.Channel.Block.Frame frames = 2; + */ + inr.numass.data.NumassProto.Point.Channel.Block.Frame getFrames(int index); + /** + *
+         * Frames array
+         * 
+ * + * repeated .inr.numass.data.Point.Channel.Block.Frame frames = 2; + */ + int getFramesCount(); + /** + *
+         * Frames array
+         * 
+ * + * repeated .inr.numass.data.Point.Channel.Block.Frame frames = 2; + */ + java.util.List + getFramesOrBuilderList(); + /** + *
+         * Frames array
+         * 
+ * + * repeated .inr.numass.data.Point.Channel.Block.Frame frames = 2; + */ + inr.numass.data.NumassProto.Point.Channel.Block.FrameOrBuilder getFramesOrBuilder( + int index); + + /** + *
+         * Events array
+         * 
+ * + * .inr.numass.data.Point.Channel.Block.Events events = 3; + */ + boolean hasEvents(); + /** + *
+         * Events array
+         * 
+ * + * .inr.numass.data.Point.Channel.Block.Events events = 3; + */ + inr.numass.data.NumassProto.Point.Channel.Block.Events getEvents(); + /** + *
+         * Events array
+         * 
+ * + * .inr.numass.data.Point.Channel.Block.Events events = 3; + */ + inr.numass.data.NumassProto.Point.Channel.Block.EventsOrBuilder getEventsOrBuilder(); + + /** + *
+         * block size in nanos. If missing, take from meta.
+         * 
+ * + * uint64 length = 4; + */ + long getLength(); + + /** + *
+         * tick size in nanos. Obsolete, to be removed
+         * 
+ * + * uint64 bin_size = 5; + */ + long getBinSize(); + } + /** + *
+       *A continuous measurement block
+       * 
+ * + * Protobuf type {@code inr.numass.data.Point.Channel.Block} + */ + public static final class Block extends + com.google.protobuf.GeneratedMessageV3 implements + // @@protoc_insertion_point(message_implements:inr.numass.data.Point.Channel.Block) + BlockOrBuilder { + private static final long serialVersionUID = 0L; + // Use Block.newBuilder() to construct. + private Block(com.google.protobuf.GeneratedMessageV3.Builder builder) { + super(builder); + } + private Block() { + time_ = 0L; + frames_ = java.util.Collections.emptyList(); + length_ = 0L; + binSize_ = 0L; + } + + @java.lang.Override + public final com.google.protobuf.UnknownFieldSet + getUnknownFields() { + return this.unknownFields; + } + private Block( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + this(); + if (extensionRegistry == null) { + throw new java.lang.NullPointerException(); + } + int mutable_bitField0_ = 0; + com.google.protobuf.UnknownFieldSet.Builder unknownFields = + com.google.protobuf.UnknownFieldSet.newBuilder(); + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + case 8: { + + time_ = input.readUInt64(); + break; + } + case 18: { + if (!((mutable_bitField0_ & 0x00000002) == 0x00000002)) { + frames_ = new java.util.ArrayList(); + mutable_bitField0_ |= 0x00000002; + } + frames_.add( + input.readMessage(inr.numass.data.NumassProto.Point.Channel.Block.Frame.parser(), extensionRegistry)); + break; + } + case 26: { + inr.numass.data.NumassProto.Point.Channel.Block.Events.Builder subBuilder = null; + if (events_ != null) { + subBuilder = events_.toBuilder(); + } + events_ = input.readMessage(inr.numass.data.NumassProto.Point.Channel.Block.Events.parser(), extensionRegistry); + if (subBuilder != null) { + subBuilder.mergeFrom(events_); + events_ = subBuilder.buildPartial(); + } + + break; + } + case 32: { + + length_ = input.readUInt64(); + break; + } + case 40: { + + binSize_ = input.readUInt64(); + break; + } + default: { + if (!parseUnknownFieldProto3( + input, unknownFields, extensionRegistry, tag)) { + done = true; + } + break; + } + } + } + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(this); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException( + e).setUnfinishedMessage(this); + } finally { + if (((mutable_bitField0_ & 0x00000002) == 0x00000002)) { + frames_ = java.util.Collections.unmodifiableList(frames_); + } + this.unknownFields = unknownFields.build(); + makeExtensionsImmutable(); + } + } + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return inr.numass.data.NumassProto.internal_static_inr_numass_data_Point_Channel_Block_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable + internalGetFieldAccessorTable() { + return inr.numass.data.NumassProto.internal_static_inr_numass_data_Point_Channel_Block_fieldAccessorTable + .ensureFieldAccessorsInitialized( + inr.numass.data.NumassProto.Point.Channel.Block.class, inr.numass.data.NumassProto.Point.Channel.Block.Builder.class); + } + + public interface FrameOrBuilder extends + // @@protoc_insertion_point(interface_extends:inr.numass.data.Point.Channel.Block.Frame) + com.google.protobuf.MessageOrBuilder { + + /** + *
+           * Time in nanos from the beginning of the block
+           * 
+ * + * uint64 time = 1; + */ + long getTime(); + + /** + *
+           * Frame data as an array of int16 mesured in arbitrary channels
+           * 
+ * + * bytes data = 2; + */ + com.google.protobuf.ByteString getData(); + } + /** + *
+         * Raw data frame
+         * 
+ * + * Protobuf type {@code inr.numass.data.Point.Channel.Block.Frame} + */ + public static final class Frame extends + com.google.protobuf.GeneratedMessageV3 implements + // @@protoc_insertion_point(message_implements:inr.numass.data.Point.Channel.Block.Frame) + FrameOrBuilder { + private static final long serialVersionUID = 0L; + // Use Frame.newBuilder() to construct. + private Frame(com.google.protobuf.GeneratedMessageV3.Builder builder) { + super(builder); + } + private Frame() { + time_ = 0L; + data_ = com.google.protobuf.ByteString.EMPTY; + } + + @java.lang.Override + public final com.google.protobuf.UnknownFieldSet + getUnknownFields() { + return this.unknownFields; + } + private Frame( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + this(); + if (extensionRegistry == null) { + throw new java.lang.NullPointerException(); + } + int mutable_bitField0_ = 0; + com.google.protobuf.UnknownFieldSet.Builder unknownFields = + com.google.protobuf.UnknownFieldSet.newBuilder(); + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + case 8: { + + time_ = input.readUInt64(); + break; + } + case 18: { + + data_ = input.readBytes(); + break; + } + default: { + if (!parseUnknownFieldProto3( + input, unknownFields, extensionRegistry, tag)) { + done = true; + } + break; + } + } + } + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(this); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException( + e).setUnfinishedMessage(this); + } finally { + this.unknownFields = unknownFields.build(); + makeExtensionsImmutable(); + } + } + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return inr.numass.data.NumassProto.internal_static_inr_numass_data_Point_Channel_Block_Frame_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable + internalGetFieldAccessorTable() { + return inr.numass.data.NumassProto.internal_static_inr_numass_data_Point_Channel_Block_Frame_fieldAccessorTable + .ensureFieldAccessorsInitialized( + inr.numass.data.NumassProto.Point.Channel.Block.Frame.class, inr.numass.data.NumassProto.Point.Channel.Block.Frame.Builder.class); + } + + public static final int TIME_FIELD_NUMBER = 1; + private long time_; + /** + *
+           * Time in nanos from the beginning of the block
+           * 
+ * + * uint64 time = 1; + */ + public long getTime() { + return time_; + } + + public static final int DATA_FIELD_NUMBER = 2; + private com.google.protobuf.ByteString data_; + /** + *
+           * Frame data as an array of int16 mesured in arbitrary channels
+           * 
+ * + * bytes data = 2; + */ + public com.google.protobuf.ByteString getData() { + return data_; + } + + private byte memoizedIsInitialized = -1; + @java.lang.Override + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized == 1) return true; + if (isInitialized == 0) return false; + + memoizedIsInitialized = 1; + return true; + } + + @java.lang.Override + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + if (time_ != 0L) { + output.writeUInt64(1, time_); + } + if (!data_.isEmpty()) { + output.writeBytes(2, data_); + } + unknownFields.writeTo(output); + } + + @java.lang.Override + public int getSerializedSize() { + int size = memoizedSize; + if (size != -1) return size; + + size = 0; + if (time_ != 0L) { + size += com.google.protobuf.CodedOutputStream + .computeUInt64Size(1, time_); + } + if (!data_.isEmpty()) { + size += com.google.protobuf.CodedOutputStream + .computeBytesSize(2, data_); + } + size += unknownFields.getSerializedSize(); + memoizedSize = size; + return size; + } + + @java.lang.Override + public boolean equals(final java.lang.Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof inr.numass.data.NumassProto.Point.Channel.Block.Frame)) { + return super.equals(obj); + } + inr.numass.data.NumassProto.Point.Channel.Block.Frame other = (inr.numass.data.NumassProto.Point.Channel.Block.Frame) obj; + + boolean result = true; + result = result && (getTime() + == other.getTime()); + result = result && getData() + .equals(other.getData()); + result = result && unknownFields.equals(other.unknownFields); + return result; + } + + @java.lang.Override + public int hashCode() { + if (memoizedHashCode != 0) { + return memoizedHashCode; + } + int hash = 41; + hash = (19 * hash) + getDescriptor().hashCode(); + hash = (37 * hash) + TIME_FIELD_NUMBER; + hash = (53 * hash) + com.google.protobuf.Internal.hashLong( + getTime()); + hash = (37 * hash) + DATA_FIELD_NUMBER; + hash = (53 * hash) + getData().hashCode(); + hash = (29 * hash) + unknownFields.hashCode(); + memoizedHashCode = hash; + return hash; + } + + public static inr.numass.data.NumassProto.Point.Channel.Block.Frame parseFrom( + java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static inr.numass.data.NumassProto.Point.Channel.Block.Frame parseFrom( + java.nio.ByteBuffer data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static inr.numass.data.NumassProto.Point.Channel.Block.Frame parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static inr.numass.data.NumassProto.Point.Channel.Block.Frame parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static inr.numass.data.NumassProto.Point.Channel.Block.Frame parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static inr.numass.data.NumassProto.Point.Channel.Block.Frame parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static inr.numass.data.NumassProto.Point.Channel.Block.Frame parseFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseWithIOException(PARSER, input); + } + public static inr.numass.data.NumassProto.Point.Channel.Block.Frame parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseWithIOException(PARSER, input, extensionRegistry); + } + public static inr.numass.data.NumassProto.Point.Channel.Block.Frame parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseDelimitedWithIOException(PARSER, input); + } + public static inr.numass.data.NumassProto.Point.Channel.Block.Frame parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseDelimitedWithIOException(PARSER, input, extensionRegistry); + } + public static inr.numass.data.NumassProto.Point.Channel.Block.Frame parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseWithIOException(PARSER, input); + } + public static inr.numass.data.NumassProto.Point.Channel.Block.Frame parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseWithIOException(PARSER, input, extensionRegistry); + } + + @java.lang.Override + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder() { + return DEFAULT_INSTANCE.toBuilder(); + } + public static Builder newBuilder(inr.numass.data.NumassProto.Point.Channel.Block.Frame prototype) { + return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); + } + @java.lang.Override + public Builder toBuilder() { + return this == DEFAULT_INSTANCE + ? new Builder() : new Builder().mergeFrom(this); + } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** + *
+           * Raw data frame
+           * 
+ * + * Protobuf type {@code inr.numass.data.Point.Channel.Block.Frame} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessageV3.Builder implements + // @@protoc_insertion_point(builder_implements:inr.numass.data.Point.Channel.Block.Frame) + inr.numass.data.NumassProto.Point.Channel.Block.FrameOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return inr.numass.data.NumassProto.internal_static_inr_numass_data_Point_Channel_Block_Frame_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable + internalGetFieldAccessorTable() { + return inr.numass.data.NumassProto.internal_static_inr_numass_data_Point_Channel_Block_Frame_fieldAccessorTable + .ensureFieldAccessorsInitialized( + inr.numass.data.NumassProto.Point.Channel.Block.Frame.class, inr.numass.data.NumassProto.Point.Channel.Block.Frame.Builder.class); + } + + // Construct using inr.numass.data.NumassProto.Point.Channel.Block.Frame.newBuilder() + private Builder() { + maybeForceBuilderInitialization(); + } + + private Builder( + com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { + super(parent); + maybeForceBuilderInitialization(); + } + private void maybeForceBuilderInitialization() { + if (com.google.protobuf.GeneratedMessageV3 + .alwaysUseFieldBuilders) { + } + } + @java.lang.Override + public Builder clear() { + super.clear(); + time_ = 0L; + + data_ = com.google.protobuf.ByteString.EMPTY; + + return this; + } + + @java.lang.Override + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return inr.numass.data.NumassProto.internal_static_inr_numass_data_Point_Channel_Block_Frame_descriptor; + } + + @java.lang.Override + public inr.numass.data.NumassProto.Point.Channel.Block.Frame getDefaultInstanceForType() { + return inr.numass.data.NumassProto.Point.Channel.Block.Frame.getDefaultInstance(); + } + + @java.lang.Override + public inr.numass.data.NumassProto.Point.Channel.Block.Frame build() { + inr.numass.data.NumassProto.Point.Channel.Block.Frame result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + @java.lang.Override + public inr.numass.data.NumassProto.Point.Channel.Block.Frame buildPartial() { + inr.numass.data.NumassProto.Point.Channel.Block.Frame result = new inr.numass.data.NumassProto.Point.Channel.Block.Frame(this); + result.time_ = time_; + result.data_ = data_; + onBuilt(); + return result; + } + + @java.lang.Override + public Builder clone() { + return (Builder) super.clone(); + } + @java.lang.Override + public Builder setField( + com.google.protobuf.Descriptors.FieldDescriptor field, + java.lang.Object value) { + return (Builder) super.setField(field, value); + } + @java.lang.Override + public Builder clearField( + com.google.protobuf.Descriptors.FieldDescriptor field) { + return (Builder) super.clearField(field); + } + @java.lang.Override + public Builder clearOneof( + com.google.protobuf.Descriptors.OneofDescriptor oneof) { + return (Builder) super.clearOneof(oneof); + } + @java.lang.Override + public Builder setRepeatedField( + com.google.protobuf.Descriptors.FieldDescriptor field, + int index, java.lang.Object value) { + return (Builder) super.setRepeatedField(field, index, value); + } + @java.lang.Override + public Builder addRepeatedField( + com.google.protobuf.Descriptors.FieldDescriptor field, + java.lang.Object value) { + return (Builder) super.addRepeatedField(field, value); + } + @java.lang.Override + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof inr.numass.data.NumassProto.Point.Channel.Block.Frame) { + return mergeFrom((inr.numass.data.NumassProto.Point.Channel.Block.Frame)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(inr.numass.data.NumassProto.Point.Channel.Block.Frame other) { + if (other == inr.numass.data.NumassProto.Point.Channel.Block.Frame.getDefaultInstance()) return this; + if (other.getTime() != 0L) { + setTime(other.getTime()); + } + if (other.getData() != com.google.protobuf.ByteString.EMPTY) { + setData(other.getData()); + } + this.mergeUnknownFields(other.unknownFields); + onChanged(); + return this; + } + + @java.lang.Override + public final boolean isInitialized() { + return true; + } + + @java.lang.Override + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + inr.numass.data.NumassProto.Point.Channel.Block.Frame parsedMessage = null; + try { + parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + parsedMessage = (inr.numass.data.NumassProto.Point.Channel.Block.Frame) e.getUnfinishedMessage(); + throw e.unwrapIOException(); + } finally { + if (parsedMessage != null) { + mergeFrom(parsedMessage); + } + } + return this; + } + + private long time_ ; + /** + *
+             * Time in nanos from the beginning of the block
+             * 
+ * + * uint64 time = 1; + */ + public long getTime() { + return time_; + } + /** + *
+             * Time in nanos from the beginning of the block
+             * 
+ * + * uint64 time = 1; + */ + public Builder setTime(long value) { + + time_ = value; + onChanged(); + return this; + } + /** + *
+             * Time in nanos from the beginning of the block
+             * 
+ * + * uint64 time = 1; + */ + public Builder clearTime() { + + time_ = 0L; + onChanged(); + return this; + } + + private com.google.protobuf.ByteString data_ = com.google.protobuf.ByteString.EMPTY; + /** + *
+             * Frame data as an array of int16 mesured in arbitrary channels
+             * 
+ * + * bytes data = 2; + */ + public com.google.protobuf.ByteString getData() { + return data_; + } + /** + *
+             * Frame data as an array of int16 mesured in arbitrary channels
+             * 
+ * + * bytes data = 2; + */ + public Builder setData(com.google.protobuf.ByteString value) { + if (value == null) { + throw new NullPointerException(); + } + + data_ = value; + onChanged(); + return this; + } + /** + *
+             * Frame data as an array of int16 mesured in arbitrary channels
+             * 
+ * + * bytes data = 2; + */ + public Builder clearData() { + + data_ = getDefaultInstance().getData(); + onChanged(); + return this; + } + @java.lang.Override + public final Builder setUnknownFields( + final com.google.protobuf.UnknownFieldSet unknownFields) { + return super.setUnknownFieldsProto3(unknownFields); + } + + @java.lang.Override + public final Builder mergeUnknownFields( + final com.google.protobuf.UnknownFieldSet unknownFields) { + return super.mergeUnknownFields(unknownFields); + } + + + // @@protoc_insertion_point(builder_scope:inr.numass.data.Point.Channel.Block.Frame) + } + + // @@protoc_insertion_point(class_scope:inr.numass.data.Point.Channel.Block.Frame) + private static final inr.numass.data.NumassProto.Point.Channel.Block.Frame DEFAULT_INSTANCE; + static { + DEFAULT_INSTANCE = new inr.numass.data.NumassProto.Point.Channel.Block.Frame(); + } + + public static inr.numass.data.NumassProto.Point.Channel.Block.Frame getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static final com.google.protobuf.Parser + PARSER = new com.google.protobuf.AbstractParser() { + @java.lang.Override + public Frame parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return new Frame(input, extensionRegistry); + } + }; + + public static com.google.protobuf.Parser parser() { + return PARSER; + } + + @java.lang.Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + + @java.lang.Override + public inr.numass.data.NumassProto.Point.Channel.Block.Frame getDefaultInstanceForType() { + return DEFAULT_INSTANCE; + } + + } + + public interface EventsOrBuilder extends + // @@protoc_insertion_point(interface_extends:inr.numass.data.Point.Channel.Block.Events) + com.google.protobuf.MessageOrBuilder { + + /** + *
+           * Array of time in nanos from the beginning of the block
+           * 
+ * + * repeated uint64 times = 1; + */ + java.util.List getTimesList(); + /** + *
+           * Array of time in nanos from the beginning of the block
+           * 
+ * + * repeated uint64 times = 1; + */ + int getTimesCount(); + /** + *
+           * Array of time in nanos from the beginning of the block
+           * 
+ * + * repeated uint64 times = 1; + */ + long getTimes(int index); + + /** + *
+           * Array of amplitudes of events in channels
+           * 
+ * + * repeated uint64 amplitudes = 2; + */ + java.util.List getAmplitudesList(); + /** + *
+           * Array of amplitudes of events in channels
+           * 
+ * + * repeated uint64 amplitudes = 2; + */ + int getAmplitudesCount(); + /** + *
+           * Array of amplitudes of events in channels
+           * 
+ * + * repeated uint64 amplitudes = 2; + */ + long getAmplitudes(int index); + } + /** + *
+         * Event block obtained directly from  device of from frame analysis
+         * In order to save space, times and amplitudes are in separate arrays.
+         * Amplitude and time with the same index correspond to the same event
+         * 
+ * + * Protobuf type {@code inr.numass.data.Point.Channel.Block.Events} + */ + public static final class Events extends + com.google.protobuf.GeneratedMessageV3 implements + // @@protoc_insertion_point(message_implements:inr.numass.data.Point.Channel.Block.Events) + EventsOrBuilder { + private static final long serialVersionUID = 0L; + // Use Events.newBuilder() to construct. + private Events(com.google.protobuf.GeneratedMessageV3.Builder builder) { + super(builder); + } + private Events() { + times_ = java.util.Collections.emptyList(); + amplitudes_ = java.util.Collections.emptyList(); + } + + @java.lang.Override + public final com.google.protobuf.UnknownFieldSet + getUnknownFields() { + return this.unknownFields; + } + private Events( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + this(); + if (extensionRegistry == null) { + throw new java.lang.NullPointerException(); + } + int mutable_bitField0_ = 0; + com.google.protobuf.UnknownFieldSet.Builder unknownFields = + com.google.protobuf.UnknownFieldSet.newBuilder(); + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + case 8: { + if (!((mutable_bitField0_ & 0x00000001) == 0x00000001)) { + times_ = new java.util.ArrayList(); + mutable_bitField0_ |= 0x00000001; + } + times_.add(input.readUInt64()); + break; + } + case 10: { + int length = input.readRawVarint32(); + int limit = input.pushLimit(length); + if (!((mutable_bitField0_ & 0x00000001) == 0x00000001) && input.getBytesUntilLimit() > 0) { + times_ = new java.util.ArrayList(); + mutable_bitField0_ |= 0x00000001; + } + while (input.getBytesUntilLimit() > 0) { + times_.add(input.readUInt64()); + } + input.popLimit(limit); + break; + } + case 16: { + if (!((mutable_bitField0_ & 0x00000002) == 0x00000002)) { + amplitudes_ = new java.util.ArrayList(); + mutable_bitField0_ |= 0x00000002; + } + amplitudes_.add(input.readUInt64()); + break; + } + case 18: { + int length = input.readRawVarint32(); + int limit = input.pushLimit(length); + if (!((mutable_bitField0_ & 0x00000002) == 0x00000002) && input.getBytesUntilLimit() > 0) { + amplitudes_ = new java.util.ArrayList(); + mutable_bitField0_ |= 0x00000002; + } + while (input.getBytesUntilLimit() > 0) { + amplitudes_.add(input.readUInt64()); + } + input.popLimit(limit); + break; + } + default: { + if (!parseUnknownFieldProto3( + input, unknownFields, extensionRegistry, tag)) { + done = true; + } + break; + } + } + } + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(this); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException( + e).setUnfinishedMessage(this); + } finally { + if (((mutable_bitField0_ & 0x00000001) == 0x00000001)) { + times_ = java.util.Collections.unmodifiableList(times_); + } + if (((mutable_bitField0_ & 0x00000002) == 0x00000002)) { + amplitudes_ = java.util.Collections.unmodifiableList(amplitudes_); + } + this.unknownFields = unknownFields.build(); + makeExtensionsImmutable(); + } + } + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return inr.numass.data.NumassProto.internal_static_inr_numass_data_Point_Channel_Block_Events_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable + internalGetFieldAccessorTable() { + return inr.numass.data.NumassProto.internal_static_inr_numass_data_Point_Channel_Block_Events_fieldAccessorTable + .ensureFieldAccessorsInitialized( + inr.numass.data.NumassProto.Point.Channel.Block.Events.class, inr.numass.data.NumassProto.Point.Channel.Block.Events.Builder.class); + } + + public static final int TIMES_FIELD_NUMBER = 1; + private java.util.List times_; + /** + *
+           * Array of time in nanos from the beginning of the block
+           * 
+ * + * repeated uint64 times = 1; + */ + public java.util.List + getTimesList() { + return times_; + } + /** + *
+           * Array of time in nanos from the beginning of the block
+           * 
+ * + * repeated uint64 times = 1; + */ + public int getTimesCount() { + return times_.size(); + } + /** + *
+           * Array of time in nanos from the beginning of the block
+           * 
+ * + * repeated uint64 times = 1; + */ + public long getTimes(int index) { + return times_.get(index); + } + private int timesMemoizedSerializedSize = -1; + + public static final int AMPLITUDES_FIELD_NUMBER = 2; + private java.util.List amplitudes_; + /** + *
+           * Array of amplitudes of events in channels
+           * 
+ * + * repeated uint64 amplitudes = 2; + */ + public java.util.List + getAmplitudesList() { + return amplitudes_; + } + /** + *
+           * Array of amplitudes of events in channels
+           * 
+ * + * repeated uint64 amplitudes = 2; + */ + public int getAmplitudesCount() { + return amplitudes_.size(); + } + /** + *
+           * Array of amplitudes of events in channels
+           * 
+ * + * repeated uint64 amplitudes = 2; + */ + public long getAmplitudes(int index) { + return amplitudes_.get(index); + } + private int amplitudesMemoizedSerializedSize = -1; + + private byte memoizedIsInitialized = -1; + @java.lang.Override + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized == 1) return true; + if (isInitialized == 0) return false; + + memoizedIsInitialized = 1; + return true; + } + + @java.lang.Override + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + getSerializedSize(); + if (getTimesList().size() > 0) { + output.writeUInt32NoTag(10); + output.writeUInt32NoTag(timesMemoizedSerializedSize); + } + for (int i = 0; i < times_.size(); i++) { + output.writeUInt64NoTag(times_.get(i)); + } + if (getAmplitudesList().size() > 0) { + output.writeUInt32NoTag(18); + output.writeUInt32NoTag(amplitudesMemoizedSerializedSize); + } + for (int i = 0; i < amplitudes_.size(); i++) { + output.writeUInt64NoTag(amplitudes_.get(i)); + } + unknownFields.writeTo(output); + } + + @java.lang.Override + public int getSerializedSize() { + int size = memoizedSize; + if (size != -1) return size; + + size = 0; + { + int dataSize = 0; + for (int i = 0; i < times_.size(); i++) { + dataSize += com.google.protobuf.CodedOutputStream + .computeUInt64SizeNoTag(times_.get(i)); + } + size += dataSize; + if (!getTimesList().isEmpty()) { + size += 1; + size += com.google.protobuf.CodedOutputStream + .computeInt32SizeNoTag(dataSize); + } + timesMemoizedSerializedSize = dataSize; + } + { + int dataSize = 0; + for (int i = 0; i < amplitudes_.size(); i++) { + dataSize += com.google.protobuf.CodedOutputStream + .computeUInt64SizeNoTag(amplitudes_.get(i)); + } + size += dataSize; + if (!getAmplitudesList().isEmpty()) { + size += 1; + size += com.google.protobuf.CodedOutputStream + .computeInt32SizeNoTag(dataSize); + } + amplitudesMemoizedSerializedSize = dataSize; + } + size += unknownFields.getSerializedSize(); + memoizedSize = size; + return size; + } + + @java.lang.Override + public boolean equals(final java.lang.Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof inr.numass.data.NumassProto.Point.Channel.Block.Events)) { + return super.equals(obj); + } + inr.numass.data.NumassProto.Point.Channel.Block.Events other = (inr.numass.data.NumassProto.Point.Channel.Block.Events) obj; + + boolean result = true; + result = result && getTimesList() + .equals(other.getTimesList()); + result = result && getAmplitudesList() + .equals(other.getAmplitudesList()); + result = result && unknownFields.equals(other.unknownFields); + return result; + } + + @java.lang.Override + public int hashCode() { + if (memoizedHashCode != 0) { + return memoizedHashCode; + } + int hash = 41; + hash = (19 * hash) + getDescriptor().hashCode(); + if (getTimesCount() > 0) { + hash = (37 * hash) + TIMES_FIELD_NUMBER; + hash = (53 * hash) + getTimesList().hashCode(); + } + if (getAmplitudesCount() > 0) { + hash = (37 * hash) + AMPLITUDES_FIELD_NUMBER; + hash = (53 * hash) + getAmplitudesList().hashCode(); + } + hash = (29 * hash) + unknownFields.hashCode(); + memoizedHashCode = hash; + return hash; + } + + public static inr.numass.data.NumassProto.Point.Channel.Block.Events parseFrom( + java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static inr.numass.data.NumassProto.Point.Channel.Block.Events parseFrom( + java.nio.ByteBuffer data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static inr.numass.data.NumassProto.Point.Channel.Block.Events parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static inr.numass.data.NumassProto.Point.Channel.Block.Events parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static inr.numass.data.NumassProto.Point.Channel.Block.Events parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static inr.numass.data.NumassProto.Point.Channel.Block.Events parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static inr.numass.data.NumassProto.Point.Channel.Block.Events parseFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseWithIOException(PARSER, input); + } + public static inr.numass.data.NumassProto.Point.Channel.Block.Events parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseWithIOException(PARSER, input, extensionRegistry); + } + public static inr.numass.data.NumassProto.Point.Channel.Block.Events parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseDelimitedWithIOException(PARSER, input); + } + public static inr.numass.data.NumassProto.Point.Channel.Block.Events parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseDelimitedWithIOException(PARSER, input, extensionRegistry); + } + public static inr.numass.data.NumassProto.Point.Channel.Block.Events parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseWithIOException(PARSER, input); + } + public static inr.numass.data.NumassProto.Point.Channel.Block.Events parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseWithIOException(PARSER, input, extensionRegistry); + } + + @java.lang.Override + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder() { + return DEFAULT_INSTANCE.toBuilder(); + } + public static Builder newBuilder(inr.numass.data.NumassProto.Point.Channel.Block.Events prototype) { + return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); + } + @java.lang.Override + public Builder toBuilder() { + return this == DEFAULT_INSTANCE + ? new Builder() : new Builder().mergeFrom(this); + } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** + *
+           * Event block obtained directly from  device of from frame analysis
+           * In order to save space, times and amplitudes are in separate arrays.
+           * Amplitude and time with the same index correspond to the same event
+           * 
+ * + * Protobuf type {@code inr.numass.data.Point.Channel.Block.Events} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessageV3.Builder implements + // @@protoc_insertion_point(builder_implements:inr.numass.data.Point.Channel.Block.Events) + inr.numass.data.NumassProto.Point.Channel.Block.EventsOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return inr.numass.data.NumassProto.internal_static_inr_numass_data_Point_Channel_Block_Events_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable + internalGetFieldAccessorTable() { + return inr.numass.data.NumassProto.internal_static_inr_numass_data_Point_Channel_Block_Events_fieldAccessorTable + .ensureFieldAccessorsInitialized( + inr.numass.data.NumassProto.Point.Channel.Block.Events.class, inr.numass.data.NumassProto.Point.Channel.Block.Events.Builder.class); + } + + // Construct using inr.numass.data.NumassProto.Point.Channel.Block.Events.newBuilder() + private Builder() { + maybeForceBuilderInitialization(); + } + + private Builder( + com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { + super(parent); + maybeForceBuilderInitialization(); + } + private void maybeForceBuilderInitialization() { + if (com.google.protobuf.GeneratedMessageV3 + .alwaysUseFieldBuilders) { + } + } + @java.lang.Override + public Builder clear() { + super.clear(); + times_ = java.util.Collections.emptyList(); + bitField0_ = (bitField0_ & ~0x00000001); + amplitudes_ = java.util.Collections.emptyList(); + bitField0_ = (bitField0_ & ~0x00000002); + return this; + } + + @java.lang.Override + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return inr.numass.data.NumassProto.internal_static_inr_numass_data_Point_Channel_Block_Events_descriptor; + } + + @java.lang.Override + public inr.numass.data.NumassProto.Point.Channel.Block.Events getDefaultInstanceForType() { + return inr.numass.data.NumassProto.Point.Channel.Block.Events.getDefaultInstance(); + } + + @java.lang.Override + public inr.numass.data.NumassProto.Point.Channel.Block.Events build() { + inr.numass.data.NumassProto.Point.Channel.Block.Events result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + @java.lang.Override + public inr.numass.data.NumassProto.Point.Channel.Block.Events buildPartial() { + inr.numass.data.NumassProto.Point.Channel.Block.Events result = new inr.numass.data.NumassProto.Point.Channel.Block.Events(this); + int from_bitField0_ = bitField0_; + if (((bitField0_ & 0x00000001) == 0x00000001)) { + times_ = java.util.Collections.unmodifiableList(times_); + bitField0_ = (bitField0_ & ~0x00000001); + } + result.times_ = times_; + if (((bitField0_ & 0x00000002) == 0x00000002)) { + amplitudes_ = java.util.Collections.unmodifiableList(amplitudes_); + bitField0_ = (bitField0_ & ~0x00000002); + } + result.amplitudes_ = amplitudes_; + onBuilt(); + return result; + } + + @java.lang.Override + public Builder clone() { + return (Builder) super.clone(); + } + @java.lang.Override + public Builder setField( + com.google.protobuf.Descriptors.FieldDescriptor field, + java.lang.Object value) { + return (Builder) super.setField(field, value); + } + @java.lang.Override + public Builder clearField( + com.google.protobuf.Descriptors.FieldDescriptor field) { + return (Builder) super.clearField(field); + } + @java.lang.Override + public Builder clearOneof( + com.google.protobuf.Descriptors.OneofDescriptor oneof) { + return (Builder) super.clearOneof(oneof); + } + @java.lang.Override + public Builder setRepeatedField( + com.google.protobuf.Descriptors.FieldDescriptor field, + int index, java.lang.Object value) { + return (Builder) super.setRepeatedField(field, index, value); + } + @java.lang.Override + public Builder addRepeatedField( + com.google.protobuf.Descriptors.FieldDescriptor field, + java.lang.Object value) { + return (Builder) super.addRepeatedField(field, value); + } + @java.lang.Override + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof inr.numass.data.NumassProto.Point.Channel.Block.Events) { + return mergeFrom((inr.numass.data.NumassProto.Point.Channel.Block.Events)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(inr.numass.data.NumassProto.Point.Channel.Block.Events other) { + if (other == inr.numass.data.NumassProto.Point.Channel.Block.Events.getDefaultInstance()) return this; + if (!other.times_.isEmpty()) { + if (times_.isEmpty()) { + times_ = other.times_; + bitField0_ = (bitField0_ & ~0x00000001); + } else { + ensureTimesIsMutable(); + times_.addAll(other.times_); + } + onChanged(); + } + if (!other.amplitudes_.isEmpty()) { + if (amplitudes_.isEmpty()) { + amplitudes_ = other.amplitudes_; + bitField0_ = (bitField0_ & ~0x00000002); + } else { + ensureAmplitudesIsMutable(); + amplitudes_.addAll(other.amplitudes_); + } + onChanged(); + } + this.mergeUnknownFields(other.unknownFields); + onChanged(); + return this; + } + + @java.lang.Override + public final boolean isInitialized() { + return true; + } + + @java.lang.Override + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + inr.numass.data.NumassProto.Point.Channel.Block.Events parsedMessage = null; + try { + parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + parsedMessage = (inr.numass.data.NumassProto.Point.Channel.Block.Events) e.getUnfinishedMessage(); + throw e.unwrapIOException(); + } finally { + if (parsedMessage != null) { + mergeFrom(parsedMessage); + } + } + return this; + } + private int bitField0_; + + private java.util.List times_ = java.util.Collections.emptyList(); + private void ensureTimesIsMutable() { + if (!((bitField0_ & 0x00000001) == 0x00000001)) { + times_ = new java.util.ArrayList(times_); + bitField0_ |= 0x00000001; + } + } + /** + *
+             * Array of time in nanos from the beginning of the block
+             * 
+ * + * repeated uint64 times = 1; + */ + public java.util.List + getTimesList() { + return java.util.Collections.unmodifiableList(times_); + } + /** + *
+             * Array of time in nanos from the beginning of the block
+             * 
+ * + * repeated uint64 times = 1; + */ + public int getTimesCount() { + return times_.size(); + } + /** + *
+             * Array of time in nanos from the beginning of the block
+             * 
+ * + * repeated uint64 times = 1; + */ + public long getTimes(int index) { + return times_.get(index); + } + /** + *
+             * Array of time in nanos from the beginning of the block
+             * 
+ * + * repeated uint64 times = 1; + */ + public Builder setTimes( + int index, long value) { + ensureTimesIsMutable(); + times_.set(index, value); + onChanged(); + return this; + } + /** + *
+             * Array of time in nanos from the beginning of the block
+             * 
+ * + * repeated uint64 times = 1; + */ + public Builder addTimes(long value) { + ensureTimesIsMutable(); + times_.add(value); + onChanged(); + return this; + } + /** + *
+             * Array of time in nanos from the beginning of the block
+             * 
+ * + * repeated uint64 times = 1; + */ + public Builder addAllTimes( + java.lang.Iterable values) { + ensureTimesIsMutable(); + com.google.protobuf.AbstractMessageLite.Builder.addAll( + values, times_); + onChanged(); + return this; + } + /** + *
+             * Array of time in nanos from the beginning of the block
+             * 
+ * + * repeated uint64 times = 1; + */ + public Builder clearTimes() { + times_ = java.util.Collections.emptyList(); + bitField0_ = (bitField0_ & ~0x00000001); + onChanged(); + return this; + } + + private java.util.List amplitudes_ = java.util.Collections.emptyList(); + private void ensureAmplitudesIsMutable() { + if (!((bitField0_ & 0x00000002) == 0x00000002)) { + amplitudes_ = new java.util.ArrayList(amplitudes_); + bitField0_ |= 0x00000002; + } + } + /** + *
+             * Array of amplitudes of events in channels
+             * 
+ * + * repeated uint64 amplitudes = 2; + */ + public java.util.List + getAmplitudesList() { + return java.util.Collections.unmodifiableList(amplitudes_); + } + /** + *
+             * Array of amplitudes of events in channels
+             * 
+ * + * repeated uint64 amplitudes = 2; + */ + public int getAmplitudesCount() { + return amplitudes_.size(); + } + /** + *
+             * Array of amplitudes of events in channels
+             * 
+ * + * repeated uint64 amplitudes = 2; + */ + public long getAmplitudes(int index) { + return amplitudes_.get(index); + } + /** + *
+             * Array of amplitudes of events in channels
+             * 
+ * + * repeated uint64 amplitudes = 2; + */ + public Builder setAmplitudes( + int index, long value) { + ensureAmplitudesIsMutable(); + amplitudes_.set(index, value); + onChanged(); + return this; + } + /** + *
+             * Array of amplitudes of events in channels
+             * 
+ * + * repeated uint64 amplitudes = 2; + */ + public Builder addAmplitudes(long value) { + ensureAmplitudesIsMutable(); + amplitudes_.add(value); + onChanged(); + return this; + } + /** + *
+             * Array of amplitudes of events in channels
+             * 
+ * + * repeated uint64 amplitudes = 2; + */ + public Builder addAllAmplitudes( + java.lang.Iterable values) { + ensureAmplitudesIsMutable(); + com.google.protobuf.AbstractMessageLite.Builder.addAll( + values, amplitudes_); + onChanged(); + return this; + } + /** + *
+             * Array of amplitudes of events in channels
+             * 
+ * + * repeated uint64 amplitudes = 2; + */ + public Builder clearAmplitudes() { + amplitudes_ = java.util.Collections.emptyList(); + bitField0_ = (bitField0_ & ~0x00000002); + onChanged(); + return this; + } + @java.lang.Override + public final Builder setUnknownFields( + final com.google.protobuf.UnknownFieldSet unknownFields) { + return super.setUnknownFieldsProto3(unknownFields); + } + + @java.lang.Override + public final Builder mergeUnknownFields( + final com.google.protobuf.UnknownFieldSet unknownFields) { + return super.mergeUnknownFields(unknownFields); + } + + + // @@protoc_insertion_point(builder_scope:inr.numass.data.Point.Channel.Block.Events) + } + + // @@protoc_insertion_point(class_scope:inr.numass.data.Point.Channel.Block.Events) + private static final inr.numass.data.NumassProto.Point.Channel.Block.Events DEFAULT_INSTANCE; + static { + DEFAULT_INSTANCE = new inr.numass.data.NumassProto.Point.Channel.Block.Events(); + } + + public static inr.numass.data.NumassProto.Point.Channel.Block.Events getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static final com.google.protobuf.Parser + PARSER = new com.google.protobuf.AbstractParser() { + @java.lang.Override + public Events parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return new Events(input, extensionRegistry); + } + }; + + public static com.google.protobuf.Parser parser() { + return PARSER; + } + + @java.lang.Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + + @java.lang.Override + public inr.numass.data.NumassProto.Point.Channel.Block.Events getDefaultInstanceForType() { + return DEFAULT_INSTANCE; + } + + } + + private int bitField0_; + public static final int TIME_FIELD_NUMBER = 1; + private long time_; + /** + *
+         * Block start in epoch nanos
+         * 
+ * + * uint64 time = 1; + */ + public long getTime() { + return time_; + } + + public static final int FRAMES_FIELD_NUMBER = 2; + private java.util.List frames_; + /** + *
+         * Frames array
+         * 
+ * + * repeated .inr.numass.data.Point.Channel.Block.Frame frames = 2; + */ + public java.util.List getFramesList() { + return frames_; + } + /** + *
+         * Frames array
+         * 
+ * + * repeated .inr.numass.data.Point.Channel.Block.Frame frames = 2; + */ + public java.util.List + getFramesOrBuilderList() { + return frames_; + } + /** + *
+         * Frames array
+         * 
+ * + * repeated .inr.numass.data.Point.Channel.Block.Frame frames = 2; + */ + public int getFramesCount() { + return frames_.size(); + } + /** + *
+         * Frames array
+         * 
+ * + * repeated .inr.numass.data.Point.Channel.Block.Frame frames = 2; + */ + public inr.numass.data.NumassProto.Point.Channel.Block.Frame getFrames(int index) { + return frames_.get(index); + } + /** + *
+         * Frames array
+         * 
+ * + * repeated .inr.numass.data.Point.Channel.Block.Frame frames = 2; + */ + public inr.numass.data.NumassProto.Point.Channel.Block.FrameOrBuilder getFramesOrBuilder( + int index) { + return frames_.get(index); + } + + public static final int EVENTS_FIELD_NUMBER = 3; + private inr.numass.data.NumassProto.Point.Channel.Block.Events events_; + /** + *
+         * Events array
+         * 
+ * + * .inr.numass.data.Point.Channel.Block.Events events = 3; + */ + public boolean hasEvents() { + return events_ != null; + } + /** + *
+         * Events array
+         * 
+ * + * .inr.numass.data.Point.Channel.Block.Events events = 3; + */ + public inr.numass.data.NumassProto.Point.Channel.Block.Events getEvents() { + return events_ == null ? inr.numass.data.NumassProto.Point.Channel.Block.Events.getDefaultInstance() : events_; + } + /** + *
+         * Events array
+         * 
+ * + * .inr.numass.data.Point.Channel.Block.Events events = 3; + */ + public inr.numass.data.NumassProto.Point.Channel.Block.EventsOrBuilder getEventsOrBuilder() { + return getEvents(); + } + + public static final int LENGTH_FIELD_NUMBER = 4; + private long length_; + /** + *
+         * block size in nanos. If missing, take from meta.
+         * 
+ * + * uint64 length = 4; + */ + public long getLength() { + return length_; + } + + public static final int BIN_SIZE_FIELD_NUMBER = 5; + private long binSize_; + /** + *
+         * tick size in nanos. Obsolete, to be removed
+         * 
+ * + * uint64 bin_size = 5; + */ + public long getBinSize() { + return binSize_; + } + + private byte memoizedIsInitialized = -1; + @java.lang.Override + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized == 1) return true; + if (isInitialized == 0) return false; + + memoizedIsInitialized = 1; + return true; + } + + @java.lang.Override + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + if (time_ != 0L) { + output.writeUInt64(1, time_); + } + for (int i = 0; i < frames_.size(); i++) { + output.writeMessage(2, frames_.get(i)); + } + if (events_ != null) { + output.writeMessage(3, getEvents()); + } + if (length_ != 0L) { + output.writeUInt64(4, length_); + } + if (binSize_ != 0L) { + output.writeUInt64(5, binSize_); + } + unknownFields.writeTo(output); + } + + @java.lang.Override + public int getSerializedSize() { + int size = memoizedSize; + if (size != -1) return size; + + size = 0; + if (time_ != 0L) { + size += com.google.protobuf.CodedOutputStream + .computeUInt64Size(1, time_); + } + for (int i = 0; i < frames_.size(); i++) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(2, frames_.get(i)); + } + if (events_ != null) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(3, getEvents()); + } + if (length_ != 0L) { + size += com.google.protobuf.CodedOutputStream + .computeUInt64Size(4, length_); + } + if (binSize_ != 0L) { + size += com.google.protobuf.CodedOutputStream + .computeUInt64Size(5, binSize_); + } + size += unknownFields.getSerializedSize(); + memoizedSize = size; + return size; + } + + @java.lang.Override + public boolean equals(final java.lang.Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof inr.numass.data.NumassProto.Point.Channel.Block)) { + return super.equals(obj); + } + inr.numass.data.NumassProto.Point.Channel.Block other = (inr.numass.data.NumassProto.Point.Channel.Block) obj; + + boolean result = true; + result = result && (getTime() + == other.getTime()); + result = result && getFramesList() + .equals(other.getFramesList()); + result = result && (hasEvents() == other.hasEvents()); + if (hasEvents()) { + result = result && getEvents() + .equals(other.getEvents()); + } + result = result && (getLength() + == other.getLength()); + result = result && (getBinSize() + == other.getBinSize()); + result = result && unknownFields.equals(other.unknownFields); + return result; + } + + @java.lang.Override + public int hashCode() { + if (memoizedHashCode != 0) { + return memoizedHashCode; + } + int hash = 41; + hash = (19 * hash) + getDescriptor().hashCode(); + hash = (37 * hash) + TIME_FIELD_NUMBER; + hash = (53 * hash) + com.google.protobuf.Internal.hashLong( + getTime()); + if (getFramesCount() > 0) { + hash = (37 * hash) + FRAMES_FIELD_NUMBER; + hash = (53 * hash) + getFramesList().hashCode(); + } + if (hasEvents()) { + hash = (37 * hash) + EVENTS_FIELD_NUMBER; + hash = (53 * hash) + getEvents().hashCode(); + } + hash = (37 * hash) + LENGTH_FIELD_NUMBER; + hash = (53 * hash) + com.google.protobuf.Internal.hashLong( + getLength()); + hash = (37 * hash) + BIN_SIZE_FIELD_NUMBER; + hash = (53 * hash) + com.google.protobuf.Internal.hashLong( + getBinSize()); + hash = (29 * hash) + unknownFields.hashCode(); + memoizedHashCode = hash; + return hash; + } + + public static inr.numass.data.NumassProto.Point.Channel.Block parseFrom( + java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static inr.numass.data.NumassProto.Point.Channel.Block parseFrom( + java.nio.ByteBuffer data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static inr.numass.data.NumassProto.Point.Channel.Block parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static inr.numass.data.NumassProto.Point.Channel.Block parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static inr.numass.data.NumassProto.Point.Channel.Block parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static inr.numass.data.NumassProto.Point.Channel.Block parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static inr.numass.data.NumassProto.Point.Channel.Block parseFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseWithIOException(PARSER, input); + } + public static inr.numass.data.NumassProto.Point.Channel.Block parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseWithIOException(PARSER, input, extensionRegistry); + } + public static inr.numass.data.NumassProto.Point.Channel.Block parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseDelimitedWithIOException(PARSER, input); + } + public static inr.numass.data.NumassProto.Point.Channel.Block parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseDelimitedWithIOException(PARSER, input, extensionRegistry); + } + public static inr.numass.data.NumassProto.Point.Channel.Block parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseWithIOException(PARSER, input); + } + public static inr.numass.data.NumassProto.Point.Channel.Block parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseWithIOException(PARSER, input, extensionRegistry); + } + + @java.lang.Override + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder() { + return DEFAULT_INSTANCE.toBuilder(); + } + public static Builder newBuilder(inr.numass.data.NumassProto.Point.Channel.Block prototype) { + return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); + } + @java.lang.Override + public Builder toBuilder() { + return this == DEFAULT_INSTANCE + ? new Builder() : new Builder().mergeFrom(this); + } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** + *
+         *A continuous measurement block
+         * 
+ * + * Protobuf type {@code inr.numass.data.Point.Channel.Block} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessageV3.Builder implements + // @@protoc_insertion_point(builder_implements:inr.numass.data.Point.Channel.Block) + inr.numass.data.NumassProto.Point.Channel.BlockOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return inr.numass.data.NumassProto.internal_static_inr_numass_data_Point_Channel_Block_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable + internalGetFieldAccessorTable() { + return inr.numass.data.NumassProto.internal_static_inr_numass_data_Point_Channel_Block_fieldAccessorTable + .ensureFieldAccessorsInitialized( + inr.numass.data.NumassProto.Point.Channel.Block.class, inr.numass.data.NumassProto.Point.Channel.Block.Builder.class); + } + + // Construct using inr.numass.data.NumassProto.Point.Channel.Block.newBuilder() + private Builder() { + maybeForceBuilderInitialization(); + } + + private Builder( + com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { + super(parent); + maybeForceBuilderInitialization(); + } + private void maybeForceBuilderInitialization() { + if (com.google.protobuf.GeneratedMessageV3 + .alwaysUseFieldBuilders) { + getFramesFieldBuilder(); + } + } + @java.lang.Override + public Builder clear() { + super.clear(); + time_ = 0L; + + if (framesBuilder_ == null) { + frames_ = java.util.Collections.emptyList(); + bitField0_ = (bitField0_ & ~0x00000002); + } else { + framesBuilder_.clear(); + } + if (eventsBuilder_ == null) { + events_ = null; + } else { + events_ = null; + eventsBuilder_ = null; + } + length_ = 0L; + + binSize_ = 0L; + + return this; + } + + @java.lang.Override + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return inr.numass.data.NumassProto.internal_static_inr_numass_data_Point_Channel_Block_descriptor; + } + + @java.lang.Override + public inr.numass.data.NumassProto.Point.Channel.Block getDefaultInstanceForType() { + return inr.numass.data.NumassProto.Point.Channel.Block.getDefaultInstance(); + } + + @java.lang.Override + public inr.numass.data.NumassProto.Point.Channel.Block build() { + inr.numass.data.NumassProto.Point.Channel.Block result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + @java.lang.Override + public inr.numass.data.NumassProto.Point.Channel.Block buildPartial() { + inr.numass.data.NumassProto.Point.Channel.Block result = new inr.numass.data.NumassProto.Point.Channel.Block(this); + int from_bitField0_ = bitField0_; + int to_bitField0_ = 0; + result.time_ = time_; + if (framesBuilder_ == null) { + if (((bitField0_ & 0x00000002) == 0x00000002)) { + frames_ = java.util.Collections.unmodifiableList(frames_); + bitField0_ = (bitField0_ & ~0x00000002); + } + result.frames_ = frames_; + } else { + result.frames_ = framesBuilder_.build(); + } + if (eventsBuilder_ == null) { + result.events_ = events_; + } else { + result.events_ = eventsBuilder_.build(); + } + result.length_ = length_; + result.binSize_ = binSize_; + result.bitField0_ = to_bitField0_; + onBuilt(); + return result; + } + + @java.lang.Override + public Builder clone() { + return (Builder) super.clone(); + } + @java.lang.Override + public Builder setField( + com.google.protobuf.Descriptors.FieldDescriptor field, + java.lang.Object value) { + return (Builder) super.setField(field, value); + } + @java.lang.Override + public Builder clearField( + com.google.protobuf.Descriptors.FieldDescriptor field) { + return (Builder) super.clearField(field); + } + @java.lang.Override + public Builder clearOneof( + com.google.protobuf.Descriptors.OneofDescriptor oneof) { + return (Builder) super.clearOneof(oneof); + } + @java.lang.Override + public Builder setRepeatedField( + com.google.protobuf.Descriptors.FieldDescriptor field, + int index, java.lang.Object value) { + return (Builder) super.setRepeatedField(field, index, value); + } + @java.lang.Override + public Builder addRepeatedField( + com.google.protobuf.Descriptors.FieldDescriptor field, + java.lang.Object value) { + return (Builder) super.addRepeatedField(field, value); + } + @java.lang.Override + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof inr.numass.data.NumassProto.Point.Channel.Block) { + return mergeFrom((inr.numass.data.NumassProto.Point.Channel.Block)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(inr.numass.data.NumassProto.Point.Channel.Block other) { + if (other == inr.numass.data.NumassProto.Point.Channel.Block.getDefaultInstance()) return this; + if (other.getTime() != 0L) { + setTime(other.getTime()); + } + if (framesBuilder_ == null) { + if (!other.frames_.isEmpty()) { + if (frames_.isEmpty()) { + frames_ = other.frames_; + bitField0_ = (bitField0_ & ~0x00000002); + } else { + ensureFramesIsMutable(); + frames_.addAll(other.frames_); + } + onChanged(); + } + } else { + if (!other.frames_.isEmpty()) { + if (framesBuilder_.isEmpty()) { + framesBuilder_.dispose(); + framesBuilder_ = null; + frames_ = other.frames_; + bitField0_ = (bitField0_ & ~0x00000002); + framesBuilder_ = + com.google.protobuf.GeneratedMessageV3.alwaysUseFieldBuilders ? + getFramesFieldBuilder() : null; + } else { + framesBuilder_.addAllMessages(other.frames_); + } + } + } + if (other.hasEvents()) { + mergeEvents(other.getEvents()); + } + if (other.getLength() != 0L) { + setLength(other.getLength()); + } + if (other.getBinSize() != 0L) { + setBinSize(other.getBinSize()); + } + this.mergeUnknownFields(other.unknownFields); + onChanged(); + return this; + } + + @java.lang.Override + public final boolean isInitialized() { + return true; + } + + @java.lang.Override + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + inr.numass.data.NumassProto.Point.Channel.Block parsedMessage = null; + try { + parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + parsedMessage = (inr.numass.data.NumassProto.Point.Channel.Block) e.getUnfinishedMessage(); + throw e.unwrapIOException(); + } finally { + if (parsedMessage != null) { + mergeFrom(parsedMessage); + } + } + return this; + } + private int bitField0_; + + private long time_ ; + /** + *
+           * Block start in epoch nanos
+           * 
+ * + * uint64 time = 1; + */ + public long getTime() { + return time_; + } + /** + *
+           * Block start in epoch nanos
+           * 
+ * + * uint64 time = 1; + */ + public Builder setTime(long value) { + + time_ = value; + onChanged(); + return this; + } + /** + *
+           * Block start in epoch nanos
+           * 
+ * + * uint64 time = 1; + */ + public Builder clearTime() { + + time_ = 0L; + onChanged(); + return this; + } + + private java.util.List frames_ = + java.util.Collections.emptyList(); + private void ensureFramesIsMutable() { + if (!((bitField0_ & 0x00000002) == 0x00000002)) { + frames_ = new java.util.ArrayList(frames_); + bitField0_ |= 0x00000002; + } + } + + private com.google.protobuf.RepeatedFieldBuilderV3< + inr.numass.data.NumassProto.Point.Channel.Block.Frame, inr.numass.data.NumassProto.Point.Channel.Block.Frame.Builder, inr.numass.data.NumassProto.Point.Channel.Block.FrameOrBuilder> framesBuilder_; + + /** + *
+           * Frames array
+           * 
+ * + * repeated .inr.numass.data.Point.Channel.Block.Frame frames = 2; + */ + public java.util.List getFramesList() { + if (framesBuilder_ == null) { + return java.util.Collections.unmodifiableList(frames_); + } else { + return framesBuilder_.getMessageList(); + } + } + /** + *
+           * Frames array
+           * 
+ * + * repeated .inr.numass.data.Point.Channel.Block.Frame frames = 2; + */ + public int getFramesCount() { + if (framesBuilder_ == null) { + return frames_.size(); + } else { + return framesBuilder_.getCount(); + } + } + /** + *
+           * Frames array
+           * 
+ * + * repeated .inr.numass.data.Point.Channel.Block.Frame frames = 2; + */ + public inr.numass.data.NumassProto.Point.Channel.Block.Frame getFrames(int index) { + if (framesBuilder_ == null) { + return frames_.get(index); + } else { + return framesBuilder_.getMessage(index); + } + } + /** + *
+           * Frames array
+           * 
+ * + * repeated .inr.numass.data.Point.Channel.Block.Frame frames = 2; + */ + public Builder setFrames( + int index, inr.numass.data.NumassProto.Point.Channel.Block.Frame value) { + if (framesBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + ensureFramesIsMutable(); + frames_.set(index, value); + onChanged(); + } else { + framesBuilder_.setMessage(index, value); + } + return this; + } + /** + *
+           * Frames array
+           * 
+ * + * repeated .inr.numass.data.Point.Channel.Block.Frame frames = 2; + */ + public Builder setFrames( + int index, inr.numass.data.NumassProto.Point.Channel.Block.Frame.Builder builderForValue) { + if (framesBuilder_ == null) { + ensureFramesIsMutable(); + frames_.set(index, builderForValue.build()); + onChanged(); + } else { + framesBuilder_.setMessage(index, builderForValue.build()); + } + return this; + } + /** + *
+           * Frames array
+           * 
+ * + * repeated .inr.numass.data.Point.Channel.Block.Frame frames = 2; + */ + public Builder addFrames(inr.numass.data.NumassProto.Point.Channel.Block.Frame value) { + if (framesBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + ensureFramesIsMutable(); + frames_.add(value); + onChanged(); + } else { + framesBuilder_.addMessage(value); + } + return this; + } + /** + *
+           * Frames array
+           * 
+ * + * repeated .inr.numass.data.Point.Channel.Block.Frame frames = 2; + */ + public Builder addFrames( + int index, inr.numass.data.NumassProto.Point.Channel.Block.Frame value) { + if (framesBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + ensureFramesIsMutable(); + frames_.add(index, value); + onChanged(); + } else { + framesBuilder_.addMessage(index, value); + } + return this; + } + /** + *
+           * Frames array
+           * 
+ * + * repeated .inr.numass.data.Point.Channel.Block.Frame frames = 2; + */ + public Builder addFrames( + inr.numass.data.NumassProto.Point.Channel.Block.Frame.Builder builderForValue) { + if (framesBuilder_ == null) { + ensureFramesIsMutable(); + frames_.add(builderForValue.build()); + onChanged(); + } else { + framesBuilder_.addMessage(builderForValue.build()); + } + return this; + } + /** + *
+           * Frames array
+           * 
+ * + * repeated .inr.numass.data.Point.Channel.Block.Frame frames = 2; + */ + public Builder addFrames( + int index, inr.numass.data.NumassProto.Point.Channel.Block.Frame.Builder builderForValue) { + if (framesBuilder_ == null) { + ensureFramesIsMutable(); + frames_.add(index, builderForValue.build()); + onChanged(); + } else { + framesBuilder_.addMessage(index, builderForValue.build()); + } + return this; + } + /** + *
+           * Frames array
+           * 
+ * + * repeated .inr.numass.data.Point.Channel.Block.Frame frames = 2; + */ + public Builder addAllFrames( + java.lang.Iterable values) { + if (framesBuilder_ == null) { + ensureFramesIsMutable(); + com.google.protobuf.AbstractMessageLite.Builder.addAll( + values, frames_); + onChanged(); + } else { + framesBuilder_.addAllMessages(values); + } + return this; + } + /** + *
+           * Frames array
+           * 
+ * + * repeated .inr.numass.data.Point.Channel.Block.Frame frames = 2; + */ + public Builder clearFrames() { + if (framesBuilder_ == null) { + frames_ = java.util.Collections.emptyList(); + bitField0_ = (bitField0_ & ~0x00000002); + onChanged(); + } else { + framesBuilder_.clear(); + } + return this; + } + /** + *
+           * Frames array
+           * 
+ * + * repeated .inr.numass.data.Point.Channel.Block.Frame frames = 2; + */ + public Builder removeFrames(int index) { + if (framesBuilder_ == null) { + ensureFramesIsMutable(); + frames_.remove(index); + onChanged(); + } else { + framesBuilder_.remove(index); + } + return this; + } + /** + *
+           * Frames array
+           * 
+ * + * repeated .inr.numass.data.Point.Channel.Block.Frame frames = 2; + */ + public inr.numass.data.NumassProto.Point.Channel.Block.Frame.Builder getFramesBuilder( + int index) { + return getFramesFieldBuilder().getBuilder(index); + } + /** + *
+           * Frames array
+           * 
+ * + * repeated .inr.numass.data.Point.Channel.Block.Frame frames = 2; + */ + public inr.numass.data.NumassProto.Point.Channel.Block.FrameOrBuilder getFramesOrBuilder( + int index) { + if (framesBuilder_ == null) { + return frames_.get(index); } else { + return framesBuilder_.getMessageOrBuilder(index); + } + } + /** + *
+           * Frames array
+           * 
+ * + * repeated .inr.numass.data.Point.Channel.Block.Frame frames = 2; + */ + public java.util.List + getFramesOrBuilderList() { + if (framesBuilder_ != null) { + return framesBuilder_.getMessageOrBuilderList(); + } else { + return java.util.Collections.unmodifiableList(frames_); + } + } + /** + *
+           * Frames array
+           * 
+ * + * repeated .inr.numass.data.Point.Channel.Block.Frame frames = 2; + */ + public inr.numass.data.NumassProto.Point.Channel.Block.Frame.Builder addFramesBuilder() { + return getFramesFieldBuilder().addBuilder( + inr.numass.data.NumassProto.Point.Channel.Block.Frame.getDefaultInstance()); + } + /** + *
+           * Frames array
+           * 
+ * + * repeated .inr.numass.data.Point.Channel.Block.Frame frames = 2; + */ + public inr.numass.data.NumassProto.Point.Channel.Block.Frame.Builder addFramesBuilder( + int index) { + return getFramesFieldBuilder().addBuilder( + index, inr.numass.data.NumassProto.Point.Channel.Block.Frame.getDefaultInstance()); + } + /** + *
+           * Frames array
+           * 
+ * + * repeated .inr.numass.data.Point.Channel.Block.Frame frames = 2; + */ + public java.util.List + getFramesBuilderList() { + return getFramesFieldBuilder().getBuilderList(); + } + private com.google.protobuf.RepeatedFieldBuilderV3< + inr.numass.data.NumassProto.Point.Channel.Block.Frame, inr.numass.data.NumassProto.Point.Channel.Block.Frame.Builder, inr.numass.data.NumassProto.Point.Channel.Block.FrameOrBuilder> + getFramesFieldBuilder() { + if (framesBuilder_ == null) { + framesBuilder_ = new com.google.protobuf.RepeatedFieldBuilderV3< + inr.numass.data.NumassProto.Point.Channel.Block.Frame, inr.numass.data.NumassProto.Point.Channel.Block.Frame.Builder, inr.numass.data.NumassProto.Point.Channel.Block.FrameOrBuilder>( + frames_, + ((bitField0_ & 0x00000002) == 0x00000002), + getParentForChildren(), + isClean()); + frames_ = null; + } + return framesBuilder_; + } + + private inr.numass.data.NumassProto.Point.Channel.Block.Events events_ = null; + private com.google.protobuf.SingleFieldBuilderV3< + inr.numass.data.NumassProto.Point.Channel.Block.Events, inr.numass.data.NumassProto.Point.Channel.Block.Events.Builder, inr.numass.data.NumassProto.Point.Channel.Block.EventsOrBuilder> eventsBuilder_; + /** + *
+           * Events array
+           * 
+ * + * .inr.numass.data.Point.Channel.Block.Events events = 3; + */ + public boolean hasEvents() { + return eventsBuilder_ != null || events_ != null; + } + /** + *
+           * Events array
+           * 
+ * + * .inr.numass.data.Point.Channel.Block.Events events = 3; + */ + public inr.numass.data.NumassProto.Point.Channel.Block.Events getEvents() { + if (eventsBuilder_ == null) { + return events_ == null ? inr.numass.data.NumassProto.Point.Channel.Block.Events.getDefaultInstance() : events_; + } else { + return eventsBuilder_.getMessage(); + } + } + /** + *
+           * Events array
+           * 
+ * + * .inr.numass.data.Point.Channel.Block.Events events = 3; + */ + public Builder setEvents(inr.numass.data.NumassProto.Point.Channel.Block.Events value) { + if (eventsBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + events_ = value; + onChanged(); + } else { + eventsBuilder_.setMessage(value); + } + + return this; + } + /** + *
+           * Events array
+           * 
+ * + * .inr.numass.data.Point.Channel.Block.Events events = 3; + */ + public Builder setEvents( + inr.numass.data.NumassProto.Point.Channel.Block.Events.Builder builderForValue) { + if (eventsBuilder_ == null) { + events_ = builderForValue.build(); + onChanged(); + } else { + eventsBuilder_.setMessage(builderForValue.build()); + } + + return this; + } + /** + *
+           * Events array
+           * 
+ * + * .inr.numass.data.Point.Channel.Block.Events events = 3; + */ + public Builder mergeEvents(inr.numass.data.NumassProto.Point.Channel.Block.Events value) { + if (eventsBuilder_ == null) { + if (events_ != null) { + events_ = + inr.numass.data.NumassProto.Point.Channel.Block.Events.newBuilder(events_).mergeFrom(value).buildPartial(); + } else { + events_ = value; + } + onChanged(); + } else { + eventsBuilder_.mergeFrom(value); + } + + return this; + } + /** + *
+           * Events array
+           * 
+ * + * .inr.numass.data.Point.Channel.Block.Events events = 3; + */ + public Builder clearEvents() { + if (eventsBuilder_ == null) { + events_ = null; + onChanged(); + } else { + events_ = null; + eventsBuilder_ = null; + } + + return this; + } + /** + *
+           * Events array
+           * 
+ * + * .inr.numass.data.Point.Channel.Block.Events events = 3; + */ + public inr.numass.data.NumassProto.Point.Channel.Block.Events.Builder getEventsBuilder() { + + onChanged(); + return getEventsFieldBuilder().getBuilder(); + } + /** + *
+           * Events array
+           * 
+ * + * .inr.numass.data.Point.Channel.Block.Events events = 3; + */ + public inr.numass.data.NumassProto.Point.Channel.Block.EventsOrBuilder getEventsOrBuilder() { + if (eventsBuilder_ != null) { + return eventsBuilder_.getMessageOrBuilder(); + } else { + return events_ == null ? + inr.numass.data.NumassProto.Point.Channel.Block.Events.getDefaultInstance() : events_; + } + } + /** + *
+           * Events array
+           * 
+ * + * .inr.numass.data.Point.Channel.Block.Events events = 3; + */ + private com.google.protobuf.SingleFieldBuilderV3< + inr.numass.data.NumassProto.Point.Channel.Block.Events, inr.numass.data.NumassProto.Point.Channel.Block.Events.Builder, inr.numass.data.NumassProto.Point.Channel.Block.EventsOrBuilder> + getEventsFieldBuilder() { + if (eventsBuilder_ == null) { + eventsBuilder_ = new com.google.protobuf.SingleFieldBuilderV3< + inr.numass.data.NumassProto.Point.Channel.Block.Events, inr.numass.data.NumassProto.Point.Channel.Block.Events.Builder, inr.numass.data.NumassProto.Point.Channel.Block.EventsOrBuilder>( + getEvents(), + getParentForChildren(), + isClean()); + events_ = null; + } + return eventsBuilder_; + } + + private long length_ ; + /** + *
+           * block size in nanos. If missing, take from meta.
+           * 
+ * + * uint64 length = 4; + */ + public long getLength() { + return length_; + } + /** + *
+           * block size in nanos. If missing, take from meta.
+           * 
+ * + * uint64 length = 4; + */ + public Builder setLength(long value) { + + length_ = value; + onChanged(); + return this; + } + /** + *
+           * block size in nanos. If missing, take from meta.
+           * 
+ * + * uint64 length = 4; + */ + public Builder clearLength() { + + length_ = 0L; + onChanged(); + return this; + } + + private long binSize_ ; + /** + *
+           * tick size in nanos. Obsolete, to be removed
+           * 
+ * + * uint64 bin_size = 5; + */ + public long getBinSize() { + return binSize_; + } + /** + *
+           * tick size in nanos. Obsolete, to be removed
+           * 
+ * + * uint64 bin_size = 5; + */ + public Builder setBinSize(long value) { + + binSize_ = value; + onChanged(); + return this; + } + /** + *
+           * tick size in nanos. Obsolete, to be removed
+           * 
+ * + * uint64 bin_size = 5; + */ + public Builder clearBinSize() { + + binSize_ = 0L; + onChanged(); + return this; + } + @java.lang.Override + public final Builder setUnknownFields( + final com.google.protobuf.UnknownFieldSet unknownFields) { + return super.setUnknownFieldsProto3(unknownFields); + } + + @java.lang.Override + public final Builder mergeUnknownFields( + final com.google.protobuf.UnknownFieldSet unknownFields) { + return super.mergeUnknownFields(unknownFields); + } + + + // @@protoc_insertion_point(builder_scope:inr.numass.data.Point.Channel.Block) + } + + // @@protoc_insertion_point(class_scope:inr.numass.data.Point.Channel.Block) + private static final inr.numass.data.NumassProto.Point.Channel.Block DEFAULT_INSTANCE; + static { + DEFAULT_INSTANCE = new inr.numass.data.NumassProto.Point.Channel.Block(); + } + + public static inr.numass.data.NumassProto.Point.Channel.Block getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static final com.google.protobuf.Parser + PARSER = new com.google.protobuf.AbstractParser() { + @java.lang.Override + public Block parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return new Block(input, extensionRegistry); + } + }; + + public static com.google.protobuf.Parser parser() { + return PARSER; + } + + @java.lang.Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + + @java.lang.Override + public inr.numass.data.NumassProto.Point.Channel.Block getDefaultInstanceForType() { + return DEFAULT_INSTANCE; + } + + } + + private int bitField0_; + public static final int ID_FIELD_NUMBER = 1; + private long id_; + /** + *
+       * The number of measuring channel
+       * 
+ * + * uint64 id = 1; + */ + public long getId() { + return id_; + } + + public static final int BLOCKS_FIELD_NUMBER = 2; + private java.util.List blocks_; + /** + *
+       * Blocks
+       * 
+ * + * repeated .inr.numass.data.Point.Channel.Block blocks = 2; + */ + public java.util.List getBlocksList() { + return blocks_; + } + /** + *
+       * Blocks
+       * 
+ * + * repeated .inr.numass.data.Point.Channel.Block blocks = 2; + */ + public java.util.List + getBlocksOrBuilderList() { + return blocks_; + } + /** + *
+       * Blocks
+       * 
+ * + * repeated .inr.numass.data.Point.Channel.Block blocks = 2; + */ + public int getBlocksCount() { + return blocks_.size(); + } + /** + *
+       * Blocks
+       * 
+ * + * repeated .inr.numass.data.Point.Channel.Block blocks = 2; + */ + public inr.numass.data.NumassProto.Point.Channel.Block getBlocks(int index) { + return blocks_.get(index); + } + /** + *
+       * Blocks
+       * 
+ * + * repeated .inr.numass.data.Point.Channel.Block blocks = 2; + */ + public inr.numass.data.NumassProto.Point.Channel.BlockOrBuilder getBlocksOrBuilder( + int index) { + return blocks_.get(index); + } + + private byte memoizedIsInitialized = -1; + @java.lang.Override + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized == 1) return true; + if (isInitialized == 0) return false; + + memoizedIsInitialized = 1; + return true; + } + + @java.lang.Override + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + if (id_ != 0L) { + output.writeUInt64(1, id_); + } + for (int i = 0; i < blocks_.size(); i++) { + output.writeMessage(2, blocks_.get(i)); + } + unknownFields.writeTo(output); + } + + @java.lang.Override + public int getSerializedSize() { + int size = memoizedSize; + if (size != -1) return size; + + size = 0; + if (id_ != 0L) { + size += com.google.protobuf.CodedOutputStream + .computeUInt64Size(1, id_); + } + for (int i = 0; i < blocks_.size(); i++) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(2, blocks_.get(i)); + } + size += unknownFields.getSerializedSize(); + memoizedSize = size; + return size; + } + + @java.lang.Override + public boolean equals(final java.lang.Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof inr.numass.data.NumassProto.Point.Channel)) { + return super.equals(obj); + } + inr.numass.data.NumassProto.Point.Channel other = (inr.numass.data.NumassProto.Point.Channel) obj; + + boolean result = true; + result = result && (getId() + == other.getId()); + result = result && getBlocksList() + .equals(other.getBlocksList()); + result = result && unknownFields.equals(other.unknownFields); + return result; + } + + @java.lang.Override + public int hashCode() { + if (memoizedHashCode != 0) { + return memoizedHashCode; + } + int hash = 41; + hash = (19 * hash) + getDescriptor().hashCode(); + hash = (37 * hash) + ID_FIELD_NUMBER; + hash = (53 * hash) + com.google.protobuf.Internal.hashLong( + getId()); + if (getBlocksCount() > 0) { + hash = (37 * hash) + BLOCKS_FIELD_NUMBER; + hash = (53 * hash) + getBlocksList().hashCode(); + } + hash = (29 * hash) + unknownFields.hashCode(); + memoizedHashCode = hash; + return hash; + } + + public static inr.numass.data.NumassProto.Point.Channel parseFrom( + java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static inr.numass.data.NumassProto.Point.Channel parseFrom( + java.nio.ByteBuffer data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static inr.numass.data.NumassProto.Point.Channel parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static inr.numass.data.NumassProto.Point.Channel parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static inr.numass.data.NumassProto.Point.Channel parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static inr.numass.data.NumassProto.Point.Channel parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static inr.numass.data.NumassProto.Point.Channel parseFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseWithIOException(PARSER, input); + } + public static inr.numass.data.NumassProto.Point.Channel parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseWithIOException(PARSER, input, extensionRegistry); + } + public static inr.numass.data.NumassProto.Point.Channel parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseDelimitedWithIOException(PARSER, input); + } + public static inr.numass.data.NumassProto.Point.Channel parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseDelimitedWithIOException(PARSER, input, extensionRegistry); + } + public static inr.numass.data.NumassProto.Point.Channel parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseWithIOException(PARSER, input); + } + public static inr.numass.data.NumassProto.Point.Channel parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseWithIOException(PARSER, input, extensionRegistry); + } + + @java.lang.Override + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder() { + return DEFAULT_INSTANCE.toBuilder(); + } + public static Builder newBuilder(inr.numass.data.NumassProto.Point.Channel prototype) { + return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); + } + @java.lang.Override + public Builder toBuilder() { + return this == DEFAULT_INSTANCE + ? new Builder() : new Builder().mergeFrom(this); + } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** + *
+       * A single channel for multichannel detector readout
+       * 
+ * + * Protobuf type {@code inr.numass.data.Point.Channel} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessageV3.Builder implements + // @@protoc_insertion_point(builder_implements:inr.numass.data.Point.Channel) + inr.numass.data.NumassProto.Point.ChannelOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return inr.numass.data.NumassProto.internal_static_inr_numass_data_Point_Channel_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable + internalGetFieldAccessorTable() { + return inr.numass.data.NumassProto.internal_static_inr_numass_data_Point_Channel_fieldAccessorTable + .ensureFieldAccessorsInitialized( + inr.numass.data.NumassProto.Point.Channel.class, inr.numass.data.NumassProto.Point.Channel.Builder.class); + } + + // Construct using inr.numass.data.NumassProto.Point.Channel.newBuilder() + private Builder() { + maybeForceBuilderInitialization(); + } + + private Builder( + com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { + super(parent); + maybeForceBuilderInitialization(); + } + private void maybeForceBuilderInitialization() { + if (com.google.protobuf.GeneratedMessageV3 + .alwaysUseFieldBuilders) { + getBlocksFieldBuilder(); + } + } + @java.lang.Override + public Builder clear() { + super.clear(); + id_ = 0L; + + if (blocksBuilder_ == null) { + blocks_ = java.util.Collections.emptyList(); + bitField0_ = (bitField0_ & ~0x00000002); + } else { + blocksBuilder_.clear(); + } + return this; + } + + @java.lang.Override + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return inr.numass.data.NumassProto.internal_static_inr_numass_data_Point_Channel_descriptor; + } + + @java.lang.Override + public inr.numass.data.NumassProto.Point.Channel getDefaultInstanceForType() { + return inr.numass.data.NumassProto.Point.Channel.getDefaultInstance(); + } + + @java.lang.Override + public inr.numass.data.NumassProto.Point.Channel build() { + inr.numass.data.NumassProto.Point.Channel result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + @java.lang.Override + public inr.numass.data.NumassProto.Point.Channel buildPartial() { + inr.numass.data.NumassProto.Point.Channel result = new inr.numass.data.NumassProto.Point.Channel(this); + int from_bitField0_ = bitField0_; + int to_bitField0_ = 0; + result.id_ = id_; + if (blocksBuilder_ == null) { + if (((bitField0_ & 0x00000002) == 0x00000002)) { + blocks_ = java.util.Collections.unmodifiableList(blocks_); + bitField0_ = (bitField0_ & ~0x00000002); + } + result.blocks_ = blocks_; + } else { + result.blocks_ = blocksBuilder_.build(); + } + result.bitField0_ = to_bitField0_; + onBuilt(); + return result; + } + + @java.lang.Override + public Builder clone() { + return (Builder) super.clone(); + } + @java.lang.Override + public Builder setField( + com.google.protobuf.Descriptors.FieldDescriptor field, + java.lang.Object value) { + return (Builder) super.setField(field, value); + } + @java.lang.Override + public Builder clearField( + com.google.protobuf.Descriptors.FieldDescriptor field) { + return (Builder) super.clearField(field); + } + @java.lang.Override + public Builder clearOneof( + com.google.protobuf.Descriptors.OneofDescriptor oneof) { + return (Builder) super.clearOneof(oneof); + } + @java.lang.Override + public Builder setRepeatedField( + com.google.protobuf.Descriptors.FieldDescriptor field, + int index, java.lang.Object value) { + return (Builder) super.setRepeatedField(field, index, value); + } + @java.lang.Override + public Builder addRepeatedField( + com.google.protobuf.Descriptors.FieldDescriptor field, + java.lang.Object value) { + return (Builder) super.addRepeatedField(field, value); + } + @java.lang.Override + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof inr.numass.data.NumassProto.Point.Channel) { + return mergeFrom((inr.numass.data.NumassProto.Point.Channel)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(inr.numass.data.NumassProto.Point.Channel other) { + if (other == inr.numass.data.NumassProto.Point.Channel.getDefaultInstance()) return this; + if (other.getId() != 0L) { + setId(other.getId()); + } + if (blocksBuilder_ == null) { + if (!other.blocks_.isEmpty()) { + if (blocks_.isEmpty()) { + blocks_ = other.blocks_; + bitField0_ = (bitField0_ & ~0x00000002); + } else { + ensureBlocksIsMutable(); + blocks_.addAll(other.blocks_); + } + onChanged(); + } + } else { + if (!other.blocks_.isEmpty()) { + if (blocksBuilder_.isEmpty()) { + blocksBuilder_.dispose(); + blocksBuilder_ = null; + blocks_ = other.blocks_; + bitField0_ = (bitField0_ & ~0x00000002); + blocksBuilder_ = + com.google.protobuf.GeneratedMessageV3.alwaysUseFieldBuilders ? + getBlocksFieldBuilder() : null; + } else { + blocksBuilder_.addAllMessages(other.blocks_); + } + } + } + this.mergeUnknownFields(other.unknownFields); + onChanged(); + return this; + } + + @java.lang.Override + public final boolean isInitialized() { + return true; + } + + @java.lang.Override + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + inr.numass.data.NumassProto.Point.Channel parsedMessage = null; + try { + parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + parsedMessage = (inr.numass.data.NumassProto.Point.Channel) e.getUnfinishedMessage(); + throw e.unwrapIOException(); + } finally { + if (parsedMessage != null) { + mergeFrom(parsedMessage); + } + } + return this; + } + private int bitField0_; + + private long id_ ; + /** + *
+         * The number of measuring channel
+         * 
+ * + * uint64 id = 1; + */ + public long getId() { + return id_; + } + /** + *
+         * The number of measuring channel
+         * 
+ * + * uint64 id = 1; + */ + public Builder setId(long value) { + + id_ = value; + onChanged(); + return this; + } + /** + *
+         * The number of measuring channel
+         * 
+ * + * uint64 id = 1; + */ + public Builder clearId() { + + id_ = 0L; + onChanged(); + return this; + } + + private java.util.List blocks_ = + java.util.Collections.emptyList(); + private void ensureBlocksIsMutable() { + if (!((bitField0_ & 0x00000002) == 0x00000002)) { + blocks_ = new java.util.ArrayList(blocks_); + bitField0_ |= 0x00000002; + } + } + + private com.google.protobuf.RepeatedFieldBuilderV3< + inr.numass.data.NumassProto.Point.Channel.Block, inr.numass.data.NumassProto.Point.Channel.Block.Builder, inr.numass.data.NumassProto.Point.Channel.BlockOrBuilder> blocksBuilder_; + + /** + *
+         * Blocks
+         * 
+ * + * repeated .inr.numass.data.Point.Channel.Block blocks = 2; + */ + public java.util.List getBlocksList() { + if (blocksBuilder_ == null) { + return java.util.Collections.unmodifiableList(blocks_); + } else { + return blocksBuilder_.getMessageList(); + } + } + /** + *
+         * Blocks
+         * 
+ * + * repeated .inr.numass.data.Point.Channel.Block blocks = 2; + */ + public int getBlocksCount() { + if (blocksBuilder_ == null) { + return blocks_.size(); + } else { + return blocksBuilder_.getCount(); + } + } + /** + *
+         * Blocks
+         * 
+ * + * repeated .inr.numass.data.Point.Channel.Block blocks = 2; + */ + public inr.numass.data.NumassProto.Point.Channel.Block getBlocks(int index) { + if (blocksBuilder_ == null) { + return blocks_.get(index); + } else { + return blocksBuilder_.getMessage(index); + } + } + /** + *
+         * Blocks
+         * 
+ * + * repeated .inr.numass.data.Point.Channel.Block blocks = 2; + */ + public Builder setBlocks( + int index, inr.numass.data.NumassProto.Point.Channel.Block value) { + if (blocksBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + ensureBlocksIsMutable(); + blocks_.set(index, value); + onChanged(); + } else { + blocksBuilder_.setMessage(index, value); + } + return this; + } + /** + *
+         * Blocks
+         * 
+ * + * repeated .inr.numass.data.Point.Channel.Block blocks = 2; + */ + public Builder setBlocks( + int index, inr.numass.data.NumassProto.Point.Channel.Block.Builder builderForValue) { + if (blocksBuilder_ == null) { + ensureBlocksIsMutable(); + blocks_.set(index, builderForValue.build()); + onChanged(); + } else { + blocksBuilder_.setMessage(index, builderForValue.build()); + } + return this; + } + /** + *
+         * Blocks
+         * 
+ * + * repeated .inr.numass.data.Point.Channel.Block blocks = 2; + */ + public Builder addBlocks(inr.numass.data.NumassProto.Point.Channel.Block value) { + if (blocksBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + ensureBlocksIsMutable(); + blocks_.add(value); + onChanged(); + } else { + blocksBuilder_.addMessage(value); + } + return this; + } + /** + *
+         * Blocks
+         * 
+ * + * repeated .inr.numass.data.Point.Channel.Block blocks = 2; + */ + public Builder addBlocks( + int index, inr.numass.data.NumassProto.Point.Channel.Block value) { + if (blocksBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + ensureBlocksIsMutable(); + blocks_.add(index, value); + onChanged(); + } else { + blocksBuilder_.addMessage(index, value); + } + return this; + } + /** + *
+         * Blocks
+         * 
+ * + * repeated .inr.numass.data.Point.Channel.Block blocks = 2; + */ + public Builder addBlocks( + inr.numass.data.NumassProto.Point.Channel.Block.Builder builderForValue) { + if (blocksBuilder_ == null) { + ensureBlocksIsMutable(); + blocks_.add(builderForValue.build()); + onChanged(); + } else { + blocksBuilder_.addMessage(builderForValue.build()); + } + return this; + } + /** + *
+         * Blocks
+         * 
+ * + * repeated .inr.numass.data.Point.Channel.Block blocks = 2; + */ + public Builder addBlocks( + int index, inr.numass.data.NumassProto.Point.Channel.Block.Builder builderForValue) { + if (blocksBuilder_ == null) { + ensureBlocksIsMutable(); + blocks_.add(index, builderForValue.build()); + onChanged(); + } else { + blocksBuilder_.addMessage(index, builderForValue.build()); + } + return this; + } + /** + *
+         * Blocks
+         * 
+ * + * repeated .inr.numass.data.Point.Channel.Block blocks = 2; + */ + public Builder addAllBlocks( + java.lang.Iterable values) { + if (blocksBuilder_ == null) { + ensureBlocksIsMutable(); + com.google.protobuf.AbstractMessageLite.Builder.addAll( + values, blocks_); + onChanged(); + } else { + blocksBuilder_.addAllMessages(values); + } + return this; + } + /** + *
+         * Blocks
+         * 
+ * + * repeated .inr.numass.data.Point.Channel.Block blocks = 2; + */ + public Builder clearBlocks() { + if (blocksBuilder_ == null) { + blocks_ = java.util.Collections.emptyList(); + bitField0_ = (bitField0_ & ~0x00000002); + onChanged(); + } else { + blocksBuilder_.clear(); + } + return this; + } + /** + *
+         * Blocks
+         * 
+ * + * repeated .inr.numass.data.Point.Channel.Block blocks = 2; + */ + public Builder removeBlocks(int index) { + if (blocksBuilder_ == null) { + ensureBlocksIsMutable(); + blocks_.remove(index); + onChanged(); + } else { + blocksBuilder_.remove(index); + } + return this; + } + /** + *
+         * Blocks
+         * 
+ * + * repeated .inr.numass.data.Point.Channel.Block blocks = 2; + */ + public inr.numass.data.NumassProto.Point.Channel.Block.Builder getBlocksBuilder( + int index) { + return getBlocksFieldBuilder().getBuilder(index); + } + /** + *
+         * Blocks
+         * 
+ * + * repeated .inr.numass.data.Point.Channel.Block blocks = 2; + */ + public inr.numass.data.NumassProto.Point.Channel.BlockOrBuilder getBlocksOrBuilder( + int index) { + if (blocksBuilder_ == null) { + return blocks_.get(index); } else { + return blocksBuilder_.getMessageOrBuilder(index); + } + } + /** + *
+         * Blocks
+         * 
+ * + * repeated .inr.numass.data.Point.Channel.Block blocks = 2; + */ + public java.util.List + getBlocksOrBuilderList() { + if (blocksBuilder_ != null) { + return blocksBuilder_.getMessageOrBuilderList(); + } else { + return java.util.Collections.unmodifiableList(blocks_); + } + } + /** + *
+         * Blocks
+         * 
+ * + * repeated .inr.numass.data.Point.Channel.Block blocks = 2; + */ + public inr.numass.data.NumassProto.Point.Channel.Block.Builder addBlocksBuilder() { + return getBlocksFieldBuilder().addBuilder( + inr.numass.data.NumassProto.Point.Channel.Block.getDefaultInstance()); + } + /** + *
+         * Blocks
+         * 
+ * + * repeated .inr.numass.data.Point.Channel.Block blocks = 2; + */ + public inr.numass.data.NumassProto.Point.Channel.Block.Builder addBlocksBuilder( + int index) { + return getBlocksFieldBuilder().addBuilder( + index, inr.numass.data.NumassProto.Point.Channel.Block.getDefaultInstance()); + } + /** + *
+         * Blocks
+         * 
+ * + * repeated .inr.numass.data.Point.Channel.Block blocks = 2; + */ + public java.util.List + getBlocksBuilderList() { + return getBlocksFieldBuilder().getBuilderList(); + } + private com.google.protobuf.RepeatedFieldBuilderV3< + inr.numass.data.NumassProto.Point.Channel.Block, inr.numass.data.NumassProto.Point.Channel.Block.Builder, inr.numass.data.NumassProto.Point.Channel.BlockOrBuilder> + getBlocksFieldBuilder() { + if (blocksBuilder_ == null) { + blocksBuilder_ = new com.google.protobuf.RepeatedFieldBuilderV3< + inr.numass.data.NumassProto.Point.Channel.Block, inr.numass.data.NumassProto.Point.Channel.Block.Builder, inr.numass.data.NumassProto.Point.Channel.BlockOrBuilder>( + blocks_, + ((bitField0_ & 0x00000002) == 0x00000002), + getParentForChildren(), + isClean()); + blocks_ = null; + } + return blocksBuilder_; + } + @java.lang.Override + public final Builder setUnknownFields( + final com.google.protobuf.UnknownFieldSet unknownFields) { + return super.setUnknownFieldsProto3(unknownFields); + } + + @java.lang.Override + public final Builder mergeUnknownFields( + final com.google.protobuf.UnknownFieldSet unknownFields) { + return super.mergeUnknownFields(unknownFields); + } + + + // @@protoc_insertion_point(builder_scope:inr.numass.data.Point.Channel) + } + + // @@protoc_insertion_point(class_scope:inr.numass.data.Point.Channel) + private static final inr.numass.data.NumassProto.Point.Channel DEFAULT_INSTANCE; + static { + DEFAULT_INSTANCE = new inr.numass.data.NumassProto.Point.Channel(); + } + + public static inr.numass.data.NumassProto.Point.Channel getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static final com.google.protobuf.Parser + PARSER = new com.google.protobuf.AbstractParser() { + @java.lang.Override + public Channel parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return new Channel(input, extensionRegistry); + } + }; + + public static com.google.protobuf.Parser parser() { + return PARSER; + } + + @java.lang.Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + + @java.lang.Override + public inr.numass.data.NumassProto.Point.Channel getDefaultInstanceForType() { + return DEFAULT_INSTANCE; + } + + } + + public static final int CHANNELS_FIELD_NUMBER = 1; + private java.util.List channels_; + /** + *
+     * Array of measuring channels
+     * 
+ * + * repeated .inr.numass.data.Point.Channel channels = 1; + */ + public java.util.List getChannelsList() { + return channels_; + } + /** + *
+     * Array of measuring channels
+     * 
+ * + * repeated .inr.numass.data.Point.Channel channels = 1; + */ + public java.util.List + getChannelsOrBuilderList() { + return channels_; + } + /** + *
+     * Array of measuring channels
+     * 
+ * + * repeated .inr.numass.data.Point.Channel channels = 1; + */ + public int getChannelsCount() { + return channels_.size(); + } + /** + *
+     * Array of measuring channels
+     * 
+ * + * repeated .inr.numass.data.Point.Channel channels = 1; + */ + public inr.numass.data.NumassProto.Point.Channel getChannels(int index) { + return channels_.get(index); + } + /** + *
+     * Array of measuring channels
+     * 
+ * + * repeated .inr.numass.data.Point.Channel channels = 1; + */ + public inr.numass.data.NumassProto.Point.ChannelOrBuilder getChannelsOrBuilder( + int index) { + return channels_.get(index); + } + + private byte memoizedIsInitialized = -1; + @java.lang.Override + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized == 1) return true; + if (isInitialized == 0) return false; + + memoizedIsInitialized = 1; + return true; + } + + @java.lang.Override + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + for (int i = 0; i < channels_.size(); i++) { + output.writeMessage(1, channels_.get(i)); + } + unknownFields.writeTo(output); + } + + @java.lang.Override + public int getSerializedSize() { + int size = memoizedSize; + if (size != -1) return size; + + size = 0; + for (int i = 0; i < channels_.size(); i++) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(1, channels_.get(i)); + } + size += unknownFields.getSerializedSize(); + memoizedSize = size; + return size; + } + + @java.lang.Override + public boolean equals(final java.lang.Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof inr.numass.data.NumassProto.Point)) { + return super.equals(obj); + } + inr.numass.data.NumassProto.Point other = (inr.numass.data.NumassProto.Point) obj; + + boolean result = true; + result = result && getChannelsList() + .equals(other.getChannelsList()); + result = result && unknownFields.equals(other.unknownFields); + return result; + } + + @java.lang.Override + public int hashCode() { + if (memoizedHashCode != 0) { + return memoizedHashCode; + } + int hash = 41; + hash = (19 * hash) + getDescriptor().hashCode(); + if (getChannelsCount() > 0) { + hash = (37 * hash) + CHANNELS_FIELD_NUMBER; + hash = (53 * hash) + getChannelsList().hashCode(); + } + hash = (29 * hash) + unknownFields.hashCode(); + memoizedHashCode = hash; + return hash; + } + + public static inr.numass.data.NumassProto.Point parseFrom( + java.nio.ByteBuffer data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static inr.numass.data.NumassProto.Point parseFrom( + java.nio.ByteBuffer data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static inr.numass.data.NumassProto.Point parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static inr.numass.data.NumassProto.Point parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static inr.numass.data.NumassProto.Point parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static inr.numass.data.NumassProto.Point parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static inr.numass.data.NumassProto.Point parseFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseWithIOException(PARSER, input); + } + public static inr.numass.data.NumassProto.Point parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseWithIOException(PARSER, input, extensionRegistry); + } + public static inr.numass.data.NumassProto.Point parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseDelimitedWithIOException(PARSER, input); + } + public static inr.numass.data.NumassProto.Point parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseDelimitedWithIOException(PARSER, input, extensionRegistry); + } + public static inr.numass.data.NumassProto.Point parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseWithIOException(PARSER, input); + } + public static inr.numass.data.NumassProto.Point parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return com.google.protobuf.GeneratedMessageV3 + .parseWithIOException(PARSER, input, extensionRegistry); + } + + @java.lang.Override + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder() { + return DEFAULT_INSTANCE.toBuilder(); + } + public static Builder newBuilder(inr.numass.data.NumassProto.Point prototype) { + return DEFAULT_INSTANCE.toBuilder().mergeFrom(prototype); + } + @java.lang.Override + public Builder toBuilder() { + return this == DEFAULT_INSTANCE + ? new Builder() : new Builder().mergeFrom(this); + } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** + * Protobuf type {@code inr.numass.data.Point} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessageV3.Builder implements + // @@protoc_insertion_point(builder_implements:inr.numass.data.Point) + inr.numass.data.NumassProto.PointOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return inr.numass.data.NumassProto.internal_static_inr_numass_data_Point_descriptor; + } + + @java.lang.Override + protected com.google.protobuf.GeneratedMessageV3.FieldAccessorTable + internalGetFieldAccessorTable() { + return inr.numass.data.NumassProto.internal_static_inr_numass_data_Point_fieldAccessorTable + .ensureFieldAccessorsInitialized( + inr.numass.data.NumassProto.Point.class, inr.numass.data.NumassProto.Point.Builder.class); + } + + // Construct using inr.numass.data.NumassProto.Point.newBuilder() + private Builder() { + maybeForceBuilderInitialization(); + } + + private Builder( + com.google.protobuf.GeneratedMessageV3.BuilderParent parent) { + super(parent); + maybeForceBuilderInitialization(); + } + private void maybeForceBuilderInitialization() { + if (com.google.protobuf.GeneratedMessageV3 + .alwaysUseFieldBuilders) { + getChannelsFieldBuilder(); + } + } + @java.lang.Override + public Builder clear() { + super.clear(); + if (channelsBuilder_ == null) { + channels_ = java.util.Collections.emptyList(); + bitField0_ = (bitField0_ & ~0x00000001); + } else { + channelsBuilder_.clear(); + } + return this; + } + + @java.lang.Override + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return inr.numass.data.NumassProto.internal_static_inr_numass_data_Point_descriptor; + } + + @java.lang.Override + public inr.numass.data.NumassProto.Point getDefaultInstanceForType() { + return inr.numass.data.NumassProto.Point.getDefaultInstance(); + } + + @java.lang.Override + public inr.numass.data.NumassProto.Point build() { + inr.numass.data.NumassProto.Point result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + @java.lang.Override + public inr.numass.data.NumassProto.Point buildPartial() { + inr.numass.data.NumassProto.Point result = new inr.numass.data.NumassProto.Point(this); + int from_bitField0_ = bitField0_; + if (channelsBuilder_ == null) { + if (((bitField0_ & 0x00000001) == 0x00000001)) { + channels_ = java.util.Collections.unmodifiableList(channels_); + bitField0_ = (bitField0_ & ~0x00000001); + } + result.channels_ = channels_; + } else { + result.channels_ = channelsBuilder_.build(); + } + onBuilt(); + return result; + } + + @java.lang.Override + public Builder clone() { + return (Builder) super.clone(); + } + @java.lang.Override + public Builder setField( + com.google.protobuf.Descriptors.FieldDescriptor field, + java.lang.Object value) { + return (Builder) super.setField(field, value); + } + @java.lang.Override + public Builder clearField( + com.google.protobuf.Descriptors.FieldDescriptor field) { + return (Builder) super.clearField(field); + } + @java.lang.Override + public Builder clearOneof( + com.google.protobuf.Descriptors.OneofDescriptor oneof) { + return (Builder) super.clearOneof(oneof); + } + @java.lang.Override + public Builder setRepeatedField( + com.google.protobuf.Descriptors.FieldDescriptor field, + int index, java.lang.Object value) { + return (Builder) super.setRepeatedField(field, index, value); + } + @java.lang.Override + public Builder addRepeatedField( + com.google.protobuf.Descriptors.FieldDescriptor field, + java.lang.Object value) { + return (Builder) super.addRepeatedField(field, value); + } + @java.lang.Override + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof inr.numass.data.NumassProto.Point) { + return mergeFrom((inr.numass.data.NumassProto.Point)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(inr.numass.data.NumassProto.Point other) { + if (other == inr.numass.data.NumassProto.Point.getDefaultInstance()) return this; + if (channelsBuilder_ == null) { + if (!other.channels_.isEmpty()) { + if (channels_.isEmpty()) { + channels_ = other.channels_; + bitField0_ = (bitField0_ & ~0x00000001); + } else { + ensureChannelsIsMutable(); + channels_.addAll(other.channels_); + } + onChanged(); + } + } else { + if (!other.channels_.isEmpty()) { + if (channelsBuilder_.isEmpty()) { + channelsBuilder_.dispose(); + channelsBuilder_ = null; + channels_ = other.channels_; + bitField0_ = (bitField0_ & ~0x00000001); + channelsBuilder_ = + com.google.protobuf.GeneratedMessageV3.alwaysUseFieldBuilders ? + getChannelsFieldBuilder() : null; + } else { + channelsBuilder_.addAllMessages(other.channels_); + } + } + } + this.mergeUnknownFields(other.unknownFields); + onChanged(); + return this; + } + + @java.lang.Override + public final boolean isInitialized() { + return true; + } + + @java.lang.Override + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + inr.numass.data.NumassProto.Point parsedMessage = null; + try { + parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + parsedMessage = (inr.numass.data.NumassProto.Point) e.getUnfinishedMessage(); + throw e.unwrapIOException(); + } finally { + if (parsedMessage != null) { + mergeFrom(parsedMessage); + } + } + return this; + } + private int bitField0_; + + private java.util.List channels_ = + java.util.Collections.emptyList(); + private void ensureChannelsIsMutable() { + if (!((bitField0_ & 0x00000001) == 0x00000001)) { + channels_ = new java.util.ArrayList(channels_); + bitField0_ |= 0x00000001; + } + } + + private com.google.protobuf.RepeatedFieldBuilderV3< + inr.numass.data.NumassProto.Point.Channel, inr.numass.data.NumassProto.Point.Channel.Builder, inr.numass.data.NumassProto.Point.ChannelOrBuilder> channelsBuilder_; + + /** + *
+       * Array of measuring channels
+       * 
+ * + * repeated .inr.numass.data.Point.Channel channels = 1; + */ + public java.util.List getChannelsList() { + if (channelsBuilder_ == null) { + return java.util.Collections.unmodifiableList(channels_); + } else { + return channelsBuilder_.getMessageList(); + } + } + /** + *
+       * Array of measuring channels
+       * 
+ * + * repeated .inr.numass.data.Point.Channel channels = 1; + */ + public int getChannelsCount() { + if (channelsBuilder_ == null) { + return channels_.size(); + } else { + return channelsBuilder_.getCount(); + } + } + /** + *
+       * Array of measuring channels
+       * 
+ * + * repeated .inr.numass.data.Point.Channel channels = 1; + */ + public inr.numass.data.NumassProto.Point.Channel getChannels(int index) { + if (channelsBuilder_ == null) { + return channels_.get(index); + } else { + return channelsBuilder_.getMessage(index); + } + } + /** + *
+       * Array of measuring channels
+       * 
+ * + * repeated .inr.numass.data.Point.Channel channels = 1; + */ + public Builder setChannels( + int index, inr.numass.data.NumassProto.Point.Channel value) { + if (channelsBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + ensureChannelsIsMutable(); + channels_.set(index, value); + onChanged(); + } else { + channelsBuilder_.setMessage(index, value); + } + return this; + } + /** + *
+       * Array of measuring channels
+       * 
+ * + * repeated .inr.numass.data.Point.Channel channels = 1; + */ + public Builder setChannels( + int index, inr.numass.data.NumassProto.Point.Channel.Builder builderForValue) { + if (channelsBuilder_ == null) { + ensureChannelsIsMutable(); + channels_.set(index, builderForValue.build()); + onChanged(); + } else { + channelsBuilder_.setMessage(index, builderForValue.build()); + } + return this; + } + /** + *
+       * Array of measuring channels
+       * 
+ * + * repeated .inr.numass.data.Point.Channel channels = 1; + */ + public Builder addChannels(inr.numass.data.NumassProto.Point.Channel value) { + if (channelsBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + ensureChannelsIsMutable(); + channels_.add(value); + onChanged(); + } else { + channelsBuilder_.addMessage(value); + } + return this; + } + /** + *
+       * Array of measuring channels
+       * 
+ * + * repeated .inr.numass.data.Point.Channel channels = 1; + */ + public Builder addChannels( + int index, inr.numass.data.NumassProto.Point.Channel value) { + if (channelsBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + ensureChannelsIsMutable(); + channels_.add(index, value); + onChanged(); + } else { + channelsBuilder_.addMessage(index, value); + } + return this; + } + /** + *
+       * Array of measuring channels
+       * 
+ * + * repeated .inr.numass.data.Point.Channel channels = 1; + */ + public Builder addChannels( + inr.numass.data.NumassProto.Point.Channel.Builder builderForValue) { + if (channelsBuilder_ == null) { + ensureChannelsIsMutable(); + channels_.add(builderForValue.build()); + onChanged(); + } else { + channelsBuilder_.addMessage(builderForValue.build()); + } + return this; + } + /** + *
+       * Array of measuring channels
+       * 
+ * + * repeated .inr.numass.data.Point.Channel channels = 1; + */ + public Builder addChannels( + int index, inr.numass.data.NumassProto.Point.Channel.Builder builderForValue) { + if (channelsBuilder_ == null) { + ensureChannelsIsMutable(); + channels_.add(index, builderForValue.build()); + onChanged(); + } else { + channelsBuilder_.addMessage(index, builderForValue.build()); + } + return this; + } + /** + *
+       * Array of measuring channels
+       * 
+ * + * repeated .inr.numass.data.Point.Channel channels = 1; + */ + public Builder addAllChannels( + java.lang.Iterable values) { + if (channelsBuilder_ == null) { + ensureChannelsIsMutable(); + com.google.protobuf.AbstractMessageLite.Builder.addAll( + values, channels_); + onChanged(); + } else { + channelsBuilder_.addAllMessages(values); + } + return this; + } + /** + *
+       * Array of measuring channels
+       * 
+ * + * repeated .inr.numass.data.Point.Channel channels = 1; + */ + public Builder clearChannels() { + if (channelsBuilder_ == null) { + channels_ = java.util.Collections.emptyList(); + bitField0_ = (bitField0_ & ~0x00000001); + onChanged(); + } else { + channelsBuilder_.clear(); + } + return this; + } + /** + *
+       * Array of measuring channels
+       * 
+ * + * repeated .inr.numass.data.Point.Channel channels = 1; + */ + public Builder removeChannels(int index) { + if (channelsBuilder_ == null) { + ensureChannelsIsMutable(); + channels_.remove(index); + onChanged(); + } else { + channelsBuilder_.remove(index); + } + return this; + } + /** + *
+       * Array of measuring channels
+       * 
+ * + * repeated .inr.numass.data.Point.Channel channels = 1; + */ + public inr.numass.data.NumassProto.Point.Channel.Builder getChannelsBuilder( + int index) { + return getChannelsFieldBuilder().getBuilder(index); + } + /** + *
+       * Array of measuring channels
+       * 
+ * + * repeated .inr.numass.data.Point.Channel channels = 1; + */ + public inr.numass.data.NumassProto.Point.ChannelOrBuilder getChannelsOrBuilder( + int index) { + if (channelsBuilder_ == null) { + return channels_.get(index); } else { + return channelsBuilder_.getMessageOrBuilder(index); + } + } + /** + *
+       * Array of measuring channels
+       * 
+ * + * repeated .inr.numass.data.Point.Channel channels = 1; + */ + public java.util.List + getChannelsOrBuilderList() { + if (channelsBuilder_ != null) { + return channelsBuilder_.getMessageOrBuilderList(); + } else { + return java.util.Collections.unmodifiableList(channels_); + } + } + /** + *
+       * Array of measuring channels
+       * 
+ * + * repeated .inr.numass.data.Point.Channel channels = 1; + */ + public inr.numass.data.NumassProto.Point.Channel.Builder addChannelsBuilder() { + return getChannelsFieldBuilder().addBuilder( + inr.numass.data.NumassProto.Point.Channel.getDefaultInstance()); + } + /** + *
+       * Array of measuring channels
+       * 
+ * + * repeated .inr.numass.data.Point.Channel channels = 1; + */ + public inr.numass.data.NumassProto.Point.Channel.Builder addChannelsBuilder( + int index) { + return getChannelsFieldBuilder().addBuilder( + index, inr.numass.data.NumassProto.Point.Channel.getDefaultInstance()); + } + /** + *
+       * Array of measuring channels
+       * 
+ * + * repeated .inr.numass.data.Point.Channel channels = 1; + */ + public java.util.List + getChannelsBuilderList() { + return getChannelsFieldBuilder().getBuilderList(); + } + private com.google.protobuf.RepeatedFieldBuilderV3< + inr.numass.data.NumassProto.Point.Channel, inr.numass.data.NumassProto.Point.Channel.Builder, inr.numass.data.NumassProto.Point.ChannelOrBuilder> + getChannelsFieldBuilder() { + if (channelsBuilder_ == null) { + channelsBuilder_ = new com.google.protobuf.RepeatedFieldBuilderV3< + inr.numass.data.NumassProto.Point.Channel, inr.numass.data.NumassProto.Point.Channel.Builder, inr.numass.data.NumassProto.Point.ChannelOrBuilder>( + channels_, + ((bitField0_ & 0x00000001) == 0x00000001), + getParentForChildren(), + isClean()); + channels_ = null; + } + return channelsBuilder_; + } + @java.lang.Override + public final Builder setUnknownFields( + final com.google.protobuf.UnknownFieldSet unknownFields) { + return super.setUnknownFieldsProto3(unknownFields); + } + + @java.lang.Override + public final Builder mergeUnknownFields( + final com.google.protobuf.UnknownFieldSet unknownFields) { + return super.mergeUnknownFields(unknownFields); + } + + + // @@protoc_insertion_point(builder_scope:inr.numass.data.Point) + } + + // @@protoc_insertion_point(class_scope:inr.numass.data.Point) + private static final inr.numass.data.NumassProto.Point DEFAULT_INSTANCE; + static { + DEFAULT_INSTANCE = new inr.numass.data.NumassProto.Point(); + } + + public static inr.numass.data.NumassProto.Point getDefaultInstance() { + return DEFAULT_INSTANCE; + } + + private static final com.google.protobuf.Parser + PARSER = new com.google.protobuf.AbstractParser() { + @java.lang.Override + public Point parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return new Point(input, extensionRegistry); + } + }; + + public static com.google.protobuf.Parser parser() { + return PARSER; + } + + @java.lang.Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + + @java.lang.Override + public inr.numass.data.NumassProto.Point getDefaultInstanceForType() { + return DEFAULT_INSTANCE; + } + + } + + private static final com.google.protobuf.Descriptors.Descriptor + internal_static_inr_numass_data_Point_descriptor; + private static final + com.google.protobuf.GeneratedMessageV3.FieldAccessorTable + internal_static_inr_numass_data_Point_fieldAccessorTable; + private static final com.google.protobuf.Descriptors.Descriptor + internal_static_inr_numass_data_Point_Channel_descriptor; + private static final + com.google.protobuf.GeneratedMessageV3.FieldAccessorTable + internal_static_inr_numass_data_Point_Channel_fieldAccessorTable; + private static final com.google.protobuf.Descriptors.Descriptor + internal_static_inr_numass_data_Point_Channel_Block_descriptor; + private static final + com.google.protobuf.GeneratedMessageV3.FieldAccessorTable + internal_static_inr_numass_data_Point_Channel_Block_fieldAccessorTable; + private static final com.google.protobuf.Descriptors.Descriptor + internal_static_inr_numass_data_Point_Channel_Block_Frame_descriptor; + private static final + com.google.protobuf.GeneratedMessageV3.FieldAccessorTable + internal_static_inr_numass_data_Point_Channel_Block_Frame_fieldAccessorTable; + private static final com.google.protobuf.Descriptors.Descriptor + internal_static_inr_numass_data_Point_Channel_Block_Events_descriptor; + private static final + com.google.protobuf.GeneratedMessageV3.FieldAccessorTable + internal_static_inr_numass_data_Point_Channel_Block_Events_fieldAccessorTable; + + public static com.google.protobuf.Descriptors.FileDescriptor + getDescriptor() { + return descriptor; + } + private static com.google.protobuf.Descriptors.FileDescriptor + descriptor; + static { + java.lang.String[] descriptorData = { + "\n\034inr/numas/numass-proto.proto\022\017inr.numa" + + "ss.data\"\214\003\n\005Point\0220\n\010channels\030\001 \003(\0132\036.in" + + "r.numass.data.Point.Channel\032\320\002\n\007Channel\022" + + "\n\n\002id\030\001 \001(\004\0224\n\006blocks\030\002 \003(\0132$.inr.numass" + + ".data.Point.Channel.Block\032\202\002\n\005Block\022\014\n\004t" + + "ime\030\001 \001(\004\022:\n\006frames\030\002 \003(\0132*.inr.numass.d" + + "ata.Point.Channel.Block.Frame\022;\n\006events\030" + + "\003 \001(\0132+.inr.numass.data.Point.Channel.Bl" + + "ock.Events\022\016\n\006length\030\004 \001(\004\022\020\n\010bin_size\030\005" + + " \001(\004\032#\n\005Frame\022\014\n\004time\030\001 \001(\004\022\014\n\004data\030\002 \001(" + + "\014\032+\n\006Events\022\r\n\005times\030\001 \003(\004\022\022\n\namplitudes" + + "\030\002 \003(\004b\006proto3" + }; + com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = + new com.google.protobuf.Descriptors.FileDescriptor. InternalDescriptorAssigner() { + public com.google.protobuf.ExtensionRegistry assignDescriptors( + com.google.protobuf.Descriptors.FileDescriptor root) { + descriptor = root; + return null; + } + }; + com.google.protobuf.Descriptors.FileDescriptor + .internalBuildGeneratedFileFrom(descriptorData, + new com.google.protobuf.Descriptors.FileDescriptor[] { + }, assigner); + internal_static_inr_numass_data_Point_descriptor = + getDescriptor().getMessageTypes().get(0); + internal_static_inr_numass_data_Point_fieldAccessorTable = new + com.google.protobuf.GeneratedMessageV3.FieldAccessorTable( + internal_static_inr_numass_data_Point_descriptor, + new java.lang.String[] { "Channels", }); + internal_static_inr_numass_data_Point_Channel_descriptor = + internal_static_inr_numass_data_Point_descriptor.getNestedTypes().get(0); + internal_static_inr_numass_data_Point_Channel_fieldAccessorTable = new + com.google.protobuf.GeneratedMessageV3.FieldAccessorTable( + internal_static_inr_numass_data_Point_Channel_descriptor, + new java.lang.String[] { "Id", "Blocks", }); + internal_static_inr_numass_data_Point_Channel_Block_descriptor = + internal_static_inr_numass_data_Point_Channel_descriptor.getNestedTypes().get(0); + internal_static_inr_numass_data_Point_Channel_Block_fieldAccessorTable = new + com.google.protobuf.GeneratedMessageV3.FieldAccessorTable( + internal_static_inr_numass_data_Point_Channel_Block_descriptor, + new java.lang.String[] { "Time", "Frames", "Events", "Length", "BinSize", }); + internal_static_inr_numass_data_Point_Channel_Block_Frame_descriptor = + internal_static_inr_numass_data_Point_Channel_Block_descriptor.getNestedTypes().get(0); + internal_static_inr_numass_data_Point_Channel_Block_Frame_fieldAccessorTable = new + com.google.protobuf.GeneratedMessageV3.FieldAccessorTable( + internal_static_inr_numass_data_Point_Channel_Block_Frame_descriptor, + new java.lang.String[] { "Time", "Data", }); + internal_static_inr_numass_data_Point_Channel_Block_Events_descriptor = + internal_static_inr_numass_data_Point_Channel_Block_descriptor.getNestedTypes().get(1); + internal_static_inr_numass_data_Point_Channel_Block_Events_fieldAccessorTable = new + com.google.protobuf.GeneratedMessageV3.FieldAccessorTable( + internal_static_inr_numass_data_Point_Channel_Block_Events_descriptor, + new java.lang.String[] { "Times", "Amplitudes", }); + } + + // @@protoc_insertion_point(outer_class_scope) +} diff --git a/numass-core/numass-data-proto/src/main/kotlin/inr/numass/data/NumassEnvelopeType.kt b/numass-core/numass-data-proto/src/main/kotlin/inr/numass/data/NumassEnvelopeType.kt new file mode 100644 index 00000000..7a85fab5 --- /dev/null +++ b/numass-core/numass-data-proto/src/main/kotlin/inr/numass/data/NumassEnvelopeType.kt @@ -0,0 +1,140 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 inr.numass.data + +import hep.dataforge.io.envelopes.* +import hep.dataforge.values.Value +import hep.dataforge.values.parseValue +import org.slf4j.LoggerFactory +import java.io.IOException +import java.nio.ByteBuffer +import java.nio.channels.FileChannel +import java.nio.file.Path +import java.nio.file.StandardOpenOption +import java.util.* + +/** + * An envelope type for legacy numass tags. Reads legacy tag and writes DF02 tags + */ +class NumassEnvelopeType : EnvelopeType { + + override val code: Int = DefaultEnvelopeType.DEFAULT_ENVELOPE_CODE + + override val name: String = "numass" + + override fun description(): String = "Numass legacy envelope" + + /** + * Read as legacy + */ + override fun getReader(properties: Map): EnvelopeReader { + return NumassEnvelopeReader() + } + + /** + * Write as default + */ + override fun getWriter(properties: Map): EnvelopeWriter { + return DefaultEnvelopeWriter(this, MetaType.resolve(properties)) + } + + class LegacyTag : EnvelopeTag() { + override val startSequence: ByteArray + get() = LEGACY_START_SEQUENCE + + override val endSequence: ByteArray + get() = LEGACY_END_SEQUENCE + + /** + * Get the length of tag in bytes. -1 means undefined size in case tag was modified + * + * @return + */ + override val length: Int + get() = 30 + + /** + * Read leagscy version 1 tag without leading tag head + * + * @param buffer + * @return + * @throws IOException + */ + override fun readHeader(buffer: ByteBuffer): Map { + val res = HashMap() + + val type = buffer.getInt(2) + res[Envelope.TYPE_PROPERTY] = Value.of(type) + + val metaTypeCode = buffer.getShort(10) + val metaType = MetaType.resolve(metaTypeCode) + + if (metaType != null) { + res[Envelope.META_TYPE_PROPERTY] = metaType.name.parseValue() + } else { + LoggerFactory.getLogger(EnvelopeTag::class.java).warn("Could not resolve meta type. Using default") + } + + val metaLength = Integer.toUnsignedLong(buffer.getInt(14)) + res[Envelope.META_LENGTH_PROPERTY] = Value.of(metaLength) + val dataLength = Integer.toUnsignedLong(buffer.getInt(22)) + res[Envelope.DATA_LENGTH_PROPERTY] = Value.of(dataLength) + return res + } + } + + private class NumassEnvelopeReader : DefaultEnvelopeReader() { + override fun newTag(): EnvelopeTag { + return LegacyTag() + } + } + + companion object { + val INSTANCE = NumassEnvelopeType() + + val LEGACY_START_SEQUENCE = byteArrayOf('#'.toByte(), '!'.toByte()) + val LEGACY_END_SEQUENCE = byteArrayOf('!'.toByte(), '#'.toByte(), '\r'.toByte(), '\n'.toByte()) + + /** + * Replacement for standard type infer to include legacy type + * + * @param path + * @return + */ + fun infer(path: Path): EnvelopeType? { + return try { + FileChannel.open(path, StandardOpenOption.READ).use { + val buffer = it.map(FileChannel.MapMode.READ_ONLY, 0, 6) + when { + //TODO use templates from appropriate types + buffer.get(0) == '#'.toByte() && buffer.get(1) == '!'.toByte() -> INSTANCE + buffer.get(0) == '#'.toByte() && buffer.get(1) == '!'.toByte() && + buffer.get(4) == 'T'.toByte() && buffer.get(5) == 'L'.toByte() -> TaglessEnvelopeType.INSTANCE + buffer.get(0) == '#'.toByte() && buffer.get(1) == '~'.toByte() -> DefaultEnvelopeType.INSTANCE + else -> null + } + } + } catch (ex: Exception) { + LoggerFactory.getLogger(EnvelopeType::class.java).warn("Could not infer envelope type of file {} due to exception: {}", path, ex) + null + } + + } + + } + +} diff --git a/numass-core/numass-data-proto/src/main/kotlin/inr/numass/data/NumassFileEnvelope.kt b/numass-core/numass-data-proto/src/main/kotlin/inr/numass/data/NumassFileEnvelope.kt new file mode 100644 index 00000000..381a164c --- /dev/null +++ b/numass-core/numass-data-proto/src/main/kotlin/inr/numass/data/NumassFileEnvelope.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 inr.numass.data + +import hep.dataforge.meta.Meta +import hep.dataforge.storage.files.MutableFileEnvelope +import java.nio.ByteBuffer +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardOpenOption + +class NumassFileEnvelope(path: Path) : MutableFileEnvelope(path) { + + private val tag by lazy { Files.newByteChannel(path, StandardOpenOption.READ).use { NumassEnvelopeType.LegacyTag().read(it) } } + + override val dataOffset: Long by lazy { (tag.length + tag.metaSize).toLong() } + + override var dataLength: Int + get() = tag.dataSize + set(value) { + if (value > Int.MAX_VALUE) { + throw RuntimeException("Too large data block") + } + tag.dataSize = value + if (channel.write(tag.toBytes(), 0L) < tag.length) { + throw error("Tag is not overwritten.") + } + } + + + override val meta: Meta by lazy { + val buffer = ByteBuffer.allocate(tag.metaSize).also { + channel.read(it, tag.length.toLong()) + } + tag.metaType.reader.readBuffer(buffer) + } +} + diff --git a/numass-core/numass-data-proto/src/main/kotlin/inr/numass/data/ProtoNumassPoint.kt b/numass-core/numass-data-proto/src/main/kotlin/inr/numass/data/ProtoNumassPoint.kt new file mode 100644 index 00000000..6aa6ffc2 --- /dev/null +++ b/numass-core/numass-data-proto/src/main/kotlin/inr/numass/data/ProtoNumassPoint.kt @@ -0,0 +1,177 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 inr.numass.data + +import hep.dataforge.io.envelopes.Envelope +import hep.dataforge.meta.Meta +import inr.numass.data.api.* +import org.slf4j.LoggerFactory +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.nio.file.Path +import java.time.Duration +import java.time.Instant +import java.util.stream.IntStream +import java.util.stream.Stream +import java.util.zip.Inflater + +/** + * Protobuf based numass point + * Created by darksnake on 09.07.2017. + */ +class ProtoNumassPoint(override val meta: Meta, val protoBuilder: () -> NumassProto.Point) : NumassPoint { + + val proto: NumassProto.Point + get() = protoBuilder() + + override val blocks: List + get() = proto.channelsList + .flatMap { channel -> + channel.blocksList + .map { block -> ProtoBlock(channel.id.toInt(), block, this) } + .sortedBy { it.startTime } + } + + override val channels: Map + get() = proto.channelsList.groupBy { it.id.toInt() }.mapValues { entry -> + MetaBlock(entry.value.flatMap { it.blocksList }.map { ProtoBlock(entry.key, it, this) }) + } + + override val voltage: Double = meta.getDouble("external_meta.HV1_value", super.voltage) + + override val index: Int = meta.getInt("external_meta.point_index", super.index) + + override val startTime: Instant + get() = if (meta.hasValue("start_time")) { + meta.getValue("start_time").time + } else { + super.startTime + } + + override val length: Duration + get() = if (meta.hasValue("acquisition_time")) { + Duration.ofMillis((meta.getDouble("acquisition_time") * 1000).toLong()) + } else { + super.length + } + + companion object { + fun readFile(path: Path): ProtoNumassPoint { + return fromEnvelope(NumassFileEnvelope(path)) + } + + + /** + * Get valid data stream utilizing compression if it is present + */ + private fun Envelope.dataStream(): InputStream = if (this.meta.getString("compression", "none") == "zlib") { + //TODO move to new type of data + val inflatter = Inflater() + val array: ByteArray = with(data.buffer) { + if (hasArray()) { + array() + } else { + ByteArray(this.limit()).also { + this.position(0) + get(it) + } + } + } + inflatter.setInput(array) + val bos = ByteArrayOutputStream() + val buffer = ByteArray(8192) + while (!inflatter.finished()) { + val size = inflatter.inflate(buffer) + bos.write(buffer, 0, size) + } + val unzippeddata = bos.toByteArray() + inflatter.end() + ByteArrayInputStream(unzippeddata) + } else { + this.data.stream + } + + fun fromEnvelope(envelope: Envelope): ProtoNumassPoint { + return ProtoNumassPoint(envelope.meta) { + envelope.dataStream().use { + NumassProto.Point.parseFrom(it) + } + } + } + +// fun readFile(path: String, context: Context = Global): ProtoNumassPoint { +// return readFile(context.getFile(path).absolutePath) +// } + + fun ofEpochNanos(nanos: Long): Instant { + val seconds = Math.floorDiv(nanos, 1e9.toInt().toLong()) + val reminder = (nanos % 1e9).toInt() + return Instant.ofEpochSecond(seconds, reminder.toLong()) + } + } +} + +class ProtoBlock( + override val channel: Int, + private val block: NumassProto.Point.Channel.Block, + val parent: NumassPoint? = null +) : NumassBlock { + + override val startTime: Instant + get() = ProtoNumassPoint.ofEpochNanos(block.time) + + override val length: Duration = when { + block.length > 0 -> Duration.ofNanos(block.length) + parent?.meta?.hasValue("acquisition_time") ?: false -> + Duration.ofMillis((parent!!.meta.getDouble("acquisition_time") * 1000).toLong()) + parent?.meta?.hasValue("params.b_size") ?: false -> + Duration.ofNanos((parent!!.meta.getDouble("params.b_size") * 320).toLong()) + else -> { + error("No length information on block") +// LoggerFactory.getLogger(javaClass).warn("No length information on block. Trying to infer from first and last events") +// val times = events.map { it.timeOffset }.toList() +// val nanos = (times.max()!! - times.min()!!) +// Duration.ofNanos(nanos) +// Duration.ofMillis(380) + } + } + + override val events: Stream + get() = if (block.hasEvents()) { + val events = block.events + if (events.timesCount != events.amplitudesCount) { + LoggerFactory.getLogger(javaClass) + .error("The block is broken. Number of times is ${events.timesCount} and number of amplitudes is ${events.amplitudesCount}") + } + IntStream.range(0, events.timesCount) + .mapToObj { i -> NumassEvent(events.getAmplitudes(i).toShort(), events.getTimes(i), this) } + } else { + Stream.empty() + } + + + override val frames: Stream + get() { + val tickSize = Duration.ofNanos(block.binSize) + return block.framesList.stream().map { frame -> + val time = startTime.plusNanos(frame.time) + val data = frame.data.asReadOnlyByteBuffer() + NumassFrame(time, tickSize, data.asShortBuffer()) + } + } +} \ No newline at end of file diff --git a/numass-core/numass-data-proto/src/main/proto/inr/numas/numass-proto.proto b/numass-core/numass-data-proto/src/main/proto/inr/numas/numass-proto.proto new file mode 100644 index 00000000..454e8d8b --- /dev/null +++ b/numass-core/numass-data-proto/src/main/proto/inr/numas/numass-proto.proto @@ -0,0 +1,33 @@ +syntax = "proto3"; + +package inr.numass.data; + +message Point { + // A single channel for multichannel detector readout + message Channel { + //A continuous measurement block + message Block { + // Raw data frame + message Frame { + uint64 time = 1; // Time in nanos from the beginning of the block + bytes data = 2; // Frame data as an array of int16 mesured in arbitrary channels + } + // Event block obtained directly from device of from frame analysis + // In order to save space, times and amplitudes are in separate arrays. + // Amplitude and time with the same index correspond to the same event + message Events { + repeated uint64 times = 1; // Array of time in nanos from the beginning of the block + repeated uint64 amplitudes = 2; // Array of amplitudes of events in channels + } + + uint64 time = 1; // Block start in epoch nanos + repeated Frame frames = 2; // Frames array + Events events = 3; // Events array + uint64 length = 4; // block size in nanos. If missing, take from meta. + uint64 bin_size = 5; // tick size in nanos. Obsolete, to be removed + } + uint64 id = 1; // The number of measuring channel + repeated Block blocks = 2; // Blocks + } + repeated Channel channels = 1; // Array of measuring channels +} \ No newline at end of file diff --git a/numass-core/numass-signal-processing/build.gradle.kts b/numass-core/numass-signal-processing/build.gradle.kts new file mode 100644 index 00000000..708da62d --- /dev/null +++ b/numass-core/numass-signal-processing/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + idea + kotlin("jvm") +} + + +repositories { + mavenLocal() + mavenCentral() +} + +dependencies { + api(project(":dataforge-maths")) + api(project(":numass-core:numass-data-api")) + + // https://mvnrepository.com/artifact/org.apache.commons/commons-collections4 + implementation(group = "org.apache.commons", name = "commons-collections4", version = "4.3") + +} \ No newline at end of file diff --git a/numass-core/numass-signal-processing/src/main/kotlin/inr/numass/data/ChernovProcessor.kt b/numass-core/numass-signal-processing/src/main/kotlin/inr/numass/data/ChernovProcessor.kt new file mode 100644 index 00000000..d0c2c892 --- /dev/null +++ b/numass-core/numass-signal-processing/src/main/kotlin/inr/numass/data/ChernovProcessor.kt @@ -0,0 +1,100 @@ +package inr.numass.data + +import inr.numass.data.api.* +import org.apache.commons.collections4.queue.CircularFifoQueue +import org.apache.commons.math3.fitting.PolynomialCurveFitter +import org.apache.commons.math3.fitting.WeightedObservedPoint +import org.slf4j.LoggerFactory +import java.nio.ShortBuffer +import java.util.stream.Stream +import kotlin.streams.asStream + + +private fun ShortBuffer.clone(): ShortBuffer { + val clone = ShortBuffer.allocate(capacity()) + rewind()//copy from the beginning + clone.put(this) + rewind() + clone.flip() + return clone +} + + +class ChernovProcessor( + val threshold: Short, + val signalRange: IntRange, + val tickSize: Int = 320, + val signal: (Double) -> Double +) : SignalProcessor { + + private val fitter = PolynomialCurveFitter.create(2) + + private val signalMax = signal(0.0) + + /** + * position an amplitude of peak relative to buffer end (negative) + */ + private fun CircularFifoQueue.findMax(): Pair { + val data = this.mapIndexed { index, value -> + WeightedObservedPoint( + 1.0, + index.toDouble() - size + 1, // final point in zero + value.toDouble() + ) + } + val (c, b, a) = fitter.fit(data) + if (a > 0) error("Minimum!") + val x = -b / 2 / a + val y = -(b * b - 4 * a * c) / 4 / a + return x to y + } + + fun processBuffer(buffer: ShortBuffer): Sequence { + + val ringBuffer = CircularFifoQueue(5) + + fun roll() { + ringBuffer.add(buffer.get()) + } + + return sequence { + while (buffer.remaining() > 1) { + roll() + if (ringBuffer.isAtFullCapacity) { + if (ringBuffer.all { it > threshold && it <= ringBuffer[2] }) { + //Found bending, evaluating event + //TODO check end of frame + try { + val (pos, amp) = ringBuffer.findMax() + + val timeInTicks = (pos + buffer.position() - 1) + + val event = OrphanNumassEvent(amp.toShort(), (timeInTicks * tickSize).toLong()) + yield(event) + + //subtracting event from buffer copy + for (x in (signalRange.first + timeInTicks.toInt())..(signalRange.endInclusive + timeInTicks.toInt())) { + //TODO check all roundings + if (x >= 0 && x < buffer.limit()) { + val oldValue = buffer.get(x) + val newValue = oldValue - amp * signal(x - timeInTicks) / signalMax + buffer.put(x, newValue.toShort()) + } + } + println(buffer.array().joinToString()) + } catch (ex: Exception) { + LoggerFactory.getLogger(javaClass).error("Something went wrong", ex) + } + roll() + } + } + } + } + } + + override fun process(parent: NumassBlock, frame: NumassFrame): Stream { + val buffer = frame.signal.clone() + return processBuffer(buffer).map { it.adopt(parent) }.asStream() + } +} + diff --git a/numass-core/numass-signal-processing/src/test/kotlin/inr/numass/data/ChernovProcessorTest.kt b/numass-core/numass-signal-processing/src/test/kotlin/inr/numass/data/ChernovProcessorTest.kt new file mode 100644 index 00000000..6facc5a3 --- /dev/null +++ b/numass-core/numass-signal-processing/src/test/kotlin/inr/numass/data/ChernovProcessorTest.kt @@ -0,0 +1,26 @@ +package inr.numass.data + +import org.apache.commons.math3.analysis.function.Gaussian +import org.junit.Assert.assertTrue +import org.junit.Test +import java.nio.ShortBuffer + +class ChernovProcessorTest { + val gaussian = Gaussian(1000.0, 0.0, 3.0) + val processor = ChernovProcessor(10, -12..12, tickSize = 100) { gaussian.value(it) } + + val events = mapOf(10.0 to 1.0, 16.0 to 0.5) + + val buffer = ShortArray(40) { i -> + events.entries.sumByDouble { (pos, amp) -> amp * gaussian.value(pos - i.toDouble()) }.toShort() + } + + @Test + fun testPeaks() { + println(buffer.joinToString()) + val peaks = processor.processBuffer(ShortBuffer.wrap(buffer)).toList() + assertTrue(peaks.isNotEmpty()) + println(peaks.joinToString()) + } + +} \ No newline at end of file diff --git a/numass-core/src/main/kotlin/inr/numass/NumassProperties.kt b/numass-core/src/main/kotlin/inr/numass/NumassProperties.kt new file mode 100644 index 00000000..7f40b1af --- /dev/null +++ b/numass-core/src/main/kotlin/inr/numass/NumassProperties.kt @@ -0,0 +1,63 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package inr.numass + +import hep.dataforge.context.Global +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException +import java.util.* + +/** + * + * @author Alexander Nozik + */ +object NumassProperties { + + private val numassPropertiesFile: File + @Throws(IOException::class) + get() { + var file = File(Global.userDirectory, "numass") + if (!file.exists()) { + file.mkdirs() + } + file = File(file, "numass.cfg") + if (!file.exists()) { + file.createNewFile() + } + return file + } + + fun getNumassProperty(key: String): String? { + try { + val props = Properties() + props.load(FileInputStream(numassPropertiesFile)) + return props.getProperty(key) + } catch (ex: IOException) { + return null + } + + } + + @Synchronized + fun setNumassProperty(key: String, value: String?) { + try { + val props = Properties() + val store = numassPropertiesFile + props.load(FileInputStream(store)) + if (value == null) { + props.remove(key) + } else { + props.setProperty(key, value) + } + props.store(FileOutputStream(store), "") + } catch (ex: IOException) { + Global.logger.error("Failed to save numass properties", ex) + } + + } +} diff --git a/numass-core/src/main/kotlin/inr/numass/data/NumassDataUtils.kt b/numass-core/src/main/kotlin/inr/numass/data/NumassDataUtils.kt new file mode 100644 index 00000000..ab03bb28 --- /dev/null +++ b/numass-core/src/main/kotlin/inr/numass/data/NumassDataUtils.kt @@ -0,0 +1,123 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 inr.numass.data + +import hep.dataforge.io.envelopes.Envelope +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import inr.numass.data.api.* +import inr.numass.data.storage.ClassicNumassPoint +import org.slf4j.LoggerFactory +import kotlin.streams.asSequence + + +/** + * Created by darksnake on 30-Jan-17. + */ +object NumassDataUtils { + fun join(setName: String, sets: Collection): NumassSet { + return object : NumassSet { + override suspend fun getHvData() = TODO() + + override val points: List by lazy { + val points = sets.flatMap { it.points }.groupBy { it.voltage } + return@lazy points.entries.map { entry -> SimpleNumassPoint.build(entry.value, entry.key) } + } + + override val meta: Meta by lazy { + val metaBuilder = MetaBuilder() + sets.forEach { set -> metaBuilder.putNode(set.name, set.meta) } + metaBuilder + } + + override val name = setName + } + } + + fun joinByIndex(setName: String, sets: Collection): NumassSet { + return object : NumassSet { + override suspend fun getHvData() = TODO() + + override val points: List by lazy { + val points = sets.flatMap { it.points }.groupBy { it.index } + return@lazy points.mapNotNull { (index, points) -> + val voltage = points.first().voltage + if (!points.all { it.voltage == voltage }) { + LoggerFactory.getLogger(javaClass) + .warn("Not all points with index $index have voltage $voltage") + null + } else { + SimpleNumassPoint.build(points, voltage, index) + } + } + } + + override val meta: Meta by lazy { + val metaBuilder = MetaBuilder() + sets.forEach { set -> metaBuilder.putNode(set.name, set.meta) } + metaBuilder + } + + override val name = setName + } + } + + + fun adapter(): SpectrumAdapter { + return SpectrumAdapter("Uset", "CR", "CRerr", "Time") + } + + fun read(envelope: Envelope): NumassPoint { + return if (envelope.dataType?.startsWith("numass.point.classic") ?: envelope.meta.hasValue("split")) { + ClassicNumassPoint(envelope) + } else { + ProtoNumassPoint.fromEnvelope(envelope) + } + } +} + +suspend fun NumassBlock.transformChain(transform: (NumassEvent, NumassEvent) -> Pair?): NumassBlock { + return SimpleBlock.produce(this.startTime, this.length) { + this.events.asSequence() + .sortedBy { it.timeOffset } + .zipWithNext(transform) + .filterNotNull() + .map { OrphanNumassEvent(it.first, it.second) }.asIterable() + } +} + +suspend fun NumassBlock.filterChain(condition: (NumassEvent, NumassEvent) -> Boolean): NumassBlock { + return SimpleBlock.produce(this.startTime, this.length) { + this.events.asSequence() + .sortedBy { it.timeOffset } + .zipWithNext().filter { condition.invoke(it.first, it.second) }.map { it.second }.asIterable() + } +} + +suspend fun NumassBlock.filter(condition: (NumassEvent) -> Boolean): NumassBlock { + return SimpleBlock.produce(this.startTime, this.length) { + this.events.asSequence().filter(condition).asIterable() + } +} + +suspend fun NumassBlock.transform(transform: (NumassEvent) -> OrphanNumassEvent): NumassBlock { + return SimpleBlock.produce(this.startTime, this.length) { + this.events.asSequence() + .map { transform(it) } + .asIterable() + } +} \ No newline at end of file diff --git a/numass-core/src/main/kotlin/inr/numass/data/SpectrumAdapter.kt b/numass-core/src/main/kotlin/inr/numass/data/SpectrumAdapter.kt new file mode 100644 index 00000000..0fa7169c --- /dev/null +++ b/numass-core/src/main/kotlin/inr/numass/data/SpectrumAdapter.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.data + +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.tables.Adapters.* +import hep.dataforge.tables.BasicAdapter +import hep.dataforge.tables.ValuesAdapter +import hep.dataforge.values.Value +import hep.dataforge.values.ValueMap +import hep.dataforge.values.Values +import hep.dataforge.values.asValue +import java.lang.Math.sqrt +import java.util.* +import java.util.stream.Stream + +/** + * @author Darksnake + */ +class SpectrumAdapter : BasicAdapter { + + constructor(meta: Meta) : super(meta) {} + + constructor(xName: String, yName: String, yErrName: String, measurementTime: String) : super(MetaBuilder(ValuesAdapter.ADAPTER_KEY) + .setValue(X_VALUE_KEY, xName) + .setValue(Y_VALUE_KEY, yName) + .setValue(Y_ERROR_KEY, yErrName) + .setValue(POINT_LENGTH_NAME, measurementTime) + .build() + ) { + } + + constructor(xName: String, yName: String, measurementTime: String) : super(MetaBuilder(ValuesAdapter.ADAPTER_KEY) + .setValue(X_VALUE_KEY, xName) + .setValue(Y_VALUE_KEY, yName) + .setValue(POINT_LENGTH_NAME, measurementTime) + .build() + ) { + } + + fun getTime(point: Values): Double { + return this.optComponent(point, POINT_LENGTH_NAME).map { it.double }.orElse(1.0) + } + + fun buildSpectrumDataPoint(x: Double, count: Long, t: Double): Values { + return ValueMap.of(arrayOf(getComponentName(X_VALUE_KEY), getComponentName(Y_VALUE_KEY), getComponentName(POINT_LENGTH_NAME)), + x, count, t) + } + + fun buildSpectrumDataPoint(x: Double, count: Long, countErr: Double, t: Double): Values { + return ValueMap.of( + arrayOf(getComponentName(X_VALUE_KEY), getComponentName(Y_VALUE_KEY), getComponentName(Y_ERROR_KEY), getComponentName(POINT_LENGTH_NAME)), + x, count, countErr, t + ) + } + + + override fun optComponent(values: Values, component: String): Optional { + when (component) { + "count" -> return super.optComponent(values, Y_VALUE_KEY) + Y_VALUE_KEY -> return super.optComponent(values, Y_VALUE_KEY) + .map { it -> it.double / getTime(values) } + .map { it.asValue() } + Y_ERROR_KEY -> { + val err = super.optComponent(values, Y_ERROR_KEY) + return if (err.isPresent) { + Optional.of(Value.of(err.get().double / getTime(values))) + } else { + val y = getComponent(values, Y_VALUE_KEY).double + when { + y < 0 -> Optional.empty() + y == 0.0 -> //avoid infinite weights + Optional.of(Value.of(1.0 / sqrt(getTime(values)))) + else -> Optional.of(Value.of(sqrt(y / getTime(values)))) + } + } + } + + else -> return super.optComponent(values, component) + } + } + + override fun listComponents(): Stream { + return Stream.concat(super.listComponents(), Stream.of(X_VALUE_KEY, Y_VALUE_KEY, POINT_LENGTH_NAME)).distinct() + } + + companion object { + private const val POINT_LENGTH_NAME = "time" + } +} diff --git a/numass-core/src/main/kotlin/inr/numass/data/analyzers/AbstractAnalyzer.kt b/numass-core/src/main/kotlin/inr/numass/data/analyzers/AbstractAnalyzer.kt new file mode 100644 index 00000000..4c4b4684 --- /dev/null +++ b/numass-core/src/main/kotlin/inr/numass/data/analyzers/AbstractAnalyzer.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2017 Alexander Nozik. + * + * 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 inr.numass.data.analyzers + +import hep.dataforge.meta.Meta +import hep.dataforge.tables.Adapters.* +import hep.dataforge.tables.ListTable +import hep.dataforge.tables.Table +import hep.dataforge.tables.TableFormat +import hep.dataforge.tables.TableFormatBuilder +import hep.dataforge.toList +import inr.numass.data.api.NumassBlock +import inr.numass.data.api.NumassEvent +import inr.numass.data.api.NumassPoint.Companion.HV_KEY +import inr.numass.data.api.NumassSet +import inr.numass.data.api.SignalProcessor +import java.util.stream.Stream + +/** + * Created by darksnake on 11.07.2017. + */ +abstract class AbstractAnalyzer @JvmOverloads constructor(private val processor: SignalProcessor? = null) : + NumassAnalyzer { + + /** + * Return unsorted stream of events including events from frames. + * In theory, events after processing could be unsorted due to mixture of frames and events. + * In practice usually block have either frame or events, but not both. + * + * @param block + * @return + */ + override fun getEvents(block: NumassBlock, meta: Meta): List { + val range = meta.getRange() + return getAllEvents(block).filter { event -> + event.amplitude.toInt() in range + }.toList() + } + + protected fun Meta.getRange(): IntRange { + val loChannel = getInt("window.lo", 0) + val upChannel = getInt("window.up", Integer.MAX_VALUE) + return loChannel until upChannel + } + + protected fun getAllEvents(block: NumassBlock): Stream { + return when { + block.frames.count() == 0L -> block.events + processor == null -> throw IllegalArgumentException("Signal processor needed to analyze frames") + else -> Stream.concat(block.events, block.frames.flatMap { processor.process(block, it) }) + } + } + + /** + * Get table format for summary table + * + * @param config + * @return + */ + protected open fun getTableFormat(config: Meta): TableFormat { + return TableFormatBuilder() + .addNumber(HV_KEY, X_VALUE_KEY) + .addNumber(NumassAnalyzer.LENGTH_KEY) + .addNumber(NumassAnalyzer.COUNT_KEY) + .addNumber(NumassAnalyzer.COUNT_RATE_KEY, Y_VALUE_KEY) + .addNumber(NumassAnalyzer.COUNT_RATE_ERROR_KEY, Y_ERROR_KEY) + .addColumn(NumassAnalyzer.WINDOW_KEY) + .addTime() + .build() + } + + + override fun analyzeSet(set: NumassSet, config: Meta): Table { + val format = getTableFormat(config) + + return ListTable.Builder(format) + .rows(set.points.map { point -> analyzeParent(point, config) }) + .build() + } + + companion object { + val NAME_LIST = arrayOf( + NumassAnalyzer.LENGTH_KEY, + NumassAnalyzer.COUNT_KEY, + NumassAnalyzer.COUNT_RATE_KEY, + NumassAnalyzer.COUNT_RATE_ERROR_KEY, + NumassAnalyzer.WINDOW_KEY, + NumassAnalyzer.TIME_KEY + ) + } +} diff --git a/numass-core/src/main/kotlin/inr/numass/data/analyzers/DebunchAnalyzer.kt b/numass-core/src/main/kotlin/inr/numass/data/analyzers/DebunchAnalyzer.kt new file mode 100644 index 00000000..36910827 --- /dev/null +++ b/numass-core/src/main/kotlin/inr/numass/data/analyzers/DebunchAnalyzer.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2017 Alexander Nozik. + * + * 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 inr.numass.data.analyzers + +import hep.dataforge.meta.Meta +import hep.dataforge.values.Values +import inr.numass.data.api.NumassBlock +import inr.numass.data.api.SignalProcessor + +/** + * Block analyzer that can perform debunching + * Created by darksnake on 11.07.2017. + */ +class DebunchAnalyzer @JvmOverloads constructor(private val processor: SignalProcessor? = null) : AbstractAnalyzer(processor) { + + override fun analyze(block: NumassBlock, config: Meta): Values { + TODO() + } +} diff --git a/numass-core/src/main/kotlin/inr/numass/data/analyzers/NumassAnalyzer.kt b/numass-core/src/main/kotlin/inr/numass/data/analyzers/NumassAnalyzer.kt new file mode 100644 index 00000000..28b14911 --- /dev/null +++ b/numass-core/src/main/kotlin/inr/numass/data/analyzers/NumassAnalyzer.kt @@ -0,0 +1,279 @@ +/* + * Copyright 2017 Alexander Nozik. + * + * 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 inr.numass.data.analyzers + +import hep.dataforge.meta.Meta +import hep.dataforge.tables.* +import hep.dataforge.tables.Adapters.* +import hep.dataforge.values.Value +import hep.dataforge.values.ValueMap +import hep.dataforge.values.Values +import inr.numass.data.api.* +import inr.numass.data.api.NumassPoint.Companion.HV_KEY +import java.util.* +import java.util.concurrent.atomic.AtomicLong +import java.util.concurrent.atomic.AtomicReference +import java.util.stream.IntStream +import kotlin.streams.asSequence + +/** + * A general raw data analysis utility. Could have different implementations + * Created by darksnake on 06-Jul-17. + */ +interface NumassAnalyzer { + + /** + * Perform analysis on block. The values for count rate, its error and point length in nanos must + * exist, but occasionally additional values could also be presented. + * + * @param block + * @return + */ + fun analyze(block: NumassBlock, config: Meta = Meta.empty()): Values + + /** + * Analysis result for point including hv information + * @param point + * @param config + * @return + */ + fun analyzeParent(point: ParentBlock, config: Meta = Meta.empty()): Values { +// //Add properties to config +// val newConfig = config.builder.apply { +// if (point is NumassPoint) { +// setValue("voltage", point.voltage) +// setValue("index", point.index) +// } +// setValue("channel", point.channel) +// } + val map = HashMap(analyze(point, config).asMap()) + if (point is NumassPoint) { + map[HV_KEY] = Value.of(point.voltage) + } + + return ValueMap(map) + } + + /** + * Return unsorted stream of events including events from frames + * + * @param block + * @return + */ + fun getEvents(block: NumassBlock, meta: Meta = Meta.empty()): List + + /** + * Analyze the whole set. And return results as a table + * + * @param set + * @param config + * @return + */ + fun analyzeSet(set: NumassSet, config: Meta): Table + + /** + * Get the approximate number of events in block. Not all analyzers support precise event counting + * + * @param block + * @param config + * @return + */ + fun getCount(block: NumassBlock, config: Meta): Long { + return analyze(block, config).getValue(COUNT_KEY).number.toLong() + } + + /** + * Get approximate effective point length in nanos. It is not necessary corresponds to real point length. + * + * @param block + * @param config + * @return + */ + fun getLength(block: NumassBlock, config: Meta = Meta.empty()): Long { + return analyze(block, config).getValue(LENGTH_KEY).number.toLong() + } + + fun getAmplitudeSpectrum(block: NumassBlock, config: Meta = Meta.empty()): Table { + val seconds = block.length.toMillis().toDouble() / 1000.0 + return getEvents(block, config).asSequence().getAmplitudeSpectrum(seconds, config) + } + + companion object { + const val CHANNEL_KEY = "channel" + const val COUNT_KEY = "count" + const val LENGTH_KEY = "length" + const val COUNT_RATE_KEY = "cr" + const val COUNT_RATE_ERROR_KEY = "crErr" + + const val WINDOW_KEY = "window" + const val TIME_KEY = "timestamp" + + val AMPLITUDE_ADAPTER: ValuesAdapter = Adapters.buildXYAdapter(CHANNEL_KEY, COUNT_RATE_KEY) + +// val MAX_CHANNEL = 10000 + } +} + +/** + * Calculate number of counts in the given channel + * + * @param spectrum + * @param loChannel + * @param upChannel + * @return + */ +fun Table.countInWindow(loChannel: Short, upChannel: Short): Long { + return this.rows.filter { row -> + row.getInt(NumassAnalyzer.CHANNEL_KEY) in loChannel..(upChannel - 1) + }.mapToLong { it -> it.getValue(NumassAnalyzer.COUNT_KEY).number.toLong() }.sum() +} + +/** + * Calculate the amplitude spectrum for a given block. The s + * + * @param this@getAmplitudeSpectrum + * @param length length in seconds, used for count rate calculation + * @param config + * @return + */ +fun Sequence.getAmplitudeSpectrum( + length: Double, + config: Meta = Meta.empty() +): Table { + val format = TableFormatBuilder() + .addNumber(NumassAnalyzer.CHANNEL_KEY, X_VALUE_KEY) + .addNumber(NumassAnalyzer.COUNT_KEY) + .addNumber(NumassAnalyzer.COUNT_RATE_KEY, Y_VALUE_KEY) + .addNumber(NumassAnalyzer.COUNT_RATE_ERROR_KEY, Y_ERROR_KEY) + .updateMeta { metaBuilder -> metaBuilder.setNode("config", config) } + .build() + + //optimized for fastest computation + val spectrum: MutableMap = HashMap() + forEach { event -> + val channel = event.amplitude.toInt() + spectrum.getOrPut(channel) { + AtomicLong(0) + }.incrementAndGet() + } + + + val minChannel = config.getInt("window.lo") { spectrum.keys.min() ?: 0 } + val maxChannel = config.getInt("window.up") { spectrum.keys.max() ?: 4096 } + + return ListTable.Builder(format) + .rows(IntStream.range(minChannel, maxChannel) + .mapToObj { i -> + val value = spectrum[i]?.get() ?: 0 + ValueMap.of( + format.namesAsArray(), + i, + value, + value.toDouble() / length, + Math.sqrt(value.toDouble()) / length + ) + } + ).build() +} + +/** + * Apply window and binning to a spectrum. Empty bins are filled with zeroes + * + * @param binSize + * @param loChannel autodefined if negative + * @param upChannel autodefined if negative + * @return + */ +@JvmOverloads +fun Table.withBinning(binSize: Int, loChannel: Int? = null, upChannel: Int? = null): Table { + val format = TableFormatBuilder() + .addNumber(NumassAnalyzer.CHANNEL_KEY, X_VALUE_KEY) + .addNumber(NumassAnalyzer.COUNT_KEY, Y_VALUE_KEY) + .addNumber(NumassAnalyzer.COUNT_RATE_KEY) + .addNumber(NumassAnalyzer.COUNT_RATE_ERROR_KEY) + .addNumber("binSize") + val builder = ListTable.Builder(format) + + var chan = loChannel + ?: this.getColumn(NumassAnalyzer.CHANNEL_KEY).stream().mapToInt { it.int }.min().orElse(0) + + val top = upChannel + ?: this.getColumn(NumassAnalyzer.CHANNEL_KEY).stream().mapToInt { it.int }.max().orElse(1) + + while (chan < top - binSize) { + val count = AtomicLong(0) + val countRate = AtomicReference(0.0) + val countRateDispersion = AtomicReference(0.0) + + val binLo = chan + val binUp = chan + binSize + + this.rows.filter { row -> + row.getInt(NumassAnalyzer.CHANNEL_KEY) in binLo..(binUp - 1) + }.forEach { row -> + count.addAndGet(row.getValue(NumassAnalyzer.COUNT_KEY, 0).long) + countRate.accumulateAndGet(row.getDouble(NumassAnalyzer.COUNT_RATE_KEY, 0.0)) { d1, d2 -> d1 + d2 } + countRateDispersion.accumulateAndGet( + Math.pow( + row.getDouble(NumassAnalyzer.COUNT_RATE_ERROR_KEY, 0.0), + 2.0 + ) + ) { d1, d2 -> d1 + d2 } + } + val bin = Math.min(binSize, top - chan) + builder.row( + chan.toDouble() + bin.toDouble() / 2.0, + count.get(), + countRate.get(), + Math.sqrt(countRateDispersion.get()), + bin + ) + chan += binSize + } + return builder.build() +} + +/** + * Subtract reference spectrum. + * + * @param sp1 + * @param sp2 + * @return + */ +fun subtractAmplitudeSpectrum(sp1: Table, sp2: Table): Table { + val format = TableFormatBuilder() + .addNumber(NumassAnalyzer.CHANNEL_KEY, X_VALUE_KEY) + .addNumber(NumassAnalyzer.COUNT_RATE_KEY, Y_VALUE_KEY) + .addNumber(NumassAnalyzer.COUNT_RATE_ERROR_KEY, Y_ERROR_KEY) + .build() + + val builder = ListTable.Builder(format) + + sp1.forEach { row1 -> + val channel = row1.getDouble(NumassAnalyzer.CHANNEL_KEY) + val row2 = sp2.rows.asSequence().find { it.getDouble(NumassAnalyzer.CHANNEL_KEY) == channel } + ?: ValueMap.ofPairs(NumassAnalyzer.COUNT_RATE_KEY to 0.0, NumassAnalyzer.COUNT_RATE_ERROR_KEY to 0.0) + + val value = + Math.max(row1.getDouble(NumassAnalyzer.COUNT_RATE_KEY) - row2.getDouble(NumassAnalyzer.COUNT_RATE_KEY), 0.0) + val error1 = row1.getDouble(NumassAnalyzer.COUNT_RATE_ERROR_KEY) + val error2 = row2.getDouble(NumassAnalyzer.COUNT_RATE_ERROR_KEY) + val error = Math.sqrt(error1 * error1 + error2 * error2) + builder.row(channel, value, error) + } + return builder.build() +} \ No newline at end of file diff --git a/numass-core/src/main/kotlin/inr/numass/data/analyzers/SimpleAnalyzer.kt b/numass-core/src/main/kotlin/inr/numass/data/analyzers/SimpleAnalyzer.kt new file mode 100644 index 00000000..4c63f61c --- /dev/null +++ b/numass-core/src/main/kotlin/inr/numass/data/analyzers/SimpleAnalyzer.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2017 Alexander Nozik. + * + * 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 inr.numass.data.analyzers + +import hep.dataforge.description.ValueDef +import hep.dataforge.meta.Meta +import hep.dataforge.values.ValueMap +import hep.dataforge.values.ValueType +import hep.dataforge.values.Values +import inr.numass.data.api.NumassBlock +import inr.numass.data.api.SignalProcessor + +/** + * A simple event counter + * Created by darksnake on 07.07.2017. + */ +@ValueDef(key = "deadTime", type = [ValueType.NUMBER], def = "0.0", info = "Dead time in nanoseconds for correction") +class SimpleAnalyzer @JvmOverloads constructor(private val processor: SignalProcessor? = null) : AbstractAnalyzer(processor) { + + + override fun analyze(block: NumassBlock, config: Meta): Values { + val loChannel = config.getInt("window.lo", 0) + val upChannel = config.getInt("window.up", Integer.MAX_VALUE) + + val count = getEvents(block, config).count() + val length = block.length.toNanos().toDouble() / 1e9 + + val deadTime = config.getDouble("deadTime", 0.0) + + val countRate = if (deadTime > 0) { + val mu = count.toDouble() / length + mu / (1.0 - deadTime * 1e-9 * mu) + } else { + count.toDouble() / length + } + val countRateError = Math.sqrt(count.toDouble()) / length + + return ValueMap.of(AbstractAnalyzer.NAME_LIST, + length, + count, + countRate, + countRateError, + arrayOf(loChannel, upChannel), + block.startTime) + } + + +} diff --git a/numass-core/src/main/kotlin/inr/numass/data/analyzers/TimeAnalyzer.kt b/numass-core/src/main/kotlin/inr/numass/data/analyzers/TimeAnalyzer.kt new file mode 100644 index 00000000..303a0dc1 --- /dev/null +++ b/numass-core/src/main/kotlin/inr/numass/data/analyzers/TimeAnalyzer.kt @@ -0,0 +1,297 @@ +/* + * Copyright 2017 Alexander Nozik. + * + * 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 inr.numass.data.analyzers + +import hep.dataforge.description.ValueDef +import hep.dataforge.description.ValueDefs +import hep.dataforge.meta.Meta +import hep.dataforge.tables.Adapters.* +import hep.dataforge.tables.TableFormat +import hep.dataforge.tables.TableFormatBuilder +import hep.dataforge.values.* +import inr.numass.data.analyzers.NumassAnalyzer.Companion.COUNT_KEY +import inr.numass.data.analyzers.NumassAnalyzer.Companion.COUNT_RATE_ERROR_KEY +import inr.numass.data.analyzers.NumassAnalyzer.Companion.COUNT_RATE_KEY +import inr.numass.data.analyzers.NumassAnalyzer.Companion.LENGTH_KEY +import inr.numass.data.analyzers.TimeAnalyzer.AveragingMethod.* +import inr.numass.data.api.* +import inr.numass.data.api.NumassPoint.Companion.HV_KEY +import java.util.* +import java.util.concurrent.atomic.AtomicLong +import kotlin.collections.List +import kotlin.collections.asSequence +import kotlin.collections.count +import kotlin.collections.first +import kotlin.collections.map +import kotlin.collections.set +import kotlin.collections.sortBy +import kotlin.collections.sumBy +import kotlin.collections.sumByDouble +import kotlin.collections.toMutableList +import kotlin.math.* +import kotlin.streams.asSequence + + +/** + * An analyzer which uses time information from events + * Created by darksnake on 11.07.2017. + */ +@ValueDefs( + ValueDef( + key = "separateParallelBlocks", + type = [ValueType.BOOLEAN], + info = "If true, then parallel blocks will be forced to be evaluated separately" + ), + ValueDef( + key = "chunkSize", + type = [ValueType.NUMBER], + def = "-1", + info = "The number of events in chunk to split the chain into. If negative, no chunks are used" + ) +) +open class TimeAnalyzer(processor: SignalProcessor? = null) : AbstractAnalyzer(processor) { + + override fun analyze(block: NumassBlock, config: Meta): Values { + //In case points inside points + if (block is ParentBlock && (block.isSequential || config.getBoolean("separateParallelBlocks", false))) { + return analyzeParent(block, config) + } + + val t0 = getT0(block, config).toLong() + + val chunkSize = config.getInt("chunkSize", -1) + + val count = super.getEvents(block, config).count() + val length = block.length.toNanos().toDouble() / 1e9 + + val res = when { + count < 1000 -> ValueMap.ofPairs( + LENGTH_KEY to length, + COUNT_KEY to count, + COUNT_RATE_KEY to count.toDouble() / length, + COUNT_RATE_ERROR_KEY to sqrt(count.toDouble()) / length + ) + chunkSize > 0 -> getEventsWithDelay(block, config) + .chunked(chunkSize) { analyzeSequence(it.asSequence(), t0) } + .toList() + .mean(config.getEnum("mean", WEIGHTED)) + else -> analyzeSequence(getEventsWithDelay(block, config), t0) + } + + return ValueMap.Builder(res) + .putValue("blockLength", length) + .putValue(NumassAnalyzer.WINDOW_KEY, config.getRange()) + .putValue(NumassAnalyzer.TIME_KEY, block.startTime) + .putValue(T0_KEY, t0.toDouble() / 1000.0) + .build() + } + + + private fun analyzeSequence(sequence: Sequence>, t0: Long): Values { + val totalN = AtomicLong(0) + val totalT = AtomicLong(0) + sequence.filter { pair -> pair.second >= t0 } + .forEach { pair -> + totalN.incrementAndGet() + //TODO add progress listener here + totalT.addAndGet(pair.second) + } + + if (totalN.toInt() == 0) { + error("Zero number of intervals") + } + + val countRate = + 1e6 * totalN.get() / (totalT.get() / 1000 - t0 * totalN.get() / 1000)//1e9 / (totalT.get() / totalN.get() - t0); + val countRateError = countRate / sqrt(totalN.get().toDouble()) + val length = totalT.get() / 1e9 + val count = (length * countRate).toLong() + + return ValueMap.ofPairs( + LENGTH_KEY to length, + COUNT_KEY to count, + COUNT_RATE_KEY to countRate, + COUNT_RATE_ERROR_KEY to countRateError + ) + + } + + override fun analyzeParent(point: ParentBlock, config: Meta): Values { + //Average count rates, do not sum events + val res = point.blocks.map { it -> analyze(it, config) } + + val map = HashMap(res.mean(config.getEnum("mean", WEIGHTED)).asMap()) + if (point is NumassPoint) { + map[HV_KEY] = Value.of(point.voltage) + } + return ValueMap(map) + } + + enum class AveragingMethod { + ARITHMETIC, + WEIGHTED, + GEOMETRIC + } + + /** + * Combine multiple blocks from the same point into one + * + * @return + */ + private fun List.mean(method: AveragingMethod): Values { + + if (this.isEmpty()) { + return ValueMap.Builder() + .putValue(LENGTH_KEY, 0) + .putValue(COUNT_KEY, 0) + .putValue(COUNT_RATE_KEY, 0) + .putValue(COUNT_RATE_ERROR_KEY, 0) + .build() + } + + val totalTime = sumByDouble { it.getDouble(LENGTH_KEY) } + + val (countRate, countRateDispersion) = when (method) { + ARITHMETIC -> Pair( + sumByDouble { it.getDouble(COUNT_RATE_KEY) } / size, + sumByDouble { it.getDouble(COUNT_RATE_ERROR_KEY).pow(2.0) } / size / size + ) + WEIGHTED -> Pair( + sumByDouble { it.getDouble(COUNT_RATE_KEY) * it.getDouble(LENGTH_KEY) } / totalTime, + sumByDouble { (it.getDouble(COUNT_RATE_ERROR_KEY) * it.getDouble(LENGTH_KEY) / totalTime).pow(2.0) } + ) + GEOMETRIC -> { + val mean = exp(sumByDouble { ln(it.getDouble(COUNT_RATE_KEY)) } / size) + val variance = (mean / size).pow(2.0) * sumByDouble { + (it.getDouble(COUNT_RATE_ERROR_KEY) / it.getDouble( + COUNT_RATE_KEY + )).pow(2.0) + } + Pair(mean, variance) + } + } + + return ValueMap.Builder(first()) + .putValue(LENGTH_KEY, totalTime) + .putValue(COUNT_KEY, sumBy { it.getInt(COUNT_KEY) }) + .putValue(COUNT_RATE_KEY, countRate) + .putValue(COUNT_RATE_ERROR_KEY, sqrt(countRateDispersion)) + .build() + } + + @ValueDefs( + ValueDef(key = "t0", type = arrayOf(ValueType.NUMBER), info = "Constant t0 cut"), + ValueDef( + key = "t0.crFraction", + type = arrayOf(ValueType.NUMBER), + info = "The relative fraction of events that should be removed by time cut" + ), + ValueDef(key = "t0.min", type = arrayOf(ValueType.NUMBER), def = "0", info = "Minimal t0") + ) + protected fun getT0(block: NumassBlock, meta: Meta): Int { + return if (meta.hasValue("t0")) { + meta.getInt("t0") + } else if (meta.hasMeta("t0")) { + val fraction = meta.getDouble("t0.crFraction") + val cr = estimateCountRate(block) + if (cr < meta.getDouble("t0.minCR", 0.0)) { + 0 + } else { + max(-1e9 / cr * ln(1.0 - fraction), meta.getDouble("t0.min", 0.0)).toInt() + } + } else { + 0 + } + + } + + private fun estimateCountRate(block: NumassBlock): Double { + return block.events.count().toDouble() / block.length.toMillis() * 1000 + } + + fun zipEvents(block: NumassBlock, config: Meta): Sequence> { + return getAllEvents(block).asSequence().zipWithNext() + } + + /** + * The chain of event with delays in nanos + * + * @param block + * @param config + * @return + */ + fun getEventsWithDelay(block: NumassBlock, config: Meta): Sequence> { + val inverted = config.getBoolean("inverted", true) + //range is included in super.getEvents + val events = super.getEvents(block, config).toMutableList() + + if (config.getBoolean("sortEvents", false) || (block is ParentBlock && !block.isSequential)) { + //sort in place if needed + events.sortBy { it.timeOffset } + } + + return events.asSequence().zipWithNext { prev, next -> + val delay = max(next.timeOffset - prev.timeOffset, 0) + if (inverted) { + Pair(next, delay) + } else { + Pair(prev, delay) + } + } + } + + /** + * The filtered stream of events + * + * @param block + * @param meta + * @return + */ + override fun getEvents(block: NumassBlock, meta: Meta): List { + val t0 = getT0(block, meta).toLong() + return getEventsWithDelay(block, meta) + .filter { pair -> pair.second >= t0 } + .map { it.first }.toList() + } + + public override fun getTableFormat(config: Meta): TableFormat { + return TableFormatBuilder() + .addNumber(HV_KEY, X_VALUE_KEY) + .addNumber(LENGTH_KEY) + .addNumber(COUNT_KEY) + .addNumber(COUNT_RATE_KEY, Y_VALUE_KEY) + .addNumber(COUNT_RATE_ERROR_KEY, Y_ERROR_KEY) + .addColumn(NumassAnalyzer.WINDOW_KEY) + .addTime() + .addNumber(T0_KEY) + .build() + } + + companion object { + const val T0_KEY = "t0" + + val NAME_LIST = arrayOf( + LENGTH_KEY, + COUNT_KEY, + COUNT_RATE_KEY, + COUNT_RATE_ERROR_KEY, + NumassAnalyzer.WINDOW_KEY, + NumassAnalyzer.TIME_KEY, + T0_KEY + ) + } +} diff --git a/numass-core/src/main/kotlin/inr/numass/data/legacy/NumassDatFile.kt b/numass-core/src/main/kotlin/inr/numass/data/legacy/NumassDatFile.kt new file mode 100644 index 00000000..85c05a4f --- /dev/null +++ b/numass-core/src/main/kotlin/inr/numass/data/legacy/NumassDatFile.kt @@ -0,0 +1,233 @@ +package inr.numass.data.legacy + +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.tables.Table +import inr.numass.data.api.* +import inr.numass.data.api.NumassPoint.Companion.HV_KEY +import java.io.IOException +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.channels.SeekableByteChannel +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardOpenOption.READ +import java.time.Duration +import java.time.LocalDateTime +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter +import java.util.* + +/** + * Created by darksnake on 08.07.2017. + */ +class NumassDatFile @Throws(IOException::class) +constructor(override val name: String, private val path: Path, meta: Meta) : NumassSet { + + override suspend fun getHvData(): Table? = null + + override val meta: Meta + + private val hVdev: Double + get() = meta.getDouble("dat.hvDev", 2.468555393226049) + + //TODO check point start + override val points: List + get() = try { + Files.newByteChannel(path, READ).use { channel -> + var lab: Int + val points = ArrayList() + do { + points.add(readPoint(channel)) + lab = readBlock(channel, 1).get().toInt() + } while (lab != 0xff) + return points + } + } catch (ex: IOException) { + throw RuntimeException(ex) + } + + init { + val head = readHead(path)//2048 + this.meta = MetaBuilder(meta) + .setValue("info", head) + .setValue(NumassPoint.START_TIME_KEY, readDate(head)) + .build() + } + + private fun hasUset(): Boolean { + return meta.getBoolean("dat.uSet", true) + } + + @Throws(IOException::class) + private fun readHead(path: Path): String { + Files.newByteChannel(path, READ).use { channel -> + channel.position(0) + val buffer = ByteBuffer.allocate(2048) + channel.read(buffer) + return String(buffer.array()).replace("\u0000".toRegex(), "") + } + } + + /** + * Read the block at current position + * + * @param channel + * @param length + * @return + * @throws IOException + */ + @Throws(IOException::class) + private fun readBlock(channel: SeekableByteChannel, length: Int): ByteBuffer { + val res = ByteBuffer.allocate(length) + channel.read(res) + res.order(ByteOrder.LITTLE_ENDIAN) + res.flip() + return res + } + + /** + * Read the point at current position + * + * @param channel + * @return + * @throws IOException + */ + @Synchronized + @Throws(IOException::class) + private fun readPoint(channel: SeekableByteChannel): NumassPoint { + + val rx = readBlock(channel, 32) + + val voltage = rx.int + + var length = rx.short//(short) (rx[6] + 256 * rx[7]); + val phoneFlag = rx.get(19).toInt() != 0//(rx[19] != 0); + + + var timeDiv: Double = when (length.toInt()) { + 5, 10 -> 2e7 + 15, 20 -> 1e7 + 50 -> 5e6 + 100 -> 2.5e6 + 200 -> 1.25e6 + else -> throw IOException("Unknown time divider in input data") + } + + if (phoneFlag) { + timeDiv /= 20.0 + length = (length * 20).toShort() + } + + val events = ArrayList() + var lab = readBlock(channel, 1).get().toInt() + + while (lab == 0xBF) { + val buffer = readBlock(channel, 5) + lab = buffer.get(4).toInt() + } + + do { + events.add(readEvent(channel, lab, timeDiv)) + lab = readBlock(channel, 1).get().toInt() + } while (lab != 0xAF) + + //point end + val ending = readBlock(channel, 64) + + val hours = ending.get(37).toInt() + val minutes = ending.get(38).toInt() + + val start = LocalDateTime.from(startTime) + var absoluteTime = start.withHour(hours).withMinute(minutes) + + //проверÑем, не проÑкочили ли мы полночь + if (absoluteTime.isBefore(start)) { + absoluteTime = absoluteTime.plusDays(1) + } + + + val uRead = ending.getInt(39) + + val uSet: Double + uSet = if (!this.hasUset()) { + uRead.toDouble() / 10.0 / hVdev + } else { + voltage / 10.0 + } + + val block = SimpleBlock(absoluteTime.toInstant(ZoneOffset.UTC), Duration.ofSeconds(length.toLong()), events) + + val pointMeta = MetaBuilder("point") + .setValue(HV_KEY, uSet) + .setValue("uRead", uRead.toDouble() / 10.0 / hVdev) + .setValue("source", "legacy") + + + return SimpleNumassPoint(listOf(block), pointMeta) + } + + @Throws(IOException::class) + private fun readDate(head: String): LocalDateTime { + // Должны Ñчитать 14 Ñимволов + val sc = Scanner(head) + sc.nextLine() + val dateStr = sc.nextLine().trim { it <= ' ' } + //DD.MM.YY HH:MM + //12:35:16 19-11-2013 + val format = DateTimeFormatter.ofPattern("HH:mm:ss dd-MM-yyyy") + + return LocalDateTime.parse(dateStr, format) + } + + @Throws(IOException::class) + private fun readEvent(channel: SeekableByteChannel, b: Int, timeDiv: Double): OrphanNumassEvent { + val chanel: Short + val time: Long + + val hb = b and 0x0f + val lab = b and 0xf0 + + when (lab) { + 0x10 -> { + chanel = readChanel(channel, hb) + time = readTime(channel) + } + 0x20 -> { + chanel = 0 + time = readTime(channel) + } + 0x40 -> { + time = 0 + chanel = readChanel(channel, hb) + } + 0x80 -> { + time = 0 + chanel = 0 + } + else -> throw IOException("Event head expected") + } + + return OrphanNumassEvent(chanel, (time / timeDiv).toLong()) + } + + @Throws(IOException::class) + private fun readChanel(channel: SeekableByteChannel, hb: Int): Short { + assert(hb < 127) + val buffer = readBlock(channel, 1) + return (buffer.get() + 256 * hb).toShort() + } + + @Throws(IOException::class) + private fun readTime(channel: SeekableByteChannel): Long { + val rx = readBlock(channel, 4) + return rx.long//rx[0] + 256 * rx[1] + 65536 * rx[2] + 256 * 65536 * rx[3]; + } + + // private void skip(int length) throws IOException { + // long n = stream.skip(length); + // if (n != length) { + // stream.skip(length - n); + // } + // } +} diff --git a/numass-core/src/main/kotlin/inr/numass/data/storage/ClassicNumassPoint.kt b/numass-core/src/main/kotlin/inr/numass/data/storage/ClassicNumassPoint.kt new file mode 100644 index 00000000..baf51138 --- /dev/null +++ b/numass-core/src/main/kotlin/inr/numass/data/storage/ClassicNumassPoint.kt @@ -0,0 +1,112 @@ +package inr.numass.data.storage + +import hep.dataforge.io.envelopes.Envelope +import hep.dataforge.meta.Meta +import inr.numass.data.NumassFileEnvelope +import inr.numass.data.api.NumassBlock +import inr.numass.data.api.NumassEvent +import inr.numass.data.api.NumassFrame +import inr.numass.data.api.NumassPoint +import org.slf4j.LoggerFactory +import java.io.IOException +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.file.Path +import java.time.Duration +import java.time.Instant +import java.util.stream.Stream +import java.util.stream.StreamSupport + +/** + * Created by darksnake on 08.07.2017. + */ +class ClassicNumassPoint(private val envelope: Envelope) : NumassPoint { + + override val meta: Meta = envelope.meta + + override val voltage: Double = meta.getDouble("external_meta.HV1_value", super.voltage) + + override val index: Int = meta.getInt("external_meta.point_index", super.index) + + override val blocks: List by lazy { + val length: Long = if (envelope.meta.hasValue("external_meta.acquisition_time")) { + envelope.meta.getValue("external_meta.acquisition_time").long + } else { + envelope.meta.getValue("acquisition_time").long + } + listOf(ClassicBlock(startTime, Duration.ofSeconds(length))) + } + + override val startTime: Instant + get() = if (meta.hasValue("start_time")) { + meta.getValue("start_time").time + } else { + super.startTime + } + + + //TODO split blocks using meta + private inner class ClassicBlock( + override val startTime: Instant, + override val length: Duration + ) : NumassBlock, Iterable { + + override val events: Stream + get() = StreamSupport.stream(this.spliterator(), false) + + override fun iterator(): Iterator { + val timeCoef = envelope.meta.getDouble("time_coeff", 50.0) + try { + val buffer = ByteBuffer.allocate(7000) + buffer.order(ByteOrder.LITTLE_ENDIAN) + val channel = envelope.data.channel + channel.read(buffer) + buffer.flip() + return object : Iterator { + + override fun hasNext(): Boolean { + try { + return if (buffer.hasRemaining()) { + true + } else { + buffer.flip() + val num = channel.read(buffer) + if (num > 0) { + buffer.flip() + true + } else { + false + } + } + } catch (e: IOException) { + LoggerFactory.getLogger(this@ClassicNumassPoint.javaClass) + .error("Unexpected IOException when reading block", e) + return false + } + + } + + override fun next(): NumassEvent { + val amp = java.lang.Short.toUnsignedInt(buffer.short).toShort() + val time = Integer.toUnsignedLong(buffer.int) + val status = buffer.get() // status is ignored + return NumassEvent(amp, (time * timeCoef).toLong(), this@ClassicBlock) + } + } + } catch (ex: IOException) { + throw RuntimeException(ex) + } + + } + + + override val frames: Stream + get() = Stream.empty() + } + + companion object { + fun readFile(path: Path): ClassicNumassPoint { + return ClassicNumassPoint(NumassFileEnvelope(path)) + } + } +} diff --git a/numass-core/src/main/kotlin/inr/numass/data/storage/NumassDataFactory.kt b/numass-core/src/main/kotlin/inr/numass/data/storage/NumassDataFactory.kt new file mode 100644 index 00000000..89d8d50a --- /dev/null +++ b/numass-core/src/main/kotlin/inr/numass/data/storage/NumassDataFactory.kt @@ -0,0 +1,47 @@ +package inr.numass.data.storage + +import hep.dataforge.context.Context +import hep.dataforge.data.DataFactory +import hep.dataforge.data.DataNodeBuilder +import hep.dataforge.meta.Meta +import hep.dataforge.names.Name +import hep.dataforge.storage.Storage +import hep.dataforge.storage.StorageElement +import inr.numass.data.api.NumassSet +import kotlinx.coroutines.runBlocking + +/** + * Created by darksnake on 03-Feb-17. + */ +class NumassDataFactory : DataFactory(NumassSet::class.java) { + + override val name = "numass" + + /** + * Build the sequence of name + */ + private fun Storage.sequence(prefix: Name = Name.empty()): Sequence> { + return sequence { + runBlocking { children }.forEach { + val newName = prefix + it.name + yield(Pair(newName, it)) + if (it is Storage) { + yieldAll(it.sequence(newName)) + } + } + } + + } + + override fun fill(builder: DataNodeBuilder, context: Context, meta: Meta) { + runBlocking { + val storage = NumassDirectory.read(context, meta.getString("path")) as Storage + storage.sequence().forEach { pair -> + val value = pair.second + if (value is NumassSet) { + builder.putStatic(pair.first.unescaped, value, value.meta) + } + } + } + } +} diff --git a/numass-core/src/main/kotlin/inr/numass/data/storage/NumassDataLoader.kt b/numass-core/src/main/kotlin/inr/numass/data/storage/NumassDataLoader.kt new file mode 100644 index 00000000..e6a4c6d7 --- /dev/null +++ b/numass-core/src/main/kotlin/inr/numass/data/storage/NumassDataLoader.kt @@ -0,0 +1,194 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.data.storage + +import hep.dataforge.connections.ConnectionHelper +import hep.dataforge.context.Context +import hep.dataforge.io.ColumnedDataReader +import hep.dataforge.io.envelopes.Envelope +import hep.dataforge.meta.Meta +import hep.dataforge.providers.Provider +import hep.dataforge.storage.Loader +import hep.dataforge.storage.StorageElement +import hep.dataforge.storage.files.FileStorageElement +import hep.dataforge.tables.Table +import inr.numass.data.NumassDataUtils +import inr.numass.data.NumassEnvelopeType +import inr.numass.data.api.NumassPoint +import inr.numass.data.api.NumassSet +import org.slf4j.LoggerFactory +import java.io.IOException +import java.nio.file.Files +import java.nio.file.Path +import java.time.Instant +import kotlin.reflect.KClass +import kotlin.streams.toList + + +/** + * The reader for numass main detector data directory or zip format; + * + * @author darksnake + */ +class NumassDataLoader( + override val context: Context, + override val parent: StorageElement?, + override val name: String, + override val path: Path +) : Loader, NumassSet, Provider, FileStorageElement { + + override val type: KClass = NumassPoint::class + + private val _connectionHelper = ConnectionHelper(this) + + override fun getConnectionHelper(): ConnectionHelper = _connectionHelper + + + override val meta: Meta by lazy { + val metaPath = path.resolve("meta") + NumassEnvelopeType.infer(metaPath)?.reader?.read(metaPath)?.meta ?: Meta.empty() + } + + override suspend fun getHvData(): Table? { + val hvEnvelope = path.resolve(HV_FRAGMENT_NAME).let { + NumassEnvelopeType.infer(it)?.reader?.read(it) ?: error("Can't read hv file") + } + return try { + ColumnedDataReader(hvEnvelope.data.stream, "timestamp", "block", "value").toTable() + } catch (ex: IOException) { + LoggerFactory.getLogger(javaClass).error("Failed to load HV data from file", ex) + null + } + } + + + private val pointEnvelopes: List by lazy { + Files.list(path) + .filter { it.fileName.toString().startsWith(POINT_FRAGMENT_NAME) } + .map { + NumassEnvelopeType.infer(it)?.reader?.read(it) ?: error("Can't read point file") + }.toList() + } + + val isReversed: Boolean + get() = this.meta.getBoolean("iteration_info.reverse", false) + + val description: String + get() = this.meta.getString("description", "").replace("\\n", "\n") + + + override val points: List + get() = pointEnvelopes.map { + NumassDataUtils.read(it) + } + + + override val startTime: Instant + get() = meta.optValue("start_time").map { it.time }.orElseGet { super.startTime } + + override fun close() { + //do nothing + } + + + companion object { +// +// @Throws(IOException::class) +// fun fromFile(storage: Storage, zipFile: Path): NumassDataLoader { +// throw UnsupportedOperationException("TODO") +// } +// +// +// /** +// * Construct numass loader from directory +// * +// * @param storage +// * @param directory +// * @return +// * @throws IOException +// */ +// @Throws(IOException::class) +// fun fromDir(storage: Storage, directory: Path, name: String = FileStorage.entryName(directory)): NumassDataLoader { +// if (!Files.isDirectory(directory)) { +// throw IllegalArgumentException("Numass data directory required") +// } +// val annotation = MetaBuilder("loader") +// .putValue("type", "numass") +// .putValue("numass.loaderFormat", "dir") +// // .setValue("file.timeCreated", Instant.ofEpochMilli(directory.getContent().getLastModifiedTime())) +// .build() +// +// //FIXME envelopes are lazy do we need to do additional lazy evaluations here? +// val items = LinkedHashMap>() +// +// Files.list(directory).filter { file -> +// val fileName = file.fileName.toString() +// (fileName == META_FRAGMENT_NAME +// || fileName == HV_FRAGMENT_NAME +// || fileName.startsWith(POINT_FRAGMENT_NAME)) +// }.forEach { file -> +// try { +// items[FileStorage.entryName(file)] = Supplier { NumassFileEnvelope.open(file, true) } +// } catch (ex: Exception) { +// LoggerFactory.getLogger(NumassDataLoader::class.java) +// .error("Can't load numass data directory " + FileStorage.entryName(directory), ex) +// } +// } +// +// return NumassDataLoader(storage, name, annotation, items) +// } +// +// fun fromDir(context: Context, directory: Path, name: String = FileStorage.entryName(directory)): NumassDataLoader { +// return fromDir(DummyStorage(context), directory, name) +// } +// +// /** +// * "start_time": "2016-04-20T04:08:50", +// * +// * @param meta +// * @return +// */ +// private fun readTime(meta: Meta): Instant { +// return if (meta.hasValue("start_time")) { +// meta.getValue("start_time").time +// } else { +// Instant.EPOCH +// } +// } + + /** + * The name of informational meta file in numass data directory + */ + const val META_FRAGMENT_NAME = "meta" + + /** + * The beginning of point fragment name + */ + const val POINT_FRAGMENT_NAME = "p" + + /** + * The beginning of hv fragment name + */ + const val HV_FRAGMENT_NAME = "voltage" + } +} + + +fun Context.readNumassSet(path:Path):NumassDataLoader{ + return NumassDataLoader(this,null,path.fileName.toString(),path) +} + + diff --git a/numass-core/src/main/kotlin/inr/numass/data/storage/NumassStorage.kt b/numass-core/src/main/kotlin/inr/numass/data/storage/NumassStorage.kt new file mode 100644 index 00000000..68fff09a --- /dev/null +++ b/numass-core/src/main/kotlin/inr/numass/data/storage/NumassStorage.kt @@ -0,0 +1,295 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.data.storage + +import hep.dataforge.context.Context +import hep.dataforge.context.Global +import hep.dataforge.events.Event +import hep.dataforge.events.EventBuilder +import hep.dataforge.meta.Meta +import hep.dataforge.storage.StorageElement +import hep.dataforge.storage.files.FileStorage +import hep.dataforge.storage.files.FileStorageElement +import inr.numass.data.NumassEnvelopeType +import kotlinx.coroutines.runBlocking +import java.nio.file.Files +import java.nio.file.Path + +/** + * Numass storage directory. Works as a normal directory, but creates a numass loader from each directory with meta + */ +class NumassDirectory : FileStorage.Directory() { + override val name: String = NUMASS_DIRECTORY_TYPE + + override suspend fun read(context: Context, path: Path, parent: StorageElement?): FileStorageElement? { + val meta = FileStorage.resolveMeta(path){ NumassEnvelopeType.infer(it)?.reader?.read(it)?.meta } + return if (Files.isDirectory(path) && meta != null) { + NumassDataLoader(context, parent, path.fileName.toString(), path) + } else { + super.read(context, path, parent) + } + } + + companion object { + val INSTANCE = NumassDirectory() + const val NUMASS_DIRECTORY_TYPE = "inr.numass.storage.directory" + + /** + * Simple read for scripting and debug + */ + fun read(context: Context = Global, path: String): FileStorageElement?{ + return runBlocking { INSTANCE.read(context, context.getDataFile(path).absolutePath)} + } + } +} + +class NumassDataPointEvent(meta: Meta) : Event(meta) { + + val fileSize: Int = meta.getInt(FILE_SIZE_KEY, 0) + + val fileName: String = meta.getString(FILE_NAME_KEY) + + override fun toString(): String { + return String.format("(%s) [%s] : pushed numass data file with name '%s' and size '%d'", + time().toString(), sourceTag(), fileName, fileSize) + } + + companion object { + + + const val FILE_NAME_KEY = "fileName" + const val FILE_SIZE_KEY = "fileSize" + + fun build(source: String, fileName: String, fileSize: Int): NumassDataPointEvent { + return NumassDataPointEvent(builder(source, fileName, fileSize).buildEventMeta()) + } + + fun builder(source: String, fileName: String, fileSize: Int): EventBuilder<*> { + return EventBuilder.make("numass.storage.pushData") + .setSource(source) + .setMetaValue(FILE_NAME_KEY, fileName) + .setMetaValue(FILE_SIZE_KEY, fileSize) + } + } + +} + +// +///** +// * The file storage containing numass data directories or zips. +// * +// * +// * Any subdirectory is treated as numass data directory. Any zip must have +// * `NUMASS_ZIP_EXTENSION` extension to be recognized. Any other files are +// * ignored. +// * +// * +// * @author Alexander Nozik +// */ +//class NumassStorage : FileStorage { +// +// val description: String +// get() = meta.getString("description", "") +// +// private constructor(parent: FileStorage, config: Meta, shelf: String) : super(parent, config, shelf) +// +// constructor(context: Context, config: Meta, path: Path) : super(context, config, path) +// +// init { +// refresh() +// } +// +// override fun refresh() { +// try { +// this.shelves.clear() +// this.loaders.clear() +// Files.list(dataDir).forEach { file -> +// try { +// if (Files.isDirectory(file)) { +// val metaFile = file.resolve(NumassDataLoader.META_FRAGMENT_NAME) +// if (Files.exists(metaFile)) { +// this.loaders[entryName(file)] = NumassDataLoader.fromDir(this, file) +// } else { +// this.shelves[entryName(file)] = NumassStorage(this, meta, entryName(file)) +// } +// } else if (file.fileName.endsWith(NUMASS_ZIP_EXTENSION)) { +// this.loaders[entryName(file)] = NumassDataLoader.fromFile(this, file) +// } else { +// //updating non-numass loader files +// updateFile(file) +// } +// } catch (ex: IOException) { +// LoggerFactory.getLogger(javaClass).error("Error while creating numass loader", ex) +// } catch (ex: StorageException) { +// LoggerFactory.getLogger(javaClass).error("Error while creating numass group", ex) +// } +// } +// } catch (ex: IOException) { +// throw RuntimeException(ex) +// } +// +// } +// +// @Throws(StorageException::class) +// fun pushNumassData(path: String?, fileName: String, data: ByteBuffer) { +// if (path == null || path.isEmpty()) { +// pushNumassData(fileName, data) +// } else { +// val st = buildShelf(path) as NumassStorage +// st.pushNumassData(fileName, data) +// } +// } +// +// /** +// * Read nm.zip content and write it as a new nm.zip file +// * +// * @param fileName +// */ +// @Throws(StorageException::class) +// fun pushNumassData(fileName: String, data: ByteBuffer) { +// //FIXME move zip to internal +// try { +// val nmFile = dataDir.resolve(fileName + NUMASS_ZIP_EXTENSION) +// if (Files.exists(nmFile)) { +// LoggerFactory.getLogger(javaClass).warn("Trying to rewrite existing numass data file {}", nmFile.toString()) +// } +// Files.newByteChannel(nmFile, CREATE, WRITE).use { channel -> channel.write(data) } +// +// dispatchEvent(NumassDataPointEvent.build(name, fileName, Files.size(nmFile).toInt())) +// } catch (ex: IOException) { +// throw StorageException(ex) +// } +// +// } +// +// @Throws(StorageException::class) +// override fun createShelf(shelfConfiguration: Meta, shelfName: String): NumassStorage { +// return NumassStorage(this, shelfConfiguration, shelfName) +// } +// +// /** +// * A list of legacy DAT files in the directory +// * +// * @return +// */ +// fun legacyFiles(): List { +// try { +// val files = ArrayList() +// Files.list(dataDir).forEach { file -> +// if (Files.isRegularFile(file) && file.fileName.toString().toLowerCase().endsWith(".dat")) { +// //val name = file.fileName.toString() +// try { +// files.add(NumassDatFile(file, Meta.empty())) +// } catch (ex: Exception) { +// LoggerFactory.getLogger(javaClass).error("Error while reading legacy numass file " + file.fileName, ex) +// } +// +// } +// } +// return files +// } catch (ex: IOException) { +// throw RuntimeException(ex) +// } +// +// } +// +// @Throws(Exception::class) +// override fun close() { +// super.close() +// //close remote file system after use +// try { +// dataDir.fileSystem.close() +// } catch (ex: UnsupportedOperationException) { +// +// } +// +// } +// + +// +// companion object { +// +// const val NUMASS_ZIP_EXTENSION = ".nm.zip" +// const val NUMASS_DATA_LOADER_TYPE = "numassData" +// } +// +//} + +//class NumassStorageFactory : StorageType { +// +// override fun type(): String { +// return "numass" +// } +// +// override fun build(context: Context, meta: Meta): Storage { +// if (meta.hasValue("path")) { +// val uri = URI.create(meta.getString("path")) +// val path: Path +// if (uri.scheme.startsWith("ssh")) { +// try { +// val username = meta.getString("userName", uri.userInfo) +// //String host = meta.getString("host", uri.getHost()); +// val port = meta.getInt("port", 22) +// val env = SFTPEnvironment() +// .withUsername(username) +// .withPassword(meta.getString("password", "").toCharArray()) +// val fs = FileSystems.newFileSystem(uri, env, context.classLoader) +// path = fs.getPath(uri.path) +// } catch (e: Exception) { +// throw RuntimeException(e) +// } +// +// } else { +// path = Paths.get(uri) +// } +// if(!Files.exists(path)){ +// context.logger.info("File $path does not exist. Creating a new storage directory.") +// Files.createDirectories(path) +// } +// return NumassStorage(context, meta, path) +// } else { +// context.logger.warn("A storage path not provided. Creating default root storage in the working directory") +// return NumassStorage(context, meta, context.workDir) +// } +// } +// +// companion object { +// +// /** +// * Build local storage with Global context. Used for tests. +// * +// * @param file +// * @return +// */ +// fun buildLocal(context: Context, file: Path, readOnly: Boolean, monitor: Boolean): FileStorage { +// val manager = context.load(StorageManager::class.java, Meta.empty()) +// return manager.buildStorage(buildStorageMeta(file.toUri(), readOnly, monitor)) as FileStorage +// } +// +// fun buildLocal(context: Context, path: String, readOnly: Boolean, monitor: Boolean): FileStorage { +// val file = context.dataDir.resolve(path) +// return buildLocal(context, file, readOnly, monitor) +// } +// +// fun buildStorageMeta(path: URI, readOnly: Boolean, monitor: Boolean): MetaBuilder { +// return MetaBuilder("storage") +// .setValue("path", path.toString()) +// .setValue("type", "numass") +// .setValue("readOnly", readOnly) +// .setValue("monitor", monitor) +// } +// } +//} diff --git a/numass-core/src/main/resources/META-INF/services/hep.dataforge.data.DataLoader b/numass-core/src/main/resources/META-INF/services/hep.dataforge.data.DataLoader new file mode 100644 index 00000000..285df7fe --- /dev/null +++ b/numass-core/src/main/resources/META-INF/services/hep.dataforge.data.DataLoader @@ -0,0 +1 @@ +inr.numass.data.storage.NumassDataFactory \ No newline at end of file diff --git a/numass-core/src/main/resources/META-INF/services/hep.dataforge.io.envelopes.EnvelopeType b/numass-core/src/main/resources/META-INF/services/hep.dataforge.io.envelopes.EnvelopeType new file mode 100644 index 00000000..5b65b45a --- /dev/null +++ b/numass-core/src/main/resources/META-INF/services/hep.dataforge.io.envelopes.EnvelopeType @@ -0,0 +1 @@ +inr.numass.data.NumassEnvelopeType \ No newline at end of file diff --git a/numass-core/src/main/resources/META-INF/services/hep.dataforge.storage.StorageElementType b/numass-core/src/main/resources/META-INF/services/hep.dataforge.storage.StorageElementType new file mode 100644 index 00000000..87511cd8 --- /dev/null +++ b/numass-core/src/main/resources/META-INF/services/hep.dataforge.storage.StorageElementType @@ -0,0 +1 @@ +inr.numass.data.storage.NumassDirectory \ No newline at end of file diff --git a/numass-main/build.gradle b/numass-main/build.gradle new file mode 100644 index 00000000..378f8fb6 --- /dev/null +++ b/numass-main/build.gradle @@ -0,0 +1,90 @@ +plugins { + id 'groovy' + id 'application' +} + +apply plugin: 'kotlin' + +//apply plugin: 'org.openjfx.javafxplugin' +// +//javafx { +// modules = [ 'javafx.controls' ] +//} + +//if (!hasProperty('mainClass')) { +// ext.mainClass = 'inr.numass.LaunchGrindShell' +//} +mainClassName = 'inr.numass.LaunchGrindShell' + +description = "Main numass project" + +compileKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} +compileTestKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} + +compileGroovy.dependsOn(compileKotlin) +compileGroovy.classpath += files(compileKotlin.destinationDir) + +dependencies { + compile group: 'commons-cli', name: 'commons-cli', version: '1.+' + compile group: 'commons-io', name: 'commons-io', version: '2.+' + compile project(':numass-core') + compile project(':numass-core:numass-signal-processing') + compileOnly "org.jetbrains.kotlin:kotlin-main-kts:1.3.21" + compile project(':dataforge-stat:dataforge-minuit') + compile project(':grind:grind-terminal') + compile project(":dataforge-gui") + //compile "hep.dataforge:dataforge-html" + + // https://mvnrepository.com/artifact/org.ehcache/ehcache + //compile group: 'org.ehcache', name: 'ehcache', version: '3.4.0' + +} + +task repl(dependsOn: classes, type: JavaExec) { + group "numass" + main 'inr.numass.LaunchGrindShell' + classpath = sourceSets.main.runtimeClasspath + description "Start Grind repl" + standardInput = System.in + standardOutput = System.out + if (project.hasProperty("cmd")) { + args = cmd.split().toList() + } +} + +task grindTask(dependsOn: classes, type: JavaExec) { + group "numass" + main 'inr.numass.RunTask' + classpath = sourceSets.main.runtimeClasspath + description "Run a task in a numass workspace" + standardInput = System.in + standardOutput = System.out +} + +task simulate(dependsOn: classes, type: JavaExec) { + group "numass" + main 'inr.numass.scripts.Simulate' + classpath = sourceSets.main.runtimeClasspath + description "Simulate spectrum" +} + +task underflow(dependsOn: classes, type: JavaExec) { + group "numass" + main 'inr.numass.scripts.underflow.Underflow' + classpath = sourceSets.main.runtimeClasspath +} + +task scanTreeStartScript(type: CreateStartScripts, dependsOn: installDist) { + applicationName = 'scanTree' + classpath = fileTree('build/install/numass-main/lib') + mainClassName = 'inr.numass.scripts.ScanTreeKt' + outputDir = file('build/install/numass-main/bin') +} \ No newline at end of file diff --git a/numass-main/src/main/groovy/inr/numass/LaunchGrindShell.groovy b/numass-main/src/main/groovy/inr/numass/LaunchGrindShell.groovy new file mode 100644 index 00000000..a1d7971e --- /dev/null +++ b/numass-main/src/main/groovy/inr/numass/LaunchGrindShell.groovy @@ -0,0 +1,46 @@ +package inr.numass + +import hep.dataforge.context.Context +import hep.dataforge.context.Global +import hep.dataforge.fx.output.FXOutputManager +import hep.dataforge.grind.terminal.GrindTerminal +import hep.dataforge.grind.workspace.GrindWorkspace +import hep.dataforge.plots.jfreechart.JFreeChartPlugin +import hep.dataforge.workspace.FileBasedWorkspace +import hep.dataforge.workspace.Workspace +import groovy.cli.commons.CliBuilder + +/** + * Created by darksnake on 29-Aug-16. + */ + + +def cli = new CliBuilder() +cli.c(longOpt: "config", args: 1, "The name of configuration file") +println cli.usage + +def cfgPath = cli.parse(args).c; +println "Loading config file from $cfgPath" +println "Starting Grind shell" + + +try { + + def grindContext = Context.build("GRIND") + //start fx plugin in global and set global output to fx manager + Global.INSTANCE.load(JFreeChartPlugin) + grindContext.output = FXOutputManager.display() + + GrindTerminal.system(grindContext).launch { + if (cfgPath) { + Workspace numass = FileBasedWorkspace.build(context, new File(cfgPath as String).toPath()) + bind("numass", new GrindWorkspace(numass)) + } else { + println "No configuration path. Provide path via --config option" + } + } +} catch (Exception ex) { + ex.printStackTrace(); +} finally { + Global.INSTANCE.terminate(); +} diff --git a/numass-main/src/main/groovy/inr/numass/RunTask.groovy b/numass-main/src/main/groovy/inr/numass/RunTask.groovy new file mode 100644 index 00000000..ef268054 --- /dev/null +++ b/numass-main/src/main/groovy/inr/numass/RunTask.groovy @@ -0,0 +1,15 @@ +package inr.numass + +import hep.dataforge.workspace.FileBasedWorkspace +import hep.dataforge.workspace.Workspace + +/** + * Created by darksnake on 18-Apr-17. + */ + + +cfgPath = "D:\\Work\\Numass\\sterile2016_10\\workspace.groovy" + +Workspace numass = FileBasedWorkspace.build(context, new File(cfgPath).toPath()) + +numass.scansum "fill_1" \ No newline at end of file diff --git a/numass-main/src/main/groovy/inr/numass/data/PointAnalyzer.groovy b/numass-main/src/main/groovy/inr/numass/data/PointAnalyzer.groovy new file mode 100644 index 00000000..4aeed110 --- /dev/null +++ b/numass-main/src/main/groovy/inr/numass/data/PointAnalyzer.groovy @@ -0,0 +1,48 @@ +package inr.numass.data + +import groovy.transform.CompileStatic +import hep.dataforge.grind.Grind +import hep.dataforge.maths.histogram.Histogram +import hep.dataforge.maths.histogram.UnivariateHistogram +import hep.dataforge.values.Values +import inr.numass.data.analyzers.TimeAnalyzer +import inr.numass.data.api.NumassBlock + +import java.util.stream.LongStream + +/** + * Created by darksnake on 27-Jun-17. + */ +@CompileStatic +class PointAnalyzer { + + static final TimeAnalyzer analyzer = new TimeAnalyzer(); + + static Histogram histogram(NumassBlock point, int loChannel = 0, int upChannel = 10000, double binSize = 0.5, int binNum = 500) { + return UnivariateHistogram.buildUniform(0d, binSize * binNum, binSize) + .fill( + kotlin.streams.jdk8.StreamsKt.asStream(analyzer.getEventsWithDelay(point, Grind.buildMeta("window.lo": loChannel, "window.up": upChannel))).mapToDouble { + it.second / 1000 as double + } + ) + } + + static Histogram histogram(LongStream stream, double binSize = 0.5, int binNum = 500) { + return UnivariateHistogram.buildUniform(0d, binSize * binNum, binSize).fill(stream.mapToDouble { + it / 1000 as double + }) + } + + static Values analyze(Map values = Collections.emptyMap(), NumassBlock block, Closure metaClosure = null) { + return analyzer.analyze(block, Grind.buildMeta(values, metaClosure)) + } +// +// static class Result { +// double cr; +// double crErr; +// long num; +// double t0; +// int loChannel; +// int upChannel; +// } +} diff --git a/numass-main/src/main/groovy/inr/numass/scripts/CountRateSummary.groovy b/numass-main/src/main/groovy/inr/numass/scripts/CountRateSummary.groovy new file mode 100644 index 00000000..4a168020 --- /dev/null +++ b/numass-main/src/main/groovy/inr/numass/scripts/CountRateSummary.groovy @@ -0,0 +1,23 @@ +package inr.numass.scripts + + +import hep.dataforge.tables.Table +import hep.dataforge.workspace.FileBasedWorkspace +import hep.dataforge.workspace.Workspace + +import java.nio.file.Paths + +/** + * Created by darksnake on 26-Dec-16. + */ + + +Workspace numass = FileBasedWorkspace.build(Paths.get("D:/Work/Numass/sterile2016_10/workspace.groovy")) + +numass.runTask("prepare", "fill_1_all").forEachData(Table) { + Table table = it.get(); + def dp18 = table.find { it["Uset"] == 18000 } + def dp17 = table.find { it["Uset"] == 17000 } + println "${it.name}\t${dp18["CR"]}\t${dp18["CRerr"]}\t${dp17["CR"]}\t${dp17["CRerr"]}" +} + diff --git a/numass-main/src/main/groovy/inr/numass/scripts/FindExIonRatio.groovy b/numass-main/src/main/groovy/inr/numass/scripts/FindExIonRatio.groovy new file mode 100644 index 00000000..d4b1b35e --- /dev/null +++ b/numass-main/src/main/groovy/inr/numass/scripts/FindExIonRatio.groovy @@ -0,0 +1,109 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.scripts + +import hep.dataforge.maths.integration.UnivariateIntegrator +import hep.dataforge.plots.PlotFrame +import hep.dataforge.plots.data.XYFunctionPlot +import hep.dataforge.plots.jfreechart.JFreeChartFrame +import hep.dataforge.stat.fit.ParamSet +import inr.numass.models.misc.LossCalculator +import org.apache.commons.math3.analysis.UnivariateFunction +import org.apache.commons.math3.analysis.solvers.BisectionSolver + +ParamSet params = new ParamSet() + .setParValue("exPos", 12.76) + .setParValue("ionPos", 13.95) + .setParValue("exW", 1.2) + .setParValue("ionW", 13.5) + .setParValue("exIonRatio", 4.55) + + + + +UnivariateFunction scatterFunction = LossCalculator.getSingleScatterFunction(params); + +PlotFrame frame = JFreeChartFrame.drawFrame("Differential scatter function", null); +frame.add(XYFunctionPlot.plotFunction("differential", scatterFunction, 0, 100, 400)); + +UnivariateIntegrator integrator = NumassContext.defaultIntegrator; + +double border = 13.6; + +UnivariateFunction ratioFunction = { e -> integrator.integrate(0, e, scatterFunction) / integrator.integrate(e, 100, scatterFunction) } + +double ratio = ratioFunction.value(border); +println "The true excitation to ionization ratio with border energy $border is $ratio"; + + +double resolution = 1.5d; + + +def X = 0.527; + +LossCalculator calculator = LossCalculator.INSTANCE; + +List lossProbs = calculator.getGunLossProbabilities(X); + +UnivariateFunction newScatterFunction = { double d -> + double res = scatterFunction.value(d); + for (i = 1; i < lossProbs.size(); i++) { + res += lossProbs.get(i) * calculator.getLossValue(i, d, 0); + } + return res; +} + + +UnivariateFunction resolutionValue = { double e -> + if (e <= 0d) { + return 0d; + } else if (e >= resolution) { + return 1d; + } else { + return e / resolution; + } +}; + + +UnivariateFunction integral = { double u -> + if (u <= 0d) { + return 0d; + } else { + UnivariateFunction integrand = { double e -> resolutionValue.value(u - e) * newScatterFunction.value(e) }; + return integrator.integrate(0d, u, integrand) + } +} + + +frame.add(XYFunctionPlot.plotFunction("integral", integral, 0, 100, 800)); + +BisectionSolver solver = new BisectionSolver(1e-3); + +UnivariateFunction integralShifted = { u -> + def integr = integral.value(u); + return integr / (1 - integr) - ratio; +} + +double integralBorder = solver.solve(400, integralShifted, 10d, 20d); + +println "The integral border is $integralBorder"; + +double newBorder = 14.43 +double integralValue = integral.value(newBorder); + +double err = Math.abs(integralValue / (1 - integralValue) / ratio - 1d) + +println "The relative error ic case of using $newBorder instead of real one is $err"; \ No newline at end of file diff --git a/numass-main/src/main/groovy/inr/numass/scripts/LossNormCalculation.groovy b/numass-main/src/main/groovy/inr/numass/scripts/LossNormCalculation.groovy new file mode 100644 index 00000000..afee80a4 --- /dev/null +++ b/numass-main/src/main/groovy/inr/numass/scripts/LossNormCalculation.groovy @@ -0,0 +1,46 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package inr.numass.scripts + +import hep.dataforge.maths.integration.GaussRuleIntegrator +import hep.dataforge.maths.integration.UnivariateIntegrator +import org.apache.commons.math3.analysis.UnivariateFunction + +UnivariateIntegrator integrator = new GaussRuleIntegrator(400); +def exPos = 12.695; +def ionPos = 13.29; +def exW = 1.22; +def ionW = 11.99; +def exIonRatio = 3.6; + +def cutoff = 25d + +UnivariateFunction func = {double eps -> + if (eps <= 0) { + return 0; + } + double z1 = eps - exPos; + double ex = exIonRatio * Math.exp(-2 * z1 * z1 / exW / exW); + + double z = 4 * (eps - ionPos) * (eps - ionPos); + double ion = 1 / (1 + z / ionW / ionW); + + double res; + if (eps < exPos) { + res = ex; + } else { + res = Math.max(ex, ion); + } + + return res; +}; + +//caclulating lorentz integral analythically +double tailNorm = (Math.atan((ionPos - cutoff) * 2d / ionW) + 0.5 * Math.PI) * ionW / 2d; +final double norm = integrator.integrate(0d, cutoff, func) + tailNorm; + +println 1/norm; \ No newline at end of file diff --git a/numass-main/src/main/groovy/inr/numass/scripts/LossTailCalculation.groovy b/numass-main/src/main/groovy/inr/numass/scripts/LossTailCalculation.groovy new file mode 100644 index 00000000..cc018f8a --- /dev/null +++ b/numass-main/src/main/groovy/inr/numass/scripts/LossTailCalculation.groovy @@ -0,0 +1,41 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package inr.numass.scripts + +import hep.dataforge.maths.integration.GaussRuleIntegrator +import hep.dataforge.maths.integration.UnivariateIntegrator +import org.apache.commons.math3.analysis.UnivariateFunction + +UnivariateIntegrator integrator = new GaussRuleIntegrator(400); + +def exPos = 12.587; +def ionPos = 11.11; +def exW = 1.20; +def ionW = 11.02; +def exIonRatio = 2.43; + +def cutoff = 20d + +UnivariateFunction loss = LossCalculator.getSingleScatterFunction(exPos, ionPos, exW, ionW, exIonRatio); + + +println integrator.integrate(0, 600, loss); +println integrator.integrate(0, cutoff, loss); +println integrator.integrate(cutoff, 600d, loss); + +println (integrator.integrate(0, cutoff, loss) + integrator.integrate(cutoff, 3000d, loss)); +//double tailValue = (Math.atan((ionPos-cutoff)*2d/ionW) + 0.5*Math.PI)*ionW/2; +//println tailValue +//println integrator.integrate(loss,0,100); +//println integrator.integrate(loss,100,600); + +//def lorentz = {eps-> +// double z = 4 * (eps - ionPos) * (eps - ionPos); +// 1 / (1 + z / ionW / ionW); +//} +// +//println(integrator.integrate(lorentz, cutoff, 800)) \ No newline at end of file diff --git a/numass-main/src/main/groovy/inr/numass/scripts/OldTest.groovy b/numass-main/src/main/groovy/inr/numass/scripts/OldTest.groovy new file mode 100644 index 00000000..371599f6 --- /dev/null +++ b/numass-main/src/main/groovy/inr/numass/scripts/OldTest.groovy @@ -0,0 +1,104 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.scripts + +import hep.dataforge.context.Global +import hep.dataforge.stat.fit.FitManager +import hep.dataforge.stat.fit.FitState +import hep.dataforge.stat.fit.MINUITPlugin +import hep.dataforge.stat.fit.ParamSet +import hep.dataforge.stat.models.XYModel +import hep.dataforge.tables.ListTable +import inr.numass.data.SpectrumAdapter +import inr.numass.models.BetaSpectrum +import inr.numass.models.ModularSpectrum +import inr.numass.models.NBkgSpectrum +import inr.numass.models.ResolutionFunction +import org.apache.commons.math3.analysis.BivariateFunction + +import static inr.numass.utils.OldDataReader.readData + +PrintWriter out = Global.out(); +Locale.setDefault(Locale.US); + +new MINUITPlugin().startGlobal(); + +FitManager fm = new FitManager(); + +// setSeed(543982); +File fssfile = new File("c:\\Users\\Darksnake\\Dropbox\\PlayGround\\FS.txt"); + +BivariateFunction resolution = new ResolutionFunction(2.28e-4); +//resolution.setTailFunction(ResolutionFunction.getRealTail()) + +ModularSpectrum sp = new ModularSpectrum(new BetaSpectrum(fssfile), resolution, 18395d, 18580d); +sp.setCaching(false); +//RangedNamedSetSpectrum beta = new BetaSpectrum(fssfile); +//ModularSpectrum sp = new ModularSpectrum(beta, 2.28e-4, 18395d, 18580d); + +// ModularTritiumSpectrum beta = new ModularTritiumSpectrum(2.28e-4, 18395d, 18580d, "d:\\PlayGround\\FS.txt"); +NBkgSpectrum spectrum = new NBkgSpectrum(sp); +XYModel model = new XYModel("tritium", spectrum, new SpectrumAdapter()); + +ParamSet allPars = new ParamSet(); + +allPars.setParValue("N", 602533.94); +//значение 6е-6 ÑоответÑтвует полной интенÑтивноÑти 6е7 раÑпадов в Ñекунду +//Проблема была в переполнении Ñчетчика Ñобытий в генераторе. Заменил на long. Возможно Ñтоит поÑтавить туда чиÑло Ñ Ð¿Ð»Ð°Ð²Ð°ÑŽÑ‰ÐµÐ¹ точкой +allPars.setParError("N", 1000); +allPars.setParDomain("N", 0d, Double.POSITIVE_INFINITY); +allPars.setParValue("bkg", 0.012497889); +allPars.setParError("bkg", 1e-4); +allPars.setParValue("E0", 18575.986); +allPars.setParError("E0", 0.05); +allPars.setParValue("mnu2", 0d); +allPars.setParError("mnu2", 1d); +allPars.setParValue("msterile2", 50 * 50); +allPars.setParValue("U2", 0); +allPars.setParError("U2", 1e-2); +allPars.setParDomain("U2", -1d, 1d); +allPars.setParValue("X", 0.47); +allPars.setParError("X", 0.014); +allPars.setParDomain("X", 0d, Double.POSITIVE_INFINITY); +allPars.setParValue("trap", 1d); +allPars.setParError("trap", 0.2d); +allPars.setParDomain("trap", 0d, Double.POSITIVE_INFINITY); + +ListTable data = readData("c:\\Users\\Darksnake\\Dropbox\\PlayGround\\RUN23.DAT", 18400d); + +FitState state = new FitState(data, model, allPars); + +FitState res = fm.runDefaultStage(state, "E0", "N", "bkg"); + +res = fm.runDefaultStage(res, "E0", "N", "bkg", "mnu2"); + +res.print(out); + +//spectrum.counter.print(onComplete); +// +//// fm.setPriorProb(new Gaussian("X", 0.47, 0.47*0.03)); +//// fm.setPriorProb(new MultivariateGaussianPrior(allPars.getSubSet("X","trap"))); +//res = fm.runStage(res, "MINUIT", "run", "E0", "N", "bkg", "mnu2"); +//// +//res.print(onComplete); + +//sp.setCaching(true); +//sp.setSuppressWarnings(true); +// +//BayesianConfidenceLimit bm = new BayesianConfidenceLimit(); +//bm.printMarginalLikelihood(onComplete, "U2", res, ["E0", "N", "bkg", "U2", "X"], 10000); + +// PrintNamed.printLike2D(Out.onComplete, "like", res, "N", "E0", 30, 60, 2); diff --git a/numass-main/src/main/groovy/inr/numass/scripts/PrintLossFunctions.groovy b/numass-main/src/main/groovy/inr/numass/scripts/PrintLossFunctions.groovy new file mode 100644 index 00000000..d91c4460 --- /dev/null +++ b/numass-main/src/main/groovy/inr/numass/scripts/PrintLossFunctions.groovy @@ -0,0 +1,61 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package inr.numass.scripts + +import inr.numass.models.misc.LossCalculator + + +LossCalculator loss = LossCalculator.INSTANCE + +def X = 0.36 + +def lossProbs = loss.getGunLossProbabilities(X); + +printf("%8s\t%8s\t%8s\t%8s\t%n", + "eps", + "p1", + "p2", + "p3" +) + +/* +'exPos' = 12.587 ± 0.049 +'ionPos' = 11.11 ± 0.50 +'exW' = 1.20 ± 0.12 +'ionW' = 11.02 ± 0.68 +'exIonRatio' = 2.43 ± 0.42 + */ + +def singleScatter = loss.getSingleScatterFunction( + 12.860, + 16.62, + 1.71, + 12.09, + 4.59 +); + +for (double d = 0; d < 30; d += 0.3) { + double ei = 18500; + double ef = ei - d; + printf("%8f\t%8f\t%8f\t%8f\t%n", + d, + lossProbs[1] * singleScatter.value(ei - ef), + lossProbs[2] * loss.getLossValue(2, ei, ef), + lossProbs[3] * loss.getLossValue(3, ei, ef) + ) +} + +for (double d = 30; d < 100; d += 1) { + double ei = 18500; + double ef = ei - d; + printf("%8f\t%8f\t%8f\t%8f\t%n", + d, + lossProbs[1] * singleScatter.value(ei - ef), + lossProbs[2] * loss.getLossValue(2, ei, ef), + lossProbs[3] * loss.getLossValue(3, ei, ef) + ) +} diff --git a/numass-main/src/main/groovy/inr/numass/scripts/ReadTextFile.groovy b/numass-main/src/main/groovy/inr/numass/scripts/ReadTextFile.groovy new file mode 100644 index 00000000..eefff99e --- /dev/null +++ b/numass-main/src/main/groovy/inr/numass/scripts/ReadTextFile.groovy @@ -0,0 +1,15 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package inr.numass.scripts + +import hep.dataforge.io.ColumnedDataReader +import hep.dataforge.io.ColumnedDataWriter +import hep.dataforge.tables.Table + +File file = new File("D:\\Work\\Numass\\sterile2016\\empty.dat" ) +Table referenceTable = new ColumnedDataReader(file).toTable(); +ColumnedDataWriter.writeTable(System.out, referenceTable,"") \ No newline at end of file diff --git a/numass-main/src/main/groovy/inr/numass/scripts/ShowSpectrum.groovy b/numass-main/src/main/groovy/inr/numass/scripts/ShowSpectrum.groovy new file mode 100644 index 00000000..d9dcbf64 --- /dev/null +++ b/numass-main/src/main/groovy/inr/numass/scripts/ShowSpectrum.groovy @@ -0,0 +1,65 @@ +package inr.numass.scripts + +import hep.dataforge.context.Global +import hep.dataforge.grind.Grind +import hep.dataforge.meta.Meta +import hep.dataforge.stat.fit.FitManager +import hep.dataforge.stat.fit.ParamSet +import hep.dataforge.stat.models.XYModel +import inr.numass.NumassPlugin + +/** + * Created by darksnake on 14-Dec-16. + */ + + +Locale.setDefault(Locale.US); +new NumassPlugin().startGlobal(); + + +def fm = Global.instance().provide("fitting", FitManager.class).getFitManager(); +def mm = fm.modelManager + + +Meta modelMeta = Grind.buildMeta(modelName: "sterile") { + resolution(width: 8.3e-5, tailAlpha: 3e-3) + transmission(trapping: "function::numass.trap.nominal") // numass.trap.nominal = 1.2e-4 - 4.5e-9 * Ei +} + +/* + + 'N' = 2.76515e+06 ± 2.4e+03 (0.00000,Infinity) + 'bkg' = 41.195 ± 0.053 + 'E0' = 18576.35 ± 0.32 + 'mnu2' = 0.00 ± 0.010 + 'msterile2' = 1000000.00 ± 1.0 + 'U2' = 0.00314 ± 0.0010 + 'X' = 0.12000 ± 0.010 (0.00000,Infinity) + 'trap' = 1.089 ± 0.026 + */ + +Meta paramMeta = Grind.buildMeta("params") { + N(value: 2.76515e+06, err: 30, lower: 0) + bkg(value: 41.195, err: 0.1) + E0(value: 18576.35, err: 0.1) + mnu2(value: 0, err: 0.01) + msterile2(value: 1000**2, err: 1) + U2(value: 0.00314, err: 1e-3) + X(value: 0.12000, err: 0.01, lower: 0) + trap(value: 1.089, err: 0.05) +} + +XYModel model = mm.getModel(modelMeta) + +ParamSet allPars = ParamSet.fromMeta(paramMeta); + +def a = 16000; +def b = 18600; +def step = 50; + + +for (double x = a; x < b; x += step) { + println "${x}\t${model.value(x, allPars)}" +} + +Global.terminate() \ No newline at end of file diff --git a/numass-main/src/main/groovy/inr/numass/scripts/ShowTransmission.groovy b/numass-main/src/main/groovy/inr/numass/scripts/ShowTransmission.groovy new file mode 100644 index 00000000..f4dbfeb6 --- /dev/null +++ b/numass-main/src/main/groovy/inr/numass/scripts/ShowTransmission.groovy @@ -0,0 +1,77 @@ +package inr.numass.scripts + +import hep.dataforge.context.Context +import hep.dataforge.context.Global +import hep.dataforge.grind.GrindShell +import hep.dataforge.grind.helpers.PlotHelper +import hep.dataforge.stat.fit.ParamSet +import inr.numass.NumassPlugin +import inr.numass.models.sterile.NumassResolution +import inr.numass.models.sterile.SterileNeutrinoSpectrum + +import static hep.dataforge.grind.Grind.buildMeta + +Context ctx = Global.instance() +ctx.getPlugins().load(FXPlotManager) +ctx.getPlugins().load(NumassPlugin.class) + +GrindShell shell = new GrindShell(ctx) + +shell.eval { + PlotHelper plot = plots + + + ParamSet params = new ParamSet(buildMeta { + N(value: 2.7e+06, err: 30, lower: 0) + bkg(value: 5.0, err: 0.1) + E0(value: 18575.0, err: 0.1) + mnu2(value: 0, err: 0.01) + msterile2(value: 1000**2, err: 1) + U2(value: 0.0, err: 1e-3) + X(value: 0.0, err: 0.01, lower: 0) + trap(value: 1.0, err: 0.05) + }) + + def meta1 = buildMeta { + resolution(width: 8.3e-5, tail: "(0.99797 - 3.05346E-7*D - 5.45738E-10 * D**2 - 6.36105E-14 * D**3)") + } + + def meta2 = buildMeta { + resolution(width: 8.3e-5, tail: "(0.99797 - 3.05346E-7*D - 5.45738E-10 * D**2 - 6.36105E-14 * D**3)*(1-5e-3*sqrt(E/1000))") + } + + def resolution1 = new NumassResolution( + ctx, + meta1.getMeta("resolution") + ) + + def resolution2 = new NumassResolution( + ctx, + meta2.getMeta("resolution") + ) + + plot.plot(frame: "resolution", from: 13500, to: 19000) { x -> + resolution1.value(x, 14000, params) + } + + plot.plot(frame: "resolution", from: 13500, to: 19000) { x -> + resolution2.value(x, 14000, params) + } + + def spectrum1 = new SterileNeutrinoSpectrum(ctx, meta1) + def spectrum2 = new SterileNeutrinoSpectrum(ctx, meta2) + + def x = [] + def y1 = [] + def y2 = [] + (13500..19000).step(100).each { + x << it + y1 << spectrum1.value(it, params) + y2 << spectrum2.value(it, params) + } + + plot.plot(x, y1, "spectrum1", "spectrum") + plot.plot(x, y2, "spectrum2", "spectrum") + +} + diff --git a/numass-main/src/main/groovy/inr/numass/scripts/SignificanceTest.groovy b/numass-main/src/main/groovy/inr/numass/scripts/SignificanceTest.groovy new file mode 100644 index 00000000..eba201e8 --- /dev/null +++ b/numass-main/src/main/groovy/inr/numass/scripts/SignificanceTest.groovy @@ -0,0 +1,110 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.scripts + +import hep.dataforge.context.Global +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.stat.fit.ParamSet +import inr.numass.data.SpectrumInformation +import inr.numass.models.BetaSpectrum +import inr.numass.models.ModularSpectrum +import inr.numass.models.NBkgSpectrum +import inr.numass.models.ResolutionFunction +import org.apache.commons.math3.analysis.UnivariateFunction + +import static java.util.Locale.setDefault + +setDefault(Locale.US); +Global global = Global.instance(); +// global.loadModule(new MINUIT()); + +// FitManager fm = new FitManager("data 2013"); +UnivariateFunction reolutionTail = {x -> + if (x > 1500) { + return 0.98; + } else //Intercept = 1.00051, Slope = -1.3552E-5 + { + return 1.00051 - 1.3552E-5 * x; + } +}; + +ModularSpectrum beta = new ModularSpectrum(new BetaSpectrum(), + new ResolutionFunction(8.3e-5, reolutionTail), 14490d, 19001d); +beta.setCaching(false); +NBkgSpectrum spectrum = new NBkgSpectrum(beta); + +// XYModel model = new XYModel("tritium", spectrum); +ParamSet allPars = new ParamSet(); + +allPars.setParValue("N", 3090.1458); +//значение 6е-6 ÑоответÑтвует полной интенÑтивноÑти 6е7 раÑпадов в Ñекунду +//Проблема была в переполнении Ñчетчика Ñобытий в генераторе. Заменил на long. Возможно Ñтоит поÑтавить туда чиÑло Ñ Ð¿Ð»Ð°Ð²Ð°ÑŽÑ‰ÐµÐ¹ точкой +allPars.setParError("N", 6); +allPars.setParDomain("N", 0d, Double.POSITIVE_INFINITY); +allPars.setParValue("bkg", 2.2110028); +allPars.setParError("bkg", 0.03); +allPars.setParValue("E0", 18580.742); +allPars.setParError("E0", 2); +allPars.setParValue("mnu2", 0d); +allPars.setParError("mnu2", 1d); +allPars.setParValue("msterile2", 1000 * 1000); +allPars.setParValue("U2", 0); +allPars.setParError("U2", 1e-4); +allPars.setParDomain("U2", -1d, 1d); +allPars.setParValue("X", 1.0); +allPars.setParError("X", 0.01); +allPars.setParDomain("X", 0d, Double.POSITIVE_INFINITY); +allPars.setParValue("trap", 1.0d); +allPars.setParError("trap", 0.01d); +allPars.setParDomain("trap", 0d, Double.POSITIVE_INFINITY); + +SpectrumInformation sign = new SpectrumInformation(spectrum); + +// double Elow = 14000d; +// double Eup = 18600d; +// int numpoints = (int) ((Eup - Elow) / 50); +// double time = 1e6 / numpoints; +// DataSet config = getUniformSpectrumConfiguration(Elow, Eup, time, numpoints); +// NamedMatrix infoMatrix = sign.getInformationMatrix(allPars, config,"U2","E0","N"); +// +// PrintNamed.printNamedMatrix(Out.out, infoMatrix); +// NamedMatrix cov = sign.getExpetedCovariance(allPars, config,"U2","E0","N"); +// +// PrintWriter onComplete = Global.onComplete(); +// +// printNamedMatrix(out, cov); +// +// cov = sign.getExpetedCovariance(allPars, config,"U2","E0","N","X"); +// +// printNamedMatrix(out, cov); +//PlotPlugin pm = new PlotPlugin(); + +Map functions = new HashMap<>(); + +functions.put("U2", sign.getSignificanceFunction(allPars, "U2", "U2")); +// functions.put("UX", sign.getSignificanceFunction(allPars, "U2", "X")); +functions.put("X", sign.getSignificanceFunction(allPars, "X", "X")); +functions.put("trap", sign.getSignificanceFunction(allPars, "trap", "trap")); +functions.put("E0", sign.getSignificanceFunction(allPars, "E0", "E0")); + +MetaBuilder builder = new MetaBuilder("significance"); +builder.putValue("from", 14000d); +builder.putValue("to", 18500d); + +pm.plotFunction(builder.build(), functions); + +// printFuntionSimple(out(), func, 14000d, 18600d, 200); + diff --git a/numass-main/src/main/groovy/inr/numass/scripts/Simulate.groovy b/numass-main/src/main/groovy/inr/numass/scripts/Simulate.groovy new file mode 100644 index 00000000..1e29b6d6 --- /dev/null +++ b/numass-main/src/main/groovy/inr/numass/scripts/Simulate.groovy @@ -0,0 +1,102 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.scripts + +import hep.dataforge.context.Global +import hep.dataforge.io.ColumnedDataWriter +import hep.dataforge.meta.Meta +import hep.dataforge.stat.fit.FitHelper +import hep.dataforge.stat.fit.FitResult +import hep.dataforge.stat.fit.FitStage +import hep.dataforge.stat.fit.ParamSet +import hep.dataforge.stat.models.XYModel +import inr.numass.NumassPlugin +import inr.numass.data.SpectrumAdapter +import inr.numass.data.SpectrumGenerator +import inr.numass.models.NBkgSpectrum +import inr.numass.models.sterile.SterileNeutrinoSpectrum +import inr.numass.utils.DataModelUtils + +import static java.util.Locale.setDefault + +/** + * + * @author Darksnake + */ + +setDefault(Locale.US); +new NumassPlugin().startGlobal() + +SterileNeutrinoSpectrum sp = new SterileNeutrinoSpectrum(Global.INSTANCE, Meta.empty()); +//beta.setCaching(false); + +NBkgSpectrum spectrum = new NBkgSpectrum(sp); +XYModel model = new XYModel(Meta.empty(), new SpectrumAdapter(Meta.empty()), spectrum); + +ParamSet allPars = new ParamSet(); + +allPars.setParValue("N", 2e6 / 100); +//значение 6е-6 ÑоответÑтвует полной интенÑтивноÑти 6е7 раÑпадов в Ñекунду +//Проблема была в переполнении Ñчетчика Ñобытий в генераторе. Заменил на long. Возможно Ñтоит поÑтавить туда чиÑло Ñ Ð¿Ð»Ð°Ð²Ð°ÑŽÑ‰ÐµÐ¹ точкой +allPars.setParError("N", 6); +allPars.setParDomain("N", 0d, Double.POSITIVE_INFINITY); +allPars.setParValue("bkg", 2d); +allPars.setParError("bkg", 0.03); +allPars.setParValue("E0", 18575.0); +allPars.setParError("E0", 2); +allPars.setParValue("mnu2", 0d); +allPars.setParError("mnu2", 1d); +allPars.setParValue("msterile2", 8000 * 8000); +allPars.setParValue("U2", 0); +allPars.setParError("U2", 1e-4); +allPars.setParDomain("U2", -1d, 1d); +allPars.setParValue("X", 0); +allPars.setParError("X", 0.01); +allPars.setParDomain("X", 0d, Double.POSITIVE_INFINITY); +allPars.setParValue("trap", 0d); +allPars.setParError("trap", 0.01d); +allPars.setParDomain("trap", 0d, Double.POSITIVE_INFINITY); + +// PrintNamed.printSpectrum(Global.onComplete(), spectrum, allPars, 0.0, 18700.0, 600); +//String fileName = "d:\\PlayGround\\merge\\scans.onComplete"; +// String configName = "d:\\PlayGround\\SCAN.CFG"; +// ListTable config = OldDataReader.readConfig(configName); +SpectrumGenerator generator = new SpectrumGenerator(model, allPars, 12316); + +def data = generator.generateData(DataModelUtils.getUniformSpectrumConfiguration(12000, 18500, 604800 / 100 * 100, 130)); + +//data = TritiumUtils.correctForDeadTime(data, new SpectrumAdapter(), 10e-9); +// data = data.filter("X", Value.of(15510.0), Value.of(18610.0)); +// allPars.setParValue("X", 0.4); + + +ColumnedDataWriter.writeTable(System.out, data, "--- DATA ---"); +//FitState state = new FitState(data, model, allPars); +////new PlotFitResultAction().eval(state); +// +// +//def res = fm.runStage(state, "QOW", FitStage.TASK_RUN, "N", "bkg", "E0", "U2"); + +FitResult res = new FitHelper(Global.INSTANCE).fit(data) + .model(model) + .params(allPars) + .stage("QOW", FitStage.TASK_RUN, "N", "E0") + .run(); +// +// +// +res.printState(new PrintWriter(System.out)); + diff --git a/numass-main/src/main/groovy/inr/numass/scripts/SimulateGun.groovy b/numass-main/src/main/groovy/inr/numass/scripts/SimulateGun.groovy new file mode 100644 index 00000000..8071bfe0 --- /dev/null +++ b/numass-main/src/main/groovy/inr/numass/scripts/SimulateGun.groovy @@ -0,0 +1,67 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.scripts + +import hep.dataforge.context.Global +import hep.dataforge.io.FittingIOUtils +import hep.dataforge.stat.fit.FitManager +import hep.dataforge.stat.fit.ParamSet +import hep.dataforge.stat.models.XYModel +import inr.numass.data.SpectrumAdapter +import inr.numass.models.GunSpectrum +import inr.numass.models.NBkgSpectrum + +import static java.util.Locale.setDefault + +setDefault(Locale.US); +Global global = Global.instance(); +// global.loadModule(new MINUITModule()); + +FitManager fm = new FitManager(); + +GunSpectrum gsp = new GunSpectrum(); +NBkgSpectrum spectrum = new NBkgSpectrum(gsp); + +XYModel model = new XYModel("gun", spectrum, new SpectrumAdapter()); + +ParamSet allPars = new ParamSet() +.setPar("N", 1e3, 1e2) +.setPar("pos", 18500, 0.1) +.setPar("bkg", 50, 1) +.setPar("resA", 5.3e-5, 1e-5) +.setPar("sigma", 0.3, 0.03); + +PrintNamed.printSpectrum(new PrintWriter(System.out), spectrum, allPars, 18495, 18505, 100); + +allPars.setParValue("sigma", 0.6); + +FittingIOUtils.printSpectrum(new PrintWriter(System.out), spectrum, allPars, 18495, 18505, 100); + +// //String fileName = "d:\\PlayGround\\merge\\scans.onComplete"; +//// String configName = "d:\\PlayGround\\SCAN.CFG"; +//// ListTable config = OldDataReader.readConfig(configName); +// SpectrumGenerator generator = new SpectrumGenerator(model, allPars, 12316); +// +// ListTable data = generator.generateData(DataModelUtils.getUniformSpectrumConfiguration(18495, 18505, 20, 20)); +// +//// data = data.filter("X", Value.of(15510.0), Value.of(18610.0)); +//// allPars.setParValue("X", 0.4); +// FitState state = FitTaskManager.buildState(data, model, allPars); +// +// FitState res = fm.runStage(state, "QOW", FitTask.TASK_RUN, "N", "bkg", "pos", "sigma"); +// +// res.print(onComplete()); + diff --git a/numass-main/src/main/groovy/inr/numass/scripts/SimulatePileup.groovy b/numass-main/src/main/groovy/inr/numass/scripts/SimulatePileup.groovy new file mode 100644 index 00000000..b8efe3c5 --- /dev/null +++ b/numass-main/src/main/groovy/inr/numass/scripts/SimulatePileup.groovy @@ -0,0 +1,157 @@ +///* +// * To change this license header, choose License Headers in Project Properties. +// * To change this template file, choose Tools | Templates +// * and open the template in the editor. +// */ +// +//package inr.numass.scripts +// +//import hep.dataforge.grind.Grind +//import hep.dataforge.values.Values +//import inr.numass.NumassUtils +//import inr.numass.data.api.NumassPoint +//import inr.numass.data.storage.NumassDataLoader +// +//import inr.numass.data.PileUpSimulator +//import inr.numass.utils.UnderflowCorrection +//import org.apache.commons.math3.random.JDKRandomGenerator +// +//rnd = new JDKRandomGenerator(); +// +//////Loading data +//File dataDir = new File("D:\\Work\\Numass\\data\\2016_10\\Fill_1\\set_28") +////File dataDir = new File("D:\\Work\\Numass\\data\\2016_10\\Fill_2_wide\\set_7") +//if (!dataDir.exists()) { +// println "dataDir directory does not exist" +//} +//def data = NumassDataLoader.fromLocalDir(null, dataDir).getNMPoints() +// +////File rootDir = new File("D:\\Work\\Numass\\data\\2016_10\\Fill_1") +//////File rootDir = new File("D:\\Work\\Numass\\data\\2016_10\\Fill_2_wide") +//////File rootDir = new File("D:\\Work\\Numass\\data\\2017_01\\Fill_2_wide") +//// +////NumassStorage storage = NumassStorage.buildLocalNumassRoot(rootDir, true); +//// +////Collection data = NumassDataUtils.joinSpectra( +//// StorageUtils.loaderStream(storage) +//// .filter { it.key.matches("set_3.") } +//// .map { +//// println "loading ${it.key}" +//// it.value +//// } +////) +// +////Simulation process +//Map> res = [:] +// +//List generated = new ArrayList<>(); +//List registered = new ArrayList<>(); +//List firstIteration = new ArrayList<>(); +//List secondIteration = new ArrayList<>(); +//List pileup = new ArrayList<>(); +// +//lowerChannel = 400; +//upperChannel = 1800; +// +//PileUpSimulator buildSimulator(NumassPoint point, double cr, NumassPoint reference = null, boolean extrapolate = true, double scale = 1d) { +// def cfg = Grind.buildMeta(cr: cr) { +// pulser(mean: 3450, sigma: 86.45, freq: 66.43) +// } +// //NMEventGeneratorWithPulser generator = new NMEventGeneratorWithPulser(rnd, cfg) +// +// def generator = b +// +// if (extrapolate) { +// double[] chanels = new double[RawNMPoint.MAX_CHANEL]; +// double[] values = new double[RawNMPoint.MAX_CHANEL]; +// Values fitResult = new UnderflowCorrection().fitPoint(point, 400, 600, 1800, 20); +// +// def amp = fitResult.getDouble("amp") +// def sigma = fitResult.getDouble("expConst") +// if (sigma > 0) { +// +// for (int i = 0; i < upperChannel; i++) { +// chanels[i] = i; +// if (i < lowerChannel) { +// values[i] = point.getLength()*amp * Math.exp((i as double) / sigma) +// } else { +// values[i] = Math.max(0, point.getCount(i) - (reference == null ? 0 : reference.getCount(i)) as int); +// } +// } +// generator.loadSpectrum(chanels, values) +// } else { +// generator.loadSpectrum(point, reference, lowerChannel, upperChannel); +// } +// } else { +// generator.loadSpectrum(point, reference, lowerChannel, upperChannel); +// } +// +// return new PileUpSimulator(point.length * scale, rnd, generator).withUset(point.voltage).generate(); +//} +// +//double adjustCountRate(PileUpSimulator simulator, NumassPoint point) { +// double generatedInChannel = simulator.generated().getCountInWindow(lowerChannel, upperChannel); +// double registeredInChannel = simulator.registered().getCountInWindow(lowerChannel, upperChannel); +// return (generatedInChannel / registeredInChannel) * (point.getCountInWindow(lowerChannel, upperChannel) / point.getLength()); +//} +// +//data.forEach { point -> +// double cr = NumassUtils.countRateWithDeadTime(point, lowerChannel, upperChannel, 6.55e-6); +// +// PileUpSimulator simulator = buildSimulator(point, cr); +// +// //second iteration to exclude pileup overlap +// NumassPoint pileupPoint = simulator.pileup(); +// firstIteration.add(simulator.registered()); +// +// //updating count rate +// cr = adjustCountRate(simulator, point); +// simulator = buildSimulator(point, cr, pileupPoint); +// +// pileupPoint = simulator.pileup(); +// secondIteration.add(simulator.registered()); +// +// cr = adjustCountRate(simulator, point); +// simulator = buildSimulator(point, cr, pileupPoint); +// +// generated.add(simulator.generated()); +// registered.add(simulator.registered()); +// pileup.add(simulator.pileup()); +//} +//res.put("original", data); +//res.put("generated", generated); +//res.put("registered", registered); +//// res.put("firstIteration", new SimulatedPoint("firstIteration", firstIteration)); +//// res.put("secondIteration", new SimulatedPoint("secondIteration", secondIteration)); +//res.put("pileup", pileup); +// +//def keys = res.keySet(); +// +////print spectra for selected point +//double u = 16500d; +// +//List points = res.values().collect { it.find { it.voltage == u }.getMap(20, true) } +// +//println "\n Spectrum example for U = ${u}\n" +// +//print "channel\t" +//println keys.join("\t") +// +//points.first().keySet().each { +// print "${it}\t" +// println points.collect { map -> map[it] }.join("\t") +//} +// +////printing count rate in window +//print "U\tLength\t" +//print keys.collect { it + "[total]" }.join("\t") + "\t" +//print keys.collect { it + "[pulse]" }.join("\t") + "\t" +//println keys.join("\t") +// +//for (int i = 0; i < data.size(); i++) { +// print "${data.get(i).getVoltage()}\t" +// print "${data.get(i).getLength()}\t" +// print keys.collect { res[it].get(i).getTotalCount() }.join("\t") + "\t" +// print keys.collect { res[it].get(i).getCountInWindow(3100, 3800) }.join("\t") + "\t" +// println keys.collect { res[it].get(i).getCountInWindow(400, 3100) }.join("\t") +//} diff --git a/numass-main/src/main/groovy/inr/numass/scripts/SterileDemo.groovy b/numass-main/src/main/groovy/inr/numass/scripts/SterileDemo.groovy new file mode 100644 index 00000000..abb51b17 --- /dev/null +++ b/numass-main/src/main/groovy/inr/numass/scripts/SterileDemo.groovy @@ -0,0 +1,81 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.scripts + +import hep.dataforge.context.Global +import hep.dataforge.grind.GrindMetaBuilder +import hep.dataforge.io.FittingIOUtils +import hep.dataforge.meta.Meta +import hep.dataforge.stat.fit.ParamSet +import hep.dataforge.stat.models.XYModel +import hep.dataforge.stat.parametric.ParametricFunction +import inr.numass.data.SpectrumAdapter +import inr.numass.models.NBkgSpectrum +import inr.numass.models.sterile.SterileNeutrinoSpectrum + +import static java.util.Locale.setDefault + +/** + * + * @author Darksnake + */ + +setDefault(Locale.US); + +//ParametricFunction beta = new BetaSpectrum(); + +//ModularSpectrum beta = new ModularSpectrum(new BetaSpectrum(), 8.3e-5, 13990d, 18600d); +//beta.setCaching(false) + +Meta cfg = new GrindMetaBuilder().meta() { + resolution(width: 8.3e-5) +}.build(); + +ParametricFunction beta = new SterileNeutrinoSpectrum(Global.instance(), cfg); + +NBkgSpectrum spectrum = new NBkgSpectrum(beta); +XYModel model = new XYModel(spectrum, new SpectrumAdapter()); + +ParamSet allPars = new ParamSet(); + +allPars.setPar("N", 6.6579e+05, 1.8e+03, 0d, Double.POSITIVE_INFINITY); +allPars.setPar("bkg", 0.5387, 0.050); +allPars.setPar("E0", 18574.94, 1.4); +allPars.setPar("mnu2", 0d, 1d); +allPars.setPar("msterile2", 1000d * 1000d, 0); +allPars.setPar("U2", 0.0, 1e-4, -1d, 1d); +allPars.setPar("X", 0.04, 0.01, 0d, Double.POSITIVE_INFINITY); +allPars.setPar("trap", 1.634, 0.01, 0d, Double.POSITIVE_INFINITY); + +FittingIOUtils.printSpectrum(Global.out(), spectrum, allPars, 14000, 18600.0, 400); + +//SpectrumGenerator generator = new SpectrumGenerator(model, allPars, 12316); +// +//ListTable data = generator.generateData(DataModelUtils.getUniformSpectrumConfiguration(14000d, 18500, 2000, 90)); +// +//data = NumassUtils.correctForDeadTime(data, new SpectrumAdapter(), 1e-8); +//// data = data.filter("X", Value.of(15510.0), Value.of(18610.0)); +//// allPars.setParValue("X", 0.4); +//FitState state = new FitState(data, model, allPars); +////new PlotFitResultAction().eval(state); +// +// +//FitState res = fm.runStage(state, "QOW", FitTask.TASK_RUN, "N", "bkg", "E0", "U2", "trap"); +// +// +// +//res.print(onComplete()); +// diff --git a/numass-main/src/main/groovy/inr/numass/scripts/SystTransmission.groovy b/numass-main/src/main/groovy/inr/numass/scripts/SystTransmission.groovy new file mode 100644 index 00000000..d3917e96 --- /dev/null +++ b/numass-main/src/main/groovy/inr/numass/scripts/SystTransmission.groovy @@ -0,0 +1,101 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.scripts + +import hep.dataforge.stat.fit.FitManager +import hep.dataforge.stat.fit.FitState +import hep.dataforge.stat.fit.MINUITPlugin +import hep.dataforge.stat.fit.ParamSet +import hep.dataforge.stat.models.XYModel +import hep.dataforge.tables.ListTable +import inr.numass.data.SpectrumAdapter +import inr.numass.data.SpectrumGenerator +import inr.numass.models.BetaSpectrum +import inr.numass.models.ModularSpectrum +import inr.numass.models.NBkgSpectrum +import inr.numass.models.ResolutionFunction +import inr.numass.utils.DataModelUtils + +import static java.util.Locale.setDefault + +/** + * + * @author Darksnake + */ + +setDefault(Locale.US); +new MINUITPlugin().startGlobal(); + +FitManager fm = new FitManager(); + +ResolutionFunction resolution = new ResolutionFunction(8.3e-5); +//resolution.setTailFunction(ResolutionFunction.getRealTail()); +resolution.setTailFunction(ResolutionFunction.getAngledTail(0.00325)); +ModularSpectrum beta = new ModularSpectrum(new BetaSpectrum(), resolution, 18395d, 18580d); +beta.setCaching(false); + +NBkgSpectrum spectrum = new NBkgSpectrum(beta); +XYModel model = new XYModel("tritium", spectrum, new SpectrumAdapter()); + +ParamSet allPars = new ParamSet(); + + +allPars.setPar("N", 6e9, 1e5, 0, Double.POSITIVE_INFINITY); + +allPars.setPar("bkg", 0.002, 0.005 ); + +allPars.setPar("E0", 18575.0, 0.1 ); + +allPars.setPar("mnu2", 0, 2); + +def mster = 3000;// Mass of sterile neutrino in eV + +allPars.setPar("msterile2", mster**2, 1); + +allPars.setPar("U2", 0, 1e-4); + +allPars.setPar("X", 0, 0.05, 0d, Double.POSITIVE_INFINITY); + +allPars.setPar("trap", 1, 0.01, 0d, Double.POSITIVE_INFINITY); + +int seed = 12316 +SpectrumGenerator generator = new SpectrumGenerator(model, allPars, seed); + +def config = DataModelUtils.getUniformSpectrumConfiguration(18530d, 18580, 1e7, 60) +//def config = DataModelUtils.getSpectrumConfigurationFromResource("/data/run23.cfg") + +ListTable data = generator.generateExactData(config); + +FitState state = new FitState(data, model, allPars); + +println("Simulating data with real tail") + +println("Fitting data with real parameters") + +FitState res = fm.runTask(state, "QOW", FitTask.TASK_RUN, "N", "bkg", "E0", "mnu2"); +res.print(out()); + +def mnu2 = res.getParameters().getValue("mnu2"); + +println("Setting constant tail and fitting") +resolution.setTailFunction(ResolutionFunction.getConstantTail()); + +res = fm.runTask(state, "QOW", FitTask.TASK_RUN, "N", "bkg","E0","mnu2"); +res.print(out()); + +def diff = res.getParameters().getValue("mnu2") - mnu2; + +println("\n\nSquared mass difference: ${diff}") \ No newline at end of file diff --git a/numass-main/src/main/groovy/inr/numass/scripts/Systematics.groovy b/numass-main/src/main/groovy/inr/numass/scripts/Systematics.groovy new file mode 100644 index 00000000..47ceec13 --- /dev/null +++ b/numass-main/src/main/groovy/inr/numass/scripts/Systematics.groovy @@ -0,0 +1,99 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.scripts + +import hep.dataforge.context.Global +import hep.dataforge.io.FittingIOUtils +import hep.dataforge.meta.Meta +import hep.dataforge.stat.fit.* +import hep.dataforge.stat.models.XYModel +import inr.numass.data.SpectrumAdapter +import inr.numass.data.SpectrumGenerator +import inr.numass.models.BetaSpectrum +import inr.numass.models.ModularSpectrum +import inr.numass.models.NBkgSpectrum +import inr.numass.models.ResolutionFunction +import inr.numass.utils.DataModelUtils +import org.apache.commons.math3.analysis.BivariateFunction + +import static java.util.Locale.setDefault + +/** + * + * @author Darksnake + */ + +setDefault(Locale.US); +new MINUITPlugin().startGlobal(); + +FitManager fm = Global.INSTANCE.get(FitManager) + + +BivariateFunction resolution = new ResolutionFunction(8.3e-5); + +ModularSpectrum beta = new ModularSpectrum(new BetaSpectrum(), resolution, 13490d, 18575d); +beta.setCaching(false); + +NBkgSpectrum spectrum = new NBkgSpectrum(beta); +XYModel model = new XYModel(Meta.empty(), new SpectrumAdapter(Meta.empty()), spectrum); + +ParamSet allPars = new ParamSet(); + + +allPars.setPar("N", 6e5, 10, 0, Double.POSITIVE_INFINITY); + +allPars.setPar("bkg", 2d, 0.1); + +allPars.setPar("E0", 18575.0, 0.05); + +allPars.setPar("mnu2", 0, 1); + +def mster = 3000;// Mass of sterile neutrino in eV + +allPars.setPar("msterile2", mster**2, 1); + +allPars.setPar("U2", 0, 1e-4); + +allPars.setPar("X", 0, 0.05, 0d, Double.POSITIVE_INFINITY); + +allPars.setPar("trap", 0, 0.01, 0d, Double.POSITIVE_INFINITY); + +SpectrumGenerator generator = new SpectrumGenerator(model, allPars, 12316); + +def data = generator.generateData(DataModelUtils.getUniformSpectrumConfiguration(14000d, 18200, 1e6, 60)); + +// data = data.filter("X", Value.of(15510.0), Value.of(18610.0)); +allPars.setParValue("U2", 0); +FitState state = new FitState(data, model, allPars); +//new PlotFitResultAction(Global.instance(), null).runOne(state); + +//double delta = 4e-6; + +//resolution.setTailFunction{double E, double U -> +// 1-delta*(E-U); +//} + +//resolution.setTailFunction(ResolutionFunction.getRealTail()) + +//PlotFrame frame = JFreeChartFrame.drawFrame("Transmission function", null); +//frame.add(new PlottableFunction("transmission",null, {U -> resolution.value(18500,U)},13500,18505,500)); + +def res = fm.runStage(state, "QOW", FitStage.TASK_RUN, "N", "bkg", "E0", "U2", "trap"); + + +res.printState(new PrintWriter(System.out)) + +FittingIOUtils.printResiduals(new PrintWriter(System.out), res.optState().get()) diff --git a/numass-main/src/main/groovy/inr/numass/scripts/TestExperimentalVariableLossSpectrum.groovy b/numass-main/src/main/groovy/inr/numass/scripts/TestExperimentalVariableLossSpectrum.groovy new file mode 100644 index 00000000..e700b6e5 --- /dev/null +++ b/numass-main/src/main/groovy/inr/numass/scripts/TestExperimentalVariableLossSpectrum.groovy @@ -0,0 +1,68 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.scripts + +import hep.dataforge.io.PrintFunction +import hep.dataforge.maths.integration.UnivariateIntegrator +import hep.dataforge.plots.data.XYFunctionPlot +import hep.dataforge.plots.jfreechart.JFreeChartFrame +import hep.dataforge.stat.fit.ParamSet +import inr.numass.models.ExperimentalVariableLossSpectrum +import org.apache.commons.math3.analysis.UnivariateFunction + +//double exPos = 12.94 +//double exW = 1.31 +//double ionPos = 14.13 +//double ionW = 12.79 +//double exIonRatio = 0.6059 + +ParamSet params = new ParamSet() +.setParValue("shift",0) +.setParValue("X", 0.4) +.setParValue("exPos", 12.94) +.setParValue("ionPos", 15.6) +.setParValue("exW", 1.31) +.setParValue("ionW", 12.79) +.setParValue("exIonRatio", 0.6059) + +ExperimentalVariableLossSpectrum lsp = new ExperimentalVariableLossSpectrum(19005, 8e-5, 19010,0.2); + + +JFreeChartFrame frame = JFreeChartFrame.drawFrame("Experimental Loss Test", null); +UnivariateIntegrator integrator = NumassContext.defaultIntegrator + +UnivariateFunction exFunc = lsp.excitation(params.getValue("exPos"), params.getValue("exW")); +frame.add(XYFunctionPlot.plotFunction("ex", exFunc, 0d, 50d, 500)); + +println "excitation norm factor " + integrator.integrate(0, 50, exFunc) + +UnivariateFunction ionFunc = lsp.ionization(params.getValue("ionPos"), params.getValue("ionW")); +frame.add(XYFunctionPlot.plotFunction("ion", ionFunc, 0d, 50d, 500)); + +println "ionization norm factor " + integrator.integrate(0, 200, ionFunc) + +UnivariateFunction sumFunc = lsp.singleScatterFunction(params); +frame.add(XYFunctionPlot.plotFunction("sum", sumFunc, 0d, 50d, 500)); + +println "sum norm factor " + integrator.integrate(0, 100, sumFunc) + +PrintFunction.printFunctionSimple(new PrintWriter(System.out), sumFunc, 0d, 50d, 100) + + +JFreeChartFrame integerFrame = JFreeChartFrame.drawFrame("Experimental Loss Test", null); + +UnivariateFunction integr = { d-> lsp.value(d,params)} +integerFrame.add(XYFunctionPlot.plotFunction("integr", integr, 18950d, 19005d, 500)); \ No newline at end of file diff --git a/numass-main/src/main/groovy/inr/numass/scripts/TheoreticalLossFunction.groovy b/numass-main/src/main/groovy/inr/numass/scripts/TheoreticalLossFunction.groovy new file mode 100644 index 00000000..2a3ba881 --- /dev/null +++ b/numass-main/src/main/groovy/inr/numass/scripts/TheoreticalLossFunction.groovy @@ -0,0 +1,46 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.scripts + +import hep.dataforge.plots.data.XYFunctionPlot +import hep.dataforge.plots.jfreechart.JFreeChartFrame +import org.apache.commons.math3.analysis.UnivariateFunction + + +def lorenz = {x, x0, gama -> 1/(3.14*gama*(1+(x-x0)*(x-x0)/gama/gama))} + + +def excitationSpectrum = {Map lines, double gama -> + UnivariateFunction function = {x-> + double res = 0; + lines.each{k,v -> res += lorenz(x,k,gama)*v}; + return res; + } + return function; +} + +def lines = +[ + 12.6:0.5, + 12.4:0.3, + 12.2:0.2 +] + +UnivariateFunction excitation = excitationSpectrum(lines,0.08) + +JFreeChartFrame frame = JFreeChartFrame.drawFrame("theoretical loss spectrum", null); + +frame.add(XYFunctionPlot.plotFunction("excitation", excitation, 0d, 20d, 500)); \ No newline at end of file diff --git a/numass-main/src/main/groovy/inr/numass/scripts/TritiumTest.groovy b/numass-main/src/main/groovy/inr/numass/scripts/TritiumTest.groovy new file mode 100644 index 00000000..e3283e9e --- /dev/null +++ b/numass-main/src/main/groovy/inr/numass/scripts/TritiumTest.groovy @@ -0,0 +1,119 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.scripts + +import hep.dataforge.context.Global +import hep.dataforge.data.DataSet +import hep.dataforge.stat.fit.FitManager +import hep.dataforge.stat.fit.FitState +import hep.dataforge.stat.fit.ParamSet +import hep.dataforge.stat.likelihood.BayesianConfidenceLimit +import hep.dataforge.stat.models.XYModel +import hep.dataforge.tables.ListTable +import inr.numass.data.SpectrumGenerator +import inr.numass.models.BetaSpectrum +import inr.numass.models.ModularSpectrum +import inr.numass.models.NBkgSpectrum + +import static inr.numass.utils.DataModelUtils.getUniformSpectrumConfiguration + +PrintWriter out = Global.out(); +FitManager fm = new FitManager(); + +setSeed(543982); + +// TritiumSpectrum beta = new TritiumSpectrum(2e-4, 13995d, 18580d); +File fssfile = new File("c:\\Users\\Darksnake\\Dropbox\\PlayGround\\FS.txt"); +ModularSpectrum beta = new ModularSpectrum(new BetaSpectrum(),8.3e-5, 14400d, 19010d); +beta.setCaching(false); +NBkgSpectrum spectrum = new NBkgSpectrum(beta); +XYModel model = new XYModel("tritium", spectrum); + +ParamSet allPars = new ParamSet(); + +allPars.setParValue("N", 6e5); +//значение 6е-6 ÑоответÑтвует полной интенÑтивноÑти 6е7 раÑпадов в Ñекунду +//Проблема была в переполнении Ñчетчика Ñобытий в генераторе. Заменил на long. Возможно Ñтоит поÑтавить туда чиÑло Ñ Ð¿Ð»Ð°Ð²Ð°ÑŽÑ‰ÐµÐ¹ точкой +allPars.setParError("N", 25); +allPars.setParDomain("N", 0d, Double.POSITIVE_INFINITY); +allPars.setParValue("bkg", 5); +allPars.setParError("bkg", 1e-3); +allPars.setParValue("E0", 18575d); +allPars.setParError("E0", 0.1); +allPars.setParValue("mnu2", 0d); +allPars.setParError("mnu2", 1d); +allPars.setParValue("msterile2", 1000 * 1000); +allPars.setParValue("U2", 0); +allPars.setParError("U2", 1e-4); +allPars.setParDomain("U2", 0d, 1d); +allPars.setParValue("X", 0.0); +allPars.setParDomain("X", 0d, Double.POSITIVE_INFINITY); +allPars.setParValue("trap", 1d); +allPars.setParError("trap", 0.01d); +allPars.setParDomain("trap", 0d, Double.POSITIVE_INFINITY); + +// PlotPlugin pm = new PlotPlugin(); +// String plotTitle = "Tritium spectrum"; +// pm.plotFunction(ParametricUtils.getSpectrumFunction(spectrum, allPars), 14000, 18600, 500,plotTitle, null); +// PrintNamed.printSpectrum(Out.onComplete, beta.trapping, allPars, 14000d, 18600d, 500); +// double e = 18570d; +// trans.alpha = 1e-4; +// trans.plotTransmission(System.onComplete, allPars, e, e-1000d, e+100d, 200); +SpectrumGenerator generator = new SpectrumGenerator(model, allPars); + +// ColumnedDataFile file = new ColumnedDataFile("d:\\PlayGround\\RUN36.cfg"); +// ListTable config = file.getPoints("time","X"); +double Elow = 14000d; +double Eup = 18600d; +int numpoints = (int) ((Eup - Elow) / 50); +double time = 1e6 / numpoints; // 3600 / numpoints; +DataSet config = getUniformSpectrumConfiguration(Elow, Eup, time, numpoints); +// config.addAll(DataModelUtils.getUniformSpectrumConfiguration(Eup, Elow, time, numpoints));// в обратную Ñторону + +ListTable data = generator.generateData(config); +// plotTitle = "Generated tritium spectrum data"; +// pm.plotXYScatter(data, "X", "Y",plotTitle, null); +// bareBeta.setFSS("D:\\PlayGround\\FSS.dat"); +// data = tritiumUtils.applyDrift(data, 2.8e-6); + +FitState state = fm.buildState(data, model, allPars); + +// fm.checkDerivs(); +// res.print(Out.onComplete); +// fm.checkFitDerivatives(); +FitState res = fm.runDefaultStage(state, "U2", "N", "trap"); + +res.print(out); + +// res = fm.runFrom(res); +// res = fm.generateErrorsFrom(res); +beta.setCaching(true); +beta.setSuppressWarnings(true); + +BayesianConfidenceLimit bm = new BayesianConfidenceLimit(); +// bm.setPriorProb(new OneSidedUniformPrior("trap", 0, true)); +// bm.setPriorProb(new Gaussian("trap", 1d, 0.002)); +// bm.printMarginalLikelihood(Out.onComplete,"U2", res); + +FitState conf = bm.getConfidenceInterval("U2", res, ["U2", "N", "trap"]); +// plotTitle = String.format("Marginal likelihood for parameter \'%s\'", "U2"); +// pm.plotFunction(bm.getMarginalLikelihood("U2", res), 0, 2e-3, 40,plotTitle, null); + +conf.print(out); +// PrintNamed.printLogProbRandom(Out.onComplete, res, 5000,0.5d, "E0","N"); + +spectrum.counter.print(out); + diff --git a/numass-main/src/main/groovy/inr/numass/scripts/models/TristanModel.groovy b/numass-main/src/main/groovy/inr/numass/scripts/models/TristanModel.groovy new file mode 100644 index 00000000..832e6c24 --- /dev/null +++ b/numass-main/src/main/groovy/inr/numass/scripts/models/TristanModel.groovy @@ -0,0 +1,100 @@ +package inr.numass.scripts.models + +import hep.dataforge.context.Context +import hep.dataforge.context.Global +import hep.dataforge.grind.GrindShell +import hep.dataforge.grind.helpers.PlotHelper +import hep.dataforge.meta.Meta +import hep.dataforge.stat.fit.FitManager +import hep.dataforge.stat.fit.FitStage +import hep.dataforge.stat.fit.FitState +import hep.dataforge.stat.fit.ParamSet +import hep.dataforge.stat.models.XYModel +import hep.dataforge.stat.parametric.ParametricFunction +import hep.dataforge.tables.Table +import inr.numass.NumassPlugin +import inr.numass.data.SpectrumAdapter +import inr.numass.data.SpectrumGenerator +import inr.numass.models.NBkgSpectrum +import inr.numass.models.NumassModelsKt +import inr.numass.models.misc.ModGauss +import inr.numass.models.sterile.NumassBeta +import inr.numass.utils.DataModelUtils + + +Context ctx = Global.instance() +ctx.getPlugins().load(NumassPlugin) + +new GrindShell(ctx).eval { + PlotHelper ph = plots + + def beta = new NumassBeta().getSpectrum(0) + def response = new ModGauss(5.0) + ParametricFunction spectrum = NumassModelsKt.convolute(beta, response) + + def model = new XYModel(Meta.empty(), new SpectrumAdapter(Meta.empty()), new NBkgSpectrum(spectrum)) + + ParamSet params = morph(ParamSet, [:], "params") { + N(value: 1e+14, err: 30, lower: 0) + bkg(value: 5.0, err: 0.1) + E0(value: 18575.0, err: 0.1) + mnu2(value: 0, err: 0.01) + msterile2(value: 7000**2, err: 1) + U2(value: 0.0, err: 1e-3) + //X(value: 0.0, err: 0.01, lower: 0) + //trap(value: 1.0, err: 0.05) + w(value: 150, err: 5) + //shift(value: 1, err: 1e-2) + //tailAmp(value: 0.01, err: 1e-2) + tailW(value: 300, err: 1) + } + +// double norm = NumassIntegrator.defaultIntegrator.integrate(1000d, 18500d) { +// model.value(it, params) +// } + +// println("The total number of events is $norm") +// +// ph.plotFunction(-2000d, 500d, 400, "actual", "response") { double x -> +// response.value(x, params) +// } + + SpectrumGenerator generator = new SpectrumGenerator(model, params, 12316); + + ph.plot(data: (2000..19500).step(50).collectEntries { + [it, model.value(it, params)] + }, name: "spectrum", frame: "test") + .configure(showLine: true, showSymbol: false, showErrors: false, thickness: 2, connectionType: "spline", color: "red") + + + Table data = generator.generateData(DataModelUtils.getUniformSpectrumConfiguration(7000, 19500, 1, 1000)); + + //params.setParValue("w", 151) + //params.setParValue("tailAmp", 0.011) + //params.setParValue("X", 0.01) + //params.setParValue("trap", 0.01) + //params.setParValue("mnu2", 4) + + + ph.plotFunction(-2000d, 500d, 400, "supposed", "response") { double x -> + response.value(x, params) + } + + ph.plot(data: (2000..19500).step(50).collectEntries { + [it, model.value(it, params)] + }, name: "spectrum-mod", frame: "test") + .configure(showLine: true, showSymbol: false, showErrors: false, thickness: 2, connectionType: "spline", color: "green") + + ph.plot(data: data, frame: "test", adapter: new SpectrumAdapter(Meta.empty())) + .configure(color: "blue") + + FitState state = new FitState(data, model, params); + + def fm = ctx.get(FitManager) + + def res = fm.runStage(state, "MINUIT", FitStage.TASK_RUN, "N", "bkg", "E0", "U2"); + + + res.printState(ctx.getOutput.out().newPrintWriter()); + NumassIOKt.display(res, ctx, "fit") +} \ No newline at end of file diff --git a/numass-main/src/main/groovy/inr/numass/scripts/underflow/ResponseFunction.groovy b/numass-main/src/main/groovy/inr/numass/scripts/underflow/ResponseFunction.groovy new file mode 100644 index 00000000..8683ae33 --- /dev/null +++ b/numass-main/src/main/groovy/inr/numass/scripts/underflow/ResponseFunction.groovy @@ -0,0 +1,66 @@ +package inr.numass.scripts.underflow + +import hep.dataforge.cache.CachePlugin +import hep.dataforge.context.Context +import hep.dataforge.context.Global +import hep.dataforge.data.DataNode +import hep.dataforge.grind.GrindShell +import hep.dataforge.io.ColumnedDataWriter +import hep.dataforge.meta.Meta +import hep.dataforge.tables.ColumnTable +import hep.dataforge.tables.Table +import inr.numass.NumassPlugin +import inr.numass.data.NumassDataUtils + +import static hep.dataforge.grind.Grind.buildMeta + +Context ctx = Global.instance() +ctx.getPlugins().load(FXPlotManager) +ctx.getPlugins().load(NumassPlugin.class) +ctx.getPlugins().load(CachePlugin.class) + +Meta meta = buildMeta { + data(dir: "D:\\Work\\Numass\\data\\2017_05\\Fill_2", mask: "set_.{1,3}") + generate(t0: 3e4, sort: false) +} + +def shell = new GrindShell(ctx); + +DataNode spectra = UnderflowUtils.getSpectraMap(shell, meta); + +shell.eval { + def columns = [:]; + + Map binned = [:] + + + (14500..17500).step(500).each { + Table up = binned.computeIfAbsent(it) { key -> + NumassDataUtils.spectrumWithBinning(spectra.optData(key as String).get().get(), 20, 400, 3100); + } + + Table lo = binned.computeIfAbsent(it - 500) { key -> + NumassDataUtils.spectrumWithBinning(spectra.optData(key as String).get().get(), 20, 400, 3100); + } + + columns << [channel: up.channel] + + columns << [(it as String): NumassDataUtils.subtractSpectrum(lo, up).getColumn("cr")] + } + + ColumnedDataWriter.writeTable(System.out, ColumnTable.of(columns), "Response function") + +// println() +// println() +// +// columns.clear() +// +// binned.each { key, table -> +// columns << [channel: table.channel] +// columns << [(key as String): table.cr] +// } +// +// ColumnedDataWriter.writeTable(System.out, +// ColumnTable.of(columns), +// "Spectra") +} \ No newline at end of file diff --git a/numass-main/src/main/groovy/inr/numass/scripts/underflow/Underflow.groovy b/numass-main/src/main/groovy/inr/numass/scripts/underflow/Underflow.groovy new file mode 100644 index 00000000..e1a19942 --- /dev/null +++ b/numass-main/src/main/groovy/inr/numass/scripts/underflow/Underflow.groovy @@ -0,0 +1,139 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package inr.numass.scripts.underflow + +import hep.dataforge.cache.CachePlugin +import hep.dataforge.context.Context +import hep.dataforge.context.Global +import hep.dataforge.data.DataNode +import hep.dataforge.grind.GrindShell +import hep.dataforge.grind.helpers.PlotHelper +import hep.dataforge.io.ColumnedDataWriter +import hep.dataforge.meta.Meta +import hep.dataforge.plots.PlotGroup +import hep.dataforge.plots.data.DataPlot +import hep.dataforge.tables.Adapters +import hep.dataforge.tables.Table +import inr.numass.NumassPlugin +import inr.numass.data.analyzers.NumassAnalyzerKt +import inr.numass.subthreshold.Threshold +import javafx.application.Platform + +import static hep.dataforge.grind.Grind.buildMeta +import static inr.numass.data.analyzers.NumassAnalyzer.CHANNEL_KEY +import static inr.numass.data.analyzers.NumassAnalyzer.COUNT_RATE_KEY + +Context ctx = Global.instance() +ctx.getPlugins().load(NumassPlugin) +ctx.getPlugins().load(CachePlugin) + +Meta meta = buildMeta(t0: 3e4) { + data(dir: "D:\\Work\\Numass\\data\\2017_11\\Fill_2", mask: "set_3.") + subtract(reference: 18500) + fit(xLow: 400, xHigh: 600, upper: 3000, binning: 20, method: "pow") + scan( + hi: [500, 600, 700, 800, 900], + lo: [350, 400, 450], + def: 700 + ) + window(lo: 300, up: 3000) +} + + +def shell = new GrindShell(ctx); + +DataNode
spectra = Threshold.INSTANCE.getSpectraMap(ctx, meta).computeAll(); + +shell.eval { + + //subtracting reference point + Map spectraMap + if (meta.hasValue("subtract.reference")) { + String referenceVoltage = meta["subtract.reference"] + println "subtracting reference point ${referenceVoltage}" + def referencePoint = spectra.get(referenceVoltage) + spectraMap = spectra + .findAll { it.name != referenceVoltage } + .collectEntries { + [(it.meta["voltage"]): NumassAnalyzerKt.subtractAmplitudeSpectrum(it.get(), referencePoint)] + } + } else { + spectraMap = spectra.collectEntries { return [(it.meta["voltage"]): it.get()] } + } + + //Showing selected points + def showPoints = { Map points, int binning = 20, int loChannel = 300, int upChannel = 2000 -> + def plotGroup = new PlotGroup("points"); + def adapter = Adapters.buildXYAdapter(CHANNEL_KEY, COUNT_RATE_KEY) + points.each { + plotGroup.set( + DataPlot.plot( + it.key as String, + adapter, + NumassAnalyzerKt.withBinning(it.value as Table, binning) + ) + ) + } + + //configuring and plotting + plotGroup.configure(showLine: true, showSymbol: false, showErrors: false, connectionType: "step") + def frame = (plots as PlotHelper).getManager().getPlotFrame("Spectra") + frame.configureValue("yAxis.type", "log") + frame.add(plotGroup) + } + + showPoints(spectraMap.findAll { it.key in [14100d, 14200d, 14300d, 14400d, 14800d, 15000d, 15200d, 15700d] }) + + meta["scan.hi"].each { xHigh -> + println "Caclculate correctuion for upper linearity bound: ${xHigh}" + Table correctionTable = TableTransform.filter( + Threshold.INSTANCE.calculateSubThreshold( + spectraMap, + meta.getMeta("fit").builder.setValue("xHigh", xHigh) + ), + "correction", + 0, + 2 + ) + + if (xHigh == meta["scan.def"]) { + ColumnedDataWriter.writeTable(System.out, correctionTable, "underflow parameters") + } + + Platform.runLater { + (plots as PlotHelper).plot( + data: correctionTable, + adapter: Adapters.buildXYAdapter("U", "correction"), + name: "upper_${xHigh}", + frame: "upper" + ) + } + } + + + meta["scan.lo"].each { xLow -> + println "Caclculate correctuion for lower linearity bound: ${xLow}" + Table correctionTable = TableTransform.filter( + Threshold.INSTANCE.calculateSubThreshold( + spectraMap, + meta.getMeta("fit").builder.setValue("xLow", xLow) + ), + "correction", + 0, + 2 + ) + + Platform.runLater { + (plots as PlotHelper).plot( + data: correctionTable, + adapter: Adapters.buildXYAdapter("U", "correction"), + name: "lower_${xLow}", + frame: "lower" + ) + } + } +} \ No newline at end of file diff --git a/numass-main/src/main/java/inr/numass/Numass.java b/numass-main/src/main/java/inr/numass/Numass.java new file mode 100644 index 00000000..72cda76f --- /dev/null +++ b/numass-main/src/main/java/inr/numass/Numass.java @@ -0,0 +1,67 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass; + +import hep.dataforge.context.Context; +import hep.dataforge.context.ContextBuilder; +import hep.dataforge.context.Global; +import hep.dataforge.exceptions.DescriptorException; +import hep.dataforge.meta.Meta; + +/** + * @author Darksnake + */ +public class Numass { + + public static Context buildContext(Context parent, Meta meta) { + return new ContextBuilder("NUMASS", parent) + .properties(meta) + .plugin(NumassPlugin.class) + .build(); + } + + public static Context buildContext() { + return buildContext(Global.INSTANCE, Meta.empty()); + } + + public static void printDescription(Context context) throws DescriptorException { + +// MarkupBuilder builder = new MarkupBuilder() +// .text("***Data description***", "red") +// .ln() +// .text("\t") +// .content( +// MarkupUtils.markupDescriptor(Descriptors.buildDescriptor("method::hep.dataforge.data.DataManager.read")) +// ) +// .ln() +// .text("***Allowed actions***", "red") +// .ln(); +// +// +// ActionManager am = context.get(ActionManager.class); +// +// am.listActions() +// .map(name -> am.optAction(name).get()) +// .map(ActionDescriptor::build).forEach(descriptor -> +// builder.text("\t").content(MarkupUtils.markupDescriptor(descriptor)) +// ); +// +// builder.text("***End of actions list***", "red"); +// +// +// context.getIo().getOutput().render(builder.build(), Meta.empty()); + } +} diff --git a/numass-main/src/main/java/inr/numass/actions/AdjustErrorsAction.java b/numass-main/src/main/java/inr/numass/actions/AdjustErrorsAction.java new file mode 100644 index 00000000..a3f018d6 --- /dev/null +++ b/numass-main/src/main/java/inr/numass/actions/AdjustErrorsAction.java @@ -0,0 +1,87 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package inr.numass.actions; + +import hep.dataforge.actions.OneToOneAction; +import hep.dataforge.context.Context; +import hep.dataforge.description.TypedActionDef; +import hep.dataforge.meta.Laminate; +import hep.dataforge.meta.Meta; +import hep.dataforge.tables.ListTable; +import hep.dataforge.tables.Table; +import hep.dataforge.values.ValueMap; +import hep.dataforge.values.Values; + +import java.util.ArrayList; +import java.util.List; + +/** + * Adjust errors for all numass points in the dataset + * + * @author Alexander Nozik + */ +@TypedActionDef(name = "adjustErrors", inputType = Table.class, outputType = Table.class) +public class AdjustErrorsAction extends OneToOneAction { + + public AdjustErrorsAction() { + super("adjustErrors", Table.class, Table.class); + } + + @Override + protected Table execute(Context context, String name, Table input, Laminate meta) { + List points = new ArrayList<>(); + for (Values dp : input) { + points.add(evalPoint(meta, dp)); + } + + return new ListTable(input.getFormat(), points); + } + + private Values evalPoint(Meta meta, Values dp) { + if (meta.hasMeta("point")) { + for (Meta pointMeta : meta.getMetaList("point")) { + if (pointMeta.getDouble("Uset") == dp.getDouble("Uset")) { + return adjust(dp, pointMeta); + } + } + } + + if (meta.hasMeta("range")) { + for (Meta rangeMeta : meta.getMetaList("range")) { + double from = rangeMeta.getDouble("from", 0); + double to = rangeMeta.getDouble("to", Double.POSITIVE_INFINITY); + double u = rangeMeta.getDouble("Uset"); + if (rangeMeta.getDouble("Uset") == dp.getDouble("Uset")) { + return adjust(dp, rangeMeta); + } + } + } + + if (meta.hasMeta("all")) { + return adjust(dp, meta.getMeta("all")); + } + + return dp; + } + + private Values adjust(Values dp, Meta config) { + ValueMap.Builder res = new ValueMap.Builder(dp); + if (dp.hasValue("CRerr")) { + double instability = 0; + if (dp.hasValue("CR")) { + instability = dp.getDouble("CR") * config.getDouble("instability", 0); + } + + double factor = config.getDouble("factor", 1d); + double base = config.getDouble("base", 0); + double adjusted = dp.getDouble("CRerr") * factor + instability + base; + res.putValue("CRerr", adjusted); + } else { + throw new RuntimeException("The value CRerr is not found in the data point!"); + } + return res.build(); + } +} diff --git a/numass-main/src/main/java/inr/numass/actions/MonitorCorrectAction.java b/numass-main/src/main/java/inr/numass/actions/MonitorCorrectAction.java new file mode 100644 index 00000000..96b853ab --- /dev/null +++ b/numass-main/src/main/java/inr/numass/actions/MonitorCorrectAction.java @@ -0,0 +1,234 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.actions; + +import hep.dataforge.actions.OneToOneAction; +import hep.dataforge.context.Context; +import hep.dataforge.description.TypedActionDef; +import hep.dataforge.exceptions.ContentException; +import hep.dataforge.meta.Laminate; +import hep.dataforge.meta.Meta; +import hep.dataforge.tables.ListTable; +import hep.dataforge.tables.Table; +import hep.dataforge.tables.Tables; +import hep.dataforge.values.ValueFactory; +import hep.dataforge.values.ValueMap; +import hep.dataforge.values.Values; +import inr.numass.NumassUtils; +import inr.numass.data.analyzers.NumassAnalyzer; +import inr.numass.data.api.NumassPoint; +import javafx.util.Pair; +import org.apache.commons.math3.analysis.interpolation.SplineInterpolator; +import org.apache.commons.math3.analysis.polynomials.PolynomialSplineFunction; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Map.Entry; +import java.util.TreeMap; +import java.util.concurrent.CopyOnWriteArrayList; + +import static hep.dataforge.io.output.Output.TEXT_TYPE; + +/** + * @author Darksnake + */ +@TypedActionDef(name = "monitor", inputType = Table.class, outputType = Table.class) +public class MonitorCorrectAction extends OneToOneAction { + public MonitorCorrectAction() { + super("monitor", Table.class, Table.class); + } + + //private static final String[] monitorNames = {"timestamp", NumassAnalyzer.COUNT_KEY, NumassAnalyzer.COUNT_RATE_KEY, NumassAnalyzer.COUNT_RATE_KEY}; + + private CopyOnWriteArrayList monitorPoints = new CopyOnWriteArrayList<>(); + //FIXME remove from state + + @Override + protected Table execute(Context context, String name, Table sourceData, Laminate meta) throws ContentException { + + double monitor = meta.getDouble("monitorPoint", Double.NaN); + + TreeMap index = getMonitorIndex(monitor, sourceData); + if (index.isEmpty()) { + context.getHistory().getChronicle(name).reportError("No monitor points found"); + return sourceData; + } + double norm = 0; +// double totalAv = 0; +// StringBuilder head = new StringBuilder(); +// head.append(String.format("%20s\t%10s\t%s%n", "timestamp", "Count", "CR in window")); +// for (Values dp : index.values()) { +// head.append(String.format("%20s\t%10d\t%g%n", getTime(dp).toString(), getTotal(dp), getCR(dp))); +// norm += getCR(dp) / index.size(); +// totalAv += getTotal(dp) / index.size(); +// monitorPoints.add(dp); +// } +// +// head.append(String.format("%20s\t%10g\t%g%n", "Average", totalAv, norm)); + + List dataList = new ArrayList<>(); + + for (Values dp : sourceData) { + ValueMap.Builder pb = new ValueMap.Builder(dp); + pb.putValue("Monitor", 1.0); + if (!isMonitorPoint(monitor, dp) || index.isEmpty()) { + Pair corr; + if (meta.getBoolean("spline", false)) { + corr = getSplineCorrection(index, dp, norm); + } else { + corr = getLinearCorrection(index, dp, norm); + } + double corrFactor = corr.getKey(); + double corrErr = corr.getValue(); + + double pointErr = dp.getValue(NumassAnalyzer.COUNT_RATE_ERROR_KEY).getDouble() / getCR(dp); + double err = Math.sqrt(corrErr * corrErr + pointErr * pointErr) * getCR(dp); + + if (dp.getNames().contains("Monitor")) { + pb.putValue("Monitor", ValueFactory.of(dp.getValue("Monitor").getDouble() / corrFactor)); + } else { + pb.putValue("Monitor", corrFactor); + } + + pb.putValue(NumassAnalyzer.COUNT_RATE_KEY, ValueFactory.of(dp.getValue(NumassAnalyzer.COUNT_RATE_KEY).getDouble() / corrFactor)); + pb.putValue(NumassAnalyzer.COUNT_RATE_ERROR_KEY, ValueFactory.of(err)); + } else { + double corrFactor = dp.getValue(NumassAnalyzer.COUNT_RATE_KEY).getDouble() / norm; + if (dp.getNames().contains("Monitor")) { + pb.putValue("Monitor", ValueFactory.of(dp.getValue("Monitor").getDouble() / corrFactor)); + } else { + pb.putValue("Monitor", corrFactor); + } + pb.putValue(NumassAnalyzer.COUNT_RATE_KEY, norm); + + } + + if (meta.getBoolean("calculateRelative", false)) { + pb.putValue("relCR", pb.build().getValue(NumassAnalyzer.COUNT_RATE_KEY).getDouble() / norm); + pb.putValue("relCRerr", pb.build().getValue(NumassAnalyzer.COUNT_RATE_ERROR_KEY).getDouble() / norm); + } + + dataList.add(pb.build()); + } + +// DataFormat format; +// +// if (!dataList.isEmpty()) { +// //Генерируем автоматичеÑкий формат по первой Ñтрочке +// format = DataFormat.of(dataList.getPoint(0)); +// } else { +// format = DataFormat.of(parnames); +// } + Table res = Tables.infer(dataList); + + context.getOutput().get(getName(), name, TEXT_TYPE).render(NumassUtils.INSTANCE.wrap(res, meta), Meta.empty()); + + return res; + } + + private Pair getSplineCorrection(TreeMap index, Values dp, double norm) { + double time = getTime(dp).toEpochMilli(); + + double[] xs = new double[index.size()]; + double[] ys = new double[index.size()]; + + int i = 0; + + for (Entry entry : index.entrySet()) { + xs[i] = (double) entry.getKey().toEpochMilli(); + ys[i] = getCR(entry.getValue()) / norm; + i++; + } + + PolynomialSplineFunction corrFunc = new SplineInterpolator().interpolate(xs, ys); + if (corrFunc.isValidPoint(time)) { + double averageErr = index.values().stream().mapToDouble(p -> p.getDouble(NumassAnalyzer.COUNT_RATE_ERROR_KEY)).average().getAsDouble(); + return new Pair<>(corrFunc.value(time), averageErr / norm / 2d); + } else { + return new Pair<>(1d, 0d); + } + } + + private Pair getLinearCorrection(TreeMap index, Values dp, double norm) { + Instant time = getTime(dp); + Entry previousMonitor = index.floorEntry(time); + Entry nextMonitor = index.ceilingEntry(time); + + if (previousMonitor == null) { + previousMonitor = nextMonitor; + } + + if (nextMonitor == null) { + nextMonitor = previousMonitor; + } + + double p; + if (nextMonitor.getKey().isAfter(time) && time.isAfter(previousMonitor.getKey())) { + p = 1.0 * (time.toEpochMilli() - previousMonitor.getKey().toEpochMilli()) + / (nextMonitor.getKey().toEpochMilli() - previousMonitor.getKey().toEpochMilli()); + } else { + p = 0.5; + } + + double corrFactor = (getCR(previousMonitor.getValue()) * (1 - p) + getCR(nextMonitor.getValue()) * p) / norm; + double corrErr = previousMonitor.getValue().getValue(NumassAnalyzer.COUNT_RATE_ERROR_KEY).getDouble() / getCR(previousMonitor.getValue()) / Math.sqrt(2); + return new Pair<>(corrFactor, corrErr); + } + + @Override + protected void afterAction(Context context, String name, Table res, Laminate meta) { + printMonitorData(context, meta); + super.afterAction(context, name, res, meta); + } + + private void printMonitorData(Context context, Meta meta) { + if (!monitorPoints.isEmpty()) { + String monitorFileName = meta.getString("monitorFile", "monitor"); + ListTable data = Tables.infer(monitorPoints); + + context.getOutput().get(getName(), monitorFileName, TEXT_TYPE).render(NumassUtils.INSTANCE.wrap(data, meta), Meta.empty()); +// ColumnedDataWriter.writeTable(stream, TableTransform.sort(data, "Timestamp", true), "Monitor points", monitorNames); + } + } + + private boolean isMonitorPoint(double monitor, Values point) { + return point.getValue(NumassPoint.HV_KEY).getDouble() == monitor; + } + + private Instant getTime(Values point) { + return point.getValue(NumassAnalyzer.TIME_KEY).getTime(); + } + + private int getTotal(Values point) { + return point.getValue(NumassAnalyzer.COUNT_KEY).getInt(); + } + + private double getCR(Values point) { + return point.getValue(NumassAnalyzer.COUNT_RATE_KEY).getDouble(); + } + + private TreeMap getMonitorIndex(double monitor, Iterable data) { + TreeMap res = new TreeMap<>(); + for (Values dp : data) { + if (isMonitorPoint(monitor, dp)) { + res.put(getTime(dp), dp); + } + } + return res; + } + +} diff --git a/numass-main/src/main/java/inr/numass/actions/SubstractSpectrumAction.java b/numass-main/src/main/java/inr/numass/actions/SubstractSpectrumAction.java new file mode 100644 index 00000000..26621a7e --- /dev/null +++ b/numass-main/src/main/java/inr/numass/actions/SubstractSpectrumAction.java @@ -0,0 +1,65 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package inr.numass.actions; + +import hep.dataforge.actions.OneToOneAction; +import hep.dataforge.context.Context; +import hep.dataforge.description.TypedActionDef; +import hep.dataforge.io.ColumnedDataReader; +import hep.dataforge.meta.Laminate; +import hep.dataforge.meta.Meta; +import hep.dataforge.tables.ListTable; +import hep.dataforge.tables.Table; +import hep.dataforge.values.ValueMap; +import hep.dataforge.values.Values; +import inr.numass.NumassUtils; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; + +import static hep.dataforge.io.output.Output.TEXT_TYPE; + +/** + * @author Alexander Nozik + */ +@TypedActionDef(name = "substractSpectrum", inputType = Table.class, outputType = Table.class, info = "Substract reference spectrum (background)") +public class SubstractSpectrumAction extends OneToOneAction { + + public SubstractSpectrumAction() { + super("substractSpectrum", Table.class, Table.class); + } + + @Override + protected Table execute(Context context, String name, Table input, Laminate inputMeta) { + try { + String referencePath = inputMeta.getString("file", "empty.dat"); + Path referenceFile = context.getRootDir().resolve(referencePath); + Table referenceTable = new ColumnedDataReader(referenceFile).toTable(); + ListTable.Builder builder = new ListTable.Builder(input.getFormat()); + input.getRows().forEach(point -> { + ValueMap.Builder pointBuilder = new ValueMap.Builder(point); + Optional referencePoint = referenceTable.getRows() + .filter(p -> Math.abs(p.getDouble("Uset") - point.getDouble("Uset")) < 0.1).findFirst(); + if (referencePoint.isPresent()) { + pointBuilder.putValue("CR", Math.max(0, point.getDouble("CR") - referencePoint.get().getDouble("CR"))); + pointBuilder.putValue("CRerr", Math.sqrt(Math.pow(point.getDouble("CRerr"), 2d) + Math.pow(referencePoint.get().getDouble("CRerr"), 2d))); + } else { + report(context, name, "No reference point found for Uset = {}", point.getDouble("Uset")); + } + builder.row(pointBuilder.build()); + }); + + Table res = builder.build(); + + context.getOutput().get(getName(), name, TEXT_TYPE).render(NumassUtils.INSTANCE.wrap(res, inputMeta), Meta.empty()); + return res; + } catch (IOException ex) { + throw new RuntimeException("Could not read reference file", ex); + } + } + +} diff --git a/numass-main/src/main/java/inr/numass/data/MonitorPoint.java b/numass-main/src/main/java/inr/numass/data/MonitorPoint.java new file mode 100644 index 00000000..e09bf821 --- /dev/null +++ b/numass-main/src/main/java/inr/numass/data/MonitorPoint.java @@ -0,0 +1,74 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.data; + +import java.text.ParseException; +import java.time.LocalDateTime; +import java.util.Scanner; + +/** + * + * @author Darksnake + */ +public class MonitorPoint { + +// private static SimpleDateFormat format = new SimpleDateFormat("yyyy:MM:dd:HH:mm:ss"); + + private final double monitorError; + private final double monitorValue; + private final LocalDateTime time; + + public MonitorPoint(LocalDateTime time, double monitorValue, double monitorError) { + this.time = time; + this.monitorValue = monitorValue; + this.monitorError = monitorError; + } + + public MonitorPoint(String str) throws ParseException { + Scanner sc = new Scanner(str); + String datestr = sc.next(); + //using ISO-8601 + time = LocalDateTime.parse(datestr); + monitorValue = sc.nextDouble(); + if (sc.hasNextDouble()) { + monitorError = sc.nextDouble(); + } else { + monitorError = 0; + } + } + + /** + * @return the monitorError + */ + public double getMonitorError() { + return monitorError; + } + + /** + * @return the monitorValue + */ + public double getMonitorValue() { + return monitorValue; + } + + /** + * @return the time + */ + public LocalDateTime getTime() { + return time; + } + +} diff --git a/numass-main/src/main/java/inr/numass/data/SpectrumGenerator.java b/numass-main/src/main/java/inr/numass/data/SpectrumGenerator.java new file mode 100644 index 00000000..ae4c8f8f --- /dev/null +++ b/numass-main/src/main/java/inr/numass/data/SpectrumGenerator.java @@ -0,0 +1,188 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.data; + +import hep.dataforge.meta.Meta; +import hep.dataforge.stat.RandomKt; +import hep.dataforge.stat.fit.ParamSet; +import hep.dataforge.stat.models.Generator; +import hep.dataforge.stat.models.XYModel; +import hep.dataforge.tables.Adapters; +import hep.dataforge.tables.ListTable; +import hep.dataforge.tables.Table; +import hep.dataforge.values.Values; +import org.apache.commons.math3.random.JDKRandomGenerator; +import org.apache.commons.math3.random.RandomDataGenerator; +import org.apache.commons.math3.random.RandomGenerator; + +import static java.lang.Double.isNaN; +import static java.lang.Math.sqrt; + +/** + * Генератор наборов данных Ð´Ð»Ñ Ñпектров. Ðа входе требуетÑÑ Ð½Ð°Ð±Ð¾Ñ€ данных, + * Ñодержащих X-Ñ‹ и Ð²Ñ€ÐµÐ¼Ñ Ð½Ð°Ð±Ð¾Ñ€Ð°. Могут быть иÑпользованы реальные + * ÑкÑпериментальные наборы данных. + * + * @author Darksnake + */ +public class SpectrumGenerator implements Generator { + + static final double POISSON_BOUNDARY = 100; + private GeneratorType genType = GeneratorType.POISSONIAN; + private RandomDataGenerator generator; + private ParamSet params; + private XYModel source; + private SpectrumAdapter adapter = new SpectrumAdapter(Meta.empty()); + + public SpectrumGenerator(XYModel source, ParamSet params, int seed) { + this.source = source; + this.params = params; + RandomGenerator rng = new JDKRandomGenerator(); + rng.setSeed(seed); + this.generator = new RandomDataGenerator(rng); + } + + public SpectrumGenerator(XYModel source, ParamSet params, RandomGenerator rng) { + this.source = source; + this.params = params; + this.generator = new RandomDataGenerator(rng); + } + + public SpectrumGenerator(XYModel source, ParamSet params) { + this(source, params, RandomKt.getDefaultGenerator()); + } + + @Override + public Table generateData(Iterable config) { + ListTable.Builder res = new ListTable.Builder(Adapters.getFormat(adapter)); + for (Values aConfig : config) { + res.row(this.generateDataPoint(aConfig)); + } + return res.build(); + } + + /** + * Generate spectrum points with error derived from configuration but with + * zero spread (exactly the same as model provides) + * + * @param config + * @return + */ + public Table generateExactData(Iterable config) { + ListTable.Builder res = new ListTable.Builder(Adapters.getFormat(adapter)); + for (Values aConfig : config) { + res.row(this.generateExactDataPoint(aConfig)); + } + return res.build(); + } + + public Values generateExactDataPoint(Values configPoint) { + double mu = this.getMu(configPoint); + return adapter.buildSpectrumDataPoint(this.getX(configPoint), (long) mu, this.getTime(configPoint)); + } + + @Override + public Values generateDataPoint(Values configPoint) { + double mu = this.getMu(configPoint); + if (isNaN(mu) || (mu < 0)) { + throw new IllegalStateException("Negative input parameter for generator."); + } + double y; + switch (this.genType) { + case GAUSSIAN: + double sigma = sqrt(mu); + if (mu == 0) { + y = 0;// ПроверÑем чтобы не было ÑингулÑрноÑти + } else { + y = generator.nextGaussian(mu, sigma); + } + if (y < 0) { + y = 0;//ПроверÑем, чтобы не было отрицательных значений + } + break; + case POISSONIAN: + if (mu == 0) { + y = 0; + break; + } + if (mu < POISSON_BOUNDARY) { + y = generator.nextPoisson(mu); + } else { + y = generator.nextGaussian(mu, sqrt(mu)); + } + break; + default: + throw new Error("Enum listing failed!"); + } + + double time = this.getTime(configPoint); + + return adapter.buildSpectrumDataPoint(this.getX(configPoint), (long) y, time); + } + + @Override + public String getGeneratorType() { + return this.genType.name(); + } + + private double getMu(Values point) { + return source.value(this.getX(point), params) * this.getTime(point); + } + +// private double getSigma(DataPoint point) { +// if (!point.containsName("time")) { +// Global.instance().logString("SpectrumGenerator : Neither point error nor time is defined. Suspected wrong error bars for data."); +// } +// return sqrt(this.getMu(point)); +// } + private double getTime(Values point) { + + return adapter.getTime(point); +// if (point.containsName("time")) { +// return point.getValue("time").doubleValue(); +// } else { +// /* +// * Это Ñделано на тот Ñлучай, еÑли требуетÑÑ Ñгенерить не количеÑтво +// * отÑчетов, а ÑкороÑÑ‚ÑŒ Ñчета. Правда в Ñтом Ñлуче требуетÑÑ +// * передача веÑа Ð´Ð»Ñ Ð³Ð°ÑƒÑÑовÑкого генератора +// */ +// return 1; +// } + + } + + public SpectrumAdapter getAdapter() { + return adapter; + } + + public void setAdapter(SpectrumAdapter adapter) { + this.adapter = adapter; + } + + private double getX(Values point) { + return Adapters.getXValue(adapter,point).getDouble(); + } + + public void setGeneratorType(GeneratorType type) { + this.genType = type; + } + + public enum GeneratorType { + + POISSONIAN, + GAUSSIAN + } +} diff --git a/numass-main/src/main/java/inr/numass/data/SpectrumInformation.java b/numass-main/src/main/java/inr/numass/data/SpectrumInformation.java new file mode 100644 index 00000000..3766a6da --- /dev/null +++ b/numass-main/src/main/java/inr/numass/data/SpectrumInformation.java @@ -0,0 +1,125 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.data; + +import hep.dataforge.maths.NamedMatrix; +import hep.dataforge.stat.parametric.ParametricFunction; +import hep.dataforge.tables.Adapters; +import hep.dataforge.tables.ListTable; +import hep.dataforge.values.Values; +import org.apache.commons.math3.analysis.UnivariateFunction; +import org.apache.commons.math3.linear.Array2DRowRealMatrix; +import org.apache.commons.math3.linear.RealMatrix; + +import static hep.dataforge.maths.MatrixOperations.inverse; + +/** + * + * @author Darksnake + */ +public class SpectrumInformation { + + private final ParametricFunction source; + + public SpectrumInformation(ParametricFunction source) { + this.source = source; + } + + public NamedMatrix getExpetedCovariance(Values set, ListTable data, String... parNames) { + String[] names = parNames; + if (names.length == 0) { + names = source.namesAsArray(); + } + NamedMatrix info = this.getInformationMatrix(set, data, names); + RealMatrix cov = inverse(info.getMatrix()); + return new NamedMatrix(names, cov); + } + + /** + * Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ð¾Ð½Ð½Ð°Ñ Ð¼Ð°Ñ‚Ñ€Ð¸Ñ†Ð° Фишера в предположении, что ошибки пуаÑÑоновÑкие + * + * @param set + * @param data + * @param parNames + * @return + */ + public NamedMatrix getInformationMatrix(Values set, ListTable data, String... parNames) { + SpectrumAdapter adapter = NumassDataUtils.INSTANCE.adapter(); + + String[] names = parNames; + if (names.length == 0) { + names = source.namesAsArray(); + } + assert source.getNames().contains(set.namesAsArray()); + assert source.getNames().contains(names); + RealMatrix res = new Array2DRowRealMatrix(names.length, names.length); + + for (Values dp : data) { + /*PENDING Тут имеетÑÑ Ð³Ð»Ð¾Ð±Ð°Ð»ÑŒÐ½Ð°Ñ Ð½ÐµÐ¾Ð¿Ñ‚Ð¸Ð¼Ð°Ð»ÑŒÐ½Ð¾ÑÑ‚ÑŒ ÑвÑÐ·Ð°Ð½Ð½Ð°Ñ Ñ Ñ‚ÐµÐ¼, + * что при каждом вызове вычиÑлÑÑŽÑ‚ÑÑ Ð´Ð²Ðµ производные + * Ðужно вычиÑлÑÑ‚ÑŒ Ñразу вÑÑŽ матрицу Ð´Ð»Ñ ÐºÐ°Ð¶Ð´Ð¾Ð¹ точки, тогда количеÑтво + * вызовов производных будет Ñтрого равно 1. + */ + res = res.add(getPointInfoMatrix(set, Adapters.getXValue(adapter,dp).getDouble(), adapter.getTime(dp), names).getMatrix()); + } + + return new NamedMatrix(names, res); + } + + // формула правильнаÑ! + public double getPoinSignificance(Values set, String name1, String name2, double x) { + return source.derivValue(name1, x, set) * source.derivValue(name2, x, set) / source.value(x, set); + } + + public NamedMatrix getPointInfoMatrix(Values set, double x, double t, String... parNames) { + assert source.getNames().contains(set.namesAsArray()); + + String[] names = parNames; + if (names.length == 0) { + names = set.namesAsArray(); + } + + assert source.getNames().contains(names); + + RealMatrix res = new Array2DRowRealMatrix(names.length, names.length); + + for (int i = 0; i < names.length; i++) { + for (int j = i; j < names.length; j++) { + double value = getPoinSignificance(set, names[i], names[j], x) * t; + res.setEntry(i, j, value); + if (i != j) { + res.setEntry(j, i, value); + } + } + + } + return new NamedMatrix(names, res); + + } + + /** + * ЗавиÑимоÑÑ‚ÑŒ информации Фишера (отнеÑенной к времени набора) от точки + * Ñпектра в предположении, что ошибки пуаÑÑоновÑкие + * + * @param set + * @param name1 + * @param name2 + * @return + */ + public UnivariateFunction getSignificanceFunction(final Values set, final String name1, final String name2) { + return (double d) -> getPoinSignificance(set, name1, name2, d); + } +} diff --git a/numass-main/src/main/java/inr/numass/data/package-info.java b/numass-main/src/main/java/inr/numass/data/package-info.java new file mode 100644 index 00000000..fe4fe8c7 --- /dev/null +++ b/numass-main/src/main/java/inr/numass/data/package-info.java @@ -0,0 +1,8 @@ +/** + * Created by darksnake on 31-Jan-17. + */ +package inr.numass.data; + +/** + * package is obsolete + */ diff --git a/numass-main/src/main/java/inr/numass/models/BetaSpectrum.java b/numass-main/src/main/java/inr/numass/models/BetaSpectrum.java new file mode 100644 index 00000000..b4cf690d --- /dev/null +++ b/numass-main/src/main/java/inr/numass/models/BetaSpectrum.java @@ -0,0 +1,247 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.models; + +import hep.dataforge.exceptions.NotDefinedException; +import hep.dataforge.stat.parametric.AbstractParametricFunction; +import hep.dataforge.values.ValueProvider; +import hep.dataforge.values.Values; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; + +import static java.lang.Math.*; + +/** + * @author Darksnake + */ +@Deprecated +public class BetaSpectrum extends AbstractParametricFunction implements RangedNamedSetSpectrum { + + static final double K = 1E-23; + // конÑтанта K в формуле, подбираем ее таким образом, чтобы нормировка ÑоответÑтвовала ÑкроÑти Ñчета + static final String[] list = {"E0", "mnu2", "msterile2", "U2"}; + FSS fss = null; + + public BetaSpectrum() { + super(list); + } + + public BetaSpectrum(InputStream FSStream) { + super(list); + if (FSStream != null) { + this.fss = new FSS(FSStream); + } + } + + public BetaSpectrum(File FSSFile) throws FileNotFoundException { + super(list); + if (FSSFile != null) { + this.fss = new FSS(new FileInputStream(FSSFile)); + } + } + + double derivRoot(int n, double E0, double mnu2, double E) throws NotDefinedException { + //ограничение + + double D = E0 - E;//E0-E + double res; + if (D == 0) { + return 0; + } + + if (mnu2 >= 0) { + if (E >= (E0 - sqrt(mnu2))) { + return 0; + } + double bare = sqrt(D * D - mnu2); + switch (n) { + case 0: + res = factor(E) * (2 * D * D - mnu2) / bare; + break; + case 1: + res = -factor(E) * 0.5 * D / bare; + break; + default: + return 0; + } + } else { + double mu = sqrt(-0.66 * mnu2); + if (E >= (E0 + mu)) { + return 0; + } + double root = sqrt(Math.max(D * D - mnu2, 0)); + double exp = exp(-1 - D / mu); + switch (n) { + case 0: + res = factor(E) * (D * (D + mu * exp) / root + root * (1 - exp)); + break; + case 1: + res = factor(E) * (-(D + mu * exp) / root * 0.5 - root * exp * (1 + D / mu) / 3 / mu); + break; + default: + return 0; + } + } + + return res; + } + + double derivRootsterile(String name, double E, ValueProvider pars) throws NotDefinedException { + double E0 = pars.getDouble("E0"); + double mnu2 = pars.getDouble("mnu2"); + double mst2 = pars.getDouble("msterile2"); + double u2 = pars.getDouble("U2"); + + switch (name) { + case "E0": + if (u2 == 0) { + return derivRoot(0, E0, mnu2, E); + } + return u2 * derivRoot(0, E0, mst2, E) + (1 - u2) * derivRoot(0, E0, mnu2, E); + case "mnu2": + return (1 - u2) * derivRoot(1, E0, mnu2, E); + case "msterile2": + if (u2 == 0) { + return 0; + } + return u2 * derivRoot(1, E0, mst2, E); + case "U2": + return root(E0, mst2, E) - root(E0, mnu2, E); + default: + return 0; + } + + } + + @Override + public double derivValue(String name, double E, Values pars) throws NotDefinedException { + if (this.fss == null) { + return this.derivRootsterile(name, E, pars); + } + double res = 0; + int i; + for (i = 0; i < fss.size(); i++) { + res += fss.getP(i) * this.derivRootsterile(name, E + fss.getE(i), pars); + } + return res; + } + + double factor(double E) { + return K * pfactor(E); + } + + @Override + public Double max(Values set) { + return set.getDouble("E0"); + } + + @Override + public Double min(Values set) { + return 0d; + } + + double pfactor(double E) { + double me = 0.511006E6; + double Etot = E + me; + double pe = sqrt(E * (E + 2d * me)); + double ve = pe / Etot; + double yfactor = 2d * 2d * 1d / 137.039 * Math.PI; + double y = yfactor / ve; + double Fn = y / abs(1d - exp(-y)); + double Fermi = Fn * (1.002037 - 0.001427 * ve); + double res = Fermi * pe * Etot; + return res; + } + + @Override + public boolean providesDeriv(String name) { + return true; + } + + double root(double E0, double mnu2, double E) { + /*чиÑтый бета-Ñпектр*/ + double D = E0 - E;//E0-E + double res; + double bare = factor(E) * D * sqrt(Math.max(D * D - mnu2, 0)); + if (D == 0) { + return 0; + } + if (mnu2 >= 0) { + res = Math.max(bare, 0); + } else { + if (D + 0.812 * sqrt(-mnu2) <= 0) { + return 0; //sqrt(0.66) + } + double aux = sqrt(-mnu2 * 0.66) / D; + res = Math.max(bare * (1 + aux * exp(-1 - 1 / aux)), 0); + } + return res; + } + + double rootsterile(double E, ValueProvider pars) { + double E0 = pars.getDouble("E0"); + double mnu2 = pars.getDouble("mnu2"); + double mst2 = pars.getDouble("msterile2"); + double u2 = pars.getDouble("U2"); + + if (u2 == 0) { + return root(E0, mnu2, E); + } + return u2 * root(E0, mst2, E) + (1 - u2) * root(E0, mnu2, E); + // P(rootsterile)+ (1-P)root + } + + public void setFSS(File FSSFile) throws FileNotFoundException { + if (FSSFile == null) { + this.fss = null; + } else { + this.fss = new FSS(new FileInputStream(FSSFile)); + } + } + + @Override + public double value(double E, Values pars) { + if (this.fss == null) { + return rootsterile(E, pars); + } + /*Учет Ñпектра конечных ÑоÑтоÑний*/ + int i; + double res = 0; + for (i = 0; i < fss.size(); i++) { + res += fss.getP(i) * this.rootsterile(E + fss.getE(i), pars); + } + return res; +// return rootsterile(E, pars); + + } + + @Override + protected double getDefaultParameter(String name) { + switch (name) { + case "mnu2": + return 0; + case "U2": + return 0; + case "msterile2": + return 0; + default: + return super.getDefaultParameter(name); + } + } +} diff --git a/numass-main/src/main/java/inr/numass/models/CustomNBkgSpectrum.java b/numass-main/src/main/java/inr/numass/models/CustomNBkgSpectrum.java new file mode 100644 index 00000000..6539e7b4 --- /dev/null +++ b/numass-main/src/main/java/inr/numass/models/CustomNBkgSpectrum.java @@ -0,0 +1,50 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package inr.numass.models; + +import hep.dataforge.stat.parametric.ParametricFunction; +import hep.dataforge.values.Values; +import inr.numass.NumassUtils; +import inr.numass.utils.NumassIntegrator; +import org.apache.commons.math3.analysis.UnivariateFunction; + +/** + * A spectrum with custom background, say tritium decays on walls + * + * @author Alexander Nozik + */ +public class CustomNBkgSpectrum extends NBkgSpectrum { + + public static CustomNBkgSpectrum tritiumBkgSpectrum(ParametricFunction source, double amplitude){ + UnivariateFunction differentialBkgFunction = NumassUtils.INSTANCE.tritiumBackgroundFunction(amplitude); + UnivariateFunction integralBkgFunction = + (x) -> NumassIntegrator.getDefaultIntegrator() + .integrate(x, 18580d, differentialBkgFunction); + return new CustomNBkgSpectrum(source, integralBkgFunction); + } + + private UnivariateFunction customBackgroundFunction; + + public CustomNBkgSpectrum(ParametricFunction source) { + super(source); + } + + public CustomNBkgSpectrum(ParametricFunction source, UnivariateFunction customBackgroundFunction) { + super(source); + this.customBackgroundFunction = customBackgroundFunction; + } + + @Override + public double value(double x, Values set) { + if (customBackgroundFunction == null) { + return super.value(x, set); + } else { + return super.value(x, set) + customBackgroundFunction.value(x); + } + } + + +} diff --git a/numass-main/src/main/java/inr/numass/models/EmpiricalLossSpectrum.java b/numass-main/src/main/java/inr/numass/models/EmpiricalLossSpectrum.java new file mode 100644 index 00000000..61c8994c --- /dev/null +++ b/numass-main/src/main/java/inr/numass/models/EmpiricalLossSpectrum.java @@ -0,0 +1,75 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.models; + +import hep.dataforge.exceptions.NamingException; +import hep.dataforge.exceptions.NotDefinedException; +import hep.dataforge.maths.integration.GaussRuleIntegrator; +import hep.dataforge.maths.integration.UnivariateIntegrator; +import hep.dataforge.stat.parametric.AbstractParametricFunction; +import hep.dataforge.values.Values; +import inr.numass.models.misc.LossCalculator; +import org.apache.commons.math3.analysis.BivariateFunction; +import org.apache.commons.math3.analysis.UnivariateFunction; + +import java.util.List; + +/** + * + * @author Darksnake + */ +public class EmpiricalLossSpectrum extends AbstractParametricFunction { + + public static String[] names = {"X", "shift"}; + private final UnivariateFunction transmission; + private final double eMax; + + private final UnivariateIntegrator integrator; + + public EmpiricalLossSpectrum(UnivariateFunction transmission, double eMax) throws NamingException { + super(names); + integrator = new GaussRuleIntegrator(300); + this.transmission = transmission; + this.eMax = eMax; + } + + @Override + public double derivValue(String parName, double x, Values set) { + throw new NotDefinedException(); + } + + @Override + public double value(double U, Values set) { + if (U >= eMax) { + return 0; + } + double X = set.getDouble("X"); + final double shift = set.getDouble("shift"); + + //FIXME тут толщины уÑреднены по длине иÑточника, а надо брать чиÑтого ПуаÑÑона + final List probs = LossCalculator.INSTANCE.getGunLossProbabilities(X); + final double noLossProb = probs.get(0); + final BivariateFunction lossFunction = (Ei, Ef) -> LossCalculator.INSTANCE.getLossValue(probs, Ei, Ef); + UnivariateFunction integrand = (double x) -> transmission.value(x) * lossFunction.value(x, U - shift); + return noLossProb * transmission.value(U - shift) + integrator.integrate(U, eMax, integrand); + } + + @Override + public boolean providesDeriv(String name) { + return false; + } + +} diff --git a/numass-main/src/main/java/inr/numass/models/ExperimentalVariableLossSpectrum.java b/numass-main/src/main/java/inr/numass/models/ExperimentalVariableLossSpectrum.java new file mode 100644 index 00000000..e98e0fb6 --- /dev/null +++ b/numass-main/src/main/java/inr/numass/models/ExperimentalVariableLossSpectrum.java @@ -0,0 +1,179 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.models; + +import hep.dataforge.exceptions.NotDefinedException; +import hep.dataforge.stat.parametric.AbstractParametricFunction; +import hep.dataforge.stat.parametric.ParametricFunction; +import hep.dataforge.values.ValueProvider; +import hep.dataforge.values.Values; +import org.apache.commons.math3.analysis.BivariateFunction; +import org.apache.commons.math3.analysis.UnivariateFunction; + +/** + * Experimental differential loss spectrum + * + * @author darksnake + */ +public class ExperimentalVariableLossSpectrum extends VariableLossSpectrum { + + public static ExperimentalVariableLossSpectrum withGun(double eGun, double resA, double eMax, double smootherW) { + return new ExperimentalVariableLossSpectrum(new GunSpectrum(), eMax,smootherW); + } + + public static ExperimentalVariableLossSpectrum withData(final UnivariateFunction transmission, double eMax, double smootherW) { + return new ExperimentalVariableLossSpectrum(new AbstractParametricFunction(new String[0]) { + + @Override + public double derivValue(String parName, double x, Values set) { + throw new NotDefinedException(); + } + + @Override + public boolean providesDeriv(String name) { + return false; + } + + @Override + public double value(double x, Values set) { + return transmission.value(x); + } + }, eMax,smootherW); + } + + Loss loss; + +// private double smootherW; +// public ExperimentalVariableLossSpectrum(UnivariateFunction transmission, double eMax, double smootherW) throws NamingException { +// super(transmission, eMax); +// loss = new Loss(smootherW); +// } + public ExperimentalVariableLossSpectrum(ParametricFunction transmission, double eMax, double smootherW) { + super(transmission, eMax); + loss = new Loss(smootherW); + } + +// public ExperimentalVariableLossSpectrum(double eGun, double resA, double eMax, double smootherW) throws NamingException { +// super(eGun, resA, eMax); +// loss = new Loss(smootherW); +// } + @Override + public UnivariateFunction singleScatterFunction( + double exPos, + double ionPos, + double exW, + double ionW, + double exIonRatio) { + + return (double eps) -> { + if (eps <= 0) { + return 0; + } + + return (loss.excitation(exPos, exW).value(eps) * exIonRatio + loss.ionization(ionPos, ionW).value(eps)) / (1d + exIonRatio); + }; + } + + public static class Loss { + + private BivariateFunction smoother; + + private double smootherNorm; + + public Loss(double smootherW) { + if (smootherW == 0) { + smoother = (e1, e2) -> 0; + smootherNorm = 0; + } + + smoother = (e1, e2) -> { + double delta = e1 - e2; + if (delta < 0) { + return 0; + } else { + return Math.exp(-delta * delta / 2 / smootherW / smootherW); + } + }; + + smootherNorm = Math.sqrt(2 * Math.PI) * smootherW / 2; + } + + public UnivariateFunction total( + final double exPos, + final double ionPos, + final double exW, + final double ionW, + final double exIonRatio) { + return (eps) -> (excitation(exPos, exW).value(eps) * exIonRatio + ionization(ionPos, ionW).value(eps)) / (1d + exIonRatio); + } + + public UnivariateFunction total(ValueProvider set) { + final double exPos = set.getDouble("exPos"); + final double ionPos = set.getDouble("ionPos"); + final double exW = set.getDouble("exW"); + final double ionW = set.getDouble("ionW"); + final double exIonRatio = set.getDouble("exIonRatio"); + return total(exPos, ionPos, exW, ionW, exIonRatio); + } + + /** + * Excitation spectrum + * + * @param exPos + * @param exW + * @return + */ + public UnivariateFunction excitation(double exPos, double exW) { + return (double eps) -> { + double z = eps - exPos; + double res; + + double norm = smootherNorm + Math.sqrt(Math.PI / 2) * exW / 2; + + if (z < 0) { + res = Math.exp(-2 * z * z / exW / exW); + } else { + res = smoother.value(z, 0); + } + + return res / norm; + }; + } + + /** + * Ionization spectrum + * + * @param ionPos + * @param ionW + * @return + */ + public UnivariateFunction ionization(double ionPos, double ionW) { + return (double eps) -> { + double res; + double norm = smootherNorm + ionW * Math.PI / 4d; + + if (eps - ionPos > 0) { + res = 1 / (1 + 4 * (eps - ionPos) * (eps - ionPos) / ionW / ionW); + } else { + res = smoother.value(0, eps - ionPos); + } + + return res / norm; + }; + } + } + +} diff --git a/numass-main/src/main/java/inr/numass/models/FSS.java b/numass-main/src/main/java/inr/numass/models/FSS.java new file mode 100644 index 00000000..b25aff7c --- /dev/null +++ b/numass-main/src/main/java/inr/numass/models/FSS.java @@ -0,0 +1,70 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.models; + +import hep.dataforge.io.IOUtils; +import hep.dataforge.tables.ValuesSource; +import hep.dataforge.values.Values; + +import java.io.InputStream; +import java.util.ArrayList; + +/** + * @author Darksnake + */ +public class FSS { + private final ArrayList ps = new ArrayList<>(); + private final ArrayList es = new ArrayList<>(); + private double norm; + + public FSS(InputStream stream) { + ValuesSource data = IOUtils.readColumnedData(stream, "E", "P"); + norm = 0; + for (Values dp : data) { + es.add(dp.getDouble("E")); + double p = dp.getDouble("P"); + ps.add(p); + norm += p; + } + if (ps.isEmpty()) { + throw new RuntimeException("Error reading FSS FILE. No points."); + } + } + + public double getE(int n) { + return this.es.get(n); + } + + public double getP(int n) { + return this.ps.get(n) / norm; + } + + public boolean isEmpty() { + return ps.isEmpty(); + } + + public int size() { + return ps.size(); + } + + public double[] getPs() { + return ps.stream().mapToDouble(p -> p).toArray(); + } + + public double[] getEs() { + return es.stream().mapToDouble(p -> p).toArray(); + } +} diff --git a/numass-main/src/main/java/inr/numass/models/GaussSourceSpectrum.java b/numass-main/src/main/java/inr/numass/models/GaussSourceSpectrum.java new file mode 100644 index 00000000..499cd5ef --- /dev/null +++ b/numass-main/src/main/java/inr/numass/models/GaussSourceSpectrum.java @@ -0,0 +1,91 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.models; + +import hep.dataforge.exceptions.NotDefinedException; +import hep.dataforge.stat.parametric.AbstractParametricFunction; +import hep.dataforge.values.ValueProvider; +import hep.dataforge.values.Values; + +import static java.lang.Math.exp; +import static java.lang.Math.sqrt; + +/** + * + * @author Darksnake + */ +public class GaussSourceSpectrum extends AbstractParametricFunction implements RangedNamedSetSpectrum { + + private static final String[] list = {"pos", "sigma"}; + private final double cutoff = 4d; + + public GaussSourceSpectrum() { + super(list); + } + + @Override + public double derivValue(String parName, double E, Values set) { + switch (parName) { + case "pos": + return getGaussPosDeriv(E, getPos(set), getSigma(set)); + case "sigma": + return getGaussSigmaDeriv(E, getPos(set), getSigma(set)); + default: + throw new NotDefinedException(); + } + } + + double getGauss(double E, double pos, double sigma) { + double aux = (E - pos) / sigma; + return exp(-aux * aux / 2) / sigma / sqrt(2 * Math.PI); + } + + double getGaussPosDeriv(double E, double pos, double sigma) { + return getGauss(E, pos, sigma) * (E - pos) / sigma / sigma; + } + + double getGaussSigmaDeriv(double E, double pos, double sigma) { + return getGauss(E, pos, sigma) * ((E - pos) * (E - pos) / sigma / sigma / sigma - 1 / sigma); + } + + @Override + public Double max(Values set) { + return getPos(set) + cutoff * getSigma(set); + } + + @Override + public Double min(Values set) { + return getPos(set) - cutoff * getSigma(set); + } + + private double getPos(ValueProvider set) { + return set.getDouble("pos"); + } + + private double getSigma(ValueProvider set) { + return set.getDouble("sigma"); + } + + @Override + public boolean providesDeriv(String name) { + return this.getNames().contains(name); + } + + @Override + public double value(final double E, Values set) { + return getGauss(E, getPos(set), getSigma(set)); + } +} diff --git a/numass-main/src/main/java/inr/numass/models/GunSpectrum.java b/numass-main/src/main/java/inr/numass/models/GunSpectrum.java new file mode 100644 index 00000000..11304cf7 --- /dev/null +++ b/numass-main/src/main/java/inr/numass/models/GunSpectrum.java @@ -0,0 +1,155 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.models; + +import hep.dataforge.exceptions.NotDefinedException; +import hep.dataforge.maths.integration.UnivariateIntegrator; +import hep.dataforge.stat.parametric.AbstractParametricFunction; +import hep.dataforge.values.Values; +import inr.numass.utils.NumassIntegrator; +import org.apache.commons.math3.analysis.UnivariateFunction; + +import static java.lang.Math.*; + +/** + * + * @author Darksnake + */ +public class GunSpectrum extends AbstractParametricFunction { + + private static final String[] list = {"pos", "resA", "sigma"}; + private final double cutoff = 4d; + protected final UnivariateIntegrator integrator; + + public GunSpectrum() { + super(list); + integrator = NumassIntegrator.getDefaultIntegrator(); + } + + @Override + public double derivValue(String parName, final double U, Values set) { + final double pos = set.getDouble("pos"); + final double sigma = set.getDouble("sigma"); + final double resA = set.getDouble("resA"); + + if (sigma == 0) { + throw new NotDefinedException(); + } + + UnivariateFunction integrand; + switch (parName) { + case "pos": + integrand = (double E) -> transmissionValueFast(U, E, resA) * getGaussPosDeriv(E, pos, sigma); + break; + case "sigma": + integrand = (double E) -> transmissionValueFast(U, E, resA) * getGaussSigmaDeriv(E, pos, sigma); + break; + case "resA": + integrand = (double E) -> transmissionValueFastDeriv(U, E, resA) * getGauss(E, pos, sigma); + break; + default: + throw new NotDefinedException(); + + } + + if (pos + cutoff * sigma < U) { + return 0; + } else if (pos - cutoff * sigma > U * (1 + resA)) { + return 0; + } else { + return integrator.integrate(pos - cutoff * sigma, pos + cutoff * sigma, integrand); + } + } + + double getGauss(double E, double pos, double sigma) { + if (abs(E - pos) > cutoff * sigma) { + return 0; + } + double aux = (E - pos) / sigma; + return exp(-aux * aux / 2) / sigma / sqrt(2 * Math.PI); + } + + double getGaussPosDeriv(double E, double pos, double sigma) { + return getGauss(E, pos, sigma) * (E - pos) / sigma / sigma; + } + + double getGaussSigmaDeriv(double E, double pos, double sigma) { + return getGauss(E, pos, sigma) * ((E - pos) * (E - pos) / sigma / sigma / sigma - 1 / sigma); + } + + @Override + public boolean providesDeriv(String name) { +// return false; + return this.getNames().contains(name); + } + + double transmissionValue(double U, double E, double resA, double resB) { + assert resA > 0; + assert resB > 0; + double delta = resA * E; + if (E - U < 0) { + return 0; + } else if (E - U > delta) { + return 1; + } else { + return (1 - sqrt(1 - (E - U) / E * resB)) / (1 - sqrt(1 - resA * resB)); + } + } + + double transmissionValueFast(double U, double E, double resA) { + double delta = resA * E; + if (E - U < 0) { + return 0; + } else if (E - U > delta) { + return 1; + } else { + return (E - U) / delta; + } + } + + double transmissionValueFastDeriv(double U, double E, double resA) { + double delta = resA * E; + if (E - U < 0) { + return 0; + } else if (E - U > delta) { + return 1; + } else { + return -(E - U) / delta / resA; + } + } + + @Override + public double value(final double U, Values set) { + final double pos = set.getDouble("pos"); + final double sigma = set.getDouble("sigma"); + final double resA = set.getDouble("resA"); + + if (sigma < 1e-5) { + return transmissionValueFast(U, pos, resA); + } + + UnivariateFunction integrand = (double E) -> transmissionValueFast(U, E, resA) * getGauss(E, pos, sigma); + + if (pos + cutoff * sigma < U) { + return 0; + } else if (pos - cutoff * sigma > U * (1 + resA)) { + return 1; + } else { + return integrator.integrate(pos - cutoff * sigma, pos + cutoff * sigma, integrand); + } + + } +} diff --git a/numass-main/src/main/java/inr/numass/models/GunTailSpectrum.java b/numass-main/src/main/java/inr/numass/models/GunTailSpectrum.java new file mode 100644 index 00000000..add2199f --- /dev/null +++ b/numass-main/src/main/java/inr/numass/models/GunTailSpectrum.java @@ -0,0 +1,89 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.models; + +import hep.dataforge.exceptions.NotDefinedException; +import hep.dataforge.names.NameList; +import hep.dataforge.values.ValueProvider; +import hep.dataforge.values.Values; +import org.apache.commons.math3.analysis.UnivariateFunction; + +import static java.lang.Math.*; + +public class GunTailSpectrum implements RangedNamedSetSpectrum { + + private final double cutoff = 4d; + + private final String[] list = {"pos", "tailShift", "tailAmp", "sigma"}; + + @Override + public double derivValue(String parName, double x, Values set) { + throw new NotDefinedException(); + } + + @Override + public Double max(Values set) { + return set.getDouble("pos") + cutoff * set.getDouble("sigma"); + } + + @Override + public Double min(Values set) { + return 0d; + } + + @Override + public NameList getNames() { + return new NameList(list); + } + + @Override + public boolean providesDeriv(String name) { + return false; + } + + @Override + public double value(double E, Values set) { + double pos = set.getDouble("pos"); + double amp = set.getDouble("tailAmp"); + double sigma = set.getDouble("sigma"); + + if (E >= pos + cutoff * sigma) { + return 0d; + } + + return gauss(E, pos, sigma) * (1 - amp) + amp * tail(E, pos, set); + } + + double gauss(double E, double pos, double sigma) { + if (abs(E - pos) > cutoff * sigma) { + return 0; + } + double aux = (E - pos) / sigma; + return exp(-aux * aux / 2) / sigma / sqrt(2 * Math.PI); + } + + double tail(double E, double pos, ValueProvider set) { + + double tailShift = set.getDouble("tailShift"); + + double delta = Math.max(pos - E - tailShift, 1d); + UnivariateFunction func = (double d) -> 1d / d / d; +// double tailNorm = NumassContext.defaultIntegrator.integrate(func, 0d, 300d); + + return func.value(delta); + } + +} diff --git a/numass-main/src/main/java/inr/numass/models/LossResConvolution.java b/numass-main/src/main/java/inr/numass/models/LossResConvolution.java new file mode 100644 index 00000000..9643eda0 --- /dev/null +++ b/numass-main/src/main/java/inr/numass/models/LossResConvolution.java @@ -0,0 +1,44 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.models; + +import inr.numass.utils.NumassIntegrator; +import org.apache.commons.math3.analysis.BivariateFunction; +import org.apache.commons.math3.analysis.UnivariateFunction; + +/** + * ÐÐ¾Ñ‚Ð°Ñ†Ð¸Ñ Ð¿Ñ€Ð¾ÑÑ‚Ð°Ñ - Ð½Ð°Ñ‡Ð°Ð»ÑŒÐ½Ð°Ñ ÑÐ½ÐµÑ€Ð³Ð¸Ñ Ð²Ñегда Ñлева, ÐºÐ¾Ð½ÐµÑ‡Ð½Ð°Ñ Ð²Ñегда Ñправа + * + * @author Darksnake + */ +class LossResConvolution implements BivariateFunction { + + BivariateFunction loss; + BivariateFunction res; + + LossResConvolution(BivariateFunction loss, BivariateFunction res) { + this.loss = loss; + this.res = res; + } + + @Override + public double value(final double Ein, final double U) { + UnivariateFunction integrand = (double Eout) -> loss.value(Ein, Eout) * res.value(Eout, U); + //Ð­Ð½ÐµÑ€Ð³Ð¸Ñ Ð² принципе не может быть больше начальной и меньше напрÑÐ¶ÐµÐ½Ð¸Ñ + return NumassIntegrator.getDefaultIntegrator().integrate(U, Ein, integrand); + + } +} diff --git a/numass-main/src/main/java/inr/numass/models/ModularSpectrum.java b/numass-main/src/main/java/inr/numass/models/ModularSpectrum.java new file mode 100644 index 00000000..cdcf6185 --- /dev/null +++ b/numass-main/src/main/java/inr/numass/models/ModularSpectrum.java @@ -0,0 +1,249 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.models; + +import hep.dataforge.names.NamesUtils; +import hep.dataforge.stat.parametric.AbstractParametricFunction; +import hep.dataforge.stat.parametric.ParametricFunction; +import hep.dataforge.values.ValueProvider; +import hep.dataforge.values.Values; +import inr.numass.models.misc.LossCalculator; +import org.apache.commons.math3.analysis.BivariateFunction; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +/** + * Modular spectrum for any source spectrum with separate calculation for + * different transmission components + * + * @author Darksnake + */ +public class ModularSpectrum extends AbstractParametricFunction { + + private static final String[] list = {"X", "trap"}; + private LossCalculator calculator = LossCalculator.INSTANCE; + List cacheList; + NamedSpectrumCaching trappingCache; + BivariateFunction resolution; + RangedNamedSetSpectrum sourceSpectrum; + BivariateFunction trappingFunction; + boolean caching = false; + double cacheMin; + double cacheMax; + + /** + * + * @param source + * @param resolution + * @param cacheMin - нижнÑÑ Ð³Ñ€Ð°Ð½Ð¸Ñ†Ð° кÑшированиÑ. Должна быть Ñ Ð½ÐµÐ±Ð¾Ð»ÑŒÑˆÐ¸Ð¼ + * запаÑом по отношению к данным + * @param cacheMax - верхнÑÑ Ð³Ñ€Ð°Ð½Ð¸Ñ†Ð° кÑшированиÑ. + */ + public ModularSpectrum(RangedNamedSetSpectrum source, BivariateFunction resolution, double cacheMin, double cacheMax) { + super(NamesUtils.combineNamesWithEquals(list, source.namesAsArray())); + if (cacheMin >= cacheMax) { + throw new IllegalArgumentException(); + } + this.cacheMin = cacheMin; + this.cacheMax = cacheMax; + this.resolution = resolution; + this.sourceSpectrum = source; + setupCache(); + } + + public ModularSpectrum(RangedNamedSetSpectrum source, BivariateFunction resolution) { + this(source, resolution, Double.NaN, Double.NaN); + } + + /** + * + * @param source + * @param resA - отноÑÐ¸Ñ‚ÐµÐ»ÑŒÐ½Ð°Ñ ÑˆÐ¸Ñ€Ð¸Ð½Ð° Ñ€Ð°Ð·Ñ€ÐµÑˆÐµÐ½Ð¸Ñ + * @param cacheMin - нижнÑÑ Ð³Ñ€Ð°Ð½Ð¸Ñ†Ð° кÑшированиÑ. Должна быть Ñ Ð½ÐµÐ±Ð¾Ð»ÑŒÑˆÐ¸Ð¼ + * запаÑом по отношению к данным + * @param cacheMax - верхнÑÑ Ð³Ñ€Ð°Ð½Ð¸Ñ†Ð° кÑшированиÑ, может быть без запаÑа. + */ + public ModularSpectrum(RangedNamedSetSpectrum source, double resA, double cacheMin, double cacheMax) { + this(source, new ResolutionFunction(resA), cacheMin, cacheMax); + } + + public ModularSpectrum(RangedNamedSetSpectrum source, double resA) { + this(source, new ResolutionFunction(resA)); + } + + public void setTrappingFunction(BivariateFunction trappingFunction) { + this.trappingFunction = trappingFunction; + LoggerFactory.getLogger(getClass()).info("Recalculating modular spectrum immutable"); + setupCache(); + } + + + + /** + * Отдельный метод нужен на Ñлучай, еÑли бета-Ñпектр(FSS) или разрешение + * будут менÑÑ‚ÑŒÑÑ Ð² процеÑÑе + */ + private void setupCache() { + + //обновлÑем кÑши Ð´Ð»Ñ Ñ‚Ñ€Ñппинга и упругого Ð¿Ñ€Ð¾Ñ…Ð¾Ð¶Ð´ÐµÐ½Ð¸Ñ + //Using external trappingCache function if provided + BivariateFunction trapFunc = trappingFunction != null ? trappingFunction : calculator.getTrapFunction(); + BivariateFunction trapRes = new LossResConvolution(trapFunc, resolution); + + ParametricFunction elasticSpectrum = new TransmissionConvolution(sourceSpectrum, resolution, sourceSpectrum); + ParametricFunction trapSpectrum = new TransmissionConvolution(sourceSpectrum, trapRes, sourceSpectrum); + /** + * обнулÑем кÑш раÑÑеÑÐ½Ð¸Ñ + */ + cacheList = new ArrayList<>(); + //добавлÑем нулевой порÑдок - упругое раÑÑеÑние + + TritiumSpectrumCaching elasticCache = new TritiumSpectrumCaching(elasticSpectrum, cacheMin, cacheMax); + elasticCache.setCachingEnabled(caching); + cacheList.add(elasticCache); + this.trappingCache = new TritiumSpectrumCaching(trapSpectrum, cacheMin, cacheMax); + this.trappingCache.setCachingEnabled(caching); + } + + /** + * ОбновлÑем кÑш раÑÑеÑÐ½Ð¸Ñ ÐµÑли требуемый порÑдок выше, чем тот, что еÑÑ‚ÑŒ + * + * @param order + */ + private void updateScatterCache(int order) { + if (order >= cacheList.size()) { + LoggerFactory.getLogger(getClass()) + .debug("Updating scatter immutable up to order of '{}'", order); + // здеÑÑŒ можно ÑÑкономить вызовы, Ð½Ð°Ñ‡Ð¸Ð½Ð°Ñ Ñ cacheList.size(), но надо Ñто? + for (int i = 1; i < order + 1; i++) { + BivariateFunction loss = calculator.getLossFunction(i); + BivariateFunction lossRes = new LossResConvolution(loss, resolution); + ParametricFunction inelasticSpectrum = new TransmissionConvolution(sourceSpectrum, lossRes, sourceSpectrum); + TritiumSpectrumCaching spCatch = new TritiumSpectrumCaching(inelasticSpectrum, cacheMin, cacheMax); + spCatch.setCachingEnabled(caching); + spCatch.setSuppressWarnings(true); + //TODO Ñделать пороверку + cacheList.add(i, spCatch); + } + } + } + + @Override + public double derivValue(String parName, double U, Values set) { + if (U >= sourceSpectrum.max(set)) { + return 0; + } + double X = this.getX(set); + switch (parName) { + case "X": + List probDerivs = calculator.getLossProbDerivs(X); + updateScatterCache(probDerivs.size() - 1); + double derivSum = 0; + + for (int i = 0; i < probDerivs.size(); i++) { + derivSum += probDerivs.get(i) * cacheList.get(i).value(U, set); + } + + return derivSum; + case "trap": + return this.trappingCache.value(U, set); + default: + if (sourceSpectrum.getNames().contains(parName)) { + List probs = calculator.getLossProbabilities(X); + updateScatterCache(probs.size() - 1); + double sum = 0; + + for (int i = 0; i < probs.size(); i++) { + sum += probs.get(i) * cacheList.get(i).derivValue(parName, U, set); + } + + sum += this.getTrap(set) * this.trappingCache.derivValue(parName, U, set); + return sum; + } else { + return 0; + } + } + } + + private double getTrap(ValueProvider set) { + return set.getDouble("trap"); + } + + private double getX(ValueProvider set) { + return set.getDouble("X"); + } + + @Override + public boolean providesDeriv(String name) { + return sourceSpectrum.providesDeriv(name); + } + + /** + * Set the boundaries and recalculate immutable + * + * @param cacheMin + * @param cacheMax + */ + public void setCachingBoundaries(double cacheMin, double cacheMax) { + this.cacheMin = cacheMin; + this.cacheMax = cacheMax; + setupCache(); + } + + public final void setCaching(boolean caching) { + if (caching && (cacheMin == Double.NaN || cacheMax == Double.NaN)) { + throw new IllegalStateException("Cahing boundaries are not defined"); + } + + this.caching = caching; + this.trappingCache.setCachingEnabled(caching); + this.cacheList.stream().forEach((sp) -> { + sp.setCachingEnabled(caching); + }); + } + + /** + * Suppress warnings about immutable recalculation + * @param suppress + */ + public void setSuppressWarnings(boolean suppress) { + this.trappingCache.setSuppressWarnings(suppress); + this.cacheList.stream().forEach((sp) -> { + sp.setSuppressWarnings(suppress); + }); + } + + @Override + public double value(double U, Values set) { + if (U >= sourceSpectrum.max(set)) { + return 0; + } + double X = this.getX(set); + + List probs = calculator.getLossProbabilities(X); + updateScatterCache(probs.size() - 1); + double res = 0; + + for (int i = 0; i < probs.size(); i++) { + res += probs.get(i) * cacheList.get(i).value(U, set); + } + + res += this.getTrap(set) * this.trappingCache.value(U, set); + return res; + } +} diff --git a/numass-main/src/main/java/inr/numass/models/NamedSpectrumCaching.java b/numass-main/src/main/java/inr/numass/models/NamedSpectrumCaching.java new file mode 100644 index 00000000..65e2c169 --- /dev/null +++ b/numass-main/src/main/java/inr/numass/models/NamedSpectrumCaching.java @@ -0,0 +1,231 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.models; + +import hep.dataforge.maths.MathUtils; +import hep.dataforge.maths.NamedVector; +import hep.dataforge.names.AbstractNamedSet; +import hep.dataforge.stat.parametric.AbstractParametricFunction; +import hep.dataforge.stat.parametric.ParametricFunction; +import hep.dataforge.values.Values; +import org.apache.commons.math3.analysis.UnivariateFunction; +import org.apache.commons.math3.analysis.interpolation.SplineInterpolator; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import static hep.dataforge.stat.parametric.ParametricUtils.getSpectrumDerivativeFunction; +import static hep.dataforge.stat.parametric.ParametricUtils.getSpectrumFunction; + +/** + * + * @author Darksnake + */ +public class NamedSpectrumCaching extends AbstractParametricFunction { + + double a; + double b; + private boolean cachingEnabled = true; + public int interpolationNodes = 200; + ParametricFunction source; + CacheElement spectrumCache = null; + Map spectrumDerivCache; +// CacheElement[] spectrumDerivCache; + private boolean suppressWarnings = false; + + public NamedSpectrumCaching(ParametricFunction spectrum, double a, double b) { + super(spectrum); + assert b > a; + this.a = a; + this.b = b; + this.source = spectrum; + spectrumDerivCache = new HashMap<>(source.getNames().size()); +// spectrumDerivCache = new CacheElement[source.getDimension()]; + } + + public NamedSpectrumCaching(ParametricFunction spectrum, double a, double b, int numPoints) { + this(spectrum, a, b); + this.interpolationNodes = numPoints; + } + + @Override + public double derivValue(String parName, double x, Values set) { + if (!isCachingEnabled()) { + return source.derivValue(parName, x, set); + } + + if (!spectrumDerivCache.containsKey(parName)) { + if (!suppressWarnings) { + LoggerFactory.getLogger(getClass()) + .debug("Starting initial caching of spectrum partial derivative for parameter '{}'", parName); + } + CacheElement el = new CacheElement(set, parName); + spectrumDerivCache.put(parName, el); + return el.value(x); + } else { + CacheElement el = spectrumDerivCache.get(parName); + if (sameSet(set, el.getCachedParameters())) { + return el.value(x); + } else { + try { + return transformation(el, set, x); + } catch (TransformationNotAvailable ex) { + if (!suppressWarnings) { + LoggerFactory.getLogger(getClass()) + .debug("Transformation of immutable is not available. Updating immutable."); + } + el = new CacheElement(set, parName); + spectrumDerivCache.put(parName, el); + return el.value(x); + } + } + } + } + + /** + * @return the cachingEnabled + */ + public boolean isCachingEnabled() { + return cachingEnabled; + } + + @Override + public boolean providesDeriv(String name) { + return source.providesDeriv(name); + } + + protected boolean sameSet(Values set1, Values set2) { + for (String name : this.getNames()) { + if (!Objects.equals(set1.getDouble(name), set2.getDouble(name))) { + return false; + } + } + return true; + } + + /** + * @param cachingEnabled the cachingEnabled to set + */ + public void setCachingEnabled(boolean cachingEnabled) { + this.cachingEnabled = cachingEnabled; + } + + /** + * @param suppressWarnings the suppressWarnings to set + */ + public void setSuppressWarnings(boolean suppressWarnings) { + this.suppressWarnings = suppressWarnings; + } + + /* + * ПодразумеваетÑÑ, что транÑÑ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¾Ð´Ð½Ð° и та же и Ð´Ð»Ñ Ñпектра, и Ð´Ð»Ñ Ð¿Ñ€Ð¾Ð¸Ð·Ð²Ð¾Ð´Ð½Ñ‹Ñ…. + */ + protected double transformation(CacheElement cache, Values newSet, double x) throws TransformationNotAvailable { + + + /* + * Ð’ Ñтом варианте кÑширование работает тольеко еÑли вÑе параметры в точноÑти Ñовпадают. + * Ð”Ð»Ñ ÐºÐ¾Ð½ÐºÑ€ÐµÑ‚Ð½Ñ‹Ñ… преобразований нужно переопределить Ñтот метод. + * + */ + throw new TransformationNotAvailable(); + } + + @Override + public double value(double x, Values set) { + if (!isCachingEnabled()) { + return source.value(x, set); + } + + if (spectrumCache == null) { + if (!suppressWarnings) { + LoggerFactory.getLogger(getClass()) + .debug("Starting initial caching of spectrum."); + } + spectrumCache = new CacheElement(set); + return spectrumCache.value(x); + } + + if (sameSet(set, spectrumCache.getCachedParameters())) { + return spectrumCache.value(x); + } else { + try { + return transformation(spectrumCache, set, x); + } catch (TransformationNotAvailable ex) { + if (!suppressWarnings) { + LoggerFactory.getLogger(getClass()) + .debug("Transformation of immutable is not available. Updating immutable."); + } + spectrumCache = new CacheElement(set); + return spectrumCache.value(x); + } + } + + } + + protected static class TransformationNotAvailable extends Exception { + } + + protected class CacheElement extends AbstractNamedSet implements UnivariateFunction { + + private UnivariateFunction cachedSpectrum; + private final Values cachedParameters; + String parName; + + CacheElement(Values parameters, String parName) { + super(source); + //на вÑÑкий Ñлучай обрезаем набор параметров до необходимого + String[] names = source.namesAsArray(); + this.cachedParameters = new NamedVector(names, MathUtils.getDoubleArray(parameters)); + UnivariateFunction func = getSpectrumDerivativeFunction(parName, source, parameters); + generate(func); + } + + CacheElement(Values parameters) { + super(source); + String[] names = source.namesAsArray(); + this.cachedParameters = new NamedVector(names, MathUtils.getDoubleArray(parameters)); + UnivariateFunction func = getSpectrumFunction(source, parameters); + generate(func); + } + + private void generate(UnivariateFunction func) { + SplineInterpolator interpolator = new SplineInterpolator(); + double[] x = new double[interpolationNodes]; + double[] y = new double[interpolationNodes]; + double step = (b - a) / (interpolationNodes - 1); + x[0] = a; + y[0] = func.value(a); + for (int i = 1; i < y.length; i++) { + x[i] = x[i - 1] + step; + y[i] = func.value(x[i]); + + } + this.cachedSpectrum = interpolator.interpolate(x, y); + } + + @Override + public double value(double x) { + return this.cachedSpectrum.value(x); + } + + public Values getCachedParameters() { + return this.cachedParameters; + } + } +} diff --git a/numass-main/src/main/java/inr/numass/models/RangedNamedSetSpectrum.java b/numass-main/src/main/java/inr/numass/models/RangedNamedSetSpectrum.java new file mode 100644 index 00000000..ef029c44 --- /dev/null +++ b/numass-main/src/main/java/inr/numass/models/RangedNamedSetSpectrum.java @@ -0,0 +1,26 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.models; + +import hep.dataforge.stat.parametric.ParametricFunction; + +/** + * + * @author Darksnake + */ +public interface RangedNamedSetSpectrum extends ParametricFunction, SpectrumRange{ + +} diff --git a/numass-main/src/main/java/inr/numass/models/ResolutionFunction.java b/numass-main/src/main/java/inr/numass/models/ResolutionFunction.java new file mode 100644 index 00000000..e9d3646a --- /dev/null +++ b/numass-main/src/main/java/inr/numass/models/ResolutionFunction.java @@ -0,0 +1,134 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.models; + +import hep.dataforge.maths.Interpolation; +import java.io.InputStream; +import static java.lang.Double.isNaN; +import static java.lang.Math.sqrt; +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; +import org.apache.commons.math3.analysis.BivariateFunction; +import org.apache.commons.math3.analysis.UnivariateFunction; + +/** + * Ð¤ÑƒÐ½ÐºÑ†Ð¸Ñ Ñ€Ð°Ð·Ñ€ÐµÑˆÐµÐ½Ð¸Ñ Ð² точном и упрощенном виде. ВозможноÑÑ‚ÑŒ фитировать + * разрешение пока не предуÑмотрена. + * + * @author Darksnake + */ +public class ResolutionFunction implements BivariateFunction { + + public static BivariateFunction getRealTail() { + InputStream transmissionPointStream = ResolutionFunction.class.getResourceAsStream("/numass/models/transmission"); + Scanner scanner = new Scanner(transmissionPointStream); + + Map values = new HashMap<>(); + + while (scanner.hasNextDouble()) { + values.put((18.5 - scanner.nextDouble()) * 1000, scanner.nextDouble()); + } + + UnivariateFunction f = Interpolation.interpolate(values, Interpolation.InterpolationType.LINE, Double.NaN, Double.NaN); + + return (double x, double y) -> f.value(x - y); + } + + public static BivariateFunction getAngledTail(double dropPerKv) { + return (double E, double U) -> 1 - (E - U) * dropPerKv / 1000d; + } + + /** + * (E, U) -> 1 - (E - U) * (alpha + E * beta) / 1000d + * @param alpha drop per kV at E = 0 + * @param beta dependence of drop per kV on E (in kV) + * @return + */ + public static BivariateFunction getAngledTail(double alpha, double beta) { + return (double E, double U) -> 1 - (E - U) * (alpha + E /1000d * beta) / 1000d; + } + + public static BivariateFunction getConstantTail() { + return new ConstantTailFunction(); + } + + private final double resA; + private double resB = Double.NaN; + private BivariateFunction tailFunction = new ConstantTailFunction(); + + /** + * ЕÑли иÑползуетÑÑ ÐºÐ¾Ð½Ñтруктор Ñ Ð¾Ð´Ð½Ð¸Ð¼ параметром, то по-умолчанию + * иÑпользуем упрощенную Ñхему. + * + * @param resA + */ + public ResolutionFunction(double resA) { + this.resA = resA; + } + + public ResolutionFunction(double resA, BivariateFunction tailFunction) { + this.resA = resA; + this.tailFunction = tailFunction; + } + + ResolutionFunction(double resA, double resB) { + this.resA = resA; + this.resB = resB; + } + + private double getValueFast(double E, double U) { + double delta = resA * E; + if (E - U < 0) { + return 0; + } else if (E - U > delta) { + return tailFunction.value(E, U); + } else { + return (E - U) / delta; + } + } + + public void setTailFunction(BivariateFunction tailFunction) { + this.tailFunction = tailFunction; + } + + @Override + public double value(double E, double U) { + assert resA > 0; + if (isNaN(resB)) { + return this.getValueFast(E, U); + } + assert resB > 0; + double delta = resA * E; + if (E - U < 0) { + return 0; + } else if (E - U > delta) { + return tailFunction.value(E, U); + } else { + return (1 - sqrt(1 - (E - U) / E * resB)) / (1 - sqrt(1 - resA * resB)); + } + } + + private static class ConstantTailFunction implements BivariateFunction { + + @Override + public double value(double x, double y) { + return 1; + } + + } + +} diff --git a/numass-main/src/main/java/inr/numass/models/SimpleRange.java b/numass-main/src/main/java/inr/numass/models/SimpleRange.java new file mode 100644 index 00000000..b395c11a --- /dev/null +++ b/numass-main/src/main/java/inr/numass/models/SimpleRange.java @@ -0,0 +1,50 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.models; + +import hep.dataforge.values.Values; + +/** + * + * @author Darksnake + */ +public class SimpleRange implements SpectrumRange{ + private final Double min; + private final Double max; + + public SimpleRange(Double min, Double max) { + if(min>=max){ + throw new IllegalArgumentException(); + } + this.min = min; + this.max = max; + } + + + + @Override + public Double max(Values set) { + return max; + } + + @Override + public Double min(Values set) { + return min; + } + + + +} diff --git a/numass-main/src/main/java/inr/numass/models/SpectrumRange.java b/numass-main/src/main/java/inr/numass/models/SpectrumRange.java new file mode 100644 index 00000000..f6230a79 --- /dev/null +++ b/numass-main/src/main/java/inr/numass/models/SpectrumRange.java @@ -0,0 +1,28 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.models; + +import hep.dataforge.values.Values; + + +/** + * Calculates a range in witch spectrum is not 0 + * @author Darksnake + */ +public interface SpectrumRange { + Double min(Values set); + Double max(Values set); +} diff --git a/numass-main/src/main/java/inr/numass/models/Transmission.java b/numass-main/src/main/java/inr/numass/models/Transmission.java new file mode 100644 index 00000000..2f3b1d3b --- /dev/null +++ b/numass-main/src/main/java/inr/numass/models/Transmission.java @@ -0,0 +1,44 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.models; + + +import hep.dataforge.names.NameSetContainer; +import hep.dataforge.stat.parametric.ParametricFunction; +import hep.dataforge.values.Values; +import org.apache.commons.math3.analysis.BivariateFunction; + +/** + * + * @author Darksnake + */ +public interface Transmission extends NameSetContainer{ + + double getValue(Values set, double input, double output); + double getDeriv(String name, Values set, double input, double output); + boolean providesDeriv(String name); + + ParametricFunction getConvolutedSpectrum(RangedNamedSetSpectrum bare); + + + default BivariateFunction getBivariateFunction(final Values params){ + return (double input, double output) -> getValue(params, input, output); + } + + default BivariateFunction getBivariateDerivFunction(final String name, final Values params){ + return (double input, double output) -> getDeriv(name, params, input, output); + } +} diff --git a/numass-main/src/main/java/inr/numass/models/TransmissionConvolution.java b/numass-main/src/main/java/inr/numass/models/TransmissionConvolution.java new file mode 100644 index 00000000..99c1de78 --- /dev/null +++ b/numass-main/src/main/java/inr/numass/models/TransmissionConvolution.java @@ -0,0 +1,85 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.models; + +import hep.dataforge.stat.parametric.AbstractParametricFunction; +import hep.dataforge.stat.parametric.ParametricFunction; +import hep.dataforge.values.Values; +import inr.numass.utils.NumassIntegrator; +import org.apache.commons.math3.analysis.BivariateFunction; +import org.apache.commons.math3.analysis.UnivariateFunction; + +/** + * + * @author Darksnake + */ +class TransmissionConvolution extends AbstractParametricFunction { + + BivariateFunction trans; + ParametricFunction spectrum; + + SpectrumRange range; + + TransmissionConvolution(ParametricFunction spectrum, BivariateFunction transmission, double endpoint) { + this(spectrum, transmission, new SimpleRange(0d, endpoint)); + } + + TransmissionConvolution(ParametricFunction spectrum, BivariateFunction transmission, SpectrumRange range) { + super(spectrum); + this.trans = transmission; + this.spectrum = spectrum; + this.range = range; + } + + @Override + public double derivValue(final String parName, final double U, Values set) { + double min = range.min(set); + double max = range.max(set); + + if (U >= max) { + return 0; + } + UnivariateFunction integrand = (double E) -> { + if (E <= U) { + return 0; + } + return trans.value(E, U) * spectrum.derivValue(parName, E, set); + }; + return NumassIntegrator.getDefaultIntegrator().integrate(Math.max(U, min), max + 1d, integrand); + } + + @Override + public boolean providesDeriv(String name) { + return spectrum.providesDeriv(name); + } + + @Override + public double value(final double U, Values set) { + double min = range.min(set); + double max = range.max(set); + + if (U >= max) { + return 0; + } + UnivariateFunction integrand = (double E) -> { + if (E <= U) { + return 0; + } + return trans.value(E, U) * spectrum.value(E, set); + }; + return NumassIntegrator.getDefaultIntegrator().integrate(Math.max(U, min), max + 1d, integrand); + } +} diff --git a/numass-main/src/main/java/inr/numass/models/TransmissionInterpolator.java b/numass-main/src/main/java/inr/numass/models/TransmissionInterpolator.java new file mode 100644 index 00000000..711c6cac --- /dev/null +++ b/numass-main/src/main/java/inr/numass/models/TransmissionInterpolator.java @@ -0,0 +1,187 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.models; + +import hep.dataforge.context.Context; +import hep.dataforge.io.ColumnedDataReader; +import hep.dataforge.meta.Meta; +import hep.dataforge.values.Values; +import kotlin.NotImplementedError; +import org.apache.commons.math3.analysis.UnivariateFunction; +import org.apache.commons.math3.analysis.interpolation.LinearInterpolator; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +/** + * + * @author darksnake + */ +public class TransmissionInterpolator implements UnivariateFunction { + + public static TransmissionInterpolator fromFile(Context context, String path, String xName, String yName, int nSmooth, double w, double border) { + try { + Path dataFile = context.getRootDir().resolve(path); + ColumnedDataReader reader = new ColumnedDataReader(Files.newInputStream(dataFile)); + return new TransmissionInterpolator(reader, xName, yName, nSmooth, w, border); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + @SuppressWarnings("unchecked") + public static TransmissionInterpolator fromAction(Context context, Meta actionAnnotation, + String xName, String yName, int nSmooth, double w, double border) throws InterruptedException { + + throw new NotImplementedError(); +// DataNode
node = ActionUtils.runConfig(context, actionAnnotation); +// ValuesSource data = node.getData().get(); +// return new TransmissionInterpolator(data, xName, yName, nSmooth, w, border); + } + + UnivariateFunction func; + double[] x; + double[] y; + private double xmax; + private double xmin; + + private TransmissionInterpolator(Iterable data, String xName, String yName, int nSmooth, double w, double border) { + prepareXY(data, xName, yName); + double[] smoothed = smoothXY(x, y, w, border); + //Циклы ÑÐ³Ð»Ð°Ð¶Ð¸Ð²Ð°Ð½Ð¸Ñ + for (int i = 1; i < nSmooth; i++) { + smoothed = smoothXY(x, smoothed, w, border); + } + this.func = new LinearInterpolator().interpolate(x, smoothed); + } + + public double[] getX() { + return x; + } + + /** + * @return the xmax + */ + public double getXmax() { + return xmax; + } + + /** + * @return the xmin + */ + public double getXmin() { + return xmin; + } + + public double[] getY() { + return y; + } + + /** + * Prepare and normalize data for interpolation + * + * @param data + * @param xName + * @param yName + */ + private void prepareXY(Iterable data, String xName, String yName) { + + List points = new ArrayList<>(); + + for (Values dp : data) { + points.add(dp); + } + + x = new double[points.size()]; + y = new double[points.size()]; + + xmin = Double.POSITIVE_INFINITY; + xmax = Double.NEGATIVE_INFINITY; + double ymin = Double.POSITIVE_INFINITY; + double ymax = Double.NEGATIVE_INFINITY; + for (int i = 0; i < points.size(); i++) { + x[i] = points.get(i).getDouble(xName); + y[i] = points.get(i).getDouble(yName); + if (x[i] < xmin) { + xmin = x[i]; + } + if (x[i] > xmax) { + xmax = x[i]; + } + if (y[i] < ymin) { + ymin = y[i]; + } + if (y[i] > ymax) { + ymax = y[i]; + } + } + +// ymax = y[0]-ymin; + for (int i = 0; i < y.length; i++) { + y[i] = (y[i] - ymin) / (ymax - ymin); + } + } + + private static double[] smoothXY(double x[], double[] y, double w, double border) { + int max = y.length - 1; + + double[] yUp = new double[y.length]; + double[] yDown = new double[y.length]; + + /* ÑкÑпоненциальное ÑкользÑщее Ñреднее + /https://ru.wikipedia.org/wiki/%D0%A1%D0%BA%D0%BE%D0%BB%D1%8C%D0%B7%D1%8F%D1%89%D0%B0%D1%8F_%D1%81%D1%80%D0%B5%D0%B4%D0%BD%D1%8F%D1%8F + / \textit{EMA}_t = \alpha \cdot p_t + (1-\alpha) \cdot \textit{EMA}_{t-1}, + */ + yUp[0] = y[0]; + for (int i = 1; i < y.length; i++) { + if (x[i] < border) { + yUp[i] = w * y[i] + (1 - w) * yUp[i - 1]; + } else { + yUp[i] = y[i]; + } + } + + yDown[max] = y[max]; + for (int i = max - 1; i >= 0; i--) { + if (x[i] < border) { + yDown[i] = w * y[i] + (1 - w) * yUp[i + 1]; + } else { + yDown[i] = y[i]; + } + } + + double[] res = new double[y.length]; + for (int i = 0; i < x.length; i++) { + res[i] = (yUp[i] + yDown[i]) / 2; + } + return res; + } + + @Override + public double value(double x) { + if (x <= getXmin()) { + return 1d; + } + if (x >= getXmax()) { + return 0; + } + return func.value(x); + } + +} diff --git a/numass-main/src/main/java/inr/numass/models/TritiumSpectrumCaching.java b/numass-main/src/main/java/inr/numass/models/TritiumSpectrumCaching.java new file mode 100644 index 00000000..65e7436c --- /dev/null +++ b/numass-main/src/main/java/inr/numass/models/TritiumSpectrumCaching.java @@ -0,0 +1,65 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.models; + +import hep.dataforge.maths.NamedVector; +import hep.dataforge.stat.parametric.ParametricFunction; +import hep.dataforge.values.Values; +import org.slf4j.LoggerFactory; + +import static java.lang.Math.abs; + +/** + * + * @author Darksnake + */ +public class TritiumSpectrumCaching extends NamedSpectrumCaching { + + private double delta = 50d; + + public TritiumSpectrumCaching(ParametricFunction spectrum, double a, double b) { + super(spectrum, a, b); + } + + public TritiumSpectrumCaching(ParametricFunction spectrum, double a, double b, double delta) { + super(spectrum, a, b); + this.delta = delta; + } + + @Override + protected double transformation(CacheElement cache, Values newSet, double x) throws TransformationNotAvailable { + double res; + NamedVector curSet = new NamedVector(newSet); + double E0new = newSet.getDouble("E0"); + double E0old = cache.getCachedParameters().getDouble("E0"); + double E0delta = E0new - E0old; + if (abs(E0delta) > delta) { + LoggerFactory.getLogger(getClass()) + .debug("The difference in 'E0' is too large. Caching is not available."); + throw new TransformationNotAvailable(); + } else { + res = cache.value(x - E0delta);//проверить знак + curSet.setValue("E0", E0old); + } + + if (sameSet(curSet, cache.getCachedParameters())) { + return res; + } else { + throw new TransformationNotAvailable(); + } + + } +} diff --git a/numass-main/src/main/java/inr/numass/models/VariableLossSpectrum.java b/numass-main/src/main/java/inr/numass/models/VariableLossSpectrum.java new file mode 100644 index 00000000..8ab40919 --- /dev/null +++ b/numass-main/src/main/java/inr/numass/models/VariableLossSpectrum.java @@ -0,0 +1,150 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.models; + +import hep.dataforge.exceptions.NotDefinedException; +import hep.dataforge.maths.integration.UnivariateIntegrator; +import hep.dataforge.stat.parametric.AbstractParametricFunction; +import hep.dataforge.stat.parametric.ParametricFunction; +import hep.dataforge.values.ValueProvider; +import hep.dataforge.values.Values; +import inr.numass.models.misc.LossCalculator; +import inr.numass.utils.NumassIntegrator; +import org.apache.commons.math3.analysis.BivariateFunction; +import org.apache.commons.math3.analysis.UnivariateFunction; + +import java.util.List; + +/** + * + * @author Darksnake + */ +public class VariableLossSpectrum extends AbstractParametricFunction { + + public static String[] names = {"X", "shift", "exPos", "ionPos", "exW", "ionW", "exIonRatio"}; + + public static VariableLossSpectrum withGun(double eMax) { + return new VariableLossSpectrum(new GunSpectrum(), eMax); + } + + public static VariableLossSpectrum withData(final UnivariateFunction transmission, double eMax) { + return new VariableLossSpectrum(new AbstractParametricFunction() { + + @Override + public double derivValue(String parName, double x, Values set) { + throw new NotDefinedException(); + } + + @Override + public boolean providesDeriv(String name) { + return false; + } + + @Override + public double value(double x, Values set) { + return transmission.value(x); + } + }, eMax); + } + + private final ParametricFunction transmission; + private UnivariateFunction backgroundFunction; + private final double eMax; + + protected VariableLossSpectrum(ParametricFunction transmission, double eMax) { + super(names); + this.transmission = transmission; + this.eMax = eMax; + } + + @Override + public double derivValue(String parName, double x, Values set) { + throw new NotDefinedException(); + } + + @Override + public double value(double U, Values set) { + if (U >= eMax) { + return 0; + } + double X = set.getDouble("X"); + final double shift = set.getDouble("shift"); + + final LossCalculator loss = LossCalculator.INSTANCE; + + final List probs = loss.getGunLossProbabilities(X); + final double noLossProb = probs.get(0); + + UnivariateFunction scatter = singleScatterFunction(set); + + final BivariateFunction lossFunction = (Ei, Ef) -> { + if (probs.size() == 1) { + return 0; + } + double sum = probs.get(1) * scatter.value(Ei - Ef); + for (int i = 2; i < probs.size(); i++) { + sum += probs.get(i) * loss.getLossValue(i, Ei, Ef); + } + return sum; + }; + UnivariateFunction integrand = (double x) -> transmission.value(x, set) * lossFunction.value(x, U - shift); + UnivariateIntegrator integrator; + if (eMax - U > 150) { + integrator = NumassIntegrator.getHighDensityIntegrator(); + } else { + integrator = NumassIntegrator.getDefaultIntegrator(); + } + return noLossProb * transmission.value(U - shift, set) + integrator.integrate(U, eMax, integrand); + } + + public UnivariateFunction singleScatterFunction(ValueProvider set) { + + final double exPos = set.getDouble("exPos"); + final double ionPos = set.getDouble("ionPos"); + final double exW = set.getDouble("exW"); + final double ionW = set.getDouble("ionW"); + final double exIonRatio = set.getDouble("exIonRatio"); + + return singleScatterFunction(exPos, ionPos, exW, ionW, exIonRatio); + } + + public UnivariateFunction singleScatterFunction( + final double exPos, + final double ionPos, + final double exW, + final double ionW, + final double exIonRatio) { + return LossCalculator.INSTANCE.getSingleScatterFunction(exPos, ionPos, exW, ionW, exIonRatio); + } + + @Override + public boolean providesDeriv(String name) { + return false; + } + + @Override + protected double getDefaultParameter(String name) { + switch (name) { + case "shift": + return 0; + case "X": + return 0; + default: + return super.getDefaultParameter(name); + } + } + +} diff --git a/numass-main/src/main/java/inr/numass/utils/DataModelUtils.java b/numass-main/src/main/java/inr/numass/utils/DataModelUtils.java new file mode 100644 index 00000000..f1aea522 --- /dev/null +++ b/numass-main/src/main/java/inr/numass/utils/DataModelUtils.java @@ -0,0 +1,67 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.utils; + +import hep.dataforge.tables.ListTable; +import hep.dataforge.tables.Table; +import hep.dataforge.values.ValueMap; +import hep.dataforge.values.Values; + +import java.util.Scanner; + +import static hep.dataforge.tables.Adapters.X_AXIS; + +/** + * + * @author Darksnake + */ +public class DataModelUtils { + + public static Table getUniformSpectrumConfiguration(double from, double to, double time, int numpoints) { + assert to != from; + final String[] list = {X_AXIS, "time"}; + ListTable.Builder res = new ListTable.Builder(list); + + for (int i = 0; i < numpoints; i++) { + // формула работает даже в том Ñлучае когда порÑдок точек обратный + double x = from + (to - from) / (numpoints - 1) * i; + Values point = ValueMap.Companion.of(list, x, time); + res.row(point); + } + + return res.build(); + } + + public static Table getSpectrumConfigurationFromResource(String resource) { + final String[] list = {X_AXIS, "time"}; + ListTable.Builder res = new ListTable.Builder(list); + Scanner scan = new Scanner(DataModelUtils.class.getResourceAsStream(resource)); + while (scan.hasNextLine()) { + double x = scan.nextDouble(); + int time = scan.nextInt(); + res.row(ValueMap.Companion.of(list, x, time)); + } + return res.build(); + } + +// public static ListTable maskDataSet(Iterable data, String maskForX, String maskForY, String maskForYerr, String maskForTime) { +// ListTable res = new ListTable(XYDataPoint.names); +// for (DataPoint point : data) { +// res.addRow(SpectrumDataPoint.maskDataPoint(point, maskForX, maskForY, maskForYerr, maskForTime)); +// } +// return res; +// } +} diff --git a/numass-main/src/main/java/inr/numass/utils/ExpressionUtils.java b/numass-main/src/main/java/inr/numass/utils/ExpressionUtils.java new file mode 100644 index 00000000..983b4b1b --- /dev/null +++ b/numass-main/src/main/java/inr/numass/utils/ExpressionUtils.java @@ -0,0 +1,58 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package inr.numass.utils; + +import groovy.lang.Binding; +import groovy.lang.GroovyShell; +import groovy.lang.Script; +import hep.dataforge.utils.Misc; +import org.codehaus.groovy.control.CompilerConfiguration; +import org.codehaus.groovy.control.customizers.ImportCustomizer; + +import java.util.Map; + +/** + * @author Alexander Nozik + */ +public class ExpressionUtils { + private static final Map cache = Misc.getLRUCache(100); + private static final GroovyShell shell; + + static { + // Add imports for script. + ImportCustomizer importCustomizer = new ImportCustomizer(); + // import static com.mrhaki.blog.Type.* + importCustomizer.addStaticStars("java.lang.Math"); + //importCustomizer.addStaticStars("org.apache.commons.math3.util.FastMath"); + + CompilerConfiguration configuration = new CompilerConfiguration(); + configuration.addCompilationCustomizers(importCustomizer); // Create shell and execute script. + shell = new GroovyShell(configuration); + } + + private static Script getScript(String expression) { + return cache.computeIfAbsent(expression, shell::parse); + } + + + public static double function(String expression, Map binding) { + synchronized (cache) { + Binding b = new Binding(binding); + Script script = getScript(expression); + script.setBinding(b); + return ((Number) script.run()).doubleValue(); + } + } + + public static boolean condition(String expression, Map binding){ + synchronized (cache) { + Binding b = new Binding(binding); + Script script = getScript(expression); + script.setBinding(b); + return (boolean) script.run(); + } + } +} diff --git a/numass-main/src/main/java/inr/numass/utils/NumassIntegrator.java b/numass-main/src/main/java/inr/numass/utils/NumassIntegrator.java new file mode 100644 index 00000000..51df906e --- /dev/null +++ b/numass-main/src/main/java/inr/numass/utils/NumassIntegrator.java @@ -0,0 +1,46 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package inr.numass.utils; + +import hep.dataforge.maths.integration.GaussRuleIntegrator; +import hep.dataforge.maths.integration.UnivariateIntegrator; +import org.slf4j.LoggerFactory; + +/** + * @author Alexander Nozik + */ +public class NumassIntegrator { + + private static double mult = 1.0;//for testing purposes + + private static UnivariateIntegrator fastInterator; + private static UnivariateIntegrator defaultIntegrator; + private static UnivariateIntegrator highDensityIntegrator; + + public synchronized static UnivariateIntegrator getFastInterator() { + if (fastInterator == null) { + LoggerFactory.getLogger(NumassIntegrator.class).debug("Creating fast integrator"); + fastInterator = new GaussRuleIntegrator((int) (mult * 100)); + } + return fastInterator; + } + + public synchronized static UnivariateIntegrator getDefaultIntegrator() { + if (defaultIntegrator == null) { + LoggerFactory.getLogger(NumassIntegrator.class).debug("Creating default integrator"); + defaultIntegrator = new GaussRuleIntegrator((int) (mult * 300)); + } + return defaultIntegrator; + } + + public synchronized static UnivariateIntegrator getHighDensityIntegrator() { + if (highDensityIntegrator == null) { + LoggerFactory.getLogger(NumassIntegrator.class).debug("Creating high precision integrator"); + highDensityIntegrator = new GaussRuleIntegrator((int) (mult * 500)); + } + return highDensityIntegrator; + } +} diff --git a/numass-main/src/main/java/inr/numass/utils/OldDataReader.java b/numass-main/src/main/java/inr/numass/utils/OldDataReader.java new file mode 100644 index 00000000..bf937472 --- /dev/null +++ b/numass-main/src/main/java/inr/numass/utils/OldDataReader.java @@ -0,0 +1,191 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.utils; + +import hep.dataforge.context.Global; +import hep.dataforge.meta.Meta; +import hep.dataforge.tables.Adapters; +import hep.dataforge.tables.ListTable; +import hep.dataforge.tables.Table; +import hep.dataforge.values.ValueMap; +import hep.dataforge.values.Values; +import inr.numass.data.SpectrumAdapter; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Locale; +import java.util.Scanner; + +import static java.util.Locale.setDefault; + +/** + * + * @author Darksnake + */ +public class OldDataReader { + + public static Table readConfig(String path) throws IOException { + String[] list = {"X", "time", "ushift"}; + ListTable.Builder res = new ListTable.Builder(list); + Path file = Global.INSTANCE.getRootDir().resolve(path); + Scanner sc = new Scanner(file); + sc.nextLine(); + + while (sc.hasNextLine()) { + String line = sc.nextLine(); + Scanner lineScan = new Scanner(line); + int time = lineScan.nextInt(); + double u = lineScan.nextDouble(); + double ushift = 0; + if (lineScan.hasNextDouble()) { + ushift = lineScan.nextDouble(); + } + Values point = ValueMap.Companion.of(list, u, time, ushift); + res.row(point); + } + return res.build(); + } + + public static Table readData(String path, double Elow) { + SpectrumAdapter factory = new SpectrumAdapter(Meta.empty()); + ListTable.Builder res = new ListTable.Builder(Adapters.getFormat(factory)); + Path file = Global.INSTANCE.getRootDir().resolve(path); + double x; + int count; + int time; + + setDefault(Locale.ENGLISH); + + Scanner sc; + try { + sc = new Scanner(file); + } catch (IOException ex) { + throw new RuntimeException(ex.getMessage()); + } + double dummy; +// sc.skip("\\D*"); + while (!sc.hasNextDouble()) { + sc.nextLine(); + } + while (sc.hasNextDouble() | sc.hasNextInt()) { + /*Ðадо Ñделать, чтобы Ñчитывало веÑÑŒ файл*/ + x = sc.nextDouble(); + + dummy = sc.nextInt(); + + time = sc.nextInt(); + + dummy = sc.nextInt(); + dummy = sc.nextInt(); + dummy = sc.nextInt(); + dummy = sc.nextInt(); + dummy = sc.nextInt(); + dummy = sc.nextInt(); + + count = sc.nextInt(); +// count = (int) (count / (1 - 2.8E-6 / time * count)); + + dummy = sc.nextInt(); + dummy = sc.nextDouble(); + dummy = sc.nextDouble(); + dummy = sc.nextDouble(); + Values point = factory.buildSpectrumDataPoint(x, count, time); + if (x >= Elow) { + res.row(point); + } + + } + return res.build(); + } + + public static Table readDataAsGun(String path, double Elow) { + SpectrumAdapter factory = new SpectrumAdapter(Meta.empty()); + ListTable.Builder res = new ListTable.Builder(Adapters.getFormat(factory)); + Path file = Global.INSTANCE.getRootDir().resolve(path); + double x; + long count; + int time; + + setDefault(Locale.ENGLISH); + + Scanner sc; + try { + sc = new Scanner(file); + } catch (IOException ex) { + throw new RuntimeException(ex.getMessage()); + } + double dummy; + sc.nextLine(); + while (sc.hasNext()) { + x = sc.nextDouble(); + time = sc.nextInt(); + dummy = sc.nextInt(); + count = sc.nextLong(); + dummy = sc.nextDouble(); + dummy = sc.nextDouble(); + Values point = factory.buildSpectrumDataPoint(x, count, time); + if (x > Elow) { + res.row(point); + } + } + return res.build(); + } + + public static Table readSpectrumData(String path) { + SpectrumAdapter factory = new SpectrumAdapter(Meta.empty()); + ListTable.Builder res = new ListTable.Builder(Adapters.getFormat(factory)); + Path file = Global.INSTANCE.getRootDir().resolve(path); + double x; + double count; + double time; + + double cr; + double crErr; + + setDefault(Locale.ENGLISH); + + Scanner sc; + try { + sc = new Scanner(file); + } catch (IOException ex) { + throw new RuntimeException(ex.getMessage()); + } + + while (sc.hasNext()) { + String line = sc.nextLine(); + if (!line.startsWith("*")) { + Scanner lsc = new Scanner(line); + if (lsc.hasNextDouble() || lsc.hasNextInt()) { + + x = lsc.nextDouble(); + lsc.next(); + time = lsc.nextDouble(); + lsc.next(); + lsc.next(); + count = lsc.nextDouble(); + cr = lsc.nextDouble(); + crErr = lsc.nextDouble(); + Values point = factory.buildSpectrumDataPoint(x, (long) (cr * time), crErr * time, time); +// SpectrumDataPoint point = new SpectrumDataPoint(x, (long) count, time); + + res.row(point); + } + } + } + return res.build(); + } + +} diff --git a/numass-main/src/main/java/inr/numass/utils/UnderflowCorrection.java b/numass-main/src/main/java/inr/numass/utils/UnderflowCorrection.java new file mode 100644 index 00000000..89160ef1 --- /dev/null +++ b/numass-main/src/main/java/inr/numass/utils/UnderflowCorrection.java @@ -0,0 +1,163 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package inr.numass.utils; + +import hep.dataforge.meta.Meta; +import hep.dataforge.tables.ListTable; +import hep.dataforge.tables.Table; +import hep.dataforge.tables.Tables; +import hep.dataforge.values.ValueMap; +import hep.dataforge.values.Values; +import inr.numass.data.analyzers.NumassAnalyzer; +import inr.numass.data.analyzers.NumassAnalyzerKt; +import inr.numass.data.api.NumassPoint; +import org.apache.commons.math3.analysis.ParametricUnivariateFunction; +import org.apache.commons.math3.exception.DimensionMismatchException; +import org.apache.commons.math3.fitting.SimpleCurveFitter; +import org.apache.commons.math3.fitting.WeightedObservedPoint; + +import java.util.List; +import java.util.stream.Collectors; + +import static inr.numass.data.analyzers.NumassAnalyzer.CHANNEL_KEY; +import static inr.numass.data.analyzers.NumassAnalyzer.COUNT_RATE_KEY; + +/** + * A class to calculate underflow correction + * + * @author Alexander Nozik + */ +@Deprecated +public class UnderflowCorrection { + + private NumassAnalyzer analyzer; + + + private static String[] pointNames = {"U", "amp", "expConst", "correction"}; + +// private final static int CUTOFF = -200; + +// public double get(Logable log, Meta meta, NMPoint point) { +// if (point.getVoltage() >= meta.getDouble("underflow.threshold", 17000)) { +// if (meta.hasValue("underflow.function")) { +// return TritiumUtils.pointExpression(meta.getString("underflow.function"), point); +// } else { +// return 1; +// } +// } else { +// try { +// int xLow = meta.getInt("underflow.lowerBorder", meta.getInt("lowerWindow")); +// int xHigh = meta.getInt("underflow.upperBorder", 800); +// int binning = meta.getInt("underflow.binning", 20); +// int upper = meta.getInt("upperWindow", RawNMPoint.MAX_CHANEL - 1); +// long norm = point.getCountInWindow(xLow, upper); +// double[] fitRes = getUnderflowExpParameters(point, xLow, xHigh, binning); +// double correction = fitRes[0] * fitRes[1] * (Math.exp(xLow / fitRes[1]) - 1d) / norm + 1d; +// return correction; +// } catch (Exception ex) { +// log.reportError("Failed to calculate underflow parameters for point {} with message:", point.getVoltage(), ex.getMessage()); +// return 1d; +// } +// } +// } + +// public Table fitAllPoints(Iterable data, int xLow, int xHigh, int binning) { +// ListTable.Builder builder = new ListTable.Builder("U", "amp", "expConst"); +// for (NMPoint point : data) { +// double[] fitRes = getUnderflowExpParameters(point, xLow, xHigh, binning); +// builder.row(point.getVoltage(), fitRes[0], fitRes[1]); +// } +// return builder.builder(); +// } + + public Values fitPoint(NumassPoint point, int xLow, int xHigh, int upper, int binning) { + Table spectrum = analyzer.getAmplitudeSpectrum(point, Meta.empty()); + + double norm = spectrum.getRows().filter(row -> { + int channel = row.getInt(CHANNEL_KEY); + return channel > xLow && channel < upper; + }).mapToDouble(it -> it.getValue(COUNT_RATE_KEY).getNumber().longValue()).sum(); + + double[] fitRes = getUnderflowExpParameters(spectrum, xLow, xHigh, binning); + double a = fitRes[0]; + double sigma = fitRes[1]; + + return ValueMap.Companion.of(pointNames, point.getVoltage(), a, sigma, a * sigma * Math.exp(xLow / sigma) / norm + 1d); + } + + public Table fitAllPoints(Iterable data, int xLow, int xHigh, int upper, int binning) { + ListTable.Builder builder = new ListTable.Builder(pointNames); + for (NumassPoint point : data) { + builder.row(fitPoint(point, xLow, xHigh, upper, binning)); + } + return builder.build(); + } + + /** + * Calculate underflow exponent parameters using (xLow, xHigh) window for + * extrapolation + * + * @param xLow + * @param xHigh + * @return + */ + private double[] getUnderflowExpParameters(Table spectrum, int xLow, int xHigh, int binning) { + try { + if (xHigh <= xLow) { + throw new IllegalArgumentException("Wrong borders for underflow calculation"); + } + Table binned = Tables.filter( + NumassAnalyzerKt.withBinning(spectrum, binning), + CHANNEL_KEY, + xLow, + xHigh + ); + + List points = binned.getRows() + .map(p -> new WeightedObservedPoint( + 1d,//1d / p.getValue() , //weight + p.getDouble(CHANNEL_KEY), // x + p.getDouble(COUNT_RATE_KEY) / binning) //y + ) + .collect(Collectors.toList()); + SimpleCurveFitter fitter = SimpleCurveFitter.create(new ExponentFunction(), new double[]{1d, 200d}); + return fitter.fit(points); + } catch (Exception ex) { + return new double[]{0, 0}; + } + } + + /** + * Exponential function for fitting + */ + private static class ExponentFunction implements ParametricUnivariateFunction { + @Override + public double value(double x, double... parameters) { + if (parameters.length != 2) { + throw new DimensionMismatchException(parameters.length, 2); + } + double a = parameters[0]; + double sigma = parameters[1]; + //return a * (Math.exp(x / sigma) - 1); + return a * Math.exp(x / sigma); + } + + @Override + public double[] gradient(double x, double... parameters) { + if (parameters.length != 2) { + throw new DimensionMismatchException(parameters.length, 2); + } + double a = parameters[0]; + double sigma = parameters[1]; + return new double[]{ + Math.exp(x / sigma), + -a * x / sigma / sigma * Math.exp(x / sigma) + }; + } + + } + +} diff --git a/numass-main/src/main/kotlin/inr/numass/NumassPlugin.kt b/numass-main/src/main/kotlin/inr/numass/NumassPlugin.kt new file mode 100644 index 00000000..b7d91c89 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/NumassPlugin.kt @@ -0,0 +1,333 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass + +import hep.dataforge.context.* +import hep.dataforge.fx.FXPlugin +import hep.dataforge.fx.plots.PlotContainer +import hep.dataforge.maths.functions.FunctionLibrary +import hep.dataforge.meta.Meta +import hep.dataforge.plots.jfreechart.JFreeChartFrame +import hep.dataforge.providers.Provides +import hep.dataforge.providers.ProvidesNames +import hep.dataforge.stat.models.ModelLibrary +import hep.dataforge.stat.models.WeightedXYModel +import hep.dataforge.stat.models.XYModel +import hep.dataforge.tables.Adapters +import hep.dataforge.tables.ValuesAdapter +import hep.dataforge.workspace.tasks.Task +import inr.numass.data.analyzers.NumassAnalyzer +import inr.numass.data.api.NumassPoint +import inr.numass.models.* +import inr.numass.models.sterile.SterileNeutrinoSpectrum +import inr.numass.tasks.* +import org.apache.commons.math3.analysis.BivariateFunction +import org.apache.commons.math3.util.FastMath +import kotlin.math.pow + +/** + * @author Alexander Nozik + */ +@PluginDef( + group = "inr.numass", + name = "numass", + dependsOn = ["hep.dataforge:functions", "hep.dataforge:MINUIT", "hep.dataforge:actions"], + support = false, + info = "Numass data analysis tools" +) +class NumassPlugin : BasicPlugin() { + + override fun attach(context: Context) { + // StorageManager.buildFrom(context); + super.attach(context) + //TODO Replace by local providers + loadModels(context.getOrLoad(ModelLibrary::class.java)) + loadMath(FunctionLibrary.buildFrom(context)) + } + + private val tasks = listOf( + NumassFitScanSummaryTask, + NumassFitSummaryTask, + selectTask, + analyzeTask, + mergeTask, + mergeEmptyTask, + monitorTableTask, + subtractEmptyTask, + transformTask, + filterTask, + fitTask, + plotFitTask, + histogramTask, + fitScanTask, + sliceTask, + subThresholdTask + ) + + @Provides(Task.TASK_TARGET) + fun getTask(name: String): Task<*>? { + return tasks.find { it.name == name } + } + + @ProvidesNames(Task.TASK_TARGET) + fun taskList(): List { + return tasks.map { it.name } + } + + private fun loadMath(math: FunctionLibrary) { + math.addBivariate("numass.trap.lowFields") { Ei, Ef -> 3.92e-5 * FastMath.exp(-(Ei - Ef) / 300.0) + 1.97e-4 - 6.818e-9 * Ei } + + math.addBivariate("numass.trap.nominal") { Ei, Ef -> + //return 1.64e-5 * FastMath.exp(-(Ei - Ef) / 300d) + 1.1e-4 - 4e-9 * Ei; + 1.2e-4 - 4.5e-9 * Ei + } + + math.addBivariateFactory("numass.resolutionTail") { meta -> + val alpha = meta.getDouble("tailAlpha", 0.0) + val beta = meta.getDouble("tailBeta", 0.0) + BivariateFunction { E: Double, U: Double -> 1 - (E - U) * (alpha + E / 1000.0 * beta) / 1000.0 } + } + + math.addBivariateFactory("numass.resolutionTail.2017") { meta -> + BivariateFunction { E: Double, U: Double -> + val D = E - U + 0.99797 - 3.05346E-7 * D - 5.45738E-10 * D.pow(2.0) - 6.36105E-14 * D.pow(3.0) + } + } + + math.addBivariateFactory("numass.resolutionTail.2017.mod") { meta -> + BivariateFunction { E: Double, U: Double -> + val D = E - U + val factor = 7.33 - E / 1000.0 / 3.0 + return@BivariateFunction 1.0 - (3.05346E-7 * D - 5.45738E-10 * D.pow(2.0) - 6.36105E-14 * D.pow(3.0)) * factor + } + } + } + + + /** + * Load all numass model factories + * + * @param library + */ + private fun loadModels(library: ModelLibrary) { + + // manager.addModel("modularbeta", (context, an) -> { + // double A = an.getDouble("resolution", 8.3e-5);//8.3e-5 + // double from = an.getDouble("from", 14400d); + // double to = an.getDouble("to", 19010d); + // RangedNamedSetSpectrum beta = new BetaSpectrum(getClass().getResourceAsStream("/data/FS.txt")); + // ModularSpectrum sp = new ModularSpectrum(beta, A, from, to); + // NBkgSpectrum spectrum = new NBkgSpectrum(sp); + // + // return new XYModel(spectrum, getAdapter(an)); + // }); + + library.addModel("scatter") { context, meta -> + val A = meta.getDouble("resolution", 8.3e-5)//8.3e-5 + val from = meta.getDouble("from", 0.0) + val to = meta.getDouble("to", 0.0) + + val sp: ModularSpectrum + sp = if (from == to) { + ModularSpectrum(GaussSourceSpectrum(), A) + } else { + ModularSpectrum(GaussSourceSpectrum(), A, from, to) + } + + val spectrum = NBkgSpectrum(sp) + + XYModel(meta, getAdapter(meta), spectrum) + } + + library.addModel("scatter-empiric") { context, meta -> + val eGun = meta.getDouble("eGun", 19005.0) + + val interpolator = buildInterpolator(context, meta, eGun) + + val loss = EmpiricalLossSpectrum(interpolator, eGun + 5) + val spectrum = NBkgSpectrum(loss) + + val weightReductionFactor = meta.getDouble("weightReductionFactor", 2.0) + + WeightedXYModel(meta, getAdapter(meta), spectrum) { dp -> weightReductionFactor } + } + + library.addModel("scatter-empiric-variable") { context, meta -> + val eGun = meta.getDouble("eGun", 19005.0) + + //builder transmisssion with given data, annotation and smoothing + val interpolator = buildInterpolator(context, meta, eGun) + + val loss = VariableLossSpectrum.withData(interpolator, eGun + 5) + + val tritiumBackground = meta.getDouble("tritiumBkg", 0.0) + + val spectrum: NBkgSpectrum + if (tritiumBackground == 0.0) { + spectrum = NBkgSpectrum(loss) + } else { + spectrum = CustomNBkgSpectrum.tritiumBkgSpectrum(loss, tritiumBackground) + } + + val weightReductionFactor = meta.getDouble("weightReductionFactor", 2.0) + + WeightedXYModel(meta, getAdapter(meta), spectrum) { dp -> weightReductionFactor } + } + + library.addModel("scatter-analytic-variable") { context, meta -> + val eGun = meta.getDouble("eGun", 19005.0) + + val loss = VariableLossSpectrum.withGun(eGun + 5) + + val tritiumBackground = meta.getDouble("tritiumBkg", 0.0) + + val spectrum: NBkgSpectrum + if (tritiumBackground == 0.0) { + spectrum = NBkgSpectrum(loss) + } else { + spectrum = CustomNBkgSpectrum.tritiumBkgSpectrum(loss, tritiumBackground) + } + + XYModel(meta, getAdapter(meta), spectrum) + } + + library.addModel("scatter-empiric-experimental") { context, meta -> + val eGun = meta.getDouble("eGun", 19005.0) + + //builder transmisssion with given data, annotation and smoothing + val interpolator = buildInterpolator(context, meta, eGun) + + val smoothing = meta.getDouble("lossSmoothing", 0.3) + + val loss = ExperimentalVariableLossSpectrum.withData(interpolator, eGun + 5, smoothing) + + val spectrum = NBkgSpectrum(loss) + + val weightReductionFactor = meta.getDouble("weightReductionFactor", 2.0) + + WeightedXYModel(meta, getAdapter(meta), spectrum) { dp -> weightReductionFactor } + } + + library.addModel("sterile") { context, meta -> + val sp = SterileNeutrinoSpectrum(context, meta) + val spectrum = NBkgSpectrum(sp) + + XYModel(meta, getAdapter(meta), spectrum) + } + + library.addModel("sterile-corrected") { context, meta -> + val sp = SterileNeutrinoSpectrum(context, meta) + val spectrum = NBkgSpectrumWithCorrection(sp) + + XYModel(meta, getAdapter(meta), spectrum) + } + + library.addModel("gun") { context, meta -> + val gsp = GunSpectrum() + + val tritiumBackground = meta.getDouble("tritiumBkg", 0.0) + + val spectrum: NBkgSpectrum + if (tritiumBackground == 0.0) { + spectrum = NBkgSpectrum(gsp) + } else { + spectrum = CustomNBkgSpectrum.tritiumBkgSpectrum(gsp, tritiumBackground) + } + + XYModel(meta, getAdapter(meta), spectrum) + } + + } + + private fun buildInterpolator(context: Context, an: Meta, eGun: Double): TransmissionInterpolator { + val transXName = an.getString("transXName", "Uset") + val transYName = an.getString("transYName", "CR") + + val stitchBorder = an.getDouble("stitchBorder", eGun - 7) + val nSmooth = an.getInt("nSmooth", 15) + + val w = an.getDouble("w", 0.8) + + if (an.hasValue("transFile")) { + val transmissionFile = an.getString("transFile") + + return TransmissionInterpolator + .fromFile(context, transmissionFile, transXName, transYName, nSmooth, w, stitchBorder) + } else if (an.hasMeta("transBuildAction")) { + val transBuild = an.getMeta("transBuildAction") + try { + return TransmissionInterpolator.fromAction( + context, + transBuild, + transXName, + transYName, + nSmooth, + w, + stitchBorder + ) + } catch (ex: InterruptedException) { + throw RuntimeException("Transmission builder failed") + } + + } else { + throw RuntimeException("Transmission declaration not found") + } + } + + private fun getAdapter(an: Meta): ValuesAdapter { + return if (an.hasMeta(ValuesAdapter.ADAPTER_KEY)) { + Adapters.buildAdapter(an.getMeta(ValuesAdapter.ADAPTER_KEY)) + } else { + Adapters.buildXYAdapter( + NumassPoint.HV_KEY, + NumassAnalyzer.COUNT_RATE_KEY, + NumassAnalyzer.COUNT_RATE_ERROR_KEY + ) + } + } + + class Factory : PluginFactory() { + override val type: Class = NumassPlugin::class.java + + override fun build(meta: Meta): Plugin { + return NumassPlugin() + } + } +} + +/** + * Display a JFreeChart plot frame in a separate stage window + * + * @param title + * @param width + * @param height + * @return + */ +@JvmOverloads +fun displayChart( + title: String, + context: Context = Global, + width: Double = 800.0, + height: Double = 600.0, + meta: Meta = Meta.empty() +): JFreeChartFrame { + val frame = JFreeChartFrame() + frame.configure(meta) + frame.configureValue("title", title) + context.plugins.load().display(PlotContainer(frame), width, height) + return frame +} diff --git a/numass-main/src/main/kotlin/inr/numass/NumassUtils.kt b/numass-main/src/main/kotlin/inr/numass/NumassUtils.kt new file mode 100644 index 00000000..faf27677 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/NumassUtils.kt @@ -0,0 +1,305 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass + +import hep.dataforge.context.Context +import hep.dataforge.context.Global +import hep.dataforge.data.DataNode +import hep.dataforge.data.DataSet +import hep.dataforge.data.binary.Binary +import hep.dataforge.io.envelopes.DefaultEnvelopeType +import hep.dataforge.io.envelopes.Envelope +import hep.dataforge.io.envelopes.EnvelopeBuilder +import hep.dataforge.io.envelopes.TaglessEnvelopeType +import hep.dataforge.io.output.StreamOutput +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.plots.jfreechart.JFreeChartFrame +import hep.dataforge.tables.ListTable +import hep.dataforge.tables.Table +import hep.dataforge.values.ValueMap +import hep.dataforge.values.Values +import inr.numass.data.analyzers.NumassAnalyzer +import inr.numass.data.api.NumassPoint +import inr.numass.data.api.NumassSet +import inr.numass.models.FSS +import inr.numass.utils.ExpressionUtils +import javafx.application.Platform +import kotlinx.coroutines.runBlocking +import org.apache.commons.math3.analysis.UnivariateFunction +import org.jfree.chart.plot.IntervalMarker +import org.jfree.chart.ui.RectangleInsets +import org.slf4j.Logger +import java.awt.Color +import java.awt.Font +import java.io.IOException +import java.io.OutputStream +import java.lang.Math.* +import java.time.Instant +import java.util.* + +/** + * @author Darksnake + */ +object NumassUtils { + + /** + * Integral beta spectrum background with given amplitude (total count rate + * from) + * + * @param amplitude + * @return + */ + fun tritiumBackgroundFunction(amplitude: Double): UnivariateFunction { + return UnivariateFunction { e: Double -> + /*чиÑтый бета-Ñпектр*/ + val e0 = 18575.0 + val D = e0 - e//E0-E + if (D <= 0) { + 0.0 + } else { + amplitude * factor(e) * D * D + } + } + } + + private fun factor(E: Double): Double { + val me = 0.511006E6 + val Etot = E + me + val pe = sqrt(E * (E + 2.0 * me)) + val ve = pe / Etot + val yfactor = 2.0 * 2.0 * 1.0 / 137.039 * Math.PI + val y = yfactor / ve + val Fn = y / abs(1.0 - exp(-y)) + val Fermi = Fn * (1.002037 - 0.001427 * ve) + val res = Fermi * pe * Etot + return res * 1E-23 + } + + fun wrap(obj: T, meta: Meta = Meta.empty(), serializer: OutputStream.(T) -> Unit): EnvelopeBuilder { + return EnvelopeBuilder().meta(meta).data { serializer.invoke(it, obj) } + } + + fun wrap(obj: Any, meta: Meta = Meta.empty()): EnvelopeBuilder { + return wrap(obj, meta) { StreamOutput(Global, this).render(it, meta) } + } + + + /** + * Write an envelope wrapping given data to given stream + * + * @param stream + * @param meta + * @param dataWriter + * @throws IOException + */ + fun writeEnvelope(stream: OutputStream, meta: Meta, dataWriter: (OutputStream) -> Unit) { + try { + TaglessEnvelopeType.INSTANCE.writer.write( + stream, + EnvelopeBuilder() + .meta(meta) + .data(dataWriter) + .build() + ) + stream.flush() + } catch (e: IOException) { + throw RuntimeException(e) + } + + } + + fun writeEnvelope(stream: OutputStream, envelope: Envelope) { + try { + DefaultEnvelopeType.INSTANCE.writer.write(stream, envelope) + stream.flush() + } catch (e: IOException) { + throw RuntimeException(e) + } + + } + +// fun write(stream: OutputStream, meta: Meta, something: Markedup) { +// writeEnvelope(stream, meta) { out -> +// SimpleMarkupRenderer(out).render(something.markup(meta)) +// } +// } + + /** + * Convert numass set to DataNode + * + * @param set + * @return + */ + fun setToNode(set: NumassSet): DataNode { + val builder = DataSet.edit() + builder.name = set.name + set.points.forEach { point -> + val pointMeta = MetaBuilder("point") + .putValue("voltage", point.voltage) + .putValue("index", point.meta.getInt("external_meta.point_index", -1)) + .putValue("run", point.meta.getString("external_meta.session", "")) + .putValue("group", point.meta.getString("external_meta.group", "")) + val pointName = "point_" + point.meta.getInt("external_meta.point_index", point.hashCode()) + builder.putData(pointName, point, pointMeta) + } + runBlocking { + set.getHvData()?.let { hv -> builder.putData("hv", hv, Meta.empty()) } + } + return builder.build() + } + + /** + * Convert numass set to uniform node which consists of points + * + * @param set + * @return + */ + fun pointsToNode(set: NumassSet): DataNode { + return setToNode(set).checked(NumassPoint::class.java) + } + +} + +fun getFSS(context: Context, meta: Meta): FSS? { + return if (meta.getBoolean("useFSS", true)) { + val fssBinary: Binary? = meta.optString("fssFile") + .map { fssFile -> context.getFile(fssFile).binary } + .orElse(context.getResource("data/FS.txt")) + fssBinary?.let { FSS(it.stream) } ?: throw RuntimeException("Could not load FSS file") + } else { + null + } +} + + +/** + * Evaluate groovy expression using numass point as parameter + * + * @param expression + * @param values + * @return + */ +fun pointExpression(expression: String, values: Values): Double { + val exprParams = HashMap() + //Adding all point values to expression parameters + values.names.forEach { name -> exprParams[name] = values.getValue(name).value } + //Adding aliases for commonly used parameters + exprParams["T"] = values.getDouble("length") + exprParams["U"] = values.getDouble("voltage") + exprParams["time"] = values.optTime("timestamp").orElse(Instant.EPOCH).epochSecond + + return ExpressionUtils.function(expression, exprParams) +} + +/** + * Add set markers to time chart + */ +fun JFreeChartFrame.addSetMarkers(sets: Collection) { + val jfcPlot = chart.xyPlot + val paint = Color(0.0f, 0.0f, 1.0f, 0.1f) + sets.stream().forEach { set -> + val start = set.startTime; + val stop = set.meta.optValue("end_time").map { it.time } + .orElse(start.plusSeconds(300)) + .minusSeconds(60) + val marker = IntervalMarker(start.toEpochMilli().toDouble(), stop.toEpochMilli().toDouble(), paint) + marker.label = set.name + marker.labelFont = Font("Verdana", Font.BOLD, 20); + marker.labelOffset = RectangleInsets(30.0, 30.0, 30.0, 30.0) + Platform.runLater { jfcPlot.addDomainMarker(marker) } + } +} + +/** + * Subtract one U spectrum from the other one + */ +fun subtractSpectrum(merge: Table, empty: Table, logger: Logger? = null): Table { + val builder = ListTable.Builder(merge.format) + merge.rows.forEach { point -> + val pointBuilder = ValueMap.Builder(point) + val referencePoint = empty.rows + .filter { p -> Math.abs(p.getDouble(NumassPoint.HV_KEY) - point.getDouble(NumassPoint.HV_KEY)) < 0.1 } + .findFirst() + if (referencePoint.isPresent) { + pointBuilder.putValue( + NumassAnalyzer.COUNT_RATE_KEY, + Math.max( + 0.0, + point.getDouble(NumassAnalyzer.COUNT_RATE_KEY) - referencePoint.get().getDouble(NumassAnalyzer.COUNT_RATE_KEY) + ) + ) + pointBuilder.putValue( + NumassAnalyzer.COUNT_RATE_ERROR_KEY, + Math.sqrt( + Math.pow( + point.getDouble(NumassAnalyzer.COUNT_RATE_ERROR_KEY), + 2.0 + ) + Math.pow(referencePoint.get().getDouble(NumassAnalyzer.COUNT_RATE_ERROR_KEY), 2.0) + ) + ) + } else { + logger?.warn("No reference point found for voltage = {}", point.getDouble(NumassPoint.HV_KEY)) + } + builder.row(pointBuilder.build()) + } + + return builder.build() +} + +fun Values.unbox(): Map { + val res = HashMap() + for (field in this.names) { + val value = this.getValue(field) + res[field] = value.value + } + return res +} + +//fun FitResult.display(context: Context, stage: String = "fit") { +// val model = optModel(context).get() as XYModel +// +// val adapter = model.adapter +// +// val frame = PlotUtils.getPlotManager(context) +// .getPlotFrame(stage, "plot", Meta.empty()) +// +// val func = { x: Double -> model.spectrum.value(x, parameters) } +// +// val fit = XYFunctionPlot("fit", function = func) +// fit.density = 100 +// // ensuring all data points are calculated explicitly +// data.rows.map { dp -> Adapters.getXValue(adapter, dp).doubleValue() }.sorted().forEach { fit.calculateIn(it) } +// +// frame.add(fit) +// +// frame.add(DataPlot.plot("data", adapter, data)) +// +// val residualsFrame = PlotUtils.getPlotManager(context) +// .getPlotFrame(stage, "residuals", Meta.empty()) +// +// val residual = DataPlot("residuals"); +// +// data.rows.forEach { +// val x = Adapters.getXValue(adapter, it).doubleValue() +// val y = Adapters.getYValue(adapter, it).doubleValue() +// val err = Adapters.optYError(adapter, it).orElse(1.0) +// residual += Adapters.buildXYDataPoint(x, (y - func(x)) / err, 1.0) +// } +// +// residualsFrame.add(residual) +// +//} diff --git a/numass-main/src/main/kotlin/inr/numass/WorkspaceTest.kt b/numass-main/src/main/kotlin/inr/numass/WorkspaceTest.kt new file mode 100644 index 00000000..7124d093 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/WorkspaceTest.kt @@ -0,0 +1,32 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package inr.numass + +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.workspace.BasicWorkspace +import inr.numass.data.storage.NumassDataFactory + +/** + * + * @author Alexander Nozik + */ +object WorkspaceTest { + + /** + * @param args the command line arguments + */ + @JvmStatic + fun main(args: Array) { + + val storagepath = "D:\\Work\\Numass\\data\\" + + val workspace = BasicWorkspace.builder().apply { + this.context = Numass.buildContext() + data("", NumassDataFactory(), MetaBuilder("storage").putValue("path", storagepath)) + }.build() + } + +} diff --git a/numass-main/src/main/kotlin/inr/numass/actions/AnalyzeDataAction.kt b/numass-main/src/main/kotlin/inr/numass/actions/AnalyzeDataAction.kt new file mode 100644 index 00000000..cb91e3ee --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/actions/AnalyzeDataAction.kt @@ -0,0 +1,36 @@ +package inr.numass.actions + +import hep.dataforge.actions.OneToOneAction +import hep.dataforge.context.Context +import hep.dataforge.description.TypedActionDef +import hep.dataforge.description.ValueDef +import hep.dataforge.description.ValueDefs +import hep.dataforge.meta.Laminate +import hep.dataforge.tables.Table +import hep.dataforge.values.ValueType.NUMBER +import hep.dataforge.values.ValueType.STRING +import inr.numass.NumassUtils +import inr.numass.data.analyzers.NumassAnalyzer +import inr.numass.data.api.NumassSet +import inr.numass.data.analyzers.SmartAnalyzer + +/** + * The action performs the readout of data and collection of count rate into a table + * Created by darksnake on 11.07.2017. + */ +@TypedActionDef(name = "numass.analyze", inputType = NumassSet::class, outputType = Table::class) +@ValueDefs( + ValueDef(key = "window.lo", type = arrayOf(NUMBER, STRING), def = "0", info = "Lower bound for window"), + ValueDef(key = "window.up", type = arrayOf(NUMBER, STRING), def = "10000", info = "Upper bound for window") +) +object AnalyzeDataAction : OneToOneAction("numass.analyze", NumassSet::class.java, Table::class.java) { + override fun execute(context: Context, name: String, input: NumassSet, inputMeta: Laminate): Table { + //TODO add processor here + val analyzer: NumassAnalyzer = SmartAnalyzer() + val res = analyzer.analyzeSet(input, inputMeta) + + render(context, name, NumassUtils.wrap(res, inputMeta)) +// output(context, name) { stream -> NumassUtils.write(stream, inputMeta, res) } + return res + } +} diff --git a/numass-main/src/main/kotlin/inr/numass/actions/MergeDataAction.kt b/numass-main/src/main/kotlin/inr/numass/actions/MergeDataAction.kt new file mode 100644 index 00000000..bdbca993 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/actions/MergeDataAction.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.actions + +import hep.dataforge.actions.GroupBuilder +import hep.dataforge.actions.ManyToOneAction +import hep.dataforge.context.Context +import hep.dataforge.data.DataNode +import hep.dataforge.description.NodeDef +import hep.dataforge.description.TypedActionDef +import hep.dataforge.io.render +import hep.dataforge.meta.Laminate +import hep.dataforge.meta.Meta +import hep.dataforge.tables.ListTable +import hep.dataforge.tables.MetaTableFormat +import hep.dataforge.tables.Table +import hep.dataforge.tables.Tables +import hep.dataforge.values.ValueMap +import hep.dataforge.values.Values +import inr.numass.data.analyzers.NumassAnalyzer +import inr.numass.data.api.NumassPoint +import java.util.* + +/** + * @author Darksnake + */ +@TypedActionDef(name = "numass.merge", inputType = Table::class, outputType = Table::class, info = "Merge different numass data files into one.") +@NodeDef(key = "grouping", info = "The definition of grouping rule for this merge", descriptor = "method::hep.dataforge.actions.GroupBuilder.byMeta") +object MergeDataAction : ManyToOneAction("numass.merge", Table::class.java, Table::class.java) { + + private val parnames = arrayOf(NumassPoint.HV_KEY, NumassPoint.LENGTH_KEY, NumassAnalyzer.COUNT_KEY, NumassAnalyzer.COUNT_RATE_KEY, NumassAnalyzer.COUNT_RATE_ERROR_KEY) + + override fun buildGroups(context: Context, input: DataNode
, actionMeta: Meta): List> { + val meta = inputMeta(context, input.meta, actionMeta) + return if (meta.hasValue("grouping.byValue")) { + super.buildGroups(context, input, actionMeta) + } else { + GroupBuilder.byValue(MERGE_NAME, meta.getString(MERGE_NAME, input.name)).group(input) + } + } + + override fun execute(context: Context, nodeName: String, data: Map, meta: Laminate): Table { + val res = mergeDataSets(data.values) + return ListTable(res.format, Tables.sort(res, NumassPoint.HV_KEY, true).toList()) + } + + override fun afterGroup(context: Context, groupName: String, outputMeta: Meta, output: Table) { + context.output.render(output, name = groupName, stage = name, meta = outputMeta) + super.afterGroup(context, groupName, outputMeta, output) + } + + private fun mergeDataPoints(dp1: Values?, dp2: Values?): Values? { + if (dp1 == null) { + return dp2 + } + if (dp2 == null) { + return dp1 + } + + val voltage = dp1.getValue(NumassPoint.HV_KEY).double + val t1 = dp1.getValue(NumassPoint.LENGTH_KEY).double + val t2 = dp2.getValue(NumassPoint.LENGTH_KEY).double + val time = t1 + t2 + + val total = (dp1.getValue(NumassAnalyzer.COUNT_KEY).int + dp2.getValue(NumassAnalyzer.COUNT_KEY).int).toLong() + + val cr1 = dp1.getValue(NumassAnalyzer.COUNT_RATE_KEY).double + val cr2 = dp2.getValue(NumassAnalyzer.COUNT_RATE_KEY).double + + val cr = (cr1 * t1 + cr2 * t2) / (t1 + t2) + + val err1 = dp1.getDouble(NumassAnalyzer.COUNT_RATE_ERROR_KEY) + val err2 = dp2.getDouble(NumassAnalyzer.COUNT_RATE_ERROR_KEY) + + // абÑолютные ошибки ÑкладываютÑÑ ÐºÐ²Ð°Ð´Ñ€Ð°Ñ‚Ð¸Ñ‡Ð½Ð¾ + val crErr = Math.sqrt(err1 * err1 * t1 * t1 + err2 * err2 * t2 * t2) / time + + return ValueMap.of(parnames, voltage, time, total, cr, crErr) + } + + private fun mergeDataSets(ds: Collection
): Table { + //Сливаем вÑе точки в один набор данных + val points = LinkedHashMap>() + for (d in ds) { + if (!d.format.names.contains(*parnames)) { + throw IllegalArgumentException() + } + for (dp in d) { + val uset = dp.getValue(NumassPoint.HV_KEY).double + if (!points.containsKey(uset)) { + points.put(uset, ArrayList()) + } + points[uset]?.add(dp) + } + } + + val res = ArrayList() + + points.entries.stream().map { entry -> + var curPoint: Values? = null + for (newPoint in entry.value) { + curPoint = mergeDataPoints(curPoint, newPoint) + } + curPoint + }.forEach { res.add(it) } + + return ListTable(MetaTableFormat.forNames(*parnames), res) + + } + + const val MERGE_NAME = "mergeName" + +} diff --git a/numass-main/src/main/kotlin/inr/numass/actions/PlotFitResultAction.kt b/numass-main/src/main/kotlin/inr/numass/actions/PlotFitResultAction.kt new file mode 100644 index 00000000..e1050e54 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/actions/PlotFitResultAction.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 inr.numass.actions + +import hep.dataforge.actions.OneToOneAction +import hep.dataforge.context.Context +import hep.dataforge.description.NodeDef +import hep.dataforge.description.TypedActionDef +import hep.dataforge.meta.Laminate +import hep.dataforge.meta.Meta +import hep.dataforge.plots.data.DataPlot +import hep.dataforge.plots.data.XYFunctionPlot +import hep.dataforge.plots.output.plot +import hep.dataforge.stat.fit.FitResult +import hep.dataforge.stat.models.XYModel +import hep.dataforge.tables.Adapters +import hep.dataforge.tables.ValuesAdapter +import java.util.stream.StreamSupport + +/** + * @author darksnake + */ +@TypedActionDef(name = "plotFit", info = "Plot fit result", inputType = FitResult::class, outputType = FitResult::class) +@NodeDef(key = "adapter", info = "adapter for DataSet being fitted. By default is taken from model.") +object PlotFitResultAction : OneToOneAction("plotFit", FitResult::class.java, FitResult::class.java) { + + override fun execute(context: Context, name: String, input: FitResult, metaData: Laminate): FitResult { + + val state = input.optState().orElseThrow { UnsupportedOperationException("Can't work with fit result not containing state, sorry! Will fix it later") } + + val data = input.data + if (state.model !is XYModel) { + context.history.getChronicle(name).reportError("The fit model should be instance of XYModel for this action. Action failed!") + return input + } + val model = state.model as XYModel + + val adapter: ValuesAdapter + if (metaData.hasMeta("adapter")) { + adapter = Adapters.buildAdapter(metaData.getMeta("adapter")) + } else if (state.model is XYModel) { + adapter = model.adapter + } else { + throw RuntimeException("No adapter defined for data interpretation") + } + +// val frame = PlotOutputKt.getPlotFrame(context, getName(), name, metaData.getMeta("frame", Meta.empty())) + + + val fit = XYFunctionPlot("fit", Meta.empty()) { x: Double -> model.spectrum.value(x, input.parameters) } + fit.density = 100 + fit.smoothing = true + // ensuring all data points are calculated explicitly + StreamSupport.stream(data.spliterator(), false) + .map { dp -> Adapters.getXValue(adapter, dp).double }.sorted().forEach{ fit.calculateIn(it) } + + context.plot(listOf(fit,DataPlot.plot("data", data, adapter)), name, this.name) + + return input + } + +} diff --git a/numass-main/src/main/kotlin/inr/numass/actions/SummaryAction.kt b/numass-main/src/main/kotlin/inr/numass/actions/SummaryAction.kt new file mode 100644 index 00000000..8972c2a3 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/actions/SummaryAction.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.actions + +import hep.dataforge.actions.GroupBuilder +import hep.dataforge.actions.ManyToOneAction +import hep.dataforge.context.Context +import hep.dataforge.data.DataNode +import hep.dataforge.description.TypedActionDef +import hep.dataforge.description.ValueDef +import hep.dataforge.meta.Laminate +import hep.dataforge.meta.Meta +import hep.dataforge.stat.fit.FitState +import hep.dataforge.tables.ListTable +import hep.dataforge.tables.MetaTableFormat +import hep.dataforge.tables.Table +import hep.dataforge.values.Value +import hep.dataforge.values.ValueMap +import hep.dataforge.values.asValue +import inr.numass.NumassUtils +import java.util.* +import kotlin.collections.ArrayList + +/** + * @author Darksnake + */ +@TypedActionDef(name = "summary", inputType = FitState::class, outputType = Table::class, info = "Generate summary for fit results of different datasets.") +@ValueDef(key = "parnames", multiple = true, required = true, info = "List of names of parameters for which summary should be done") +object SummaryAction : ManyToOneAction("summary", FitState::class.java,Table::class.java) { + + const val SUMMARY_NAME = "sumName" + + override fun buildGroups(context: Context, input: DataNode, actionMeta: Meta): List> { + val meta = inputMeta(context, input.meta, actionMeta) + val groups: List> + if (meta.hasValue("grouping.byValue")) { + groups = super.buildGroups(context, input, actionMeta) + } else { + groups = GroupBuilder.byValue(SUMMARY_NAME, meta.getString(SUMMARY_NAME, "summary")).group(input) + } + return groups + } + + override fun execute(context: Context, nodeName: String, input: Map, meta: Laminate): Table { + val parNames: Array + if (meta.hasValue("parnames")) { + parNames = meta.getStringArray("parnames") + } else { + throw RuntimeException("Infering parnames not suppoerted") + } + val names = ArrayList() + names.add("file") + parNames.forEach { + names.add(it) + names.add("${it}_Err") + } + names.add("chi2") + + val res = ListTable.Builder(MetaTableFormat.forNames(names)) + + val weights = DoubleArray(parNames.size) + Arrays.fill(weights, 0.0) + val av = DoubleArray(parNames.size) + Arrays.fill(av, 0.0) + + input.forEach { key: String, value: FitState -> + val values = ArrayList() + values.add(key.asValue()) + parNames.forEachIndexed { i, it -> + val `val` = Value.of(value.parameters.getDouble(it)) + values.add(`val`) + val err = Value.of(value.parameters.getError(it)) + values.add(err) + val weight = 1.0 / err.double / err.double + av[i] += `val`.double * weight + weights[i] += weight + } + values[values.size - 1] = Value.of(value.chi2) + val point = ValueMap.of(names.toTypedArray(), values) + res.row(point) + } + + val averageValues = arrayOfNulls(names.size) + averageValues[0] = "average".asValue() + averageValues[averageValues.size - 1] = Value.of(0) + + for (i in parNames.indices) { + averageValues[2 * i + 1] = Value.of(av[i] / weights[i]) + averageValues[2 * i + 2] = Value.of(1 / Math.sqrt(weights[i])) + } + + res.row(ValueMap.of(names.toTypedArray(), averageValues)) + + return res.build() + } + + override fun afterGroup(context: Context, groupName: String, outputMeta: Meta, output: Table) { + context.output[name, groupName].render(NumassUtils.wrap(output, outputMeta)) + super.afterGroup(context, groupName, outputMeta, output) + } + +} diff --git a/numass-main/src/main/kotlin/inr/numass/actions/TimeAnalyzerAction.kt b/numass-main/src/main/kotlin/inr/numass/actions/TimeAnalyzerAction.kt new file mode 100644 index 00000000..5cd8aba8 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/actions/TimeAnalyzerAction.kt @@ -0,0 +1,164 @@ +package inr.numass.actions + +import hep.dataforge.actions.OneToOneAction +import hep.dataforge.configure +import hep.dataforge.context.Context +import hep.dataforge.description.* +import hep.dataforge.maths.histogram.UnivariateHistogram +import hep.dataforge.meta.Laminate +import hep.dataforge.plots.data.DataPlot +import hep.dataforge.plots.data.XYFunctionPlot +import hep.dataforge.plots.output.plot +import hep.dataforge.tables.Adapters +import hep.dataforge.tables.Table +import hep.dataforge.values.ValueType +import inr.numass.data.analyzers.NumassAnalyzer +import inr.numass.data.analyzers.TimeAnalyzer +import inr.numass.data.analyzers.TimeAnalyzer.Companion.T0_KEY +import inr.numass.data.api.NumassPoint +import kotlin.streams.asStream + +/** + * Plot time analysis graphics + */ +@ValueDefs( + ValueDef(key = "normalize", type = arrayOf(ValueType.BOOLEAN), def = "false", info = "Normalize t0 dependencies"), + ValueDef(key = "t0", type = arrayOf(ValueType.NUMBER), def = "30e3", info = "The default t0 in nanoseconds"), + ValueDef(key = "window.lo", type = arrayOf(ValueType.NUMBER), def = "0", info = "Lower boundary for amplitude window"), + ValueDef(key = "window.up", type = arrayOf(ValueType.NUMBER), def = "10000", info = "Upper boundary for amplitude window"), + ValueDef(key = "binNum", type = arrayOf(ValueType.NUMBER), def = "1000", info = "Number of bins for time histogram"), + ValueDef(key = "binSize", type = arrayOf(ValueType.NUMBER), info = "Size of bin for time histogram. By default is defined automatically") +) +@NodeDefs( + NodeDef(key = "histogram", info = "Configuration for histogram plots"), + NodeDef(key = "plot", info = "Configuration for stat plots") +) +@TypedActionDef(name = "timeSpectrum", inputType = NumassPoint::class, outputType = Table::class) +object TimeAnalyzerAction : OneToOneAction("timeSpectrum",NumassPoint::class.java,Table::class.java) { + private val analyzer = TimeAnalyzer(); + + override fun execute(context: Context, name: String, input: NumassPoint, inputMeta: Laminate): Table { + val log = getLog(context, name); + + val analyzerMeta = inputMeta.getMetaOrEmpty("analyzer") + + val initialEstimate = analyzer.analyze(input, analyzerMeta) + val cr = initialEstimate.getDouble("cr") + + log.report("The expected count rate for ${initialEstimate.getDouble(T0_KEY)} us delay is $cr") + + val binNum = inputMeta.getInt("binNum", 1000); + val binSize = inputMeta.getDouble("binSize", 1.0 / cr * 10 / binNum * 1e6) + + val histogram = UnivariateHistogram.buildUniform(0.0, binSize * binNum, binSize) + .fill(analyzer + .getEventsWithDelay(input, analyzerMeta) + .asStream() + .mapToDouble { it.second.toDouble() / 1000.0 } + ).asTable() + + //.histogram(input, loChannel, upChannel, binSize, binNum).asTable(); + log.report("Finished histogram calculation..."); + + if (inputMeta.getBoolean("plotHist", true)) { + + val histogramPlot = DataPlot(name, adapter = Adapters.buildXYAdapter("x", "count")) + .configure { + "showLine" to true + "showSymbol" to false + "showErrors" to false + "connectionType" to "step" + }.apply { + configure(inputMeta.getMetaOrEmpty("histogram")) + }.fillData(histogram) + + + val functionPlot = XYFunctionPlot.plot(name + "_theory", 0.0, binSize * binNum) { + cr / 1e6 * initialEstimate.getInt(NumassAnalyzer.COUNT_KEY) * binSize * Math.exp(-it * cr / 1e6) + } + + context.plot(listOf(histogramPlot, functionPlot), name = "histogram", stage = this.name) { + "xAxis" to { + "title" to "delay" + "units" to "us" + } + "yAxis" to { + "type" to "log" + } + } + } + + if (inputMeta.getBoolean("plotStat", true)) { + + val statPlot = DataPlot(name, adapter = Adapters.DEFAULT_XYERR_ADAPTER).configure { + "showLine" to true + "thickness" to 4 + "title" to "${name}_${input.voltage}" + update(inputMeta.getMetaOrEmpty("plot")) + } + + val errorPlot = DataPlot(name).configure{ + "showLine" to true + "showErrors" to false + "showSymbol" to false + "thickness" to 4 + "title" to "${name}_${input.voltage}" + } + + context.plot(statPlot, name = "count rate", stage = this.name) { + "xAxis" to { + "title" to "delay" + "units" to "us" + } + "yAxis" to { + "title" to "Reconstructed count rate" + } + } + + context.plot(errorPlot, name = "error", stage = this.name){ + "xAxis" to { + "title" to "delay" + "units" to "us" + } + "yAxis" to { + "title" to "Statistical error" + } + } + + val minT0 = inputMeta.getDouble("t0.min", 0.0) + val maxT0 = inputMeta.getDouble("t0.max", 1e9 / cr) + val steps = inputMeta.getInt("t0.steps", 100) + val t0Step = inputMeta.getDouble("t0.step", (maxT0-minT0)/(steps - 1)) + + + val norm = if (inputMeta.getBoolean("normalize", false)) { + cr + } else { + 1.0 + } + + (0..steps).map { minT0 + t0Step * it }.map { t -> + val result = analyzer.analyze(input, analyzerMeta.builder.setValue("t0", t)) + + if (Thread.currentThread().isInterrupted) { + throw InterruptedException() + } + statPlot.append( + Adapters.buildXYDataPoint( + t / 1000.0, + result.getDouble("cr") / norm, + result.getDouble(NumassAnalyzer.COUNT_RATE_ERROR_KEY) / norm + ) + ) + + errorPlot.append( + Adapters.buildXYDataPoint(t/1000.0, result.getDouble(NumassAnalyzer.COUNT_RATE_ERROR_KEY) / norm) + ) + } + + + } + + return histogram; + } +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/actions/TimeSpectrumAction.kt b/numass-main/src/main/kotlin/inr/numass/actions/TimeSpectrumAction.kt new file mode 100644 index 00000000..0c202494 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/actions/TimeSpectrumAction.kt @@ -0,0 +1,136 @@ +package inr.numass.actions + +import hep.dataforge.actions.OneToOneAction +import hep.dataforge.configure +import hep.dataforge.context.Context +import hep.dataforge.description.* +import hep.dataforge.maths.histogram.UnivariateHistogram +import hep.dataforge.meta.Laminate +import hep.dataforge.meta.buildMeta +import hep.dataforge.plots.data.DataPlot +import hep.dataforge.plots.output.plot +import hep.dataforge.tables.Adapters +import hep.dataforge.tables.Table +import hep.dataforge.values.ValueType +import inr.numass.data.analyzers.NumassAnalyzer +import inr.numass.data.analyzers.TimeAnalyzer +import inr.numass.data.api.NumassPoint +import kotlin.streams.asStream + +/** + * Plot time analysis graphics + */ +@ValueDefs( + ValueDef(key = "normalize", type = arrayOf(ValueType.BOOLEAN), def = "true", info = "Normalize t0 dependencies"), + ValueDef(key = "t0", type = arrayOf(ValueType.NUMBER), def = "30e3", info = "The default t0 in nanoseconds"), + ValueDef(key = "window.lo", type = arrayOf(ValueType.NUMBER), def = "500", info = "Lower boundary for amplitude window"), + ValueDef(key = "window.up", type = arrayOf(ValueType.NUMBER), def = "10000", info = "Upper boundary for amplitude window"), + ValueDef(key = "binNum", type = arrayOf(ValueType.NUMBER), def = "1000", info = "Number of bins for time histogram"), + ValueDef(key = "binSize", type = arrayOf(ValueType.NUMBER), info = "Size of bin for time histogram. By default is defined automatically") +) +@NodeDefs( + NodeDef(key = "histogram", info = "Configuration for histogram plots"), + NodeDef(key = "plot", info = "Configuration for stat plots") +) +@TypedActionDef(name = "numass.timeSpectrum", inputType = NumassPoint::class, outputType = Table::class) +object TimeSpectrumAction : OneToOneAction( "numass.timeSpectrum", NumassPoint::class.java, Table::class.java) { + private val analyzer = TimeAnalyzer(); + + override fun execute(context: Context, name: String, input: NumassPoint, inputMeta: Laminate): Table { + val log = getLog(context, name); + + + val t0 = inputMeta.getDouble("t0", 30e3); + val loChannel = inputMeta.getInt("window.lo", 500); + val upChannel = inputMeta.getInt("window.up", 10000); + + + val trueCR = analyzer.analyze(input, buildMeta { + "t0" to t0 + "window.lo" to loChannel + "window.up" to upChannel + }).getDouble("cr") + + log.report("The expected count rate for 30 us delay is $trueCR") + + + val binNum = inputMeta.getInt("binNum", 1000); + val binSize = inputMeta.getDouble("binSize", 1.0 / trueCR * 10 / binNum * 1e6) + + val histogram = UnivariateHistogram.buildUniform(0.0, binSize * binNum, binSize) + .fill(analyzer + .getEventsWithDelay(input, inputMeta) + .asStream() + .mapToDouble { it.second / 1000.0 } + ).asTable() + + //.histogram(input, loChannel, upChannel, binSize, binNum).asTable(); + log.report("Finished histogram calculation..."); + + if (inputMeta.getBoolean("plotHist", true)) { + + val histogramPlot = DataPlot(name) + .configure { + "showLine" to true + "showSymbol" to false + "showErrors" to false + "connectionType" to "step" + node("@adapter") { + "y.value" to "count" + } + }.apply { configure(inputMeta.getMetaOrEmpty("histogram")) } + .fillData(histogram) + + + context.plot(histogramPlot, name, "histogram") { + "xAxis" to { + "title" to "delay" + "units" to "us" + } + "yAxis" to { + "type" to "log" + } + } + } + + if (inputMeta.getBoolean("plotStat", true)) { + + val statPlot = DataPlot(name).configure { + "showLine" to true + "thickness" to 4 + "title" to "${name}_${input.voltage}" + }.apply { + configure(inputMeta.getMetaOrEmpty("plot")) + } + + context.plot(statPlot, name, "stat-method") + + (1..100).map { 1000 * it }.map { t -> + val result = analyzer.analyze(input, buildMeta { + "t0" to t + "window.lo" to loChannel + "window.up" to upChannel + }) + + + val norm = if (inputMeta.getBoolean("normalize", true)) { + trueCR + } else { + 1.0 + } + + statPlot.append( + Adapters.buildXYDataPoint( + t / 1000.0, + result.getDouble("cr") / norm, + result.getDouble(NumassAnalyzer.COUNT_RATE_ERROR_KEY) / norm + ) + ) + } + + + } + + return histogram; + } +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/actions/TransformDataAction.kt b/numass-main/src/main/kotlin/inr/numass/actions/TransformDataAction.kt new file mode 100644 index 00000000..638f1d1f --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/actions/TransformDataAction.kt @@ -0,0 +1,221 @@ +package inr.numass.actions + +import hep.dataforge.Named +import hep.dataforge.actions.OneToOneAction +import hep.dataforge.context.Context +import hep.dataforge.description.NodeDef +import hep.dataforge.description.TypedActionDef +import hep.dataforge.description.ValueDef +import hep.dataforge.description.ValueDefs +import hep.dataforge.isAnonymous +import hep.dataforge.meta.Laminate +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaUtils +import hep.dataforge.tables.* +import hep.dataforge.values.ValueType.NUMBER +import hep.dataforge.values.ValueType.STRING +import hep.dataforge.values.Values +import inr.numass.data.analyzers.NumassAnalyzer.Companion.COUNT_RATE_ERROR_KEY +import inr.numass.data.analyzers.NumassAnalyzer.Companion.COUNT_RATE_KEY +import inr.numass.data.analyzers.NumassAnalyzer.Companion.TIME_KEY +import inr.numass.pointExpression +import java.util.* +import kotlin.math.pow +import kotlin.math.sqrt + +/** + * Apply corrections and transformations to analyzed data + * Created by darksnake on 11.07.2017. + */ +@TypedActionDef(name = "numass.transform", inputType = Table::class, outputType = Table::class) +@ValueDefs( + ValueDef( + key = "correction", + info = "An expression to correct count number depending on potential `U`, point length `T` and point itself as `point`" + ), + ValueDef(key = "utransform", info = "Expression for voltage transformation. Uses U as input") +) +@NodeDef( + key = "correction", + multiple = true, + descriptor = "method::inr.numass.actions.TransformDataAction.makeCorrection" +) +object TransformDataAction : OneToOneAction("numass.transform", Table::class.java, Table::class.java) { + + override fun execute(context: Context, name: String, input: Table, meta: Laminate): Table { + + var table = ColumnTable.copy(input) + + val corrections = ArrayList() + + meta.optMeta("corrections").ifPresent { cors -> + MetaUtils.nodeStream(cors) + .filter { it.first.length == 1 } + .map { it.second } + .map { makeCorrection(it) } + .forEach { corrections.add(it) } + } + + if (meta.hasValue("correction")) { + val correction = meta.getString("correction") + corrections.add(object : Correction { + override val name: String = "" + + override fun corr(point: Values): Double { + return pointExpression(correction, point) + } + }) + } + + for (correction in corrections) { + //adding correction columns + if (!correction.isAnonymous) { + table = table.buildColumn(ColumnFormat.build(correction.name, NUMBER)) { correction.corr(this) } + if (correction.hasError()) { + table = table.buildColumn(ColumnFormat.build(correction.name + ".err", NUMBER)) { + correction.corrErr(this) + } + } + } + } + + + // adding original count rate and error columns + table = table.addColumn( + ListColumn( + ColumnFormat.build("$COUNT_RATE_KEY.orig", NUMBER), + table.getColumn(COUNT_RATE_KEY).stream() + ) + ) + table = table.addColumn( + ListColumn( + ColumnFormat.build("$COUNT_RATE_ERROR_KEY.orig", NUMBER), table + .getColumn(COUNT_RATE_ERROR_KEY).stream() + ) + ) + + val cr = ArrayList() + val crErr = ArrayList() + + table.rows.forEach { point -> + val correctionFactor = corrections.stream() + .mapToDouble { cor -> cor.corr(point) } + .reduce { d1, d2 -> d1 * d2 }.orElse(1.0) + val relativeCorrectionError = sqrt( + corrections.stream() + .mapToDouble { cor -> cor.relativeErr(point) } + .reduce { d1, d2 -> d1 * d1 + d2 * d2 }.orElse(0.0) + ) + val originalCR = point.getDouble(COUNT_RATE_KEY) + val originalCRErr = point.getDouble(COUNT_RATE_ERROR_KEY) + cr.add(originalCR * correctionFactor) + if (relativeCorrectionError == 0.0) { + crErr.add(originalCRErr * correctionFactor) + } else { + crErr.add(sqrt((originalCRErr / originalCR).pow(2.0) + relativeCorrectionError.pow(2.0)) * originalCR) + } + } + + //replacing cr column + val res = table.addColumn(ListColumn.build(table.getColumn(COUNT_RATE_KEY).format, cr.stream())) + .addColumn(ListColumn.build(table.getColumn(COUNT_RATE_ERROR_KEY).format, crErr.stream())) + .sort(TIME_KEY) + + + context.output[this@TransformDataAction.name, name].render(res, meta) + return res + } + + + @ValueDefs( + ValueDef(key = "value", type = arrayOf(NUMBER, STRING), info = "Value or function to multiply count rate"), + ValueDef(key = "err", type = arrayOf(NUMBER, STRING), info = "error of the value") + ) + private fun makeCorrection(corrMeta: Meta): Correction { + val name = corrMeta.getString("name", corrMeta.name) + return if (corrMeta.hasMeta("table")) { + val x = corrMeta.getValue("table.u").list.map { it.double } + val corr = corrMeta.getValue("table.corr").list.map { it.double } + TableCorrection(name, x, corr) + } else { + val expr = corrMeta.getString("value") + val errExpr = corrMeta.getString("err", "") + ExpressionCorrection(name, expr, errExpr) + } + } + + interface Correction : Named { + + /** + * correction coefficient + * + * @param point + * @return + */ + fun corr(point: Values): Double + + /** + * correction coefficient uncertainty + * + * @param point + * @return + */ + fun corrErr(point: Values): Double { + return 0.0 + } + + fun hasError(): Boolean { + return false + } + + fun relativeErr(point: Values): Double { + val corrErr = corrErr(point) + return if (corrErr == 0.0) { + 0.0 + } else { + corrErr / corr(point) + } + } + } + + class ExpressionCorrection(override val name: String, val expr: String, val errExpr: String) : Correction { + override fun corr(point: Values): Double { + return pointExpression(expr, point) + } + + override fun corrErr(point: Values): Double { + return if (errExpr.isEmpty()) { + 0.0 + } else { + pointExpression(errExpr, point) + } + } + + override fun hasError(): Boolean { + return errExpr.isNotEmpty() + } + } + + class TableCorrection( + override val name: String, + val x: List, + val y: List, + val yErr: List? = null + ) : Correction { + override fun corr(point: Values): Double { + val voltage = point.getDouble("voltage") + val index = x.indexOfFirst { it > voltage } + //TODO add interpolation + return if (index < 0) { + y.last() + } else { + y[index] + } + } +// +// override fun corrErr(point: Values): Double = 0.0 +// +// override fun hasError(): Boolean = yErr.isNullOrEmpty() + } + +} diff --git a/numass-main/src/main/kotlin/inr/numass/data/NumassGenerator.kt b/numass-main/src/main/kotlin/inr/numass/data/NumassGenerator.kt new file mode 100644 index 00000000..487dc4c3 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/data/NumassGenerator.kt @@ -0,0 +1,138 @@ +package inr.numass.data + +import hep.dataforge.maths.chain.Chain +import hep.dataforge.maths.chain.MarkovChain +import hep.dataforge.maths.chain.StatefulChain +import hep.dataforge.stat.defaultGenerator +import hep.dataforge.tables.Table +import inr.numass.data.analyzers.NumassAnalyzer.Companion.CHANNEL_KEY +import inr.numass.data.analyzers.NumassAnalyzer.Companion.COUNT_RATE_KEY +import inr.numass.data.api.NumassBlock +import inr.numass.data.api.OrphanNumassEvent +import inr.numass.data.api.SimpleBlock +import org.apache.commons.math3.distribution.EnumeratedRealDistribution +import org.apache.commons.math3.random.RandomGenerator +import java.time.Duration +import java.time.Instant + +private fun RandomGenerator.nextExp(mean: Double): Double { + return -mean * Math.log(1 - nextDouble()) +} + +private fun RandomGenerator.nextDeltaTime(cr: Double): Long { + return (nextExp(1.0 / cr) * 1e9).toLong() +} + +suspend fun Sequence.generateBlock(start: Instant, length: Long): NumassBlock { + return SimpleBlock.produce(start, Duration.ofNanos(length)) { + takeWhile { it.timeOffset < length }.toList() + } +} + +private class MergingState(private val chains: List>) { + suspend fun poll(): OrphanNumassEvent { + val next = chains.minBy { it.value.timeOffset } ?: chains.first() + val res = next.value + next.next() + return res + } + +} + +/** + * Merge event chains in ascending time order + */ +fun List>.merge(): Chain { + return StatefulChain(MergingState(this), OrphanNumassEvent(0, 0)) { + poll() + } +} + +/** + * Apply dead time based on event that caused it + */ +fun Chain.withDeadTime(deadTime: (OrphanNumassEvent) -> Long): Chain { + return MarkovChain(this.value) { + val start = this.value + val dt = deadTime(start) + do { + val next = next() + } while (next.timeOffset - start.timeOffset < dt) + this.value + } +} + +object NumassGenerator { + + val defaultAmplitudeGenerator: RandomGenerator.(OrphanNumassEvent?, Long) -> Short = { _, _ -> ((nextDouble() + 2.0) * 100).toShort() } + + /** + * Generate an event chain with fixed count rate + * @param cr = count rate in Hz + * @param rnd = random number generator + * @param amp amplitude generator for the chain. The receiver is rng, first argument is the previous event and second argument + * is the delay between the next event. The result is the amplitude in channels + */ + fun generateEvents( + cr: Double, + rnd: RandomGenerator = defaultGenerator, + amp: RandomGenerator.(OrphanNumassEvent?, Long) -> Short = defaultAmplitudeGenerator): Chain { + return MarkovChain(OrphanNumassEvent(rnd.amp(null, 0), 0)) { event -> + val deltaT = rnd.nextDeltaTime(cr) + OrphanNumassEvent(rnd.amp(event, deltaT), event.timeOffset + deltaT) + } + } + + fun mergeEventChains(vararg chains: Chain): Chain { + return listOf(*chains).merge() + } + + + private data class BunchState(var bunchStart: Long = 0, var bunchEnd: Long = 0) + + /** + * The chain of bunched events + * @param cr count rate of events inside bunch + * @param bunchRate number of bunches per second + * @param bunchLength the length of bunch + */ + fun generateBunches( + cr: Double, + bunchRate: Double, + bunchLength: Double, + rnd: RandomGenerator = defaultGenerator, + amp: RandomGenerator.(OrphanNumassEvent?, Long) -> Short = defaultAmplitudeGenerator + ): Chain { + return StatefulChain( + BunchState(0, 0), + OrphanNumassEvent(rnd.amp(null, 0), 0)) { event -> + if (event.timeOffset >= bunchEnd) { + bunchStart = bunchEnd + rnd.nextDeltaTime(bunchRate) + bunchEnd = bunchStart + (bunchLength * 1e9).toLong() + OrphanNumassEvent(rnd.amp(null, 0), bunchStart) + } else { + val deltaT = rnd.nextDeltaTime(cr) + OrphanNumassEvent(rnd.amp(event, deltaT), event.timeOffset + deltaT) + } + } + } + + /** + * Generate a chain using provided spectrum for amplitudes + */ + fun generateEvents( + cr: Double, + rnd: RandomGenerator = defaultGenerator, + spectrum: Table): Chain { + + val channels = DoubleArray(spectrum.size()) + val values = DoubleArray(spectrum.size()) + for (i in 0 until spectrum.size()) { + channels[i] = spectrum.get(CHANNEL_KEY, i).double + values[i] = spectrum.get(COUNT_RATE_KEY, i).double + } + val distribution = EnumeratedRealDistribution(channels, values) + + return generateEvents(cr, rnd) { _, _ -> distribution.sample().toShort() } + } +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/data/PileUpSimulator.kt b/numass-main/src/main/kotlin/inr/numass/data/PileUpSimulator.kt new file mode 100644 index 00000000..b0d924ab --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/data/PileUpSimulator.kt @@ -0,0 +1,177 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package inr.numass.data + +import hep.dataforge.maths.chain.Chain +import inr.numass.data.api.NumassBlock +import inr.numass.data.api.OrphanNumassEvent +import inr.numass.data.api.SimpleBlock +import kotlinx.coroutines.runBlocking +import org.apache.commons.math3.random.RandomGenerator +import java.lang.Math.max +import java.time.Duration +import java.time.Instant +import java.util.* +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicReference + +/** + * @author [Alexander Nozik](mailto:altavir@gmail.com) + */ +class PileUpSimulator { + private val pointLength: Long + private val rnd: RandomGenerator + private val generated = ArrayList() + private val pileup = ArrayList() + private val registered = ArrayList() + private var generator: Chain + //private double uSet = 0; + private val doublePileup = AtomicInteger(0) + + + constructor(length: Long, rnd: RandomGenerator, countRate: Double) { + this.rnd = rnd + generator = NumassGenerator.generateEvents(countRate, rnd) + this.pointLength = length + } + + constructor(pointLength: Long, rnd: RandomGenerator, generator: Chain) { + this.rnd = rnd + this.pointLength = pointLength + this.generator = generator + } + +// fun withUset(uset: Double): PileUpSimulator { +// this.uSet = uset +// return this +// } + + fun generated(): NumassBlock { + return SimpleBlock(Instant.EPOCH, Duration.ofNanos(pointLength), generated) + } + + fun registered(): NumassBlock { + return SimpleBlock(Instant.EPOCH, Duration.ofNanos(pointLength), registered) + } + + fun pileup(): NumassBlock { + return SimpleBlock(Instant.EPOCH, Duration.ofNanos(pointLength), pileup) + } + + /** + * The amplitude for pileup event + * + * @param x + * @return + */ + private fun pileupChannel(x: Double, prevChanel: Short, nextChanel: Short): Short { + assert(x > 0) + //ÑмпиричеÑÐºÐ°Ñ Ñ„Ð¾Ñ€Ð¼ÑƒÐ»Ð° Ð´Ð»Ñ ÐºÐ°Ð½Ð°Ð»Ð° + val coef = max(0.0, 0.99078 + 0.05098 * x - 0.45775 * x * x + 0.10962 * x * x * x) + if (coef < 0 || coef > 1) { + throw Error() + } + + return (prevChanel + coef * nextChanel).toShort() + } + + /** + * pileup probability + * + * @param delay + * @return + */ + private fun pileup(delay: Double): Boolean { + val prob = 1.0 / (1.0 + Math.pow(delay / (2.5 + 0.2), 42.96)) + return random(prob) + } + + /** + * Probability for next event to register + * + * @param delay + * @return + */ + private fun nextEventRegistered(prevChanel: Short, delay: Double): Boolean { + val average = 6.76102 - 4.31897E-4 * prevChanel + 7.88429E-8 * prevChanel.toDouble() * prevChanel.toDouble() + 0.2 + val prob = 1.0 - 1.0 / (1.0 + Math.pow(delay / average, 75.91)) + return random(prob) + } + + private fun random(prob: Double): Boolean { + return rnd.nextDouble() <= prob + } + + @Synchronized + fun generate() { + var next: OrphanNumassEvent + //var lastRegisteredTime = 0.0 // Time of DAQ closing + val last = AtomicReference(OrphanNumassEvent(0, 0)) + + //flag that shows that previous event was pileup + var pileupFlag = false + runBlocking { + next = generator.next() + while (next.timeOffset <= pointLength) { + generated.add(next) + //not counting double pileups + if (generated.size > 1) { + val delay = (next.timeOffset - last.get().timeOffset) / us //time between events in microseconds + if (nextEventRegistered(next.amplitude, delay)) { + //just register new event + registered.add(next) + last.set(next) + pileupFlag = false + } else if (pileup(delay)) { + if (pileupFlag) { + //increase double pileup stack + doublePileup.incrementAndGet() + } else { + //pileup event + val newChannel = pileupChannel(delay, last.get().amplitude, next.amplitude) + val newEvent = OrphanNumassEvent(newChannel, next.timeOffset) + //replace already registered event by event with new channel + registered.removeAt(registered.size - 1) + registered.add(newEvent) + pileup.add(newEvent) + //do not change DAQ close time + pileupFlag = true // up the flag to avoid secondary pileup + } + } else { + // second event not registered, DAQ closed + pileupFlag = false + } + } else { + //register first event + registered.add(next) + last.set(next) + } + next = generator.next() + } + } + } + + companion object { + private const val us = 1e-6//microsecond + } + +} diff --git a/numass-main/src/main/kotlin/inr/numass/data/Visualization.kt b/numass-main/src/main/kotlin/inr/numass/data/Visualization.kt new file mode 100644 index 00000000..4c35a757 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/data/Visualization.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 inr.numass.data + +import hep.dataforge.configure +import hep.dataforge.context.Context +import hep.dataforge.meta.KMetaBuilder +import hep.dataforge.meta.buildMeta +import hep.dataforge.nullable +import hep.dataforge.plots.PlotFrame +import hep.dataforge.plots.PlotGroup +import hep.dataforge.plots.data.DataPlot +import hep.dataforge.plots.output.plotFrame +import hep.dataforge.tables.Adapters +import inr.numass.data.analyzers.NumassAnalyzer +import inr.numass.data.analyzers.SmartAnalyzer +import inr.numass.data.analyzers.withBinning +import inr.numass.data.api.NumassBlock + +fun PlotGroup.plotAmplitudeSpectrum( + numassBlock: NumassBlock, + plotName: String = "spectrum", + analyzer: NumassAnalyzer = SmartAnalyzer(), + metaBuilder: KMetaBuilder.() -> Unit = {} +) { + val meta = buildMeta("meta", metaBuilder) + val binning = meta.getInt("binning", 20) + val lo = meta.optNumber("window.lo").nullable?.toInt() + val up = meta.optNumber("window.up").nullable?.toInt() + val data = analyzer.getAmplitudeSpectrum(numassBlock, meta).withBinning(binning, lo, up) + apply { + val valueAxis = if (meta.getBoolean("normalize", false)) { + NumassAnalyzer.COUNT_RATE_KEY + } else { + NumassAnalyzer.COUNT_KEY + } + configure { + "connectionType" to "step" + "thickness" to 2 + "showLine" to true + "showSymbol" to false + "showErrors" to false + }.setType() + + val plot = DataPlot.plot( + plotName, + data, + Adapters.buildXYAdapter(NumassAnalyzer.CHANNEL_KEY, valueAxis) + ) + plot.configure(meta) + add(plot) + } +} + +fun PlotFrame.plotAmplitudeSpectrum( + numassBlock: NumassBlock, + plotName: String = "spectrum", + analyzer: NumassAnalyzer = SmartAnalyzer(), + metaBuilder: KMetaBuilder.() -> Unit = {} +) = plots.plotAmplitudeSpectrum(numassBlock, plotName, analyzer, metaBuilder) + +fun Context.plotAmplitudeSpectrum( + numassBlock: NumassBlock, + plotName: String = "spectrum", + frameName: String = plotName, + analyzer: NumassAnalyzer = SmartAnalyzer(), + metaAction: KMetaBuilder.() -> Unit = {} +) { + plotFrame(frameName) { + plotAmplitudeSpectrum(numassBlock, plotName, analyzer, metaAction) + } +} + diff --git a/numass-main/src/main/kotlin/inr/numass/data/analyzers/SmartAnalyzer.kt b/numass-main/src/main/kotlin/inr/numass/data/analyzers/SmartAnalyzer.kt new file mode 100644 index 00000000..6a992c51 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/data/analyzers/SmartAnalyzer.kt @@ -0,0 +1,103 @@ +/* + * Copyright 2017 Alexander Nozik. + * + * 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 inr.numass.data.analyzers + +import hep.dataforge.meta.Meta +import hep.dataforge.tables.ListTable +import hep.dataforge.tables.Table +import hep.dataforge.tables.TableFormat +import hep.dataforge.values.Value +import hep.dataforge.values.ValueMap +import hep.dataforge.values.ValueType +import hep.dataforge.values.Values +import inr.numass.data.ChernovProcessor +import inr.numass.data.api.* +import inr.numass.utils.ExpressionUtils + +/** + * An analyzer dispatcher which uses different analyzer for different meta + * Created by darksnake on 11.07.2017. + */ +class SmartAnalyzer(processor: SignalProcessor? = null) : AbstractAnalyzer(processor) { + private val simpleAnalyzer = SimpleAnalyzer(processor) + private val debunchAnalyzer = DebunchAnalyzer(processor) + private val timeAnalyzer = TimeAnalyzer(processor) + + private fun getAnalyzer(config: Meta): NumassAnalyzer { + return if (config.hasValue("type")) { + when (config.getString("type")) { + "simple" -> simpleAnalyzer + "time" -> timeAnalyzer + "debunch" -> debunchAnalyzer + else -> throw IllegalArgumentException("Analyzer ${config.getString("type")} not found") + } + } else { + if (config.hasValue("t0") || config.hasMeta("t0")) { + timeAnalyzer + } else { + simpleAnalyzer + } + } + } + + override fun analyze(block: NumassBlock, config: Meta): Values { + val analyzer = getAnalyzer(config) + val map = analyzer.analyze(block, config).asMap().toMutableMap() + map.putIfAbsent(TimeAnalyzer.T0_KEY, Value.of(0.0)) + return ValueMap(map) + } + + override fun getEvents(block: NumassBlock, meta: Meta): List { + return getAnalyzer(meta).getEvents(block, meta) + } + + override fun getTableFormat(config: Meta): TableFormat { + return if (config.hasValue(TimeAnalyzer.T0_KEY) || config.hasMeta(TimeAnalyzer.T0_KEY)) { + timeAnalyzer.getTableFormat(config) + } else super.getTableFormat(config) + } + + override fun analyzeSet(set: NumassSet, config: Meta): Table { + fun Value.computeExpression(point: NumassPoint): Int { + return when { + this.type == ValueType.NUMBER -> this.int + this.type == ValueType.STRING -> { + val exprParams = HashMap() + + exprParams["U"] = point.voltage + + ExpressionUtils.function(this.string, exprParams).toInt() + } + else -> error("Can't interpret $type as expression or number") + } + } + val lo = config.getValue("window.lo",0) + val up = config.getValue("window.up", Int.MAX_VALUE) + + val format = getTableFormat(config) + + return ListTable.Builder(format) + .rows(set.points.map { point -> + val newConfig = config.builder.apply{ + setValue("window.lo", lo.computeExpression(point)) + setValue("window.up", up.computeExpression(point)) + } + analyzeParent(point, newConfig) + }) + .build() + } +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/models/NBkgSpectrum.kt b/numass-main/src/main/kotlin/inr/numass/models/NBkgSpectrum.kt new file mode 100644 index 00000000..4426ed30 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/models/NBkgSpectrum.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.models + +import hep.dataforge.names.NamesUtils.combineNamesWithEquals +import hep.dataforge.stat.parametric.AbstractParametricFunction +import hep.dataforge.stat.parametric.ParametricFunction +import hep.dataforge.utils.MultiCounter +import hep.dataforge.values.ValueProvider +import hep.dataforge.values.Values + +/** + * + * @author Darksnake + */ +open class NBkgSpectrum(private val source: ParametricFunction) : AbstractParametricFunction(*combineNamesWithEquals(source.namesAsArray(), *list)) { + + var counter = MultiCounter(this.javaClass.name) + + override fun derivValue(parName: String, x: Double, set: Values): Double { + this.counter.increase(parName) + return when (parName) { + "N" -> source.value(x, set) + "bkg" -> 1.0 + else -> getN(set) * source.derivValue(parName, x, set) + } + } + + private fun getBkg(set: ValueProvider): Double { + return set.getDouble("bkg") + } + + private fun getN(set: ValueProvider): Double { + return set.getDouble("N") + } + + override fun providesDeriv(name: String): Boolean { + return when (name) { + "N","bkg" -> true + else -> this.source.providesDeriv(name) + } + } + + override fun value(x: Double, set: Values): Double { + this.counter.increase("value") + return getN(set) * source.value(x, set) + getBkg(set) + } + + override fun getDefaultParameter(name: String): Double { + return when (name) { + "bkg" -> 0.0 + "N" -> 1.0 + else -> super.getDefaultParameter(name) + } + } + + companion object { + + private val list = arrayOf("N", "bkg") + } + +} diff --git a/numass-main/src/main/kotlin/inr/numass/models/NBkgSpectrumWithCorrection.kt b/numass-main/src/main/kotlin/inr/numass/models/NBkgSpectrumWithCorrection.kt new file mode 100644 index 00000000..0c39e001 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/models/NBkgSpectrumWithCorrection.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 inr.numass.models + +import hep.dataforge.names.NamesUtils.combineNamesWithEquals +import hep.dataforge.stat.parametric.AbstractParametricFunction +import hep.dataforge.stat.parametric.ParametricFunction +import hep.dataforge.values.Values + +class NBkgSpectrumWithCorrection(val source: ParametricFunction) : AbstractParametricFunction(*combineNamesWithEquals(source.namesAsArray(), *parameters)) { + + private val Values.bkg get() = getDouble("bkg") + private val Values.n get() = getDouble("N") + private val Values.l get() = getDouble("L") + private val Values.q get() = getDouble("Q") + + override fun derivValue(parName: String, x: Double, set: Values): Double { + return when (parName) { + "bkg" -> 1.0 + "N" -> source.value(x, set) + "L" -> x / 1e3 * source.value(x, set) + "Q" -> x * x /1e6 * source.value(x, set) + else -> (set.n + x/1e3 * set.l + x * x /1e6 * set.q) * source.derivValue(parName, x, set) + } + } + + override fun value(x: Double, set: Values): Double { + return (set.n + x * set.l / 1e3 + x * x / 1e6 * set.q) * source.value(x, set) + set.bkg + } + + override fun providesDeriv(name: String): Boolean { + return name in parameters || source.providesDeriv(name) + } + + override fun getDefaultParameter(name: String): Double { + return when (name) { + "bkg" -> 0.0 + "N" -> 1.0 + "L" -> 0.0 + "Q" -> 0.0 + else -> super.getDefaultParameter(name) + } + } + + companion object { + val parameters = arrayOf("bkg, N, L, Q") + } +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/models/NumassModels.kt b/numass-main/src/main/kotlin/inr/numass/models/NumassModels.kt new file mode 100644 index 00000000..34761828 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/models/NumassModels.kt @@ -0,0 +1,152 @@ +package inr.numass.models + +import hep.dataforge.maths.integration.UnivariateIntegrator +import hep.dataforge.names.NameList +import hep.dataforge.stat.models.Model +import hep.dataforge.stat.models.ModelFactory +import hep.dataforge.stat.parametric.AbstractParametricFunction +import hep.dataforge.stat.parametric.ParametricFunction +import hep.dataforge.utils.ContextMetaFactory +import hep.dataforge.values.Values +import inr.numass.models.misc.FunctionSupport +import inr.numass.utils.NumassIntegrator +import java.util.stream.Stream + + +fun model(name: String,factory: ContextMetaFactory): ModelFactory { + return ModelFactory.build(name, factory); +} + +// spectra operations +//TODO move to stat + +/** + * Calculate sum of two parametric functions + */ +operator fun ParametricFunction.plus(func: ParametricFunction): ParametricFunction { + val mergedNames = NameList(Stream.concat(names.stream(), func.names.stream()).distinct()) + return object : AbstractParametricFunction(mergedNames) { + override fun derivValue(parName: String, x: Double, set: Values): Double { + return func.derivValue(parName, x, set) + this@plus.derivValue(parName, x, set) + } + + override fun value(x: Double, set: Values): Double { + return this@plus.value(x, set) + func.value(x, set) + } + + override fun providesDeriv(name: String): Boolean { + return this@plus.providesDeriv(name) && func.providesDeriv(name) + } + + } +} + +operator fun ParametricFunction.minus(func: ParametricFunction): ParametricFunction { + val mergedNames = NameList(Stream.concat(names.stream(), func.names.stream()).distinct()) + return object : AbstractParametricFunction(mergedNames) { + override fun derivValue(parName: String, x: Double, set: Values): Double { + return func.derivValue(parName, x, set) - this@minus.derivValue(parName, x, set) + } + + override fun value(x: Double, set: Values): Double { + return this@minus.value(x, set) - func.value(x, set) + } + + override fun providesDeriv(name: String): Boolean { + return this@minus.providesDeriv(name) && func.providesDeriv(name) + } + + } +} + +/** + * Calculate product of two parametric functions + * + */ +operator fun ParametricFunction.times(func: ParametricFunction): ParametricFunction { + val mergedNames = NameList(Stream.concat(names.stream(), func.names.stream()).distinct()) + return object : AbstractParametricFunction(mergedNames) { + override fun derivValue(parName: String, x: Double, set: Values): Double { + return this@times.value(x, set) * func.derivValue(parName, x, set) + this@times.derivValue(parName, x, set) * func.value(x, set) + } + + override fun value(x: Double, set: Values): Double { + return this@times.value(x, set) * func.value(x, set) + } + + override fun providesDeriv(name: String): Boolean { + return this@times.providesDeriv(name) && func.providesDeriv(name) + } + + } +} + +/** + * Multiply parametric function by fixed value + */ +operator fun ParametricFunction.times(num: Number): ParametricFunction { + return object : AbstractParametricFunction(names) { + override fun derivValue(parName: String, x: Double, set: Values): Double { + return this@times.value(x, set) * num.toDouble() + } + + override fun value(x: Double, set: Values): Double { + return this@times.value(x, set) * num.toDouble() + } + + override fun providesDeriv(name: String): Boolean { + return this@times.providesDeriv(name) + } + + } +} + +/** + * Calculate convolution of two parametric functions + * @param func the function with which this function should be convoluded + * @param integrator optional integrator to be used in convolution + * @param support a function defining borders for integration. It takes 3 parameter: set of parameters, + * name of the derivative (empty for value) and point in which convolution should be calculated. + */ +fun ParametricFunction.convolute( + func: ParametricFunction, + integrator: UnivariateIntegrator<*> = NumassIntegrator.getDefaultIntegrator(), + support: Values.(String, Double) -> Pair +): ParametricFunction { + val mergedNames = NameList(Stream.concat(names.stream(), func.names.stream()).distinct()) + return object : AbstractParametricFunction(mergedNames) { + override fun derivValue(parName: String, x: Double, set: Values): Double { + val (a, b) = set.support(parName, x) + return integrator.integrate(a, b) { y: Double -> + this@convolute.derivValue(parName, y, set) * func.value(x - y, set) + + this@convolute.value(y, set) * func.derivValue(parName, x - y, set) + } + } + + override fun value(x: Double, set: Values): Double { + val (a, b) = set.support("", x) + return integrator.integrate(a, b) { y: Double -> this@convolute.value(y, set) * func.value(x - y, set) } + } + + override fun providesDeriv(name: String?): Boolean { + return this@convolute.providesDeriv(name) && func.providesDeriv(name) + } + + } + +} + +@JvmOverloads +fun ParametricFunction.convolute( + func: T, + integrator: UnivariateIntegrator<*> = NumassIntegrator.getDefaultIntegrator() +): ParametricFunction where T : ParametricFunction, T : FunctionSupport { + //inverted order for correct boundaries + return func.convolute(this, integrator) { parName, x -> + if (parName.isEmpty()) { + func.getSupport(this) + } else { + func.getDerivSupport(parName, this) + } + } +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/models/mc/BetaSampler.kt b/numass-main/src/main/kotlin/inr/numass/models/mc/BetaSampler.kt new file mode 100644 index 00000000..e2442eee --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/models/mc/BetaSampler.kt @@ -0,0 +1,53 @@ +package inr.numass.models.mc + +import hep.dataforge.context.Global +import hep.dataforge.maths.chain.Chain +import hep.dataforge.meta.buildMeta +import hep.dataforge.plots.data.XYFunctionPlot +import hep.dataforge.plots.output.plotFrame +import hep.dataforge.stat.PolynomialDistribution +import hep.dataforge.stat.fit.ParamSet +import inr.numass.NumassPlugin +import inr.numass.models.sterile.SterileNeutrinoSpectrum + +fun sampleBeta(params: ParamSet): Chain { + TODO() +} + + +fun main() { + NumassPlugin().startGlobal() + val meta = buildMeta("model") { + "fast" to true + node("resolution") { + "width" to 1.22e-4 + "tailAlpha" to 1e-2 + } + } + val allPars = ParamSet() + .setPar("N", 7e+05, 1.8e+03, 0.0, java.lang.Double.POSITIVE_INFINITY) + .setPar("bkg", 1.0, 0.050) + .setPar("E0", 18575.0, 1.4) + .setPar("mnu2", 0.0, 1.0) + .setPar("msterile2", 1000.0 * 1000.0, 0.0) + .setPar("U2", 0.0, 1e-4, -1.0, 1.0) + .setPar("X", 0.0, 0.01, 0.0, java.lang.Double.POSITIVE_INFINITY) + .setPar("trap", 1.0, 0.01, 0.0, java.lang.Double.POSITIVE_INFINITY) + + val sp = SterileNeutrinoSpectrum(Global, meta) + + val spectrumPlot = XYFunctionPlot.plot("spectrum", 14000.0, 18600.0, 500) { + sp.value(it, allPars) + } + + val distribution = PolynomialDistribution(0.0, 5000.0, 3.0); + + val distributionPlot = XYFunctionPlot.plot("distribution", 14000.0, 18500.0, 500) { + 50 * distribution.density(18600.0 - it) + } + + Global.plotFrame("beta") { + add(spectrumPlot) + add(distributionPlot) + } +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/models/misc/FunctionSupport.kt b/numass-main/src/main/kotlin/inr/numass/models/misc/FunctionSupport.kt new file mode 100644 index 00000000..824b29db --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/models/misc/FunctionSupport.kt @@ -0,0 +1,28 @@ +package inr.numass.models.misc + +import hep.dataforge.maths.domains.HyperSquareDomain +import hep.dataforge.values.Values + +interface FunctionSupport { + /** + * Get support for function itself + */ + fun getSupport(params: Values): Pair + + /** + * GetSupport for function derivative + */ + fun getDerivSupport(parName: String, params: Values): Pair +} + +interface BiFunctionSupport { + /** + * Get support for function itself + */ + fun getSupport(x: Double, y: Double, params: Values): HyperSquareDomain + + /** + * GetSupport for function derivative + */ + fun getDerivSupport(parName: String, x: Double, params: Values): HyperSquareDomain +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/models/misc/Gauss.kt b/numass-main/src/main/kotlin/inr/numass/models/misc/Gauss.kt new file mode 100644 index 00000000..3a29b63f --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/models/misc/Gauss.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.models.misc + +import hep.dataforge.stat.parametric.AbstractParametricFunction +import hep.dataforge.values.ValueProvider +import hep.dataforge.values.Values + +import java.lang.Math.* + +/** + * @author Darksnake + */ +class Gauss(private val cutoff: Double = 4.0) : AbstractParametricFunction("w", "shift"), FunctionSupport { + + private fun getShift(pars: ValueProvider): Double = pars.getDouble("shift", 0.0) + + private fun getW(pars: ValueProvider): Double = pars.getDouble("w") + + override fun providesDeriv(name: String): Boolean = true + + + override fun value(d: Double, pars: Values): Double { + if (abs(d - getShift(pars)) > cutoff * getW(pars)) { + return 0.0 + } + val aux = (d - getShift(pars)) / getW(pars) + return exp(-aux * aux / 2) / getW(pars) / sqrt(2 * Math.PI) + } + + override fun derivValue(parName: String, d: Double, pars: Values): Double { + if (abs(d - getShift(pars)) > cutoff * getW(pars)) { + return 0.0 + } + val pos = getShift(pars) + val w = getW(pars) + + return when (parName) { + "shift" -> this.value(d, pars) * (d - pos) / w / w + "w" -> this.value(d, pars) * ((d - pos) * (d - pos) / w / w / w - 1 / w) + else -> return 0.0; + } + } + + override fun getSupport(params: Values): Pair { + val shift = getShift(params) + val w = getW(params) + return Pair(shift - cutoff * w, shift + cutoff * w) + } + + override fun getDerivSupport(parName: String, params: Values): Pair { + val shift = getShift(params) + val w = getW(params) + return Pair(shift - cutoff * w, shift + cutoff * w) + } +} diff --git a/numass-main/src/main/kotlin/inr/numass/models/misc/LossCalculator.kt b/numass-main/src/main/kotlin/inr/numass/models/misc/LossCalculator.kt new file mode 100644 index 00000000..7f82bcc1 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/models/misc/LossCalculator.kt @@ -0,0 +1,435 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 inr.numass.models.misc + +import hep.dataforge.maths.functions.FunctionCaching +import hep.dataforge.maths.integration.GaussRuleIntegrator +import hep.dataforge.plots.PlotFrame +import hep.dataforge.plots.data.XYFunctionPlot +import hep.dataforge.utils.Misc +import hep.dataforge.values.Values +import kotlinx.coroutines.* +import org.apache.commons.math3.analysis.BivariateFunction +import org.apache.commons.math3.analysis.UnivariateFunction +import org.apache.commons.math3.exception.OutOfRangeException +import org.slf4j.LoggerFactory +import java.lang.Math.exp +import java.util.* + +/** + * ВычиÑление произвольного порÑдка функции раÑÑеÑниÑ. Ðе учитываетÑÑ + * завиÑимоÑÑ‚ÑŒ ÑÐµÑ‡ÐµÐ½Ð¸Ñ Ð¾Ñ‚ Ñнергии Ñлектрона + * + * @author Darksnake + */ +object LossCalculator { + private val cache = HashMap>() + + private const val ION_POTENTIAL = 15.4//eV + + val adjustX = true + + + private fun getX(set: Values, eIn: Double): Double { + return if (adjustX) { + //From our article + set.getDouble("X") * Math.log(eIn / ION_POTENTIAL) * eIn * ION_POTENTIAL / 1.9580741410115568e6 + } else { + set.getDouble("X") + } + } + + fun p0(set: Values, eIn: Double): Double { + return LossCalculator.getLossProbability(0, getX(set, eIn)) + } + + fun getGunLossProbabilities(X: Double): List { + val res = ArrayList() + var prob: Double + if (X > 0) { + prob = Math.exp(-X) + } else { + // еÑли x ==0, то выживает только нулевой член, первый равен 1 + res.add(1.0) + return res + } + res.add(prob) + + var n = 0 + while (prob > SCATTERING_PROBABILITY_THRESHOLD) { + /* + * prob(n) = prob(n-1)*X/n; + */ + n++ + prob *= X / n + res.add(prob) + } + + return res + } + + fun getGunZeroLossProb(x: Double): Double { + return Math.exp(-x) + } + + + private fun CoroutineScope.getCachedSpectrum(order: Int): Deferred { + return when { + order <= 0 -> error("Non-positive loss cache order") + order == 1 -> CompletableDeferred(singleScatterFunction) + else -> cache.getOrPut(order) { + async { + LoggerFactory.getLogger(javaClass) + .debug("Scatter cache of order {} not found. Updating", order) + getNextLoss(getMargin(order), getCachedSpectrum(order - 1).await()) + } + } + } + } + + /** + * Ленивое рекурÑивное вычиÑление функции потерь через предыдущие + * + * @param order + * @return + */ + private fun getLoss(order: Int): UnivariateFunction { + return runBlocking { getCachedSpectrum(order).await() } + } + + fun getLossFunction(order: Int): BivariateFunction { + assert(order > 0) + return BivariateFunction { Ei: Double, Ef: Double -> getLossValue(order, Ei, Ef) } + } + + fun getLossProbDerivs(x: Double): List { + val res = ArrayList() + val probs = getLossProbabilities(x) + + var delta = Math.exp(-x) + res.add((delta - probs[0]) / x) + for (i in 1 until probs.size) { + delta *= x / i + res.add((delta - probs[i]) / x) + } + + return res + } + + /** + * рекурÑивно вычиÑлÑем вÑе вероÑтноÑти, котрорые выше порога + * + * + * диÑер, ÑÑ‚Ñ€.48 + * + * @param X + * @return + */ + fun calculateLossProbabilities(x: Double): List { + val res = ArrayList() + var prob: Double + if (x > 0) { + prob = 1 / x * (1 - Math.exp(-x)) + } else { + // еÑли x ==0, то выживает только нулевой член, первый равен нулю + res.add(1.0) + return res + } + res.add(prob) + + while (prob > SCATTERING_PROBABILITY_THRESHOLD) { + /* + * prob(n) = prob(n-1)-1/n! * X^n * exp(-X); + */ + var delta = Math.exp(-x) + for (i in 1 until res.size + 1) { + delta *= x / i + } + prob -= delta / x + res.add(prob) + } + + return res + } + + fun getLossProbabilities(x: Double): List = lossProbCache.getOrPut(x) { calculateLossProbabilities(x) } + + fun getLossProbability(order: Int, X: Double): Double { + if (order == 0) { + return if (X > 0) { + 1 / X * (1 - Math.exp(-X)) + } else { + 1.0 + } + } + val probs = getLossProbabilities(X) + return if (order >= probs.size) { + 0.0 + } else { + probs[order] + } + } + + fun getLossValue(order: Int, Ei: Double, Ef: Double): Double { + return when { + Ei - Ef < 5.0 -> 0.0 + Ei - Ef >= getMargin(order) -> 0.0 + else -> getLoss(order).value(Ei - Ef) + } + } + + /** + * Ñ„ÑƒÐ½ÐºÑ†Ð¸Ñ Ð¿Ð¾Ñ‚ÐµÑ€ÑŒ Ñ Ð¿Ñ€Ð¾Ð¸Ð·Ð²Ð¾Ð»ÑŒÐ½Ñ‹Ð¼Ð¸ вероÑтноÑÑ‚Ñми раÑÑеÑÐ½Ð¸Ñ + * + * @param probs + * @param Ei + * @param Ef + * @return + */ + fun getLossValue(probs: List, Ei: Double, Ef: Double): Double { + var sum = 0.0 + for (i in 1 until probs.size) { + sum += probs[i] * getLossValue(i, Ei, Ef) + } + return sum + } + + /** + * граница Ð¸Ð½Ñ‚ÐµÐ³Ñ€Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ + * + * @param order + * @return + */ + private fun getMargin(order: Int): Double { + return 80 + order * 50.0 + } + + /** + * генерирует кÑшированную функцию Ñвертки loss Ñо Ñпектром однократных + * потерь + * + * @param loss + * @return + */ + private fun getNextLoss(margin: Double, loss: UnivariateFunction?): UnivariateFunction { + val res = { x: Double -> + val integrand = UnivariateFunction { y: Double -> + try { + loss!!.value(x - y) * singleScatterFunction.value(y) + } catch (ex: OutOfRangeException) { + 0.0 + } + } + integrator.integrate(5.0, margin, integrand) + } + + return FunctionCaching.cacheUnivariateFunction(0.0, margin, 200, res) + + } + + fun getTotalLossBivariateFunction(X: Double): BivariateFunction { + return BivariateFunction { Ei: Double, Ef: Double -> getTotalLossValue(X, Ei, Ef) } + } + + /** + * Значение полной производной функции потерь Ñ ÑƒÑ‡ÐµÑ‚Ð¾Ð¼ вÑех неиÑчезающих + * порÑдков + * + * @param X + * @param eIn + * @param eOut + * @return + */ + fun getTotalLossDeriv(X: Double, eIn: Double, eOut: Double): Double { + val probs = getLossProbDerivs(X) + + var sum = 0.0 + for (i in 1 until probs.size) { + sum += probs[i] * getLossValue(i, eIn, eOut) + } + return sum + } + + fun getTotalLossDeriv(pars: Values, eIn: Double, eOut: Double) = getTotalLossDeriv(getX(pars, eIn), eIn, eOut) + + fun getTotalLossDerivBivariateFunction(X: Double) = BivariateFunction { Ei: Double, Ef: Double -> getTotalLossDeriv(X, Ei, Ef) } + + + /** + * Значение полной функции потерь Ñ ÑƒÑ‡ÐµÑ‚Ð¾Ð¼ вÑех неиÑчезающих порÑдков + * + * @param x + * @param Ei + * @param Ef + * @return + */ + fun getTotalLossValue(x: Double, Ei: Double, Ef: Double): Double { + return if (x == 0.0) { + 0.0 + } else { + val probs = getLossProbabilities(x) + (1 until probs.size).sumByDouble { i -> + probs[i] * getLossValue(i, Ei, Ef) + } + } + } + + fun getTotalLossValue(pars: Values, Ei: Double, Ef: Double): Double = getTotalLossValue(getX(pars, Ei), Ei, Ef) + + + /** + * порог по вероÑтноÑти, до которого вычиÑлÑÑŽÑ‚ÑÑ ÐºÐ¾Ð¼Ð¿Ð¾Ð½ÐµÐ½Ñ‚Ñ‹ функции потерь + */ + private const val SCATTERING_PROBABILITY_THRESHOLD = 1e-3 + private val integrator = GaussRuleIntegrator(100) + private val lossProbCache = Misc.getLRUCache>(100) + + + private val A1 = 0.204 + private val A2 = 0.0556 + private val b = 14.0 + private val pos1 = 12.6 + private val pos2 = 14.3 + private val w1 = 1.85 + private val w2 = 12.5 + + val singleScatterFunction = UnivariateFunction { eps: Double -> + when { + eps <= 0 -> 0.0 + eps <= b -> { + val z = eps - pos1 + A1 * exp(-2.0 * z * z / w1 / w1) + } + else -> { + val z = 4.0 * (eps - pos2) * (eps - pos2) + A2 / (1 + z / w2 / w2) + } + } + } + + + /** + * A generic loss function for numass experiment in "Lobashev" + * parameterization + * + * @param exPos + * @param ionPos + * @param exW + * @param ionW + * @param exIonRatio + * @return + */ + fun getSingleScatterFunction( + exPos: Double, + ionPos: Double, + exW: Double, + ionW: Double, + exIonRatio: Double): UnivariateFunction { + val func = UnivariateFunction { eps: Double -> + if (eps <= 0) { + 0.0 + } else { + val z1 = eps - exPos + val ex = exIonRatio * exp(-2.0 * z1 * z1 / exW / exW) + + val z = 4.0 * (eps - ionPos) * (eps - ionPos) + val ion = 1 / (1 + z / ionW / ionW) + + if (eps < exPos) { + ex + } else { + Math.max(ex, ion) + } + } + } + + val cutoff = 25.0 + //caclulating lorentz integral analythically + val tailNorm = (Math.atan((ionPos - cutoff) * 2.0 / ionW) + 0.5 * Math.PI) * ionW / 2.0 + val norm = integrator.integrate(0.0, cutoff, func)!! + tailNorm + return UnivariateFunction { e -> func.value(e) / norm } + } + + fun getSingleScatterFunction(set: Values): UnivariateFunction { + + val exPos = set.getDouble("exPos") + val ionPos = set.getDouble("ionPos") + val exW = set.getDouble("exW") + val ionW = set.getDouble("ionW") + val exIonRatio = set.getDouble("exIonRatio") + + return getSingleScatterFunction(exPos, ionPos, exW, ionW, exIonRatio) + } + + val trapFunction: BivariateFunction + get() = BivariateFunction { Ei: Double, Ef: Double -> + val eps = Ei - Ef + if (eps > 10) { + 1.86e-04 * exp(-eps / 25.0) + 5.5e-05 + } else { + 0.0 + } + } + + fun plotScatter(frame: PlotFrame, set: Values) { + //"X", "shift", "exPos", "ionPos", "exW", "ionW", "exIonRatio" + + // JFreeChartFrame frame = JFreeChartFrame.drawFrame("Differential scattering crosssection", null); + val X = set.getDouble("X") + + val exPos = set.getDouble("exPos") + + val ionPos = set.getDouble("ionPos") + + val exW = set.getDouble("exW") + + val ionW = set.getDouble("ionW") + + val exIonRatio = set.getDouble("exIonRatio") + + val scatterFunction = getSingleScatterFunction(exPos, ionPos, exW, ionW, exIonRatio) + + if (set.names.contains("X")) { + val probs = LossCalculator.getGunLossProbabilities(set.getDouble("X")) + val single = { e: Double -> probs[1] * scatterFunction.value(e) } + frame.add(XYFunctionPlot.plot("Single scattering", 0.0, 100.0, 1000) { x: Double -> single(x) }) + + for (i in 2 until probs.size) { + val scatter = { e: Double -> probs[i] * LossCalculator.getLossValue(i, e, 0.0) } + frame.add(XYFunctionPlot.plot(i.toString() + " scattering", 0.0, 100.0, 1000) { x: Double -> scatter(x) }) + } + + val total = UnivariateFunction { eps -> + if (probs.size == 1) { + return@UnivariateFunction 0.0 + } + var sum = probs[1] * scatterFunction.value(eps) + for (i in 2 until probs.size) { + sum += probs[i] * LossCalculator.getLossValue(i, eps, 0.0) + } + return@UnivariateFunction sum + } + + frame.add(XYFunctionPlot.plot("Total loss", 0.0, 100.0, 1000) { x: Double -> total.value(x) }) + + } else { + + frame.add(XYFunctionPlot.plot("Differential cross-section", 0.0, 100.0, 2000) { x: Double -> scatterFunction.value(x) }) + } + + } + +} diff --git a/numass-main/src/main/kotlin/inr/numass/models/misc/ModGauss.kt b/numass-main/src/main/kotlin/inr/numass/models/misc/ModGauss.kt new file mode 100644 index 00000000..d1ce7ce1 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/models/misc/ModGauss.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.models.misc + +import hep.dataforge.stat.parametric.AbstractParametricFunction +import hep.dataforge.values.ValueProvider +import hep.dataforge.values.Values + +import java.lang.Math.* + +/** + * @author Darksnake + */ +class ModGauss(private val cutoff: Double = 4.0) : AbstractParametricFunction("w", "shift", "tailAmp", "tailW"), FunctionSupport { + + private fun getShift(pars: ValueProvider): Double = pars.getDouble("shift", 0.0) + + private fun getTailAmp(pars: ValueProvider): Double = pars.getDouble("tailAmp", 0.0) + + private fun getTailW(pars: ValueProvider): Double = pars.getDouble("tailW", 100.0) + + private fun getW(pars: ValueProvider): Double = pars.getDouble("w") + + override fun providesDeriv(name: String): Boolean = true + + + override fun value(d: Double, pars: Values): Double { + val shift = getShift(pars) + if (d - shift > cutoff * getW(pars)) { + return 0.0 + } + val aux = (d - shift) / getW(pars) + val tail = if (d > getShift(pars)) { + 0.0 + } else { + val tailW = getTailW(pars) + getTailAmp(pars) / tailW * Math.exp((d - shift) / tailW) + } + return exp(-aux * aux / 2) / getW(pars) / sqrt(2 * Math.PI) + tail + } + + override fun derivValue(parName: String, d: Double, pars: Values): Double { + if (abs(d - getShift(pars)) > cutoff * getW(pars)) { + return 0.0 + } + val pos = getShift(pars) + val w = getW(pars) + val tailW = getTailW(pars) + + return when (parName) { + "shift" -> this.value(d, pars) * (d - pos) / w / w + "w" -> this.value(d, pars) * ((d - pos) * (d - pos) / w / w / w - 1 / w) + "tailAmp" -> if (d > pos) { + 0.0 + } else { + Math.exp((d - pos) / tailW) / tailW + } + else -> return 0.0; + } + } + + override fun getSupport(params: Values): Pair { + val shift = getShift(params) + return Pair(shift - cutoff * getTailW(params), shift + cutoff * getW(params)) + } + + override fun getDerivSupport(parName: String, params: Values): Pair { + val shift = getShift(params) + return Pair(shift - cutoff * getTailW(params), shift + cutoff * getW(params)) + } +} diff --git a/numass-main/src/main/kotlin/inr/numass/models/misc/TailFunctions.kt b/numass-main/src/main/kotlin/inr/numass/models/misc/TailFunctions.kt new file mode 100644 index 00000000..7e47a733 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/models/misc/TailFunctions.kt @@ -0,0 +1,41 @@ +package inr.numass.models.misc + +import hep.dataforge.stat.parametric.AbstractParametricFunction +import hep.dataforge.values.Values + +class ConstantTail(private val defaultTail: Double = 0.0) : AbstractParametricFunction("tail") { + override fun derivValue(parName: String, x: Double, set: Values): Double { + if (parName == "tail" && x <= 0) { + return 1.0 + } else { + return 0.0 + } + } + + override fun value(x: Double, set: Values): Double { + return if (x <= 0) { + set.getDouble("tail", defaultTail) + } else { + 0.0 + } + } + + override fun providesDeriv(name: String): Boolean { + return true; + } +} + +class exponentialTail : AbstractParametricFunction("tail") { + override fun derivValue(parName: String?, x: Double, set: Values?): Double { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun value(x: Double, set: Values?): Double { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun providesDeriv(name: String?): Boolean { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/models/sterile/NumassBeta.kt b/numass-main/src/main/kotlin/inr/numass/models/sterile/NumassBeta.kt new file mode 100644 index 00000000..a5977b86 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/models/sterile/NumassBeta.kt @@ -0,0 +1,225 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package inr.numass.models.sterile + +import hep.dataforge.exceptions.NotDefinedException +import hep.dataforge.stat.parametric.AbstractParametricBiFunction +import hep.dataforge.stat.parametric.AbstractParametricFunction +import hep.dataforge.stat.parametric.ParametricFunction +import hep.dataforge.values.Values + +import java.lang.Math.* + +/** + * A bi-function for beta-spectrum calculation taking energy and final state as + * input. + * + * + * dissertation p.33 + * + * + * @author [Alexander Nozik](mailto:altavir@gmail.com) + */ +class NumassBeta : AbstractParametricBiFunction(list) { + + /** + * Beta spectrum derivative + * + * @param n parameter number + * @param E0 + * @param mnu2 + * @param E + * @return + * @throws NotDefinedException + */ + @Throws(NotDefinedException::class) + private fun derivRoot(n: Int, E0: Double, mnu2: Double, E: Double): Double { + val D = E0 - E//E0-E + if (D == 0.0) { + return 0.0 + } + + return if (mnu2 >= 0) { + if (E >= E0 - sqrt(mnu2)) { + 0.0 + } else { + val bare = sqrt(D * D - mnu2) + when (n) { + 0 -> factor(E) * (2.0 * D * D - mnu2) / bare + 1 -> -factor(E) * 0.5 * D / bare + else -> 0.0 + } + } + } else { + val mu = sqrt(-0.66 * mnu2) + if (E >= E0 + mu) { + 0.0 + } else { + val root = sqrt(Math.max(D * D - mnu2, 0.0)) + val exp = exp(-1 - D / mu) + when (n) { + 0 -> factor(E) * (D * (D + mu * exp) / root + root * (1 - exp)) + 1 -> factor(E) * (-(D + mu * exp) / root * 0.5 - root * exp * (1 + D / mu) / 3.0 / mu) + else -> 0.0 + } + } + } + } + + /** + * Derivative of spectrum with sterile neutrinos + * + * @param name + * @param E + * @param E0 + * @param pars + * @return + * @throws NotDefinedException + */ + @Throws(NotDefinedException::class) + private fun derivRootsterile(name: String, E: Double, E0: Double, pars: Values): Double { + val mnu2 = getParameter("mnu2", pars) + val mst2 = getParameter("msterile2", pars) + val u2 = getParameter("U2", pars) + + return when (name) { + "E0" -> { + if (u2 == 0.0) { + derivRoot(0, E0, mnu2, E) + } else { + u2 * derivRoot(0, E0, mst2, E) + (1 - u2) * derivRoot(0, E0, mnu2, E) + } + } + "mnu2" -> (1 - u2) * derivRoot(1, E0, mnu2, E) + "msterile2" -> { + if (u2 == 0.0) { + 0.0 + } else { + u2 * derivRoot(1, E0, mst2, E) + } + } + "U2" -> root(E0, mst2, E) - root(E0, mnu2, E) + else -> 0.0 + } + + } + + /** + * The part independent of neutrino mass. Includes global normalization + * constant, momentum and Fermi correction + * + * @param E + * @return + */ + private fun factor(E: Double): Double { + val me = 0.511006E6 + val eTot = E + me + val pe = sqrt(E * (E + 2.0 * me)) + val ve = pe / eTot + val yfactor = 2.0 * 2.0 * 1.0 / 137.039 * Math.PI + val y = yfactor / ve + val fn = y / abs(1.0 - exp(-y)) + val fermi = fn * (1.002037 - 0.001427 * ve) + val res = fermi * pe * eTot + return K * res + } + + override fun providesDeriv(name: String): Boolean { + return true + } + + /** + * Bare beta spectrum with Mainz negative mass correction + * + * @param E0 + * @param mnu2 + * @param E + * @return + */ + private fun root(E0: Double, mnu2: Double, E: Double): Double { + //bare beta-spectrum + val delta = E0 - E + val bare = factor(E) * delta * sqrt(Math.max(delta * delta - mnu2, 0.0)) + return when { + mnu2 >= 0 -> Math.max(bare, 0.0) + delta == 0.0 -> 0.0 + delta + 0.812 * sqrt(-mnu2) <= 0 -> 0.0 //sqrt(0.66) + else -> { + val aux = sqrt(-mnu2 * 0.66) / delta + Math.max(bare * (1 + aux * exp(-1 - 1 / aux)), 0.0) + } + } + } + + /** + * beta-spectrum with sterile neutrinos + * + * @param E + * @param E0 + * @param pars + * @return + */ + private fun rootsterile(E: Double, E0: Double, pars: Values): Double { + val mnu2 = getParameter("mnu2", pars) + val mst2 = getParameter("msterile2", pars) + val u2 = getParameter("U2", pars) + + return if (u2 == 0.0) { + root(E0, mnu2, E) + } else { + u2 * root(E0, mst2, E) + (1 - u2) * root(E0, mnu2, E) + } +// P(rootsterile)+ (1-P)root + } + + override fun getDefaultParameter(name: String): Double { + return when (name) { + "mnu2", "U2", "msterile2" -> 0.0 + else -> super.getDefaultParameter(name) + } + } + + override fun derivValue(parName: String, fs: Double, eIn: Double, pars: Values): Double { + val e0 = getParameter("E0", pars) + return derivRootsterile(parName, eIn, e0 - fs, pars) + } + + override fun value(fs: Double, eIn: Double, pars: Values): Double { + val e0 = getParameter("E0", pars) + return rootsterile(eIn, e0 - fs, pars) + } + + /** + * Get univariate spectrum with given final state + */ + fun getSpectrum(fs: Double = 0.0): ParametricFunction { + return BetaSpectrum(fs); + } + + inner class BetaSpectrum(val fs: Double) : AbstractParametricFunction(*list) { + + override fun providesDeriv(name: String): Boolean { + return this@NumassBeta.providesDeriv(name) + } + + override fun derivValue(parName: String, x: Double, set: Values): Double { + return this@NumassBeta.derivValue(parName, fs, x, set) + } + + override fun value(x: Double, set: Values): Double { + return this@NumassBeta.value(fs, x, set) + } + + } + + + companion object { + + private const val K = 1E-23 + private val list = arrayOf("E0", "mnu2", "msterile2", "U2") + } + +} diff --git a/numass-main/src/main/kotlin/inr/numass/models/sterile/NumassResolution.kt b/numass-main/src/main/kotlin/inr/numass/models/sterile/NumassResolution.kt new file mode 100644 index 00000000..cec83f63 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/models/sterile/NumassResolution.kt @@ -0,0 +1,87 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package inr.numass.models.sterile + +import hep.dataforge.context.Context +import hep.dataforge.maths.functions.FunctionLibrary +import hep.dataforge.meta.Meta +import hep.dataforge.stat.parametric.AbstractParametricBiFunction +import hep.dataforge.values.Values +import inr.numass.models.ResolutionFunction +import inr.numass.utils.ExpressionUtils +import org.apache.commons.math3.analysis.BivariateFunction +import java.lang.Math.sqrt +import java.util.* + +/** + * @author [Alexander Nozik](mailto:altavir@gmail.com) + */ +class NumassResolution(context: Context, meta: Meta) : AbstractParametricBiFunction(list) { + + private val resA: Double = meta.getDouble("A", 8.3e-5) + private val resB = meta.getDouble("B", 0.0) + private val tailFunction: BivariateFunction = when { + meta.hasValue("tail") -> { + val tailFunctionStr = meta.getString("tail") + if (tailFunctionStr.startsWith("function::")) { + FunctionLibrary.buildFrom(context).buildBivariateFunction(tailFunctionStr.substring(10)) + } else { + BivariateFunction { E, U -> + val binding = HashMap() + binding["E"] = E + binding["U"] = U + binding["D"] = E - U + ExpressionUtils.function(tailFunctionStr, binding) + } + } + } + meta.hasValue("tailAlpha") -> { + //add polynomial function here + val alpha = meta.getDouble("tailAlpha") + val beta = meta.getDouble("tailBeta", 0.0) + BivariateFunction { E: Double, U: Double -> 1 - (E - U) * (alpha + E / 1000.0 * beta) / 1000.0 } + + } + else -> ResolutionFunction.getConstantTail() + } + + override fun derivValue(parName: String, x: Double, y: Double, set: Values): Double { + return 0.0 + } + + private fun getValueFast(E: Double, U: Double): Double { + val delta = resA * E + return when { + E - U < 0 -> 0.0 + E - U > delta -> tailFunction.value(E, U) + else -> (E - U) / delta + } + } + + override fun providesDeriv(name: String): Boolean { + return true + } + + override fun value(E: Double, U: Double, set: Values): Double { + assert(resA > 0) + if (resB <= 0) { + return this.getValueFast(E, U) + } + assert(resB > 0) + val delta = resA * E + return when { + E - U < 0 -> 0.0 + E - U > delta -> tailFunction.value(E, U) + else -> (1 - sqrt(1 - (E - U) / E * resB)) / (1 - sqrt(1 - resA * resB)) + } + } + + companion object { + + private val list = arrayOf() //leaving + } + +} diff --git a/numass-main/src/main/kotlin/inr/numass/models/sterile/NumassTransmission.kt b/numass-main/src/main/kotlin/inr/numass/models/sterile/NumassTransmission.kt new file mode 100644 index 00000000..2fbf44ec --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/models/sterile/NumassTransmission.kt @@ -0,0 +1,84 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package inr.numass.models.sterile + +import hep.dataforge.context.Context +import hep.dataforge.maths.functions.FunctionLibrary +import hep.dataforge.meta.Meta +import hep.dataforge.stat.parametric.AbstractParametricBiFunction +import hep.dataforge.values.Values +import inr.numass.models.misc.LossCalculator +import inr.numass.utils.ExpressionUtils +import org.apache.commons.math3.analysis.BivariateFunction +import org.slf4j.LoggerFactory +import java.util.* + +/** + * @author [Alexander Nozik](mailto:altavir@gmail.com) + */ +class NumassTransmission(context: Context, meta: Meta) : AbstractParametricBiFunction(list) { + private val trapFunc: BivariateFunction + //private val lossCache = HashMap() + + private val adjustX: Boolean = meta.getBoolean("adjustX", false) + + init { + if (meta.hasValue("trapping")) { + val trapFuncStr = meta.getString("trapping") + trapFunc = if (trapFuncStr.startsWith("function::")) { + FunctionLibrary.buildFrom(context).buildBivariateFunction(trapFuncStr.substring(10)) + } else { + BivariateFunction { Ei: Double, Ef: Double -> + val binding = HashMap() + binding["Ei"] = Ei + binding["Ef"] = Ef + ExpressionUtils.function(trapFuncStr, binding) + } + } + } else { + LoggerFactory.getLogger(javaClass).warn("Trapping function not defined. Using default") + trapFunc = FunctionLibrary.buildFrom(context).buildBivariateFunction("numass.trap.nominal") + } + } + + override fun derivValue(parName: String, eIn: Double, eOut: Double, set: Values): Double { + return when (parName) { + "trap" -> trapFunc.value(eIn, eOut) + "X" -> LossCalculator.getTotalLossDeriv(set, eIn, eOut) + else -> super.derivValue(parName, eIn, eOut, set) + } + } + + override fun providesDeriv(name: String): Boolean { + return true + } + + override fun value(eIn: Double, eOut: Double, set: Values): Double { + // loss part + val loss = LossCalculator.getTotalLossValue(set, eIn, eOut) + // double loss; + // + // if(eIn-eOut >= 300){ + // loss = 0; + // } else { + // UnivariateFunction lossFunction = this.lossCache.computeIfAbsent(X, theX -> + // FunctionCaching.cacheUnivariateFunction(0, 300, 400, x -> calculator.getTotalLossValue(theX, eIn, eIn - x)) + // ); + // + // loss = lossFunction.value(eIn - eOut); + // } + + //trapping part + val trap = getParameter("trap", set) * trapFunc.value(eIn, eOut) + return loss + trap + } + + companion object { + + private val list = arrayOf("trap", "X") + } + +} diff --git a/numass-main/src/main/kotlin/inr/numass/models/sterile/ParametricBiFunctionCache.kt b/numass-main/src/main/kotlin/inr/numass/models/sterile/ParametricBiFunctionCache.kt new file mode 100644 index 00000000..81c4bd4e --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/models/sterile/ParametricBiFunctionCache.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 inr.numass.models.sterile + +import hep.dataforge.names.NameList +import hep.dataforge.stat.parametric.ParametricBiFunction +import hep.dataforge.values.Values + +class ParametricBiFunctionCache(val function: ParametricBiFunction): ParametricBiFunction { + override fun derivValue(parName: String?, x: Double, y: Double, set: Values?): Double { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun getNames(): NameList { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun value(x: Double, y: Double, set: Values?): Double { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun providesDeriv(name: String?): Boolean { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/models/sterile/SterileNeutrinoSpectrum.kt b/numass-main/src/main/kotlin/inr/numass/models/sterile/SterileNeutrinoSpectrum.kt new file mode 100644 index 00000000..4d4a7360 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/models/sterile/SterileNeutrinoSpectrum.kt @@ -0,0 +1,169 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package inr.numass.models.sterile + +import hep.dataforge.context.Context +import hep.dataforge.description.NodeDef +import hep.dataforge.description.NodeDefs +import hep.dataforge.description.ValueDef +import hep.dataforge.description.ValueDefs +import hep.dataforge.exceptions.NotDefinedException +import hep.dataforge.maths.integration.UnivariateIntegrator +import hep.dataforge.meta.Meta +import hep.dataforge.stat.parametric.AbstractParametricBiFunction +import hep.dataforge.stat.parametric.AbstractParametricFunction +import hep.dataforge.stat.parametric.ParametricBiFunction +import hep.dataforge.values.ValueType.BOOLEAN +import hep.dataforge.values.Values +import inr.numass.getFSS +import inr.numass.models.FSS +import inr.numass.models.misc.LossCalculator +import inr.numass.utils.NumassIntegrator + +/** + * Compact all-in-one model for sterile neutrino spectrum + * + * @author Alexander Nozik + */ +@NodeDefs( + NodeDef(key = "resolution"), + NodeDef(key = "transmission") +) +@ValueDefs( + ValueDef(key = "fssFile", info = "The name for external FSS file. By default internal FSS file is used"), + ValueDef(key = "useFSS", type = arrayOf(BOOLEAN)) +) + +/** + * @param source variables:Eo offset,Ein; parameters: "mnu2", "msterile2", "U2" + * @param transmission variables:Ein,Eout; parameters: "A" + * @param resolution variables:Eout,U; parameters: "X", "trap" + */ + +class SterileNeutrinoSpectrum @JvmOverloads constructor( + context: Context, + configuration: Meta, + val source: ParametricBiFunction = NumassBeta(), + val transmission: ParametricBiFunction = NumassTransmission(context, configuration.getMetaOrEmpty("transmission")), + val resolution: ParametricBiFunction = NumassResolution(context, configuration.getMeta("resolution", Meta.empty())) +) : AbstractParametricFunction(*list) { + + + /** + * auxiliary function for trans-res convolution + */ + private val transRes: ParametricBiFunction = TransRes() + private val fss: FSS? = getFSS(context, configuration) + // private boolean useMC; + private val fast: Boolean = configuration.getBoolean("fast", true) + + override fun derivValue(parName: String, u: Double, set: Values): Double { + return when (parName) { + "U2", "msterile2", "mnu2", "E0" -> integrate(u, source.derivative(parName), transRes, set) + "X", "trap" -> integrate(u, source, transRes.derivative(parName), set) + else -> throw NotDefinedException() + } + } + + override fun value(u: Double, set: Values): Double { + return integrate(u, source, transRes, set) + } + + override fun providesDeriv(name: String): Boolean { + return source.providesDeriv(name) && transmission.providesDeriv(name) && resolution.providesDeriv(name) + } + + + /** + * Direct Gauss-Legendre integration + * + * @param u + * @param sourceFunction + * @param transResFunction + * @param set + * @return + */ + private fun integrate( + u: Double, + sourceFunction: ParametricBiFunction, + transResFunction: ParametricBiFunction, + set: Values): Double { + + val eMax = set.getDouble("E0") + 5.0 + + if (u >= eMax) { + return 0.0 + } + + val integrator: UnivariateIntegrator<*> = if (fast) { + when { + eMax - u < 300 -> NumassIntegrator.getFastInterator() + eMax - u > 2000 -> NumassIntegrator.getHighDensityIntegrator() + else -> NumassIntegrator.getDefaultIntegrator() + } + + } else { + NumassIntegrator.getHighDensityIntegrator() + } + + return integrator.integrate(u, eMax) { eIn -> sumByFSS(eIn, sourceFunction, set) * transResFunction.value(eIn, u, set) } + } + + private fun sumByFSS(eIn: Double, sourceFunction: ParametricBiFunction, set: Values): Double { + return if (fss == null) { + sourceFunction.value(0.0, eIn, set) + } else { + (0 until fss.size()).sumByDouble { fss.getP(it) * sourceFunction.value(fss.getE(it), eIn, set) } + } + } + + + + private inner class TransRes : AbstractParametricBiFunction(arrayOf("X", "trap")) { + + override fun providesDeriv(name: String): Boolean { + return true + } + + override fun derivValue(parName: String, eIn: Double, u: Double, set: Values): Double { + return when (parName) { + "X" -> throw NotDefinedException()//TODO implement p0 derivative + "trap" -> lossRes(transmission.derivative(parName), eIn, u, set) + else -> super.derivValue(parName, eIn, u, set) + } + } + + override fun value(eIn: Double, u: Double, set: Values): Double { + + val p0 = LossCalculator.p0(set, eIn) + return p0 * resolution.value(eIn, u, set) + lossRes(transmission, eIn, u, set) + } + + private fun lossRes(transFunc: ParametricBiFunction, eIn: Double, u: Double, set: Values): Double { + val integrand = { eOut: Double -> transFunc.value(eIn, eOut, set) * resolution.value(eOut, u, set) } + + val border = u + 30 + val firstPart = NumassIntegrator.getFastInterator().integrate(u, Math.min(eIn, border), integrand) + val secondPart: Double = if (eIn > border) { + if (fast) { + NumassIntegrator.getDefaultIntegrator().integrate(border, eIn, integrand) + } else { + NumassIntegrator.getHighDensityIntegrator().integrate(border, eIn, integrand) + } + } else { + 0.0 + } + return firstPart + secondPart + } + + } + + companion object { + + private val list = arrayOf("X", "trap", "E0", "mnu2", "msterile2", "U2") + } + +} diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/Bunches.kt b/numass-main/src/main/kotlin/inr/numass/scripts/Bunches.kt new file mode 100644 index 00000000..49e01d4a --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/Bunches.kt @@ -0,0 +1,53 @@ +package inr.numass.scripts + +import hep.dataforge.meta.buildMeta +import inr.numass.actions.TimeAnalyzerAction +import inr.numass.data.NumassGenerator +import inr.numass.data.api.SimpleNumassPoint +import inr.numass.data.generateBlock +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.channels.produce +import kotlinx.coroutines.channels.toList +import kotlinx.coroutines.runBlocking +import java.time.Instant + +fun main() { + val cr = 10.0 + val length = 1e12.toLong() + val num = 60; + + val start = Instant.now() + + val blockchannel = GlobalScope.produce { + (1..num).forEach { + val regularChain = NumassGenerator.generateEvents(cr) + val bunchChain = NumassGenerator.generateBunches(40.0, 0.01, 5.0) + + send(NumassGenerator.mergeEventChains(regularChain, bunchChain).generateBlock(start.plusNanos(it * length), length)) + } + } + + val blocks = runBlocking { + blockchannel.toList() + } + + + val point = SimpleNumassPoint.build(blocks, 10000.0) + + val meta = buildMeta { + "t0" to 1e7 + "t0Step" to 4e6 + "normalize" to false + "t0.crFraction" to 0.5 + } + + println("actual count rate: ${point.events.count().toDouble() / point.length.seconds}") + + TimeAnalyzerAction.simpleRun(point, meta) + +// val res = SmartAnalyzer().analyze(point, meta) +// .getDouble(NumassAnalyzer.COUNT_RATE_KEY) +// +// println("estimated count rate: $res") + +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/Correlation.kt b/numass-main/src/main/kotlin/inr/numass/scripts/Correlation.kt new file mode 100644 index 00000000..dc531ebc --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/Correlation.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2017 Alexander Nozik. + * + * 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 inr.numass.scripts + +import hep.dataforge.buildContext +import hep.dataforge.meta.buildMeta +import inr.numass.NumassPlugin +import inr.numass.data.NumassDataUtils +import inr.numass.data.analyzers.SmartAnalyzer +import inr.numass.data.api.NumassEvent +import inr.numass.data.api.NumassSet +import inr.numass.data.storage.NumassDirectory +import org.apache.commons.math3.stat.correlation.PearsonsCorrelation + + +private fun correlation(sequence: List): Double { + val amplitudes: MutableList = ArrayList() + val times: MutableList = ArrayList() + sequence.forEach { + amplitudes.add(it.amplitude.toDouble()) + times.add(it.timeOffset.toDouble()) + } + + return PearsonsCorrelation().correlation(amplitudes.toDoubleArray(), times.toDoubleArray()) +} + +fun main() { + + val context = buildContext("NUMASS", NumassPlugin::class.java) { + rootDir = "D:\\Work\\Numass\\sterile\\2017_05" + dataDir = "D:\\Work\\Numass\\data\\2017_05" + } + //val rootDir = File("D:\\Work\\Numass\\data\\2017_05\\Fill_2") + + val storage = NumassDirectory.read(context, "Fill_2")!! + + val sets = (2..14).map { "set_$it" } + + val loaders = sets.mapNotNull { set -> + storage.provide("loader::$set", NumassSet::class.java).orElse(null) + } + + val set = NumassDataUtils.join("sum", loaders) + + val analyzer = SmartAnalyzer(); + + val meta = buildMeta { + "window.lo" to 400 + "window.up" to 2500 + } + + println("Correlation between amplitudes and delays:") + set.points.filter { it.voltage < 16000.0 }.forEach { + val cor = correlation(analyzer.getEvents(it, meta)) + println("${it.voltage}: $cor") + } + +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/InversedChain.kt b/numass-main/src/main/kotlin/inr/numass/scripts/InversedChain.kt new file mode 100644 index 00000000..9e795ec6 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/InversedChain.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2017 Alexander Nozik. + * + * 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 inr.numass.scripts + +import hep.dataforge.buildContext +import hep.dataforge.meta.buildMeta +import hep.dataforge.plots.data.DataPlot +import inr.numass.NumassPlugin +import inr.numass.data.NumassDataUtils +import inr.numass.data.analyzers.NumassAnalyzer.Companion.AMPLITUDE_ADAPTER +import inr.numass.data.analyzers.SmartAnalyzer +import inr.numass.data.analyzers.withBinning +import inr.numass.data.api.NumassSet +import inr.numass.data.storage.NumassDirectory +import inr.numass.displayChart + + +fun main() { + + val context = buildContext("NUMASS", NumassPlugin::class.java) { + rootDir = "D:\\Work\\Numass\\sterile\\2017_11" + dataDir = "D:\\Work\\Numass\\data\\2017_11" + } + //val rootDir = File("D:\\Work\\Numass\\data\\2017_05\\Fill_2") + + val storage = NumassDirectory.read(context, "Fill_2")!! + + val sets = (10..24).map { "set_$it" } + + val loaders = sets.mapNotNull { set -> + storage.provide(set, NumassSet::class.java).orElse(null) + } + + val set = NumassDataUtils.join("sum", loaders) + + + val analyzer = SmartAnalyzer() + + val meta = buildMeta { + // "t0" to 30e3 +// "inverted" to true + "window.lo" to 400 + "window.up" to 1600 + } + + val metaForChain = meta.builder.setValue("t0", 15e3).setValue("inverted", false) + + val metaForChainInverted = metaForChain.builder.setValue("inverted", true) + + + for (hv in arrayOf(14000.0, 14500.0, 15000.0, 15500.0, 16050.0)) { + + val frame = displayChart("integral[$hv]").apply { + this.plots.setType() + this.plots.configureValue("showLine", true) + } + + val point = set.optPoint(hv).get() + + frame.add(DataPlot.plot("raw", analyzer.getAmplitudeSpectrum(point, meta).withBinning(20), AMPLITUDE_ADAPTER)) + frame.add(DataPlot.plot("filtered", analyzer.getAmplitudeSpectrum(point, metaForChain).withBinning(20), AMPLITUDE_ADAPTER)) + frame.add(DataPlot.plot("invertedFilter", analyzer.getAmplitudeSpectrum(point, metaForChainInverted).withBinning(20), AMPLITUDE_ADAPTER)) + } +} diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/InversedChainProto.kt b/numass-main/src/main/kotlin/inr/numass/scripts/InversedChainProto.kt new file mode 100644 index 00000000..f32d53db --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/InversedChainProto.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2017 Alexander Nozik. + * + * 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 inr.numass.scripts + +import hep.dataforge.buildContext +import hep.dataforge.description.Descriptors +import hep.dataforge.meta.buildMeta +import hep.dataforge.plots.data.DataPlot +import inr.numass.NumassPlugin +import inr.numass.data.ProtoNumassPoint +import inr.numass.data.analyzers.NumassAnalyzer.Companion.AMPLITUDE_ADAPTER +import inr.numass.data.analyzers.SmartAnalyzer +import inr.numass.data.analyzers.withBinning +import inr.numass.displayChart +import java.nio.file.Paths + + +fun main() { + + val context = buildContext("NUMASS", NumassPlugin::class.java) + + val analyzer = SmartAnalyzer() + + val meta = buildMeta { + "window.lo" to 800 + "window.up" to 5600 + } + + val metaForChain = meta.builder.setValue("t0", 15e3) + + val metaForChainInverted = metaForChain.builder.setValue("inverted", true) + + val point = ProtoNumassPoint.readFile(Paths.get("D:\\Work\\Numass\\data\\2017_05_frames\\Fill_3_events\\set_33\\p36(30s)(HV1=17000).df")) + + val frame = displayChart("integral").apply { + this.plots.descriptor = Descriptors.forType("plot", DataPlot::class) + this.plots.configureValue("showLine", true) + } + + frame.add(DataPlot.plot("raw", analyzer.getAmplitudeSpectrum(point, meta).withBinning(80), AMPLITUDE_ADAPTER)) + frame.add(DataPlot.plot("filtered", analyzer.getAmplitudeSpectrum(point, metaForChain).withBinning(80), AMPLITUDE_ADAPTER)) + frame.add(DataPlot.plot("invertedFilter", analyzer.getAmplitudeSpectrum(point, metaForChainInverted).withBinning(80), AMPLITUDE_ADAPTER)) + +} diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/PileupIntensityDependency.kt b/numass-main/src/main/kotlin/inr/numass/scripts/PileupIntensityDependency.kt new file mode 100644 index 00000000..e1daa6ed --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/PileupIntensityDependency.kt @@ -0,0 +1,117 @@ +/* + * Copyright 2017 Alexander Nozik. + * + * 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 inr.numass.scripts + +import hep.dataforge.buildContext +import hep.dataforge.meta.buildMeta +import hep.dataforge.nullable +import hep.dataforge.plots.PlotGroup +import hep.dataforge.plots.data.DataPlot +import hep.dataforge.tables.ColumnTable +import inr.numass.NumassPlugin +import inr.numass.data.analyzers.* +import inr.numass.data.analyzers.NumassAnalyzer.Companion.AMPLITUDE_ADAPTER +import inr.numass.data.api.NumassSet +import inr.numass.data.storage.NumassDirectory +import inr.numass.displayChart + + +fun main() { + + val context = buildContext("NUMASS", NumassPlugin::class.java) { + rootDir = "D:\\Work\\Numass\\sterile\\2017_11" + dataDir = "D:\\Work\\Numass\\data\\2017_11" + } + //val rootDir = File("D:\\Work\\Numass\\data\\2017_05\\Fill_2") + + val storage = NumassDirectory.read(context, "Fill_3b")!! + + val sets = listOf("set_2", "set_17") + + + val analyzer = SmartAnalyzer() + + val meta = buildMeta { + // "t0" to 30e3 +// "inverted" to true + "window.lo" to 400 + "window.up" to 1600 + } + + val t0 = 15e3 + + val metaForChain = meta.builder.apply { + setValue("t0", t0) + setValue("inverted", false) + } + + val metaForChainInverted = metaForChain.builder.setValue("inverted", true) + + val hv = 14000.0 + + val frame = displayChart("integral[$hv]").apply { + this.plots.setType() + this.plots.configureValue("showLine", true) + } + + val normalizedFrame = displayChart("normalized[$hv]").apply { + this.plots.setType() + this.plots.configureValue("showLine", true) + } + + sets.forEach { setName -> + val set = storage.provide(setName, NumassSet::class.java).nullable ?: error("Set does not exist") + + val point = set.optPoint(hv).get() + + val group = PlotGroup(setName) + frame.add(group) + + val rawSpectrum = analyzer.getAmplitudeSpectrum(point, meta).withBinning(20) + group.add(DataPlot.plot("raw", rawSpectrum, AMPLITUDE_ADAPTER)) + + val rawNorm = rawSpectrum.getColumn(NumassAnalyzer.COUNT_RATE_KEY).maxBy { it.double }!!.double + val normalizedSpectrum = ColumnTable.copy(rawSpectrum) + .replaceColumn(NumassAnalyzer.COUNT_RATE_KEY) { it.getDouble(NumassAnalyzer.COUNT_RATE_KEY) / rawNorm } + normalizedFrame.add(DataPlot.plot("${setName}_raw", normalizedSpectrum, AMPLITUDE_ADAPTER)) + + + println("[$setName] Raw spectrum integral: ${rawSpectrum.getColumn(NumassAnalyzer.COUNT_RATE_KEY).sumByDouble { it.double }}") + + group.add(DataPlot.plot("filtered", analyzer.getAmplitudeSpectrum(point, metaForChain).withBinning(20), AMPLITUDE_ADAPTER)) + + val filteredSpectrum = analyzer.getAmplitudeSpectrum(point, metaForChainInverted).withBinning(20) + group.add(DataPlot.plot("invertedFilter", filteredSpectrum, AMPLITUDE_ADAPTER)) + + val filteredNorm = filteredSpectrum.getColumn(NumassAnalyzer.COUNT_RATE_KEY).maxBy { it.double }!!.double + val normalizedFilteredSpectrum = ColumnTable.copy(filteredSpectrum) + .replaceColumn(NumassAnalyzer.COUNT_RATE_KEY) { it.getDouble(NumassAnalyzer.COUNT_RATE_KEY) / filteredNorm } + + normalizedFrame.add(DataPlot.plot(setName, normalizedFilteredSpectrum, AMPLITUDE_ADAPTER)) + + val sequence = TimeAnalyzer() + .getEventsWithDelay(point, metaForChainInverted) + .filter { pair -> pair.second <= t0 } + .map { it.first } + + val pileupSpectrum = sequence.getAmplitudeSpectrum(point.length.toMillis().toDouble() / 1000.0).withBinning(20) + + group.add(DataPlot.plot("pileup", pileupSpectrum, AMPLITUDE_ADAPTER)) + + println("[$setName] Pileup spectrum integral: ${pileupSpectrum.getColumn(NumassAnalyzer.COUNT_RATE_KEY).sumByDouble { it.double }}") + } +} diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/PileupIntensityDependencyGun.kt b/numass-main/src/main/kotlin/inr/numass/scripts/PileupIntensityDependencyGun.kt new file mode 100644 index 00000000..2afacec2 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/PileupIntensityDependencyGun.kt @@ -0,0 +1,119 @@ +/* + * Copyright 2017 Alexander Nozik. + * + * 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 inr.numass.scripts + +import hep.dataforge.buildContext +import hep.dataforge.meta.buildMeta +import hep.dataforge.nullable +import hep.dataforge.plots.PlotGroup +import hep.dataforge.plots.data.DataPlot +import hep.dataforge.tables.ColumnTable +import inr.numass.NumassPlugin +import inr.numass.data.analyzers.* +import inr.numass.data.analyzers.NumassAnalyzer.Companion.AMPLITUDE_ADAPTER +import inr.numass.data.api.NumassSet +import inr.numass.data.storage.NumassDirectory +import inr.numass.displayChart + +/** + * Investigation of gun data for time chain anomaliese + */ +fun main() { + + val context = buildContext("NUMASS", NumassPlugin::class.java) { + rootDir = "D:\\Work\\Numass\\sterile\\2017_11" + dataDir = "D:\\Work\\Numass\\data\\2017_11" + } + //val rootDir = File("D:\\Work\\Numass\\data\\2017_05\\Fill_2") + + val storage = NumassDirectory.read(context, "Adiabacity_19")!! + + val sets = listOf("set_2") + + + val analyzer = SmartAnalyzer() + + val meta = buildMeta { + // "t0" to 30e3 +// "inverted" to true + "window.lo" to 400 + "window.up" to 2600 + } + + val t0 = 15e3 + + val metaForChain = meta.builder.apply { + setValue("t0", t0) + setValue("inverted", false) + } + + val metaForChainInverted = metaForChain.builder.setValue("inverted", true) + + val hv = 18000.0 + + val frame = displayChart("integral[$hv]").apply { + this.plots.setType() + this.plots.configureValue("showLine", true) + } + + val normalizedFrame = displayChart("normalized[$hv]").apply { + this.plots.setType() + this.plots.configureValue("showLine", true) + } + + sets.forEach { setName -> + val set = storage.provide(setName, NumassSet::class.java).nullable ?: error("Set does not exist") + + val point = set.optPoint(hv).get() + + val group = PlotGroup(setName) + frame.add(group) + + val rawSpectrum = analyzer.getAmplitudeSpectrum(point, meta).withBinning(20) + group.add(DataPlot.plot("raw", rawSpectrum, AMPLITUDE_ADAPTER)) + + val rawNorm = rawSpectrum.getColumn(NumassAnalyzer.COUNT_RATE_KEY).maxBy { it.double }!!.double + val normalizedSpectrum = ColumnTable.copy(rawSpectrum) + .replaceColumn(NumassAnalyzer.COUNT_RATE_KEY) { it.getDouble(NumassAnalyzer.COUNT_RATE_KEY) / rawNorm } + normalizedFrame.add(DataPlot.plot("${setName}_raw", normalizedSpectrum, AMPLITUDE_ADAPTER)) + + + println("[$setName] Raw spectrum integral: ${rawSpectrum.getColumn(NumassAnalyzer.COUNT_RATE_KEY).sumByDouble { it.double }}") + + group.add(DataPlot.plot("filtered", analyzer.getAmplitudeSpectrum(point, metaForChain).withBinning(20), AMPLITUDE_ADAPTER)) + + val filteredSpectrum = analyzer.getAmplitudeSpectrum(point, metaForChainInverted).withBinning(20) + group.add(DataPlot.plot("invertedFilter", filteredSpectrum, AMPLITUDE_ADAPTER)) + + val filteredNorm = filteredSpectrum.getColumn(NumassAnalyzer.COUNT_RATE_KEY).maxBy { it.double }!!.double + val normalizedFilteredSpectrum = ColumnTable.copy(filteredSpectrum) + .replaceColumn(NumassAnalyzer.COUNT_RATE_KEY) { it.getDouble(NumassAnalyzer.COUNT_RATE_KEY) / filteredNorm } + + normalizedFrame.add(DataPlot.plot(setName, normalizedFilteredSpectrum, AMPLITUDE_ADAPTER)) + + val sequence = TimeAnalyzer() + .getEventsWithDelay(point, metaForChainInverted) + .filter { pair -> pair.second <= t0 } + .map { it.first } + + val pileupSpectrum = sequence.getAmplitudeSpectrum(point.length.toMillis().toDouble() / 1000.0).withBinning(20) + + group.add(DataPlot.plot("pileup", pileupSpectrum, AMPLITUDE_ADAPTER)) + + println("[$setName] Pileup spectrum integral: ${pileupSpectrum.getColumn(NumassAnalyzer.COUNT_RATE_KEY).sumByDouble { it.double }}") + } +} diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/PlotResiduals.kt b/numass-main/src/main/kotlin/inr/numass/scripts/PlotResiduals.kt new file mode 100644 index 00000000..141df354 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/PlotResiduals.kt @@ -0,0 +1,79 @@ +package inr.numass.scripts + +import hep.dataforge.buildContext +import hep.dataforge.fx.output.FXOutputManager +import hep.dataforge.meta.buildMeta +import hep.dataforge.plots.jfreechart.JFreeChartPlugin +import hep.dataforge.stat.fit.ParamSet +import inr.numass.NumassPlugin +import inr.numass.data.NumassDataUtils +import inr.numass.data.analyzers.SmartAnalyzer +import inr.numass.data.api.NumassSet +import inr.numass.data.storage.NumassDirectory +import inr.numass.models.NBkgSpectrum +import inr.numass.models.sterile.SterileNeutrinoSpectrum + +fun main() { + + val context = buildContext("NUMASS", NumassPlugin::class.java, JFreeChartPlugin::class.java) { + output = FXOutputManager() + rootDir = "D:\\Work\\Numass\\sterile\\2017_11" + dataDir = "D:\\Work\\Numass\\data\\2017_11" + } + + val modelMeta = buildMeta("model") { + "modelName" to "sterile" + "resolution" to { + "width" to 8.3e-5 + "tail" to "function::numass.resolutionTail.2017.mod" + } + "transmission" to { + "trapping" to "function::numass.trap.nominal" + } + } + + val spectrum = NBkgSpectrum(SterileNeutrinoSpectrum(context, modelMeta)) + + val params = ParamSet().apply { + setPar("N", 676844.0, 6.0, 0.0, Double.POSITIVE_INFINITY) + setPar("bkg", 2.0, 0.03) + setPar("E0", 18575.0, 1.0) + setPar("mnu2", 0.0, 1.0) + setParValue("msterile2", (1000 * 1000).toDouble()) + setPar("U2", 0.0, 1e-3) + setPar("X", 0.1, 0.01) + setPar("trap", 1.0, 0.01) + } + /* + 'N' = 676844 ± 8.5e+02 (0.00000,Infinity) +'L' = 0 ± 1.0 +'Q' = 0 ± 1.0 +'bkg' = 0.0771 ± 0.065 +'E0' = 18561.44 ± 0.77 +'mnu2' = 0.00 ± 0.010 +'msterile2' = 1000000.00 ± 1.0 +'U2' = 0.000 ± 0.0010 +'X' = 0.05000 ± 0.010 (0.00000,Infinity) +'trap' = 1.000 ± 0.050 + */ + + + val storage = NumassDirectory.read(context, "Fill_2")!! + + val sets = (36..42).map { "set_$it" } + + val loaders = sets.mapNotNull { set -> + storage.provide(set, NumassSet::class.java).orElse(null) + } + + val set = NumassDataUtils.join("sum", loaders) + + + val analyzer = SmartAnalyzer() + + val meta = buildMeta { + "t0" to 15e3 + "window.lo" to 450 + "window.up" to 3000 + } +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/PointSliceSpectrum.kt b/numass-main/src/main/kotlin/inr/numass/scripts/PointSliceSpectrum.kt new file mode 100644 index 00000000..69514a19 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/PointSliceSpectrum.kt @@ -0,0 +1,72 @@ +package inr.numass.scripts + +import hep.dataforge.buildContext +import hep.dataforge.meta.buildMeta +import hep.dataforge.nullable +import hep.dataforge.plots.PlotGroup +import hep.dataforge.plots.data.DataPlot +import inr.numass.NumassPlugin +import inr.numass.data.analyzers.NumassAnalyzer +import inr.numass.data.analyzers.SmartAnalyzer +import inr.numass.data.analyzers.withBinning +import inr.numass.data.api.NumassSet +import inr.numass.data.storage.NumassDirectory +import inr.numass.displayChart + +/** + * Investigating slices of single point for differences at the beginning and end + */ +fun main() { + val context = buildContext("NUMASS", NumassPlugin::class.java) { + rootDir = "D:\\Work\\Numass\\sterile\\2017_05" + dataDir = "D:\\Work\\Numass\\data\\2017_05_frames" + } + //val rootDir = File("D:\\Work\\Numass\\data\\2017_05\\Fill_2") + + val storage = NumassDirectory.read(context, "Fill_3") ?: error("Storage not found") + + + val analyzer = SmartAnalyzer() + + val meta = buildMeta { + "t0" to 15e3 +// "window.lo" to 400 +// "window.up" to 1600 + } + + val set = storage.provide("set_4", NumassSet::class.java).nullable ?: error("Set does not exist") + + val frame = displayChart("slices").apply { + plots.setType() + plots.configureValue("showLine", true) + } + + listOf(10, 58, 103).forEach { index -> + val group = PlotGroup("point_$index") + group.setType() + val point = set.find { it.index == index } ?: error("Point not found") + + +// val blockSizes = point.meta.getValue("events").list.map { it.int } +// val startTimes = point.meta.getValue("start_time").list.map { it.time } + + group.add(DataPlot.plot("spectrum", analyzer.getAmplitudeSpectrum(point, meta).withBinning(32), NumassAnalyzer.AMPLITUDE_ADAPTER)) + +// runBlocking { +// val events = point.events.toList() +// var startIndex = 0 +// val blocks = blockSizes.zip(startTimes).map { (size, startTime) -> +// SimpleBlock.produce(startTime, Duration.ofSeconds(5)) { +// events.subList(startIndex, startIndex + size) +// }.also { startIndex += size } +// } +// +// blocks.forEachIndexed { index, block -> +// group.add(DataPlot.plot("block_$index", analyzer.getAmplitudeSpectrum(block).withBinning(20), NumassAnalyzer.AMPLITUDE_ADAPTER) { +// "visible" to false +// }) +// } +// } + frame.add(group) + } +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/analysis/Run2017_05.kt b/numass-main/src/main/kotlin/inr/numass/scripts/analysis/Run2017_05.kt new file mode 100644 index 00000000..19808a85 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/analysis/Run2017_05.kt @@ -0,0 +1,20 @@ +package inr.numass.scripts.analysis + +import hep.dataforge.context.Global +import hep.dataforge.fx.output.FXOutputManager +import hep.dataforge.workspace.FileBasedWorkspace +import java.io.File + +fun main() { + FXOutputManager().startGlobal() + + val configPath = File("D:\\Work\\Numass\\sterile2017_05\\workspace.groovy").toPath() + val workspace = FileBasedWorkspace.build(Global, configPath) + workspace.context.setValue("cache.enabled", false) + + //val meta = workspace.getTarget("group_3") + + val result = workspace.runTask("fit", "group_5").first().get() + println("Complete!") + +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/analysis/Run2017_05_frames.kt b/numass-main/src/main/kotlin/inr/numass/scripts/analysis/Run2017_05_frames.kt new file mode 100644 index 00000000..ec6970c0 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/analysis/Run2017_05_frames.kt @@ -0,0 +1,20 @@ +package inr.numass.scripts.analysis + +import hep.dataforge.context.Global +import hep.dataforge.fx.output.FXOutputManager +import hep.dataforge.workspace.FileBasedWorkspace +import java.io.File + +fun main() { + FXOutputManager().startGlobal() + + val configPath = File("D:\\Work\\Numass\\sterile2017_05_frames\\workspace.groovy").toPath() + val workspace = FileBasedWorkspace.build(Global, configPath) + workspace.context.setValue("cache.enabled", false) + + //val meta = workspace.getTarget("group_3") + + val result = workspace.runTask("fit", "group_5").first().get() + println("Complete!") + +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/analysis/Run2017_11.kt b/numass-main/src/main/kotlin/inr/numass/scripts/analysis/Run2017_11.kt new file mode 100644 index 00000000..ba21ce1c --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/analysis/Run2017_11.kt @@ -0,0 +1,39 @@ +package inr.numass.scripts.analysis + +import hep.dataforge.context.Global +import hep.dataforge.fx.output.FXOutputManager +import hep.dataforge.plots.plotData +import hep.dataforge.tables.Adapters +import hep.dataforge.tables.Table +import hep.dataforge.workspace.FileBasedWorkspace +import inr.numass.displayChart +import java.io.File + +fun main() { + +// val context = buildContext("NUMASS", NumassPlugin::class.java, JFreeChartPlugin::class.java) { +// output = FXOutputManager() +// rootDir = "D:\\Work\\Numass\\sterile2017_11" +// dataDir = "D:\\Work\\Numass\\data\\2017_11" +// properties { +// "cache.enabled" to false +// } +// } + + FXOutputManager().startGlobal() + + + val configPath = File("D:\\Work\\Numass\\sterile2017_11\\workspace.groovy").toPath() + + val workspace = FileBasedWorkspace.build(Global, configPath) + + workspace.context.setValue("cache.enabled", false) + + val result = workspace.runTask("dif", "adiab_19").first().get() as Table + + displayChart("Adiabacity").apply { + plotData("Adiabacity_19", result, Adapters.buildXYAdapter("voltage", "cr")) + } + + println("Complete!") +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/analysis/Run2019_11.kt b/numass-main/src/main/kotlin/inr/numass/scripts/analysis/Run2019_11.kt new file mode 100644 index 00000000..644de20c --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/analysis/Run2019_11.kt @@ -0,0 +1,38 @@ +package inr.numass.scripts.analysis + +import hep.dataforge.context.Global +import hep.dataforge.fx.output.FXOutputManager +import hep.dataforge.plots.plotData +import hep.dataforge.tables.Adapters +import hep.dataforge.tables.Table +import hep.dataforge.workspace.FileBasedWorkspace +import inr.numass.displayChart +import java.io.File + +fun main() { + +// val context = buildContext("NUMASS", NumassPlugin::class.java, JFreeChartPlugin::class.java) { +// output = FXOutputManager() +// rootDir = "D:\\Work\\Numass\\sterile2017_11" +// dataDir = "D:\\Work\\Numass\\data\\2017_11" +// properties { +// "cache.enabled" to false +// } +// } + + FXOutputManager().startGlobal() + + val configPath = File("D:\\Work\\Numass\\sterile2019_11\\workspace.groovy").toPath() + + val workspace = FileBasedWorkspace.build(Global, configPath) + + workspace.context.setValue("cache.enabled", false) + + val result = workspace.runTask("dif", "adiab_19").first().get() as Table + + displayChart("Adiabacity").apply { + plotData("Adiabacity_19", result, Adapters.buildXYAdapter("voltage", "cr")) + } + + println("Complete!") +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/analysis/Run2020_12.kt b/numass-main/src/main/kotlin/inr/numass/scripts/analysis/Run2020_12.kt new file mode 100644 index 00000000..3c5300b1 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/analysis/Run2020_12.kt @@ -0,0 +1,44 @@ +package inr.numass.scripts.analysis + +import hep.dataforge.context.Global +import hep.dataforge.context.plugin +import hep.dataforge.fx.output.FXOutputManager +import hep.dataforge.grind.workspace.GroovyWorkspaceParser +import hep.dataforge.plots.plotData +import hep.dataforge.tables.Adapters +import hep.dataforge.tables.Table +import hep.dataforge.workspace.FileBasedWorkspace +import hep.dataforge.workspace.Workspace +import hep.dataforge.workspace.context +import hep.dataforge.workspace.data +import inr.numass.displayChart +import java.io.File + +fun main() { + +// val context = buildContext("NUMASS", NumassPlugin::class.java, JFreeChartPlugin::class.java) { +// output = FXOutputManager() +// rootDir = "D:\\Work\\Numass\\sterile2017_11" +// dataDir = "D:\\Work\\Numass\\data\\2017_11" +// properties { +// "cache.enabled" to false +// } +// } + FXOutputManager().startGlobal() + + val configPath = File("D:\\Work\\Numass\\sterile2020_12\\workspace.groovy").toPath() + + val workspace = FileBasedWorkspace.build(Global, configPath) + + //workspace.context.setValue("cache.enabled", false) + + workspace.runTask("fit","Fill_4") + +// val result = workspace.runTask("dif", "adiab_19").first().get() as Table +// +// displayChart("Adiabacity").apply { +// plotData("Adiabacity_19", result, Adapters.buildXYAdapter("voltage", "cr")) +// } +// +// println("Complete!") +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/models/DifferentialSpectrum.kt b/numass-main/src/main/kotlin/inr/numass/scripts/models/DifferentialSpectrum.kt new file mode 100644 index 00000000..25e71da8 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/models/DifferentialSpectrum.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 inr.numass.scripts.models + +import hep.dataforge.buildContext +import hep.dataforge.description.Descriptors +import hep.dataforge.meta.buildMeta +import hep.dataforge.plots.data.DataPlot +import hep.dataforge.tables.replaceColumn +import inr.numass.NumassPlugin +import inr.numass.data.NumassDataUtils +import inr.numass.data.analyzers.NumassAnalyzer +import inr.numass.data.analyzers.SmartAnalyzer +import inr.numass.data.analyzers.subtractAmplitudeSpectrum +import inr.numass.data.analyzers.withBinning +import inr.numass.data.api.NumassSet +import inr.numass.data.storage.NumassDirectory +import inr.numass.displayChart + +fun main() { + + val context = buildContext("NUMASS", NumassPlugin::class.java) { + rootDir = "D:\\Work\\Numass\\sterile\\2017_11" + dataDir = "D:\\Work\\Numass\\data\\2017_11" + } + //val rootDir = File("D:\\Work\\Numass\\data\\2017_05\\Fill_2") + + val storage = NumassDirectory.read(context, "Fill_2")!! + + val sets = (1..24).map { "set_$it" } + + val loaders = sets.mapNotNull { set -> + storage.provide("loader::$set", NumassSet::class.java).orElse(null) + } + + val analyzer = SmartAnalyzer() + + val all = NumassDataUtils.join("sum", loaders) + + + val meta = buildMeta { + "t0" to 30e3 + "inverted" to true + "window.lo" to 400 + "window.up" to 1600 + } + + val frame = displayChart("differential").apply { + this.plots.descriptor = Descriptors.forType("plot", DataPlot::class) + this.plots.configureValue("showLine", true) + } + + val integralFrame = displayChart("integral") + + for (hv in arrayOf(14000.0, 14500.0, 15000.0, 15500.0, 16050.0)) { + val point1 = all.optPoint(hv).get() + + val point0 = all.optPoint(hv + 200.0).get() + + with(NumassAnalyzer) { + + val spectrum1 = analyzer.getAmplitudeSpectrum(point1, meta).withBinning(50) + + val spectrum0 = analyzer.getAmplitudeSpectrum(point0, meta).withBinning(50) + + val res = subtractAmplitudeSpectrum(spectrum1, spectrum0) + + val norm = res.getColumn(COUNT_RATE_KEY).stream().mapToDouble { it.double }.sum() + + integralFrame.add(DataPlot.plot("point_$hv", spectrum0, AMPLITUDE_ADAPTER)) + + frame.add(DataPlot.plot("point_$hv", res.replaceColumn(COUNT_RATE_KEY) { getDouble(COUNT_RATE_KEY) / norm }, AMPLITUDE_ADAPTER)) + } + } +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/models/IntegralEndpoint.kt b/numass-main/src/main/kotlin/inr/numass/scripts/models/IntegralEndpoint.kt new file mode 100644 index 00000000..1e28837f --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/models/IntegralEndpoint.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 inr.numass.scripts.models + +import hep.dataforge.buildContext +import hep.dataforge.configure +import hep.dataforge.fx.output.FXOutputManager +import hep.dataforge.plots.Plot +import hep.dataforge.plots.data.DataPlot +import hep.dataforge.plots.jfreechart.JFreeChartPlugin +import hep.dataforge.stat.fit.ParamSet +import hep.dataforge.step +import inr.numass.NumassPlugin +import inr.numass.displayChart +import inr.numass.models.sterile.NumassBeta + +fun main() { + val context = buildContext("NUMASS", NumassPlugin::class.java, JFreeChartPlugin::class.java) { + output = FXOutputManager() + } + + val spectrum = NumassBeta()//NBkgSpectrum(SterileNeutrinoSpectrum(context, Meta.empty())) + + val t = 30 * 50 // time in seconds per point + + val params = ParamSet().apply { + setPar("N", 8e5, 6.0, 0.0, Double.POSITIVE_INFINITY) + setPar("bkg", 0.0, 0.03) + setPar("E0", 18575.0, 1.0) + setPar("mnu2", 0.0, 1.0) + setParValue("msterile2", (2.6 * 2.6)) + setPar("U2", 0.0, 1e-3) + setPar("X", 0.0, 0.01) + setPar("trap", 0.0, 0.01) + } + + fun ParamSet.update(vararg override: Pair): ParamSet = this.copy().also { set -> + override.forEach { + set.setParValue(it.first, it.second) + } + } + + fun plotSpectrum(name: String, vararg override: Pair): Plot { + val x = (18569.0..18575.0).step(0.2).toList() + val y = x.map { 1e12*spectrum.value(0.0,it, params.update(*override)) } + return DataPlot.plot(name, x.toDoubleArray(), y.toDoubleArray()) + } + + val frame = displayChart("Light neutrinos", context = context).apply { + plots.setType() + plots.configure { + "showSymbol" to false + "showLine" to true + "showErrors" to false + "thickness" to 2 + } + } + + frame.add(plotSpectrum("zero mass")) + frame.add(plotSpectrum("active neutrino 2 ev", "mnu2" to 2.0)) + frame.add(plotSpectrum("sterile neutrino 2.6 ev", "U2" to 0.4).apply { configureValue("color", "red") }) + frame.add(plotSpectrum("sterile neutrino 2.6 ev, 0.09", "U2" to 0.09).apply { configureValue("color", "brown") }) + +} diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/models/IntegralSpectrum.kt b/numass-main/src/main/kotlin/inr/numass/scripts/models/IntegralSpectrum.kt new file mode 100644 index 00000000..1b3c5ff7 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/models/IntegralSpectrum.kt @@ -0,0 +1,166 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 inr.numass.scripts.models + +import hep.dataforge.buildContext +import hep.dataforge.configure +import hep.dataforge.fx.output.FXOutputManager +import hep.dataforge.io.output.stream +import hep.dataforge.meta.Meta +import hep.dataforge.plots.Plot +import hep.dataforge.plots.data.DataPlot +import hep.dataforge.plots.jfreechart.JFreeChartPlugin +import hep.dataforge.plots.output.plotFrame +import hep.dataforge.stat.fit.FitManager +import hep.dataforge.stat.fit.FitStage +import hep.dataforge.stat.fit.FitState +import hep.dataforge.stat.fit.ParamSet +import hep.dataforge.stat.models.XYModel +import hep.dataforge.step +import hep.dataforge.tables.Adapters.X_AXIS +import hep.dataforge.values.ValueMap +import inr.numass.NumassPlugin +import inr.numass.data.SpectrumAdapter +import inr.numass.data.SpectrumGenerator +import inr.numass.models.NBkgSpectrum +import inr.numass.models.sterile.SterileNeutrinoSpectrum +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.io.PrintWriter +import kotlin.math.sqrt + + +fun main() { + + val context = buildContext("NUMASS", NumassPlugin::class.java, JFreeChartPlugin::class.java) { + output = FXOutputManager() + } + + val spectrum = NBkgSpectrum(SterileNeutrinoSpectrum(context, Meta.empty())) + + val t = 30 * 50 // time in seconds per point + + val params = ParamSet().apply { + setPar("N", 8e5, 6.0, 0.0, Double.POSITIVE_INFINITY) + setPar("bkg", 2.0, 0.03) + setPar("E0", 18575.0, 1.0) + setPar("mnu2", 0.0, 1.0) + setParValue("msterile2", (1000 * 1000).toDouble()) + setPar("U2", 1e-2, 1e-3) + setPar("X", 0.1, 0.01) + setPar("trap", 1.0, 0.01) + } + + fun ParamSet.update(vararg override: Pair): ParamSet = this.copy().also { set -> + override.forEach { + set.setParValue(it.first, it.second) + } + } + + (14000.0..18600.0 step 10.0).forEach { + println("$it\t${spectrum.value(it,params)}") + } + + fun plotSpectrum(name: String, vararg override: Pair): Plot { + val x = (14000.0..18600.0).step(100.0).toList() + val y = x.map { spectrum.value(it, params.update(*override)) } + return DataPlot.plot(name, x.toDoubleArray(), y.toDoubleArray()) + } + + fun plotResidual(name: String, vararg override: Pair): Plot { + val paramsMod = params.update(*override) + + val x = (14000.0..18600.0).step(100.0).toList() + val y = x.map { + val base = spectrum.value(it, params) + val mod = spectrum.value(it, paramsMod) + val err = sqrt(base / t) + (mod - base) / err + } + return DataPlot.plot(name, x.toDoubleArray(), y.toDoubleArray()) + } + + val adapter = SpectrumAdapter(Meta.empty()) + val fm = context.getOrLoad(FitManager::class.java) + + fun plotFitResidual(name: String, vararg override: Pair): Plot { + val paramsMod = params.update(*override) + + val x = (14000.0..18400.0).step(100.0).toList() + + val model = XYModel(Meta.empty(), adapter, spectrum) + + val generator = SpectrumGenerator(model, paramsMod, 12316); + val configuration = x.map { ValueMap.ofPairs(X_AXIS to it, "time" to t) } + val data = generator.generateData(configuration); + +// val table = ListTable.Builder(Adapters.getFormat(adapter)).apply { +// x.forEach { u -> +// row(adapter.buildSpectrumDataPoint(u, t * spectrum.value(u, paramsMod).toLong(), t.toDouble())) +// } +// }.build() + + + val state = FitState(data, model, params) + val res = fm.runStage(state, "QOW", FitStage.TASK_RUN, "N", "E0", "bkg") + + res.printState(PrintWriter(System.out)) + res.printState(PrintWriter(context.output["fitResult", name].stream)) + //context.output["fitResult",name].stream + + val y = x.map { u -> + val base = spectrum.value(u, params) + val mod = spectrum.value(u, res.parameters) + val err = sqrt(base / t) + (mod - base) / err + } + return DataPlot.plot(name, x.toDoubleArray(), y.toDoubleArray()) + } + + + context.plotFrame("fit", stage = "plots") { + plots.configure { + "showLine" to true + "showSymbol" to false + "showErrors" to false + "thickness" to 4.0 + } + plots.setType() + +plotResidual("trap", "trap" to 0.99) + context.launch(Dispatchers.Main) { + try { + +plotFitResidual("trap_fit", "trap" to 0.99) + } catch (ex: Exception) { + context.logger.error("Failed to do fit", ex) + } + } + +plotResidual("X", "X" to 0.11) + context.launch(Dispatchers.Main) { + +plotFitResidual("X_fit", "X" to 0.11) + } + +plotResidual("sterile_1", "U2" to 1e-3) + context.launch(Dispatchers.Main) { + +plotFitResidual("sterile_1_fit", "U2" to 1e-3) + } + +plotResidual("sterile_3", "msterile2" to (3000 * 3000).toDouble(), "U2" to 1e-3) + context.launch(Dispatchers.Main) { + +plotFitResidual("sterile_3_fit", "msterile2" to (3000 * 3000).toDouble(), "U2" to 1e-3) + } + + } + +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/models/ResolutionDemo.kt b/numass-main/src/main/kotlin/inr/numass/scripts/models/ResolutionDemo.kt new file mode 100644 index 00000000..0495bbe0 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/models/ResolutionDemo.kt @@ -0,0 +1,57 @@ +package inr.numass.scripts.models + +import hep.dataforge.context.Global +import hep.dataforge.fx.output.FXOutputManager +import hep.dataforge.meta.buildMeta +import hep.dataforge.plots.data.DataPlot +import hep.dataforge.plots.jfreechart.JFreeChartPlugin +import hep.dataforge.stat.fit.ParamSet +import hep.dataforge.step +import inr.numass.NumassPlugin +import inr.numass.displayChart +import inr.numass.models.NBkgSpectrum +import inr.numass.models.sterile.SterileNeutrinoSpectrum + +fun main() { + NumassPlugin().startGlobal() + JFreeChartPlugin().startGlobal() + Global.output = FXOutputManager() + + + + val params = ParamSet().apply { + setPar("N", 8e5, 6.0, 0.0, Double.POSITIVE_INFINITY) + setPar("bkg", 2.0, 0.03) + setPar("E0", 18575.0, 1.0) + setPar("mnu2", 0.0, 1.0) + setParValue("msterile2", (1000 * 1000).toDouble()) + setPar("U2", 0.0, 1e-3) + setPar("X", 0.0, 0.01) + setPar("trap", 1.0, 0.01) + } + + + + + val meta1 = buildMeta { + "resolution.A" to 8.3e-5 + } + val spectrum1 = NBkgSpectrum(SterileNeutrinoSpectrum(Global, meta1)) + + val meta2 = buildMeta { + "resolution.A" to 0 + } + val spectrum2 = NBkgSpectrum(SterileNeutrinoSpectrum(Global, meta2)) + + displayChart("compare").apply { + val x = (14000.0..18600.0).step(100.0).toList() + val y1 = x.map { spectrum1.value(it, params) } + +DataPlot.plot("simple", x.toDoubleArray(), y1.toDoubleArray()) + val y2 = x.map { spectrum2.value(it, params) } + +DataPlot.plot("normal", x.toDoubleArray(), y2.toDoubleArray()) + val dif = x.mapIndexed{ index, _ -> 1 - y1[index]/y2[index] } + +DataPlot.plot("dif", x.toDoubleArray(), dif.toDoubleArray()) + } + + +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/models/demo.kt b/numass-main/src/main/kotlin/inr/numass/scripts/models/demo.kt new file mode 100644 index 00000000..c101a4e0 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/models/demo.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 inr.numass.scripts.models + +import hep.dataforge.buildContext +import hep.dataforge.fx.output.FXOutputManager +import hep.dataforge.meta.Meta +import hep.dataforge.plots.jfreechart.JFreeChartPlugin +import hep.dataforge.stat.fit.ParamSet +import hep.dataforge.step +import inr.numass.NumassPlugin +import inr.numass.models.NBkgSpectrum +import inr.numass.models.misc.LossCalculator +import inr.numass.models.sterile.NumassTransmission +import inr.numass.models.sterile.SterileNeutrinoSpectrum +import kotlin.system.measureTimeMillis + + +fun main() { + + val context = buildContext("NUMASS", NumassPlugin::class.java, JFreeChartPlugin::class.java) { + output = FXOutputManager() + } + + val spectrum = NBkgSpectrum(SterileNeutrinoSpectrum(context, Meta.empty())) + + val params = ParamSet().apply { + setPar("N", 8e5, 6.0, 0.0, Double.POSITIVE_INFINITY) + setPar("bkg", 2.0, 0.03) + setPar("E0", 18575.0, 1.0) + setPar("mnu2", 0.0, 1.0) + setParValue("msterile2", (1000 * 1000).toDouble()) + setPar("U2", 1e-2, 1e-3) + setPar("X", 0.1, 0.01) + setPar("trap", 1.0, 0.01) + } + + println(spectrum.value(14000.0, params)) + val spectrumTime = measureTimeMillis { + (14000.0..18600.0 step 10.0).forEach { + println("$it\t${spectrum.value(it,params)}") + } + } + println("Spectrum with 460 points computed in $spectrumTime millis") +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/threshold/FitAllWithPower.kt b/numass-main/src/main/kotlin/inr/numass/scripts/threshold/FitAllWithPower.kt new file mode 100644 index 00000000..59d061b7 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/threshold/FitAllWithPower.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 inr.numass.scripts.threshold + +import hep.dataforge.buildContext +import hep.dataforge.fx.output.FXOutputManager +import hep.dataforge.io.DirectoryOutput +import hep.dataforge.io.plus +import hep.dataforge.io.render +import hep.dataforge.meta.buildMeta +import hep.dataforge.nullable +import hep.dataforge.plots.data.DataPlot +import hep.dataforge.plots.jfreechart.JFreeChartPlugin +import hep.dataforge.plots.plotData +import hep.dataforge.storage.files.FileStorage +import hep.dataforge.tables.Adapters +import hep.dataforge.tables.filter +import inr.numass.NumassPlugin +import inr.numass.data.NumassDataUtils +import inr.numass.data.api.NumassSet +import inr.numass.data.storage.NumassDirectory +import inr.numass.displayChart +import inr.numass.subthreshold.Threshold + +fun main() { + val context = buildContext("NUMASS", NumassPlugin::class.java, JFreeChartPlugin::class.java) { + rootDir = "D:\\Work\\Numass\\sterile\\2017_05_frames" + dataDir = "D:\\Work\\Numass\\data\\2017_05_frames" + output = FXOutputManager() + DirectoryOutput() + } + + val storage = NumassDirectory.read(context, "Fill_3") as? FileStorage ?: error("Storage not found") + + val meta = buildMeta { + "delta" to -300 + "method" to "pow" + "t0" to 15e3 +// "window.lo" to 400 +// "window.up" to 1600 + "xLow" to 1000 + "xHigh" to 1300 + "upper" to 6000 + "binning" to 32 + //"reference" to 18600 + } + + val frame = displayChart("correction").apply { + plots.setType() + } + + val sets = (1..14).map { "set_$it" }.mapNotNull { setName -> + storage.provide(setName, NumassSet::class.java).nullable + } + + val name = "fill_3[1-14]" + + val sum = NumassDataUtils.join(name, sets) + + val correctionTable = Threshold.calculateSubThreshold(sum, meta).filter { + it.getDouble("correction") in (1.0..1.2) + } + + frame.plotData("${name}_cor", correctionTable, Adapters.buildXYAdapter("U", "correction")) + frame.plotData("${name}_a", correctionTable, Adapters.buildXYAdapter("U", "a")) + frame.plotData("${name}_beta", correctionTable, Adapters.buildXYAdapter("U", "beta")) + + context.output.render(correctionTable,"numass.correction", name) +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/threshold/FitWithPower.kt b/numass-main/src/main/kotlin/inr/numass/scripts/threshold/FitWithPower.kt new file mode 100644 index 00000000..f22db9c3 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/threshold/FitWithPower.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 inr.numass.scripts.threshold + +import hep.dataforge.buildContext +import hep.dataforge.fx.output.FXOutputManager +import hep.dataforge.io.DirectoryOutput +import hep.dataforge.io.plus +import hep.dataforge.meta.buildMeta +import hep.dataforge.nullable +import hep.dataforge.plots.data.DataPlot +import hep.dataforge.plots.jfreechart.JFreeChartPlugin +import hep.dataforge.plots.plotData +import hep.dataforge.storage.files.FileStorage +import hep.dataforge.tables.Adapters +import inr.numass.NumassPlugin +import inr.numass.data.api.NumassSet +import inr.numass.data.storage.NumassDirectory +import inr.numass.displayChart +import inr.numass.subthreshold.Threshold + +fun main() { + val context = buildContext("NUMASS", NumassPlugin::class.java, JFreeChartPlugin::class.java) { + rootDir = "D:\\Work\\Numass\\sterile\\2017_05_frames" + dataDir = "D:\\Work\\Numass\\data\\2017_05_frames" + output = FXOutputManager() + DirectoryOutput() + } + + val storage = NumassDirectory.read(context, "Fill_3") as? FileStorage ?: error("Storage not found") + + val meta = buildMeta { + "delta" to -300 + "method" to "pow" + "t0" to 15e3 +// "window.lo" to 400 +// "window.up" to 1600 + "xLow" to 1000 + "xHigh" to 1300 + "upper" to 6000 + "binning" to 32 + //"reference" to 18600 + } + + val frame = displayChart("correction").apply { + plots.setType() + } + + listOf("set_2", "set_3", "set_4", "set_5").forEach { setName -> + val set = storage.provide(setName, NumassSet::class.java).nullable ?: error("Set does not exist") + + val correctionTable = Threshold.calculateSubThreshold(set, meta).filter { + it.getDouble("correction") in (1.0..1.2) + } + + frame.plotData(setName, correctionTable, Adapters.buildXYAdapter("U", "correction")) + } +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/threshold/threshold-2017_05-CAMAC.kt b/numass-main/src/main/kotlin/inr/numass/scripts/threshold/threshold-2017_05-CAMAC.kt new file mode 100644 index 00000000..ec740833 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/threshold/threshold-2017_05-CAMAC.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + + +import hep.dataforge.buildContext +import hep.dataforge.fx.output.FXOutputManager +import hep.dataforge.io.DirectoryOutput +import hep.dataforge.io.plus +import hep.dataforge.meta.buildMeta +import hep.dataforge.nullable +import hep.dataforge.plots.data.DataPlot +import hep.dataforge.plots.jfreechart.JFreeChartPlugin +import hep.dataforge.plots.plotData +import hep.dataforge.storage.files.FileStorage +import hep.dataforge.tables.Adapters +import inr.numass.NumassPlugin +import inr.numass.data.api.NumassSet +import inr.numass.data.storage.NumassDirectory +import inr.numass.displayChart +import inr.numass.subthreshold.Threshold + +fun main(){ + val context = buildContext("NUMASS", NumassPlugin::class.java, JFreeChartPlugin::class.java) { + rootDir = "D:\\Work\\Numass\\sterile\\2017_05" + dataDir = "D:\\Work\\Numass\\data\\2017_05" + output = FXOutputManager() + DirectoryOutput() + } + + val storage = NumassDirectory.read(context, "Fill_3") as? FileStorage ?: error("Storage not found") + + val meta = buildMeta { + "delta" to -200 + "method" to "pow" + "t0" to 15e3 +// "window.lo" to 400 +// "window.up" to 1600 + "xLow" to 450 + "xHigh" to 700 + "upper" to 3000 + "binning" to 20 + //"reference" to 18600 + } + + val frame = displayChart("correction").apply { + plots.setType() + } + + listOf("set_2", "set_3", "set_4", "set_5").forEach { setName -> + val set = storage.provide(setName, NumassSet::class.java).nullable ?: error("Set does not exist") + + val correctionTable = Threshold.calculateSubThreshold(set, meta).filter { + it.getDouble("correction") in (1.0..1.2) + } + + frame.plotData(setName, correctionTable, Adapters.buildXYAdapter("U", "correction")) + } + +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/threshold/threshold-2019_11-CAMAC.kt b/numass-main/src/main/kotlin/inr/numass/scripts/threshold/threshold-2019_11-CAMAC.kt new file mode 100644 index 00000000..d8ee14ce --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/threshold/threshold-2019_11-CAMAC.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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. + */ + + +import hep.dataforge.buildContext +import hep.dataforge.fx.output.FXOutputManager +import hep.dataforge.io.DirectoryOutput +import hep.dataforge.io.plus +import hep.dataforge.io.render +import hep.dataforge.meta.buildMeta +import hep.dataforge.nullable +import hep.dataforge.plots.data.DataPlot +import hep.dataforge.plots.jfreechart.JFreeChartPlugin +import hep.dataforge.plots.plotData +import hep.dataforge.storage.files.FileStorage +import hep.dataforge.tables.Adapters +import hep.dataforge.tables.Table +import hep.dataforge.tables.filter +import inr.numass.NumassPlugin +import inr.numass.data.api.NumassSet +import inr.numass.data.storage.NumassDirectory +import inr.numass.displayChart +import inr.numass.subthreshold.Threshold + +fun main() { + val context = + buildContext( + "NUMASS", + NumassPlugin::class.java, JFreeChartPlugin::class.java, DirectoryOutput::class.java + ) { + rootDir = "D:\\Work\\Numass\\sterile\\2019_11" + dataDir = "D:\\Work\\Numass\\data\\2019_11" + output = FXOutputManager() + DirectoryOutput() + } + + val storage = NumassDirectory.read(context, "Fill_4_Tritium") as? FileStorage ?: error("Storage not found") + + val meta = buildMeta { + "delta" to -200 + "method" to "pow" + "t0" to 15e3 +// "window.lo" to 400 +// "window.up" to 1600 + "xLow" to 350 + "xHigh" to 700 + "upper" to 3000 + "binning" to 20 + //"reference" to 18600 + } + + val frame = displayChart("correction").apply { + plots.setType() + } + + listOf("set_1").forEach { setName -> + val set = storage.provide(setName, NumassSet::class.java).nullable ?: error("Set does not exist") + + val correctionTable: Table = Threshold.calculateSubThreshold(set, meta).filter { + it.getDouble("correction") in (1.0..1.2) + } + + frame.plotData(setName, correctionTable, Adapters.buildXYAdapter("U", "correction")) + + context.output.render(correctionTable, "numass.correction", setName, meta = meta) + } + +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/timeanalysis/AnalyzeDantePoint.kt b/numass-main/src/main/kotlin/inr/numass/scripts/timeanalysis/AnalyzeDantePoint.kt new file mode 100644 index 00000000..a491663b --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/timeanalysis/AnalyzeDantePoint.kt @@ -0,0 +1,72 @@ +package inr.numass.scripts.timeanalysis + +import hep.dataforge.buildContext +import hep.dataforge.data.DataSet +import hep.dataforge.fx.output.FXOutputManager +import hep.dataforge.meta.buildMeta +import hep.dataforge.plots.jfreechart.JFreeChartPlugin +import inr.numass.NumassPlugin +import inr.numass.actions.TimeAnalyzerAction +import inr.numass.data.api.NumassPoint +import inr.numass.data.api.NumassSet +import inr.numass.data.api.SimpleNumassPoint +import inr.numass.data.storage.NumassDirectory + +fun main() { + + val context = buildContext("NUMASS", NumassPlugin::class.java, JFreeChartPlugin::class.java) { + output = FXOutputManager() + rootDir = "D:\\Work\\Numass\\sterile2018_04" + dataDir = "D:\\Work\\Numass\\data\\2018_04" + } + + val storage = NumassDirectory.read(context, "Fill_3")!! + + val meta = buildMeta { + "binNum" to 200 + //"chunkSize" to 10000 + // "mean" to TimeAnalyzer.AveragingMethod.ARITHMETIC + //"separateParallelBlocks" to true + "t0" to { + "step" to 320 + } + "analyzer" to { + "t0" to 16000 + "window" to { + "lo" to 450 + "up" to 1900 + } + } + + //"plot.showErrors" to false + } + + + val loader = storage.provide("set_9",NumassSet::class.java).get() + + val hvs = listOf(14000.0)//, 15000d, 15200d, 15400d, 15600d, 15800d] + //listOf(18500.0, 18600.0, 18995.0, 19000.0) + + val data = DataSet.edit(NumassPoint::class).apply { + hvs.forEach { hv -> + val points = loader.points.filter { + it.voltage == hv + }.toList() + if (!points.isEmpty()) { + putStatic( + "point_${hv.toInt()}", + SimpleNumassPoint.build(points, hv) + ) + } + } + }.build() + + + val result = TimeAnalyzerAction.run(context, data, meta); + + result.nodeGoal().run() + + readLine() + println("Canceling task") + result.nodeGoal().cancel() +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/timeanalysis/AnalyzePoint.kt b/numass-main/src/main/kotlin/inr/numass/scripts/timeanalysis/AnalyzePoint.kt new file mode 100644 index 00000000..58f4cb8e --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/timeanalysis/AnalyzePoint.kt @@ -0,0 +1,78 @@ +package inr.numass.scripts.timeanalysis + +import hep.dataforge.buildContext +import hep.dataforge.data.DataSet +import hep.dataforge.fx.output.FXOutputManager +import hep.dataforge.meta.buildMeta +import hep.dataforge.plots.jfreechart.JFreeChartPlugin +import inr.numass.NumassPlugin +import inr.numass.actions.TimeAnalyzerAction +import inr.numass.data.NumassDataUtils +import inr.numass.data.api.NumassPoint +import inr.numass.data.api.NumassSet +import inr.numass.data.api.SimpleNumassPoint +import inr.numass.data.storage.NumassDirectory + +fun main() { + + val context = buildContext("NUMASS", NumassPlugin::class.java, JFreeChartPlugin::class.java) { + output = FXOutputManager() + rootDir = "D:\\Work\\Numass\\sterile2017_05_frames" + dataDir = "D:\\Work\\Numass\\data\\2017_05_frames" + } + + val storage = NumassDirectory.read(context, "Fill_3")!! + + val meta = buildMeta { + "binNum" to 200 + //"chunkSize" to 10000 + // "mean" to TimeAnalyzer.AveragingMethod.ARITHMETIC + //"separateParallelBlocks" to true + "t0" to { + "step" to 320 + } + "analyzer" to { + "t0" to 16000 + "window" to { + "lo" to 1500 + "up" to 7000 + } + } + + //"plot.showErrors" to false + } + + //def sets = ((2..14) + (22..31)).collect { "set_$it" } + val sets = (11..11).map { "set_$it" } + //def sets = (16..31).collect { "set_$it" } + //def sets = (20..28).collect { "set_$it" } + + val loaders = sets.map { set -> + storage.provide(set, NumassSet::class.java).orElse(null) + }.filter { it != null } + + val all = NumassDataUtils.join("sum", loaders) + + val hvs = listOf(14000.0)//, 15000d, 15200d, 15400d, 15600d, 15800d] + //listOf(18500.0, 18600.0, 18995.0, 19000.0) + + val data = DataSet.edit(NumassPoint::class).apply { + hvs.forEach { hv -> + val points = all.points.filter { + it.voltage == hv && it.channel == 0 + }.toList() + if (!points.isEmpty()) { + putStatic("point_${hv.toInt()}", SimpleNumassPoint.build(points, hv)) + } + } + }.build() + + + val result = TimeAnalyzerAction.run(context, data, meta); + + result.nodeGoal().run() + + readLine() + println("Canceling task") + result.nodeGoal().cancel() +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/timeanalysis/Histogram.kt b/numass-main/src/main/kotlin/inr/numass/scripts/timeanalysis/Histogram.kt new file mode 100644 index 00000000..e5e430b3 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/timeanalysis/Histogram.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 inr.numass.scripts.timeanalysis + +import hep.dataforge.buildContext +import hep.dataforge.maths.histogram.SimpleHistogram +import hep.dataforge.meta.buildMeta +import inr.numass.NumassPlugin +import inr.numass.data.NumassDataUtils +import inr.numass.data.analyzers.TimeAnalyzer +import inr.numass.data.api.NumassSet +import inr.numass.data.storage.NumassDirectory +import kotlin.streams.asStream + +fun main() { + val context = buildContext("NUMASS", NumassPlugin::class.java) { + rootDir = "D:\\Work\\Numass\\sterile2018_04" + dataDir = "D:\\Work\\Numass\\data\\2018_04" + } + + val storage = NumassDirectory.read(context, "Fill_4")!! + + val meta = buildMeta { + "t0" to 3000 + "chunkSize" to 3000 + "mean" to TimeAnalyzer.AveragingMethod.ARITHMETIC + //"separateParallelBlocks" to true + "window" to { + "lo" to 0 + "up" to 4000 + } + //"plot.showErrors" to false + } + + //def sets = ((2..14) + (22..31)).collect { "set_$it" } + val sets = (2..12).map { "set_$it" } + //def sets = (16..31).collect { "set_$it" } + //def sets = (20..28).collect { "set_$it" } + + val loaders = sets.map { set -> + storage.provide("$set", NumassSet::class.java).orElse(null) + }.filter { it != null } + + val joined = NumassDataUtils.join("sum", loaders) + + val hv = 14000.0 + + val point = joined.first { it.voltage == hv } + + val histogram = SimpleHistogram(arrayOf(0.0, 0.0), arrayOf(20.0, 100.0)) + histogram.fill( + TimeAnalyzer().getEventsWithDelay(point, meta) + .filter { it.second <10000 } + .filter { it.first.channel == 0 } + .map { arrayOf(it.first.amplitude.toDouble(), it.second.toDouble()/1e3) } + .asStream() + ) + + histogram.binStream().forEach { + println("${it.getLowerBound(0)}\t${it.getLowerBound(1)}\t${it.count}") + } + +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/timeanalysis/TestAnalyzer.kt b/numass-main/src/main/kotlin/inr/numass/scripts/timeanalysis/TestAnalyzer.kt new file mode 100644 index 00000000..a55298a7 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/timeanalysis/TestAnalyzer.kt @@ -0,0 +1,59 @@ +package inr.numass.scripts.timeanalysis + +import hep.dataforge.context.Global +import hep.dataforge.fx.output.FXOutputManager +import hep.dataforge.goals.generate +import hep.dataforge.goals.join +import hep.dataforge.meta.buildMeta +import hep.dataforge.plots.jfreechart.JFreeChartPlugin +import inr.numass.NumassPlugin +import inr.numass.actions.TimeAnalyzerAction +import inr.numass.data.NumassGenerator +import inr.numass.data.analyzers.TimeAnalyzer +import inr.numass.data.api.SimpleNumassPoint +import inr.numass.data.generateBlock +import inr.numass.data.withDeadTime +import org.apache.commons.math3.random.JDKRandomGenerator +import org.apache.commons.math3.random.SynchronizedRandomGenerator +import java.time.Instant + +fun main() { + Global.output = FXOutputManager() + JFreeChartPlugin().startGlobal() + NumassPlugin().startGlobal() + + val cr = 30e3 + val length = 1e9.toLong() + val num = 50 + val dt = 6.5 + + val start = Instant.now() + + val generator = SynchronizedRandomGenerator(JDKRandomGenerator(2223)) + + val point = (1..num).map { + Global.generate { + NumassGenerator + .generateEvents(cr * (1.0 - 0.005 * it), rnd = generator) + .withDeadTime { (dt * 1000).toLong() } + .generateBlock(start.plusNanos(it * length), length) + } + }.join(Global) { blocks -> + SimpleNumassPoint.build(blocks, 12000.0) + }.get() + + + val meta = buildMeta { + "analyzer" to { + "t0" to 3000 + "chunkSize" to 5000 + "mean" to TimeAnalyzer.AveragingMethod.ARITHMETIC + } + "binNum" to 200 + "t0.max" to 5e4 + } + + TimeAnalyzerAction.simpleRun(point, meta); + + readLine() +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/timeanalysis/TestBunch.kt b/numass-main/src/main/kotlin/inr/numass/scripts/timeanalysis/TestBunch.kt new file mode 100644 index 00000000..954facb2 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/timeanalysis/TestBunch.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 inr.numass.scripts.timeanalysis + +import hep.dataforge.context.Global +import hep.dataforge.fx.output.FXOutputManager +import hep.dataforge.goals.generate +import hep.dataforge.goals.join +import hep.dataforge.meta.buildMeta +import hep.dataforge.plots.jfreechart.JFreeChartPlugin +import inr.numass.NumassPlugin +import inr.numass.actions.TimeAnalyzerAction +import inr.numass.data.NumassGenerator +import inr.numass.data.api.SimpleNumassPoint +import inr.numass.data.generateBlock +import inr.numass.data.withDeadTime +import java.time.Instant + +fun main() { + Global.output = FXOutputManager() + JFreeChartPlugin().startGlobal() + NumassPlugin().startGlobal() + + val cr = 3.0 + val length = (30000 * 1e9).toLong() + val num = 10 + val dt = 6.5 + + val start = Instant.now() + + val point = (1..num).map { + Global.generate { + val events = NumassGenerator + .generateEvents(cr) + + val bunches = NumassGenerator + .generateBunches(6.0, 0.01, 5.0) + + val discharges = NumassGenerator + .generateBunches(50.0, 0.001, 0.1) + + NumassGenerator + .mergeEventChains(events, bunches, discharges) + .withDeadTime { (dt * 1000).toLong() } + .generateBlock(start.plusNanos(it * length), length) + } + }.join(Global) { blocks -> + SimpleNumassPoint.build(blocks, 18000.0) + }.get() + + + val meta = buildMeta { + "analyzer" to { + "t0" to 50000 + } + "binNum" to 200 + "t0.max" to 1e9 + } + + TimeAnalyzerAction.simpleRun(point, meta); + + readLine() +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/timeanalysis/TestTimeAnalyzerLength.kt b/numass-main/src/main/kotlin/inr/numass/scripts/timeanalysis/TestTimeAnalyzerLength.kt new file mode 100644 index 00000000..0367e784 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/timeanalysis/TestTimeAnalyzerLength.kt @@ -0,0 +1,90 @@ +package inr.numass.scripts.timeanalysis + +import hep.dataforge.buildContext +import hep.dataforge.fx.output.FXOutputManager +import hep.dataforge.meta.buildMeta +import hep.dataforge.plots.jfreechart.JFreeChartPlugin +import inr.numass.NumassPlugin +import inr.numass.data.analyzers.TimeAnalyzer +import inr.numass.data.api.NumassSet +import inr.numass.data.storage.NumassDirectory + + +fun main() { + val context = buildContext("NUMASS", NumassPlugin::class.java, JFreeChartPlugin::class.java) { + output = FXOutputManager() + rootDir = "D:\\Work\\Numass\\sterile2017_05" + dataDir = "D:\\Work\\Numass\\data\\2017_05" + } + + val storage = NumassDirectory.read(context, "Fill_3")!! + + val loader = storage.provide("set_10", NumassSet::class.java).get() + + val point = loader.getPoints(18050.00).first() + + val analyzer = TimeAnalyzer() + + val meta = buildMeta("analyzer") { + "t0" to 3000 + "inverted" to false + //"chunkSize" to 5000 + //"mean" to TimeAnalyzer.AveragingMethod.ARITHMETIC + } + + println(analyzer.analyze(point, meta)) + + println(analyzer.getEventsWithDelay(point.firstBlock, meta ).count()) + println(point.events.count()) + println(point.firstBlock.events.count()) + +// val time = point.events.asSequence().zipWithNext().map { (p, n) -> +// n.timeOffset - p.timeOffset +// }.filter { it > 0 }.sum() + + val time = analyzer.getEventsWithDelay(point.firstBlock, meta ).map { it.second }.filter { it > 0 }.sum() + + + +// val totalN = AtomicLong(0) +// val totalT = AtomicLong(0) +// +// analyzer.getEventsWithDelay(point.firstBlock, meta ).filter { pair -> pair.second >= 3000 } +// .forEach { pair -> +// totalN.incrementAndGet() +// //TODO add progress listener here +// totalT.addAndGet(pair.second) +// } +// +// val time = totalT.get() + + println(time / 1e9) + +// +// val cr = 80.0 +// val length = 5e9.toLong() +// val num = 6 +// val dt = 6.5 +// +// val start = Instant.now() +// +// val generator = SynchronizedRandomGenerator(JDKRandomGenerator(2223)) +// +// repeat(100) { +// +// val point = (1..num).map { +// Global.generate { +// NumassGenerator +// .generateEvents(cr , rnd = generator) +//// .withDeadTime { (dt * 1000).toLong() } +// .generateBlock(start.plusNanos(it * length), length) +// } +// }.join(Global) { blocks -> +// SimpleNumassPoint.build(blocks, 12000.0) +// }.get() +// +// +// println(analyzer.analyze(point, meta)) +// +// } +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/tristan/AnalyzeTristan.kt b/numass-main/src/main/kotlin/inr/numass/scripts/tristan/AnalyzeTristan.kt new file mode 100644 index 00000000..4bab34cb --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/tristan/AnalyzeTristan.kt @@ -0,0 +1,73 @@ +package inr.numass.scripts.tristan + +import hep.dataforge.context.Global +import hep.dataforge.fx.output.FXOutputManager +import hep.dataforge.plots.jfreechart.JFreeChartPlugin +import inr.numass.data.ProtoNumassPoint +import inr.numass.data.plotAmplitudeSpectrum +import inr.numass.data.transformChain +import kotlinx.coroutines.runBlocking +import java.io.File + +fun main() { + Global.output = FXOutputManager() + JFreeChartPlugin().startGlobal() + + val file = File("D:\\Work\\Numass\\data\\2018_04\\Fill_3\\set_4\\p129(30s)(HV1=13000)").toPath() + val point = ProtoNumassPoint.readFile(file) + Global.plotAmplitudeSpectrum(point) + + point.blocks.firstOrNull { it.channel == 0 }?.let { + Global.plotAmplitudeSpectrum(it, plotName = "0") { + "title" to "pixel 0" + "binning" to 50 + } + } + + point.blocks.firstOrNull { it.channel == 4 }?.let { + Global.plotAmplitudeSpectrum(it, plotName = "4") { + "title" to "pixel 4" + "binning" to 50 + } + println("Number of events for pixel 4 is ${it.events.count()}") + } + + runBlocking { + listOf(0, 20, 50, 100, 200).forEach { window -> + + Global.plotAmplitudeSpectrum(point.transformChain { first, second -> + val dt = second.timeOffset - first.timeOffset + if (second.channel == 4 && first.channel == 0 && dt > window && dt < 1000) { + Pair((first.amplitude + second.amplitude).toShort(), second.timeOffset) + } else { + null + } + }.also { + println("Number of events for $window is ${it.events.count()}") + }, plotName = "filtered.before.$window") { + "binning" to 50 + } + + } + + listOf(0, 20, 50, 100, 200).forEach { window -> + + Global.plotAmplitudeSpectrum(point.transformChain { first, second -> + val dt = second.timeOffset - first.timeOffset + if (second.channel == 0 && first.channel == 4 && dt > window && dt < 1000) { + Pair((first.amplitude + second.amplitude).toShort(), second.timeOffset) + } else { + null + } + }.also { + println("Number of events for $window is ${it.events.count()}") + }, plotName = "filtered.after.$window") { + "binning" to 50 + } + + } + + } + + readLine() +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/tristan/FitTextTristanFiles.kt b/numass-main/src/main/kotlin/inr/numass/scripts/tristan/FitTextTristanFiles.kt new file mode 100644 index 00000000..5c1b4de1 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/tristan/FitTextTristanFiles.kt @@ -0,0 +1,162 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 inr.numass.scripts.tristan + +import hep.dataforge.buildContext +import hep.dataforge.configure +import hep.dataforge.data.DataNode +import hep.dataforge.fx.output.FXOutputManager +import hep.dataforge.io.ColumnedDataReader +import hep.dataforge.io.DirectoryOutput +import hep.dataforge.io.output.stream +import hep.dataforge.io.plus +import hep.dataforge.meta.Meta +import hep.dataforge.meta.buildMeta +import hep.dataforge.plots.data.DataPlot +import hep.dataforge.plots.jfreechart.JFreeChartPlugin +import hep.dataforge.plots.output.plotFrame +import hep.dataforge.stat.fit.FitHelper +import hep.dataforge.stat.fit.ParamSet +import hep.dataforge.tables.* +import hep.dataforge.values.ValueType +import inr.numass.NumassPlugin +import inr.numass.actions.MergeDataAction +import inr.numass.data.analyzers.NumassAnalyzer.Companion.COUNT_KEY +import inr.numass.data.analyzers.NumassAnalyzer.Companion.COUNT_RATE_ERROR_KEY +import inr.numass.data.analyzers.NumassAnalyzer.Companion.COUNT_RATE_KEY +import inr.numass.data.analyzers.NumassAnalyzer.Companion.LENGTH_KEY +import inr.numass.data.api.NumassPoint.Companion.HV_KEY +import java.io.PrintWriter +import java.nio.file.Files +import java.util.function.Predicate + +fun main() { + val context = buildContext("NUMASS") { + plugin() + plugin() + rootDir = "D:\\Work\\Numass\\TristanText\\" + dataDir = "D:\\Work\\Numass\\TristanText\\data\\" + output = FXOutputManager() + DirectoryOutput() + } + context.load() + + + val tables = DataNode.build
{ + name = "tristan" + Files.list(context.dataDir).forEach { + val name = ".*(set_\\d+).*".toRegex().matchEntire(it.fileName.toString())!!.groupValues[1] + val table = ColumnedDataReader(Files.newInputStream(it), HV_KEY, COUNT_RATE_KEY, COUNT_RATE_ERROR_KEY).toTable() + .addColumn(LENGTH_KEY, ValueType.NUMBER) { 30 } + .addColumn(COUNT_KEY, ValueType.NUMBER) { getDouble(COUNT_RATE_KEY) * 30 } + putStatic(name, table) + } + } + val adapter = Adapters.buildXYAdapter(HV_KEY, COUNT_RATE_KEY, COUNT_RATE_ERROR_KEY) + + context.plotFrame("raw", "plots") { + configure { + "legend.show" to false + } + plots.configure { + "showLine" to true + "showSymbol" to false + } + tables.forEach { (key, value) -> + add(DataPlot.plot(key, value, adapter)) + } + } + + context.plotFrame("raw_normalized", "plots") { + configure { + "legend.show" to false + } + plots.configure { + "showLine" to true + "showSymbol" to false + } + tables.forEach { (key, table) -> + val norming = table.find { it.getDouble(HV_KEY) == 13000.0 }!!.getDouble(COUNT_RATE_KEY) + val normalizedTable = table + .replaceColumn(COUNT_RATE_KEY) { getDouble(COUNT_RATE_KEY) / norming } + .replaceColumn(COUNT_RATE_ERROR_KEY) { getDouble(COUNT_RATE_ERROR_KEY) / norming } + add(DataPlot.plot(key, normalizedTable, adapter)) + } + } + + val merge = MergeDataAction.runGroup(context, tables, Meta.empty()).get() + + val filtered = Tables.filter(merge, + Predicate { + val hv = it.getDouble(HV_KEY) + hv > 12200.0 && (hv < 15500 || hv > 16500) + } + ) + + context.plotFrame("merge", "plots") { + plots.configure { + "showLine" to true + "showSymbol" to false + } + add(DataPlot.plot("merge", merge, adapter)) + } + + val meta = buildMeta { + "model" to { + "modelName" to "sterile" + "resolution" to { + "width" to 8.3e-5 + "tail" to "function::numass.resolutionTail.2017.mod" + } + "transmission" to { + "trapping" to "function::numass.trap.nominal" + } + } + "stage" to { "freePars" to listOf("N", "bkg", "E0") } + } + + val params = ParamSet().apply { + setPar("N", 4e4, 6.0, 0.0, Double.POSITIVE_INFINITY) + setPar("bkg", 2.0, 0.03) + setPar("E0", 18575.0, 1.0) + setPar("mnu2", 0.0, 1.0) + setParValue("msterile2", (1000 * 1000).toDouble()) + setPar("U2", 0.0, 1e-3) + setPar("X", 0.1, 0.01) + setPar("trap", 1.0, 0.01) + } + + context.output["numass.fit", "text"].stream.use { out -> + val log = context.history.getChronicle("log") + val writer = PrintWriter(out) + writer.printf("%n*** META ***%n") + writer.println(meta.toString()) + writer.flush() + FitHelper(context) + .fit(filtered, meta) + .setListenerStream(out) + .report(log) + .apply { + params(params) + }.run() + + writer.println() + log.entries.forEach { entry -> writer.println(entry.toString()) } + writer.println() + + } + +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/tristan/ProtoPoint.kt b/numass-main/src/main/kotlin/inr/numass/scripts/tristan/ProtoPoint.kt new file mode 100644 index 00000000..b21dea19 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/tristan/ProtoPoint.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 inr.numass.scripts.tristan + +import hep.dataforge.context.Global +import hep.dataforge.storage.files.FileStorage +import hep.dataforge.toList +import inr.numass.data.api.NumassPoint +import inr.numass.data.storage.NumassDataLoader +import inr.numass.data.storage.NumassDirectory + +fun main() { + val storage = NumassDirectory.read(Global, "D:\\Work\\Numass\\data\\2018_04\\Adiabacity_19\\") as FileStorage + val set = storage["set_4"] as NumassDataLoader + set.points.forEach { point -> + if (point.voltage == 18700.0) { + println("${point.index}:") + point.blocks.forEach { + println("\t${it.channel}: events: ${it.events.count()}, time: ${it.length}") + } + } + } + + val point: NumassPoint = set.points.first { it.index == 18 } + (0..99).forEach { bin -> + val times = point.events.filter { it.amplitude > 0 }.map { it.timeOffset }.toList() + val count = times.filter { it > bin.toDouble() / 10 * 1e9 && it < (bin + 1).toDouble() / 10 * 1e9 }.count() + println("${bin.toDouble() / 10.0}: $count") + } +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/tristan/TristanAnalyzer.kt b/numass-main/src/main/kotlin/inr/numass/scripts/tristan/TristanAnalyzer.kt new file mode 100644 index 00000000..f24617ff --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/tristan/TristanAnalyzer.kt @@ -0,0 +1,51 @@ +package inr.numass.scripts.tristan + +import hep.dataforge.meta.Meta +import hep.dataforge.meta.value +import hep.dataforge.useValue +import inr.numass.data.analyzers.TimeAnalyzer +import inr.numass.data.api.NumassBlock +import inr.numass.data.api.NumassEvent + +object TristanAnalyzer : TimeAnalyzer() { + override fun getEvents(block: NumassBlock, meta: Meta): List { + val t0 = getT0(block, meta).toLong() + val summTime = meta.getInt("summTime", 200) //time limit in nanos for event summation + var sequence = sequence { + var last: NumassEvent? = null + var amp: Int = 0 + getEventsWithDelay(block, meta).forEach { (event, time) -> + when { + last == null -> { + last = event + } + time < 0 -> error("Can't be") + time < summTime -> { + //add to amplitude + amp += event.amplitude + } + time > t0 -> { + //accept new event and reset summator + if (amp != 0) { + //construct new event with pileup + yield(NumassEvent(amp.toShort(), last!!.timeOffset, last!!.owner)) + } else { + //yield event without changes if there is no pileup + yield(last!!) + } + last = event + amp = event.amplitude.toInt() + } + //else ignore event + } + } + } + + meta.useValue("allowedChannels"){ + val list = it.list.map { it.int } + sequence = sequence.filter { it.channel in list } + } + + return sequence.toList() + } +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/tristan/ViewPoint.kt b/numass-main/src/main/kotlin/inr/numass/scripts/tristan/ViewPoint.kt new file mode 100644 index 00000000..2f43a1db --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/tristan/ViewPoint.kt @@ -0,0 +1,37 @@ +package inr.numass.scripts.tristan + +import hep.dataforge.context.Global +import hep.dataforge.fx.output.FXOutputManager +import hep.dataforge.plots.jfreechart.JFreeChartPlugin +import inr.numass.data.ProtoNumassPoint +import inr.numass.data.api.MetaBlock +import inr.numass.data.api.NumassBlock +import inr.numass.data.api.NumassPoint +import inr.numass.data.plotAmplitudeSpectrum +import java.io.File + + +private fun NumassPoint.getChannels(): Map { + return blocks.toList().groupBy { it.channel }.mapValues { entry -> + if (entry.value.size == 1) { + entry.value.first() + } else { + MetaBlock(entry.value) + } + } +} + +fun main() { + val file = File("D:\\Work\\Numass\\data\\2018_04\\Fill_3\\set_4\\p129(30s)(HV1=13000)").toPath() + val point = ProtoNumassPoint.readFile(file) + println(point.meta) + + Global.output = FXOutputManager() + JFreeChartPlugin().startGlobal() + + point.getChannels().forEach{ (num, block) -> + Global.plotAmplitudeSpectrum(numassBlock = block, plotName = num.toString()) + } + + readLine() +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/tristan/checkCorrelations.kt b/numass-main/src/main/kotlin/inr/numass/scripts/tristan/checkCorrelations.kt new file mode 100644 index 00000000..a1c4e079 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/tristan/checkCorrelations.kt @@ -0,0 +1,41 @@ +package inr.numass.scripts.tristan + +import hep.dataforge.context.Global +import hep.dataforge.fx.output.FXOutputManager +import hep.dataforge.fx.plots.group +import hep.dataforge.plots.jfreechart.JFreeChartPlugin +import hep.dataforge.plots.output.plotFrame +import inr.numass.data.plotAmplitudeSpectrum +import inr.numass.data.storage.readNumassSet +import java.io.File + +fun main() { + Global.output = FXOutputManager() + JFreeChartPlugin().startGlobal() + + val file = File("D:\\Work\\Numass\\data\\2018_04\\Fill_3\\set_36").toPath() + val set = Global.readNumassSet(file) + + + Global.plotFrame("compare") { + listOf(12000.0, 13000.0, 14000.0, 14900.0).forEach {voltage-> + val point = set.optPoint(voltage).get() + + group("${set.name}/p${point.index}[${point.voltage}]") { + plotAmplitudeSpectrum(point, "cut", analyzer = TristanAnalyzer) { +// "t0" to 3e3 + "summTime" to 200 + "sortEvents" to true + "inverted" to false + } + plotAmplitudeSpectrum(point, "uncut",analyzer = TristanAnalyzer){ + "summTime" to 0 + "sortEvents" to true + "inverted" to false + } + } + } + } + + readLine() +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/tristan/interpolateBump.kt b/numass-main/src/main/kotlin/inr/numass/scripts/tristan/interpolateBump.kt new file mode 100644 index 00000000..d6862ef7 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/tristan/interpolateBump.kt @@ -0,0 +1,35 @@ +package inr.numass.scripts.tristan + +import hep.dataforge.context.Global +import hep.dataforge.fx.output.FXOutputManager +import hep.dataforge.fx.plots.group +import hep.dataforge.plots.jfreechart.JFreeChartPlugin +import hep.dataforge.plots.output.plotFrame +import inr.numass.data.plotAmplitudeSpectrum +import inr.numass.data.storage.readNumassSet +import java.io.File + +fun main() { + Global.output = FXOutputManager() + JFreeChartPlugin().startGlobal() + + val file = File("D:\\Work\\Numass\\data\\2018_04\\Fill_3\\set_36").toPath() + val set = Global.readNumassSet(file) + + + Global.plotFrame("compare") { + listOf(12000.0, 13000.0, 14000.0, 14900.0).forEach {voltage-> + val point = set.optPoint(voltage).get() + + group("${set.name}/p${point.index}[${point.voltage}]") { + plotAmplitudeSpectrum(point, "cut") { + "t0" to 3e3 + "sortEvents" to true + } + plotAmplitudeSpectrum(point, "uncut") + } + } + } + + readLine() +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/tristan/zeroPad.kt b/numass-main/src/main/kotlin/inr/numass/scripts/tristan/zeroPad.kt new file mode 100644 index 00000000..5f77710a --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/tristan/zeroPad.kt @@ -0,0 +1,36 @@ +package inr.numass.scripts.tristan + +import hep.dataforge.context.Global +import hep.dataforge.fx.output.FXOutputManager +import hep.dataforge.fx.plots.group +import hep.dataforge.plots.jfreechart.JFreeChartPlugin +import hep.dataforge.plots.output.plotFrame +import inr.numass.data.plotAmplitudeSpectrum +import inr.numass.data.storage.readNumassSet +import java.io.File + +fun main() { + Global.output = FXOutputManager() + JFreeChartPlugin().startGlobal() + + val file = File("D:\\Work\\Numass\\data\\2018_04\\Fill_3\\set_36").toPath() + val set = Global.readNumassSet(file) + + + Global.plotFrame("compare") { + listOf(12000.0, 13000.0, 14000.0, 14900.0).forEach {voltage-> + val point = set.optPoint(voltage).get() + val block = point.channel(0)!! + + group("${set.name}/p${point.index}[${point.voltage}]") { + plotAmplitudeSpectrum(block, "cut") { + "t0" to 3e3 + "sortEvents" to true + } + plotAmplitudeSpectrum(block, "uncut") + } + } + } + + readLine() +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/utils/ScanTree.kt b/numass-main/src/main/kotlin/inr/numass/scripts/utils/ScanTree.kt new file mode 100644 index 00000000..a8a0efe3 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/utils/ScanTree.kt @@ -0,0 +1,103 @@ +package inr.numass.scripts.utils + +import hep.dataforge.context.Global +import hep.dataforge.io.XMLMetaWriter +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.meta.MetaUtils +import hep.dataforge.meta.buildMeta +import hep.dataforge.storage.Storage +import hep.dataforge.useValue +import inr.numass.data.storage.NumassDataLoader +import inr.numass.data.storage.NumassDirectory +import kotlinx.coroutines.runBlocking +import java.io.File + +private suspend fun createSummaryNode(storage: Storage): MetaBuilder { + Global.logger.info("Reading content of shelf {}", storage.fullName) + + val builder = MetaBuilder("shelf") + .setValue("name", storage.name) + .setValue("path", storage.fullName) + + storage.children.forEach { element -> + if(element is Storage && element.name.startsWith("Fill")){ + builder.putNode(createSummaryNode(element)) + } else if(element is NumassDataLoader){ + Global.logger.info("Reading content of set {}", element.fullName) + + val setBuilder = MetaBuilder("set") + .setValue("name", element.name) + .setValue("path", element.fullName) + + if (element.name.endsWith("bad")) { + setBuilder.setValue("bad", true) + } + + element.points.forEach { point -> + val pointBuilder = MetaBuilder("point") + .setValue("index", point.index) + .setValue("hv", point.voltage) + .setValue("startTime", point.startTime) +// .setNode("meta", point.meta) + + point.meta.useValue("acquisition_time") { + pointBuilder.setValue("length", it.double) + } + + point.meta.useValue("events") { value -> + pointBuilder.setValue("count", value.list.stream().mapToInt { it.int }.sum()) + } + + setBuilder.putNode(pointBuilder) + } + builder.putNode(setBuilder) + } + } + return builder +} + +fun calculateStatistics(summary: Meta, hv: Double): Meta { + var totalLength = 0.0 + var totalCount = 0L + MetaUtils.nodeStream(summary).map { it.second }.filter { it.name == "point" && it.getDouble("hv") == hv }.forEach { + totalCount += it.getInt("count") + totalLength += it.getDouble("length") + } + return buildMeta("point") { + "hv" to hv + "time" to totalLength + "count" to totalCount + } +} + +fun main(args: Array) { + val directory = if (args.isNotEmpty()) { + args.first() + } else { + "" + } + + + val output = File(directory, "summary.xml") + output.createNewFile() + + + val storage = NumassDirectory.read(Global, directory) as Storage + val summary = runBlocking { createSummaryNode(storage)} + + Global.logger.info("Writing output meta") + output.outputStream().use { + XMLMetaWriter().write(it, summary) + } + Global.logger.info("Calculating statistics") + val statistics = MetaBuilder("statistics") + (14000..18600).step(100).map { it.toDouble() }.forEach { + statistics.putNode(calculateStatistics(summary, it)) + } + + File(directory, "statistics.xml").outputStream().use { + XMLMetaWriter().write(it, statistics) + } + +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/scripts/utils/ScriptUtils.kt b/numass-main/src/main/kotlin/inr/numass/scripts/utils/ScriptUtils.kt new file mode 100644 index 00000000..ab568dde --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/scripts/utils/ScriptUtils.kt @@ -0,0 +1,2 @@ +package inr.numass.scripts.utils + diff --git a/numass-main/src/main/kotlin/inr/numass/subthreshold/Threshold.kt b/numass-main/src/main/kotlin/inr/numass/subthreshold/Threshold.kt new file mode 100644 index 00000000..5f4a7635 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/subthreshold/Threshold.kt @@ -0,0 +1,265 @@ +package inr.numass.subthreshold + +import hep.dataforge.actions.pipe +import hep.dataforge.context.Context +import hep.dataforge.data.DataNode +import hep.dataforge.data.DataSet +import hep.dataforge.meta.Meta +import hep.dataforge.meta.buildMeta +import hep.dataforge.nullable +import hep.dataforge.storage.Storage +import hep.dataforge.tables.ListTable +import hep.dataforge.tables.Table +import hep.dataforge.toList +import hep.dataforge.values.ValueMap +import hep.dataforge.values.Values +import inr.numass.data.analyzers.* +import inr.numass.data.analyzers.NumassAnalyzer.Companion.CHANNEL_KEY +import inr.numass.data.analyzers.NumassAnalyzer.Companion.COUNT_RATE_KEY +import inr.numass.data.api.NumassPoint +import inr.numass.data.api.NumassSet +import inr.numass.data.api.SimpleNumassPoint +import inr.numass.data.storage.NumassDataLoader +import inr.numass.data.storage.NumassDirectory +import kotlinx.coroutines.runBlocking +import org.apache.commons.math3.analysis.ParametricUnivariateFunction +import org.apache.commons.math3.exception.DimensionMismatchException +import org.apache.commons.math3.fitting.SimpleCurveFitter +import org.apache.commons.math3.fitting.WeightedObservedPoint +import org.slf4j.LoggerFactory +import java.util.stream.Collectors +import java.util.stream.StreamSupport +import kotlin.math.exp +import kotlin.math.ln +import kotlin.math.pow + + +object Threshold { + + suspend fun getSpectraMap(context: Context, meta: Meta): DataNode
{ + + //creating storage instance + val storage = NumassDirectory.read(context, meta.getString("data.dir")) as Storage + + fun Storage.loaders(): Sequence { + return sequence { + print("Reading ${this@loaders.fullName}") + runBlocking { this@loaders.children }.forEach { + if (it is NumassDataLoader) { + yield(it) + } else if (it is Storage) { + yieldAll(it.loaders()) + } + } + } + } + + //Reading points + //Free operation. No reading done + val sets = storage.loaders() + .filter { it.fullName.toString().matches(meta.getString("data.mask").toRegex()) } + + val analyzer = TimeAnalyzer(); + + val data = DataSet.edit(NumassPoint::class).also { dataBuilder -> + sets.sortedBy { it.startTime } + .flatMap { set -> set.points.asSequence() } + .groupBy { it.voltage } + .forEach { key, value -> + val point = SimpleNumassPoint.build(value, key) + val name = key.toInt().toString() + dataBuilder.putStatic(name, point, buildMeta("meta", "voltage" to key)); + } + }.build() + + return data.pipe(context, meta) { + result { input -> + analyzer.getAmplitudeSpectrum(input, this.meta) + } + } + } + + + /** + * Prepare the list of weighted points for fitter + */ + private fun preparePoints(spectrum: Table, xLow: Int, xHigh: Int, binning: Int): List { + if (xHigh <= xLow) { + throw IllegalArgumentException("Wrong borders for underflow calculation"); + } + val binned = spectrum.withBinning(binning, xLow, xHigh) +// val binned = TableTransform.filter( +// spectrum.withBinning(binning), +// CHANNEL_KEY, +// xLow, +// xHigh +// ) + + return binned.rows + .map { + WeightedObservedPoint( + 1.0,//1d / p.getValue() , //weight + it.getDouble(CHANNEL_KEY), // x + it.getDouble(COUNT_RATE_KEY) / binning + ) //y + } + .collect(Collectors.toList()) + } + + private fun norm(spectrum: Table, xLow: Int, upper: Int): Double { + return spectrum.rows.filter { row -> + row.getInt(CHANNEL_KEY) in (xLow + 1)..(upper - 1) + }.mapToDouble { it.getValue(COUNT_RATE_KEY).double }.sum() + } + + private val expPointNames = arrayOf("U", "amp", "expConst", "correction"); + + /** + * Exponential function for fitting + */ + private class ExponentFunction : ParametricUnivariateFunction { + override fun value(x: Double, vararg parameters: Double): Double { + if (parameters.size != 2) { + throw DimensionMismatchException(parameters.size, 2); + } + val a = parameters[0]; + val sigma = parameters[1]; + //return a * (Math.exp(x / sigma) - 1); + return a * exp(x / sigma); + } + + override fun gradient(x: Double, vararg parameters: Double): DoubleArray { + if (parameters.size != 2) { + throw DimensionMismatchException(parameters.size, 2); + } + val a = parameters[0]; + val sigma = parameters[1]; + return doubleArrayOf(exp(x / sigma), -a * x / sigma / sigma * exp(x / sigma)) + } + } + + + /** + * Exponential function $a e^{\frac{x}{\sigma}}$ + */ + private fun exponential(point: NumassPoint, spectrum: Table, config: Meta): Values { + val xLow: Int = config.getInt("xLow", 400) + val xHigh: Int = config.getInt("xHigh", 700) + val upper: Int = config.getInt("upper", 3100) + val binning: Int = config.getInt("binning", 20) + + + val fitter = SimpleCurveFitter.create(ExponentFunction(), doubleArrayOf(1.0, 200.0)) + val (a, sigma) = fitter.fit(preparePoints(spectrum, xLow, xHigh, binning)) + + val norm = norm(spectrum, xLow, upper) + + return ValueMap.ofPairs( + "index" to point.index, + "U" to point.voltage, + "a" to a, + "sigma" to sigma, + "correction" to a * sigma * exp(xLow / sigma) / norm + 1.0 + ) + } + + + private class PowerFunction(val shift: Double? = null) : ParametricUnivariateFunction { + + override fun value(x: Double, vararg parameters: Double): Double { + val a = parameters[0] + val beta = parameters[1] + val delta = shift ?: parameters[2] + + return a * (x - delta).pow(beta) + } + + override fun gradient(x: Double, vararg parameters: Double): DoubleArray { + val a = parameters[0] + val beta = parameters[1] + val delta = shift ?: parameters[2] + return if (parameters.size > 2) { + doubleArrayOf( + (x - delta).pow(beta), + a * (x - delta).pow(beta) * ln(x - delta), + -a * beta * (x - delta).pow(beta - 1) + ) + } else { + doubleArrayOf( + (x - delta).pow(beta), + a * (x - delta).pow(beta) * ln(x - delta) + ) + } + } + } + + /** + * Power function $a (x-\delta)^{\beta} + */ + private fun power(point: NumassPoint, spectrum: Table, config: Meta): Values { + val xLow: Int = config.getInt("xLow", 400) + val xHigh: Int = config.getInt("xHigh", 700) + val upper: Int = config.getInt("upper", 3100) + val binning: Int = config.getInt("binning", 20) + + //val fitter = SimpleCurveFitter.create(PowerFunction(), doubleArrayOf(1e-2, 1.5,0.0)) + //val (a, beta, delta) = fitter.fit(preparePoints(spectrum, xLow, xHigh, binning)) + + val delta = config.getDouble("delta", 0.0) + val fitter = SimpleCurveFitter.create(PowerFunction(delta), doubleArrayOf(1e-2, 1.5)) + val (a, beta) = fitter.fit(preparePoints(spectrum, xLow, xHigh, binning)) + + + val norm = norm(spectrum, xLow, upper) + + return ValueMap.ofPairs( + "index" to point.index, + "U" to point.voltage, + "a" to a, + "beta" to beta, + "delta" to delta, + "correction" to a / (beta + 1) * (xLow - delta).pow(beta + 1.0) / norm + 1.0 + ) + } + + fun calculateSubThreshold(point: NumassPoint, spectrum: Table, config: Meta): Values { + return when (config.getString("method", "exp")) { + "exp" -> exponential(point, spectrum, config) + "pow" -> power(point, spectrum, config) + else -> throw RuntimeException("Unknown sub threshold calculation method") + } + } + + fun calculateSubThreshold(set: NumassSet, config: Meta, analyzer: NumassAnalyzer = SmartAnalyzer()): Table { + val reference = config.optNumber("reference").nullable?.let { + set.getPoints(it.toDouble()).firstOrNull() ?: error("Reference point not found") + }?.let { + println("Using reference point ${it.voltage}") + analyzer.getAmplitudeSpectrum(it, config) + } + + return ListTable.Builder().apply { + StreamSupport.stream(set.spliterator(), true).map { point -> + LoggerFactory.getLogger(Threshold.javaClass).info("Starting point ${point.voltage}") + val spectrum = analyzer.getAmplitudeSpectrum(point, config).let { + if (reference == null) { + it + } else { + subtractAmplitudeSpectrum(it, reference) + } + } + LoggerFactory.getLogger(Threshold.javaClass).info("Calculating threshold ${point.voltage}") + return@map try { + calculateSubThreshold(point, spectrum, config) + } catch (ex: Exception) { + LoggerFactory.getLogger(Threshold.javaClass).error("Failed to fit point ${point.voltage}", ex) + null + } + }.toList().filterNotNull().forEach { + println(it.toString()) + row(it) + } + }.build() + } + +} \ No newline at end of file diff --git a/numass-main/src/main/kotlin/inr/numass/tasks/NumassFitScanSummaryTask.kt b/numass-main/src/main/kotlin/inr/numass/tasks/NumassFitScanSummaryTask.kt new file mode 100644 index 00000000..f1292073 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/tasks/NumassFitScanSummaryTask.kt @@ -0,0 +1,76 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package inr.numass.tasks + +import hep.dataforge.actions.ManyToOneAction +import hep.dataforge.context.Context +import hep.dataforge.data.DataNode +import hep.dataforge.data.DataSet +import hep.dataforge.description.TypedActionDef +import hep.dataforge.meta.Laminate +import hep.dataforge.meta.Meta +import hep.dataforge.stat.fit.FitResult +import hep.dataforge.stat.fit.UpperLimitGenerator +import hep.dataforge.tables.ListTable +import hep.dataforge.tables.Table +import hep.dataforge.tables.Tables +import hep.dataforge.workspace.tasks.AbstractTask +import hep.dataforge.workspace.tasks.TaskModel +import inr.numass.NumassUtils + +/** + * @author Alexander Nozik + */ +object NumassFitScanSummaryTask : AbstractTask
(Table::class.java) { + override fun run(model: TaskModel, data: DataNode): DataNode
{ + val builder = DataSet.edit(Table::class) + val action = FitSummaryAction() + val input = data.checked(FitResult::class.java) + input.nodeStream() + .filter { it -> it.count(false) > 0 } + .forEach { node -> builder.putData(node.name, action.run(model.context, node, model.meta).data!!) } + return builder.build() + } + + override fun buildModel(model: TaskModel.Builder, meta: Meta) { + model.dependsOn("fitscan", meta) + } + + + override val name = "scansum" + + @TypedActionDef(name = "sterileSummary", inputType = FitResult::class, outputType = Table::class) + private class FitSummaryAction : ManyToOneAction("sterileSummary", FitResult::class.java,Table::class.java) { + + override fun execute(context: Context, nodeName: String, input: Map, meta: Laminate): Table { + val builder = ListTable.Builder("m", "U2", "U2err", "U2limit", "E0", "trap") + input.forEach { key, fitRes -> + val pars = fitRes.parameters + + val u2Val = pars.getDouble("U2") / pars.getError("U2") + + val limit: Double = if (Math.abs(u2Val) < 3) { + UpperLimitGenerator.getConfidenceLimit(u2Val) * pars.getError("U2") + } else { + java.lang.Double.NaN + } + + builder.row( + Math.sqrt(pars.getValue("msterile2").double), + pars.getValue("U2"), + pars.getError("U2"), + limit, + pars.getValue("E0"), + pars.getValue("trap")) + } + val res = Tables.sort(builder.build(), "m", true) + context.output[name, nodeName].render(NumassUtils.wrap(res, meta)) + return res + } + + } + +} diff --git a/numass-main/src/main/kotlin/inr/numass/tasks/NumassFitScanTask.kt b/numass-main/src/main/kotlin/inr/numass/tasks/NumassFitScanTask.kt new file mode 100644 index 00000000..c1fcf7e7 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/tasks/NumassFitScanTask.kt @@ -0,0 +1,82 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package inr.numass.tasks + +import hep.dataforge.data.DataNode +import hep.dataforge.data.DataTree +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.stat.fit.FitAction +import hep.dataforge.stat.fit.FitResult +import hep.dataforge.tables.Table +import hep.dataforge.useMeta +import hep.dataforge.values.ListValue +import hep.dataforge.values.Value +import hep.dataforge.values.asValue +import hep.dataforge.workspace.tasks.AbstractTask +import hep.dataforge.workspace.tasks.TaskModel + +/** + * @author Alexander Nozik + */ +object NumassFitScanTask : AbstractTask(FitResult::class.java) { + + override fun run(model: TaskModel, data: DataNode): DataNode { + val config = model.meta + val scanParameter = config.getString("parameter", "msterile2") + + val scanValues: Value = if (config.hasValue("masses")) { + ListValue(config.getValue("masses") + .list + .map { it -> Math.pow(it.double * 1000, 2.0).asValue() } + ) + } else { + config.getValue("hep/dataforge/values", listOf(2.5e5, 1e6, 2.25e6, 4e6, 6.25e6, 9e6)) + } + + val action = FitAction() + val resultBuilder = DataTree.edit(FitResult::class) + val sourceNode = data.checked(Table::class.java) + + //do fit + + val fitConfig = config.getMeta("fit") + sourceNode.dataStream().forEach { table -> + for (i in 0 until scanValues.list.size) { + val `val` = scanValues.list[i] + val overrideMeta = MetaBuilder(fitConfig) + + val resultName = String.format("%s[%s=%s]", table.name, scanParameter, `val`.string) + // overrideMeta.setValue("@resultName", String.format("%s[%s=%s]", table.getName(), scanParameter, val.getString())); + + if (overrideMeta.hasMeta("params.$scanParameter")) { + overrideMeta.setValue("params.$scanParameter.value", `val`) + } else { + overrideMeta.getMetaList("params.param").stream() + .filter { par -> par.getString("name") == scanParameter } + .forEach { it.setValue("value", `val`) } + } + // Data
newData = new Data
(data.getGoal(),data.type(),overrideMeta); + val node = action.run(model.context, DataNode.of(resultName, table, Meta.empty()), overrideMeta) + resultBuilder.putData(table.name + ".fit_" + i, node.data!!) + } + } + + + return resultBuilder.build() + } + + override fun buildModel(model: TaskModel.Builder, meta: Meta) { + model.configure(meta.getMetaOrEmpty("scan")) + model.configure { + meta.useMeta("fit"){putNode(it)} + } + model.dependsOn("filter", meta) + } + + override val name = "fitscan" + +} diff --git a/numass-main/src/main/kotlin/inr/numass/tasks/NumassFitSummaryTask.kt b/numass-main/src/main/kotlin/inr/numass/tasks/NumassFitSummaryTask.kt new file mode 100644 index 00000000..5ce506fd --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/tasks/NumassFitSummaryTask.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.tasks + +import hep.dataforge.data.DataNode +import hep.dataforge.meta.Meta +import hep.dataforge.stat.fit.FitState +import hep.dataforge.tables.Table +import hep.dataforge.workspace.tasks.AbstractTask +import hep.dataforge.workspace.tasks.TaskModel +import inr.numass.actions.SummaryAction + +/** + * Created by darksnake on 16-Sep-16. + */ +object NumassFitSummaryTask : AbstractTask
(Table::class.java) { + override val name: String = "summary" + + override fun run(model: TaskModel, data: DataNode): DataNode
{ + val actionMeta = model.meta.getMeta("summary") + val checkedData = data.getCheckedNode("fit", FitState::class.java) + return SummaryAction.run(model.context, checkedData, actionMeta) + } + + override fun buildModel(model: TaskModel.Builder, meta: Meta) { + model.dependsOn("fit", meta, "fit") + } +} diff --git a/numass-main/src/main/kotlin/inr/numass/tasks/NumassTasks.kt b/numass-main/src/main/kotlin/inr/numass/tasks/NumassTasks.kt new file mode 100644 index 00000000..68fa6b82 --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/tasks/NumassTasks.kt @@ -0,0 +1,536 @@ +package inr.numass.tasks + +import hep.dataforge.configure +import hep.dataforge.data.* +import hep.dataforge.io.output.stream +import hep.dataforge.io.render +import hep.dataforge.meta.buildMeta +import hep.dataforge.nullable +import hep.dataforge.plots.data.DataPlot +import hep.dataforge.plots.data.XYFunctionPlot +import hep.dataforge.plots.jfreechart.JFreeChartFrame +import hep.dataforge.plots.output.PlotOutput +import hep.dataforge.plots.output.plot +import hep.dataforge.plots.output.plotFrame +import hep.dataforge.stat.fit.FitHelper +import hep.dataforge.stat.fit.FitResult +import hep.dataforge.stat.models.XYModel +import hep.dataforge.tables.* +import hep.dataforge.useMeta +import hep.dataforge.useValue +import hep.dataforge.values.ValueType +import hep.dataforge.values.Values +import hep.dataforge.values.asValue +import hep.dataforge.values.edit +import hep.dataforge.workspace.tasks.task +import inr.numass.NumassUtils +import inr.numass.actions.MergeDataAction +import inr.numass.actions.MergeDataAction.MERGE_NAME +import inr.numass.actions.TransformDataAction +import inr.numass.addSetMarkers +import inr.numass.data.analyzers.NumassAnalyzer.Companion.CHANNEL_KEY +import inr.numass.data.analyzers.NumassAnalyzer.Companion.COUNT_KEY +import inr.numass.data.analyzers.SmartAnalyzer +import inr.numass.data.analyzers.countInWindow +import inr.numass.data.api.MetaBlock +import inr.numass.data.api.NumassPoint +import inr.numass.data.api.NumassSet +import inr.numass.subtractSpectrum +import inr.numass.unbox +import inr.numass.utils.ExpressionUtils +import java.io.PrintWriter +import java.util.* +import java.util.concurrent.atomic.AtomicLong +import java.util.function.Predicate +import java.util.stream.StreamSupport +import kotlin.collections.HashMap +import kotlin.collections.set + +private val filterForward = DataFilter.byMetaValue("iteration_info.reverse") { + !(it?.boolean ?: false) +} +private val filterReverse = DataFilter.byMetaValue("iteration_info.reverse") { + it?.boolean ?: false +} + +val selectTask = task("select") { + descriptor { + info = "Select data from initial data pool" + value("forward", types = listOf(ValueType.BOOLEAN), info = "Select only forward or only backward sets") + } + model { meta -> + data("*") + configure(meta.getMetaOrEmpty("data")) + } + transform { data -> + logger.info("Starting selection from data node with size ${data.size}") + var res = data.checked(NumassSet::class.java).filter(CustomDataFilter(meta)) + + meta.useValue("forward") { + res = if (it.boolean) { + res.filter(filterForward) + } else { + res.filter(filterReverse) + } + } + + logger.info("Selected ${res.size} elements") + res + } +} + +val analyzeTask = task("analyze") { + descriptor { + info = "Count the number of events for each voltage and produce a table with the results" + } + model { meta -> + dependsOn(selectTask, meta) + configure { + "analyzer" to meta.getMetaOrEmpty("analyzer") + } + } + pipe { set -> + val res = SmartAnalyzer().analyzeSet(set, meta.getMeta("analyzer")) + val outputMeta = meta.builder.putNode("data", set.meta) + context.output.render(res, stage = "numass.analyze", name = name, meta = outputMeta) + return@pipe res + } +} + +val monitorTableTask = task("monitor") { + descriptor { + value("showPlot", types = listOf(ValueType.BOOLEAN), info = "Show plot after complete") + value("monitorPoint", types = listOf(ValueType.NUMBER), info = "The voltage for monitor point") + } + model { meta -> + dependsOn(selectTask, meta) +// if (meta.getBoolean("monitor.correctForThreshold", false)) { +// dependsOn(subThresholdTask, meta, "threshold") +// } + configure(meta.getMetaOrEmpty("monitor")) + configure { + meta.useMeta("analyzer") { putNode(it) } + setValue("@target", meta.getString("@target", meta.name)) + } + } + join { data -> + val monitorVoltage = meta.getDouble("monitorPoint", 16000.0); + val analyzer = SmartAnalyzer() + val analyzerMeta = meta.getMetaOrEmpty("analyzer") + + //val thresholdCorrection = da + //TODO add separator labels + val res = ListTable.Builder("timestamp", "count", "cr", "crErr", "index", "set") + .rows( + data.values.stream().flatMap { set -> + set.points.stream() + .filter { it.voltage == monitorVoltage } + .parallel() + .map { point -> + analyzer.analyzeParent(point, analyzerMeta).edit { + "index" to point.index + "set" to set.name + } + } + } + + ).build() + + if (meta.getBoolean("showPlot", true)) { + val plot = DataPlot.plot(name, res, Adapters.buildXYAdapter("timestamp", "cr", "crErr")) + context.plot(plot, name, "numass.monitor") { + "xAxis.title" to "time" + "xAxis.type" to "time" + "yAxis.title" to "Count rate" + "yAxis.units" to "Hz" + } + + ((context.output["numass.monitor", name] as? PlotOutput)?.frame as? JFreeChartFrame)?.addSetMarkers(data.values) + } + + context.output.render(res, stage = "numass.monitor", name = name, meta = meta) + + return@join res; + } +} + +val mergeTask = task("merge") { + model { meta -> + dependsOn(transformTask, meta) + configure(meta.getMetaOrEmpty("merge")) + } + action(MergeDataAction) +} + +val mergeEmptyTask = task("empty") { + model { meta -> + if (!meta.hasMeta("empty")) { + throw RuntimeException("Empty source data not found "); + } + //replace data node by "empty" node + val newMeta = meta.builder + .removeNode("data") + .removeNode("empty") + .setNode("data", meta.getMeta("empty")) + .setValue("merge.$MERGE_NAME", meta.getString("merge.$MERGE_NAME", "") + "_empty") + dependsOn(mergeTask, newMeta) + } + transform
{ data -> + val builder = DataSet.edit(Table::class) + data.forEach { + builder.putData(it.name + "_empty", it.anonymize()); + } + builder.build() + } +} + + +val subtractEmptyTask = task("dif") { + model { meta -> + dependsOn(mergeTask, meta, "data") + if (meta.hasMeta("empty")) { + dependsOn(mergeEmptyTask, meta, "empty") + } + } + transform
{ data -> + //ignore if there is no empty data + if (!meta.hasMeta("empty")) return@transform data + + val builder = DataTree.edit(Table::class) + val rootNode = data.getCheckedNode("data", Table::class.java) + val empty = data.getCheckedNode("empty", Table::class.java).data + ?: throw RuntimeException("No empty data found") + + rootNode.visit(Table::class.java) { input -> + val resMeta = buildMeta { + putNode("data", input.meta) + putNode("empty", empty.meta) + } + val res = DataUtils.combine(context, input, empty, Table::class.java, resMeta) { mergeData, emptyData -> + val dif = subtractSpectrum(mergeData, emptyData, context.logger) + context.output["numass.merge", input.name + "_subtract"].render(NumassUtils.wrap(dif, resMeta)) + dif + } + + builder.putData(input.name, res) + } + builder.build() + } +} + +val transformTask = task("transform") { + model { meta -> + dependsOn(analyzeTask, meta) + } + action(TransformDataAction) +} + +val filterTask = task("filter") { + model { meta -> + if (meta.hasMeta("merge")) { + dependsOn(subtractEmptyTask, meta) + } else { + dependsOn(analyzeTask, meta) + } + configure(meta.getMetaOrEmpty("filter")) + } + pipe { data -> + + if(meta.isEmpty) return@pipe data + + val result = if (meta.hasValue("from") || meta.hasValue("to")) { + val uLo = meta.getDouble("from", 0.0) + val uHi = meta.getDouble("to", java.lang.Double.POSITIVE_INFINITY) + Tables.filter(data, NumassPoint.HV_KEY, uLo, uHi) + } else if (meta.hasValue("condition")) { + Tables.filter(data, Predicate { ExpressionUtils.condition(meta.getString("condition"), it.unbox()) }) + } else { + throw RuntimeException("No filtering condition specified") + } + + context.output.render(result, name = this.name, stage = "numass.filter") + return@pipe result + } + +} + +val fitTask = task("fit") { + model { meta -> + dependsOn(filterTask, meta) + configure(meta.getMeta("fit")) + } + pipe { data -> + context.output["numass.fit", name].stream.use { out -> + val writer = PrintWriter(out) + writer.printf("%n*** META ***%n") + writer.println(meta.toString()) + writer.flush() + + FitHelper(context).fit(data, meta) + .setListenerStream(out) + .report(log) + .run() + .also { + if (meta.getBoolean("printLog", true)) { + writer.println() + log.entries.forEach { entry -> writer.println(entry.toString()) } + writer.println() + } + } + } + } +} + +val plotFitTask = task("plotFit") { + model { meta -> + dependsOn(fitTask, meta) + configure(meta.getMetaOrEmpty("plotFit")) + } + pipe { input -> + val fitModel = input.optModel(context).orElseThrow { IllegalStateException("Can't load model") } as XYModel + + val data = input.data + val adapter: ValuesAdapter = fitModel.adapter + val function = { x: Double -> fitModel.spectrum.value(x, input.parameters) } + + val fit = XYFunctionPlot("fit", function = function).apply { + density = 100 + } + + // ensuring all data points are calculated explicitly + StreamSupport.stream(data.spliterator(), false) + .map { dp -> Adapters.getXValue(adapter, dp).double }.sorted().forEach { fit.calculateIn(it) } + + val dataPlot = DataPlot.plot("data", data, adapter) + + context.plot(listOf(fit, dataPlot), name, "numass.plotFit") + + return@pipe input; + } +} + +val histogramTask = task("histogram") { + descriptor { + value("plot", types = listOf(ValueType.BOOLEAN), defaultValue = false, info = "Show plot of the spectra") + value( + "points", + multiple = true, + types = listOf(ValueType.NUMBER), + info = "The list of point voltages to build histogram" + ) + value( + "binning", + types = listOf(ValueType.NUMBER), + defaultValue = 16, + info = "The binning of resulting histogram" + ) + value( + "normalize", + types = listOf(ValueType.BOOLEAN), + defaultValue = true, + info = "If true reports the count rate in each bin, otherwise total count" + ) + info = "Combine amplitude spectra from multiple sets, but with the same U" + } + model { meta -> + dependsOn(selectTask, meta) + configure(meta.getMetaOrEmpty("histogram")) + configure { + meta.useMeta("analyzer") { putNode(it) } + setValue("@target", meta.getString("@target", meta.name)) + } + } + join { data -> + val analyzer = SmartAnalyzer() + val points = meta.optValue("points").nullable?.list?.map { it.double } + + val aggregator: MutableMap> = HashMap() + val names: SortedSet = TreeSet().also { it.add("channel") } + + log.report("Filling histogram") + + //Fill values to table + data.flatMap { it.value.points } + .filter { points == null || points.contains(it.voltage) } + .groupBy { it.voltage } + .mapValues { (_, value) -> + analyzer.getAmplitudeSpectrum(MetaBlock(value), meta.getMetaOrEmpty("analyzer")) + } + .forEach { (u, spectrum) -> + log.report("Aggregating data from U = $u") + spectrum.forEach { + val channel = it[CHANNEL_KEY].int + val count = it[COUNT_KEY].long + aggregator.getOrPut(channel) { HashMap() } + .getOrPut(u) { AtomicLong() } + .addAndGet(count) + } + names.add("U$u") + } + + val times: Map = data.flatMap { it.value.points } + .filter { points == null || points.contains(it.voltage) } + .groupBy { it.voltage } + .mapValues { + it.value.sumByDouble { it.length.toMillis().toDouble() / 1000 } + } + + val normalize = meta.getBoolean("normalize", true) + + log.report("Combining spectra") + val format = MetaTableFormat.forNames(names) + val table = buildTable(format) { + aggregator.forEach { (channel, counters) -> + val values: MutableMap = HashMap() + values[CHANNEL_KEY] = channel + counters.forEach { (u, counter) -> + if (normalize) { + values["U$u"] = counter.get().toDouble() / times.getValue(u) + } else { + values["U$u"] = counter.get() + } + } + format.names.forEach { + values.putIfAbsent(it, 0) + } + row(values) + } + }.sumByStep(CHANNEL_KEY, meta.getDouble("binning", 16.0)) //apply binning + + // send raw table to the output + context.output.render(table, stage = "numass.histogram", name = name) { + update(meta) + data.toSortedMap().forEach { (name, set) -> + putNode("data", buildMeta { + "name" to name + set.meta.useMeta("iteration_info") { "iteration" to it } + }) + } + } + + if (meta.getBoolean("plot", false)) { + context.plotFrame("$name.plot", stage = "numass.histogram") { + plots.setType() + plots.configure { + "showSymbol" to false + "showErrors" to false + "showLine" to true + "connectionType" to "step" + } + table.format.names.filter { it != "channel" }.forEach { + +DataPlot.plot(it, table, adapter = Adapters.buildXYAdapter("channel", it)) + } + } + } + + return@join table + } +} + +val sliceTask = task("slice") { + model { meta -> + dependsOn(selectTask, meta) + configure(meta.getMetaOrEmpty("slice")) + configure { + meta.useMeta("analyzer") { putNode(it) } + setValue("@target", meta.getString("@target", meta.name)) + } + } + join { data -> + val analyzer = SmartAnalyzer() + val slices = HashMap() + val formatBuilder = TableFormatBuilder() + formatBuilder.addColumn("set", ValueType.STRING) + formatBuilder.addColumn("time", ValueType.TIME) + meta.getMetaList("range").forEach { + val range = IntRange(it.getInt("from"), it.getInt("to")) + val name = it.getString("name", range.toString()) + slices[name] = range + formatBuilder.addColumn(name, ValueType.NUMBER) + } + + val table = buildTable(formatBuilder.build()) { + data.forEach { (setName, set) -> + val point = set.find { + it.index == meta.getInt("index", -1) || + it.voltage == meta.getDouble("voltage", -1.0) + } + + if (point != null) { + val amplitudeSpectrum = analyzer.getAmplitudeSpectrum(point, meta.getMetaOrEmpty("analyzer")) + val map = HashMap() + map["set"] = setName + map["time"] = point.startTime + slices.mapValuesTo(map) { (_, range) -> + amplitudeSpectrum.countInWindow( + range.start.toShort(), + range.endInclusive.toShort() + ) + } + + row(map) + } + } + } + // send raw table to the output + context.output.render(table, stage = "numass.table", name = name, meta = meta) + + return@join table + } +} + +val fitScanTask = task("fitscan") { + model { meta -> + dependsOn(filterTask, meta) + configure { + setNode(meta.getMetaOrEmpty("scan")) + setNode(meta.getMeta("fit")) + } + } + + splitAction { + val scanValues = if (meta.hasValue("scan.masses")) { + meta.getValue("scan.masses").list.map { it -> Math.pow(it.double * 1000, 2.0).asValue() } + } else { + meta.getValue("scan.values", listOf(2.5e5, 1e6, 2.25e6, 4e6, 6.25e6, 9e6)).list + } + + val scanParameter = meta.getString("parameter", "msterile2") + scanValues.forEach { scanValue -> + val resultName = String.format("%s[%s=%s]", this.name, scanParameter, scanValue.string) + val fitMeta = meta.getMeta("fit").builder.apply { + setValue("@nameSuffix", String.format("[%s=%s]", scanParameter, scanValue.string)) + if (hasMeta("params.$scanParameter")) { + setValue("params.$scanParameter.value", scanValue) + } else { + getMetaList("params.param").stream() + .filter { par -> par.getString("name") == scanParameter } + .forEach { it.setValue("value", it) } + } + } + + fragment(resultName) { + result { data -> + context.output["numass.fitscan", name].stream.use { out -> + val writer = PrintWriter(out) + writer.printf("%n*** META ***%n") + writer.println(fitMeta.toString()) + writer.flush() + + FitHelper(context).fit(data, fitMeta) + .setListenerStream(out) + .report(log) + .run() + .also { + if (fitMeta.getBoolean("printLog", true)) { + writer.println() + log.entries.forEach { entry -> writer.println(entry.toString()) } + writer.println() + } + } + } + } + } + } + } +} diff --git a/numass-main/src/main/kotlin/inr/numass/tasks/SpecialNumassTasks.kt b/numass-main/src/main/kotlin/inr/numass/tasks/SpecialNumassTasks.kt new file mode 100644 index 00000000..90df3dfe --- /dev/null +++ b/numass-main/src/main/kotlin/inr/numass/tasks/SpecialNumassTasks.kt @@ -0,0 +1,55 @@ +package inr.numass.tasks + +import hep.dataforge.io.render +import hep.dataforge.plots.data.DataPlot +import hep.dataforge.plots.output.plotFrame +import hep.dataforge.plots.plotData +import hep.dataforge.tables.Adapters +import hep.dataforge.tables.Table +import hep.dataforge.tables.filter +import hep.dataforge.useMeta +import hep.dataforge.values.ValueType +import hep.dataforge.workspace.tasks.task +import inr.numass.data.NumassDataUtils +import inr.numass.data.api.NumassSet +import inr.numass.subthreshold.Threshold + +val subThresholdTask = task("threshold") { + descriptor { + value("plot", types = listOf(ValueType.BOOLEAN), defaultValue = false, info = "Show threshold correction plot") + value( + "binning", + types = listOf(ValueType.NUMBER), + defaultValue = 16, + info = "The binning used for fit" + ) + info = "Calculate sub threshold correction" + } + model { meta -> + dependsOn(selectTask, meta) + configure(meta.getMetaOrEmpty("threshold")) + configure { + meta.useMeta("analyzer") { putNode(it) } + setValue("@target", meta.getString("@target", meta.name)) + } + } + join { data -> + val sum = NumassDataUtils.joinByIndex(name, data.values) + + val correctionTable = Threshold.calculateSubThreshold(sum, meta).filter { + it.getDouble("correction") in (1.0..1.2) + } + + if (meta.getBoolean("plot", false)) { + context.plotFrame("$name.plot", stage = "numass.threshold") { + plots.setType() + plotData("${name}_cor", correctionTable, Adapters.buildXYAdapter("U", "correction")) + plotData("${name}_a", correctionTable, Adapters.buildXYAdapter("U", "a")) + plotData("${name}_beta", correctionTable, Adapters.buildXYAdapter("U", "beta")) + } + } + + context.output.render(correctionTable, "numass.correction", name, meta = meta) + return@join correctionTable + } +} diff --git a/numass-main/src/main/resources/META-INF/services/hep.dataforge.context.PluginFactory b/numass-main/src/main/resources/META-INF/services/hep.dataforge.context.PluginFactory new file mode 100644 index 00000000..a045d442 --- /dev/null +++ b/numass-main/src/main/resources/META-INF/services/hep.dataforge.context.PluginFactory @@ -0,0 +1 @@ +inr.numass.NumassPlugin$Factory \ No newline at end of file diff --git a/numass-main/src/main/resources/data/FS.txt b/numass-main/src/main/resources/data/FS.txt new file mode 100644 index 00000000..93b30068 --- /dev/null +++ b/numass-main/src/main/resources/data/FS.txt @@ -0,0 +1,193 @@ +0.000 0.008 +0.097 0.005 +0.197 0.028 +0.297 0.055 +0.397 0.056 +0.497 0.218 +0.597 0.191 +0.697 0.434 +0.797 0.429 +0.897 0.688 +0.997 1.300 +1.097 1.078 +1.197 2.793 +1.297 3.715 +1.397 4.480 +1.497 7.176 +1.597 6.825 +1.697 5.171 +1.797 6.187 +1.897 5.023 +1.997 3.334 +2.097 2.239 +2.197 1.493 +2.297 1.008 +2.397 1.562 +2.647 0.940 +2.897 0.518 +3.147 0.249 +3.397 0.116 +3.647 0.055 +3.897 0.036 +4.397 0.007 +4.897 0.001 +20.881 0.003 +21.881 0.021 +22.881 0.109 +23.881 0.385 +24.881 0.973 +25.881 1.833 +26.881 2.671 +27.881 3.093 +28.881 2.913 +29.881 2.276 +30.881 1.503 +31.881 0.882 +32.881 0.727 +33.881 1.389 +34.881 2.175 +35.881 2.086 +36.881 1.310 +37.881 0.676 +38.725 0.010 +38.881 0.416 +39.881 0.370 +40.881 0.350 +41.881 0.269 +42.732 0.965 +42.881 0.166 +43.405 0.029 +43.881 0.091 +43.963 0.372 +44.147 0.128 +44.881 0.043 +45.881 0.016 +46.881 0.004 +47.913 0.129 +50.599 1.216 +52.553 0.440 +55.109 0.065 +55.852 0.154 +57.004 0.159 +58.092 0.000 +58.592 0.001 +59.092 0.003 +59.592 0.010 +60.092 0.026 +60.592 0.058 +61.092 0.126 +61.592 0.206 +62.092 0.301 +62.592 0.377 +63.092 0.418 +63.592 0.377 +64.092 0.301 +64.386 0.003 +64.592 0.206 +64.886 0.007 +65.092 0.126 +65.386 0.023 +65.592 0.058 +65.886 0.060 +66.092 0.026 +66.386 0.133 +66.592 0.010 +66.886 0.288 +67.092 0.003 +67.386 0.471 +67.592 0.001 +67.886 0.688 +68.092 0.000 +68.386 0.863 +68.886 0.956 +69.386 0.863 +69.886 0.688 +70.386 0.471 +70.886 0.288 +71.386 0.133 +71.725 0.306 +71.886 0.060 +72.386 0.023 +72.886 0.007 +73.386 0.003 +74.820 0.245 +76.169 0.088 +76.868 0.100 +77.221 0.273 +79.427 0.020 +80.865 0.238 +81.965 0.137 +83.429 0.151 +84.170 0.212 +84.218 0.112 +86.123 0.014 +87.374 0.010 +88.259 0.009 +88.876 0.013 +89.871 0.026 +90.690 0.023 +91.784 0.052 +93.247 0.178 +94.333 0.133 +96.192 0.026 +96.701 0.054 +97.543 0.023 +98.514 0.005 +98.840 0.010 +100.263 0.014 +100.784 0.003 +101.620 0.003 +102.426 0.005 +102.842 0.001 +103.170 0.001 +103.594 0.006 +104.236 0.002 +105.008 0.001 +105.799 0.002 +106.990 0.006 +108.711 0.010 +109.189 0.008 +109.975 0.007 +111.148 0.005 +112.339 0.013 +113.145 0.010 +113.882 0.005 +114.892 0.002 +115.612 0.002 +116.455 0.001 +117.594 0.005 +118.481 0.023 +119.245 0.023 +120.360 0.009 +121.764 0.013 +123.594 0.009 +124.247 0.005 +125.709 0.012 +127.715 0.003 +129.373 0.002 +130.271 0.004 +132.887 0.060 +133.402 0.025 +134.813 0.082 +135.371 0.006 +136.379 0.005 +136.916 0.003 +138.243 0.008 +139.737 0.010 +141.093 0.006 +142.461 0.047 +144.001 0.004 +144.391 0.007 +147.073 0.021 +148.311 0.015 +148.895 0.001 +150.849 0.004 +151.442 0.001 +152.854 0.000 +154.169 0.002 +156.093 0.001 +157.003 0.003 +158.134 0.003 +159.271 0.002 +162.054 0.007 +164.173 0.002 \ No newline at end of file diff --git a/numass-main/src/main/resources/data/d2_17_1.xml b/numass-main/src/main/resources/data/d2_17_1.xml new file mode 100644 index 00000000..672cf5a2 --- /dev/null +++ b/numass-main/src/main/resources/data/d2_17_1.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + DLOSS_16.DAT + DLOSS_17.DAT + DLOSS_18.DAT + DLOSS_19.DAT + + + + + + + + + + + DLOSS_20.DAT + DLOSS_21.DAT + DLOSS_22.DAT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/numass-main/src/main/resources/data/run23.cfg b/numass-main/src/main/resources/data/run23.cfg new file mode 100644 index 00000000..d79ff862 --- /dev/null +++ b/numass-main/src/main/resources/data/run23.cfg @@ -0,0 +1,48 @@ +18400.0 7300 +18425.0 7300 +18450.0 7300 +18475.0 7300 +18500.0 14600 +18505.0 14600 +18510.0 14600 +18515.0 14600 +18520.0 14600 +18525.0 14600 +18530.0 14600 +18535.0 14600 +18540.0 14600 +18542.0 14600 +18544.0 14600 +18546.0 14600 +18548.0 14600 +18550.0 14600 +18552.0 14600 +18553.0 14600 +18554.0 14600 +18555.0 20500 +18556.0 20500 +18557.0 20500 +18558.0 20500 +18559.0 20500 +18560.0 29200 +18561.0 29200 +18562.0 29200 +18563.0 29200 +18564.0 29200 +18565.0 29200 +18566.0 29200 +18567.0 29200 +18568.0 29200 +18569.0 29200 +18570.0 29200 +18571.0 29200 +18572.0 23300 +18573.0 23300 +18574.0 23300 +18575.0 14600 +18577.0 14600 +18580.0 14600 +18585.0 14600 +18590.0 14600 +18670.0 14600 +18770.0 14600 \ No newline at end of file diff --git a/numass-main/src/main/resources/numass/models/transmission b/numass-main/src/main/resources/numass/models/transmission new file mode 100644 index 00000000..ec39b5c7 --- /dev/null +++ b/numass-main/src/main/resources/numass/models/transmission @@ -0,0 +1,29 @@ +13 0.977595005 +13.2 0.978402952 +13.4 0.978969718 +13.6 0.979669107 +13.8 0.980320277 +14 0.981067929 +14.2 0.981863799 +14.4 0.982297898 +14.6 0.9831058 +14.8 0.983612271 +15 0.984154928 +15.2 0.984721694 +15.4 0.985409006 +15.6 0.986011958 +15.8 0.986687237 +16 0.987217817 +16.2 0.987953392 +16.4 0.988544267 +16.6 0.989062815 +16.8 0.989834576 +17 0.990232488 +17.2 0.990738959 +17.4 0.991353944 +17.6 0.991932742 +17.8 0.99257188 +18 0.993548636 +18.2 0.995176562 +18.4 0.999155907 +18.5 1.0 \ No newline at end of file diff --git a/numass-main/src/test/java/inr/numass/NumassTest.java b/numass-main/src/test/java/inr/numass/NumassTest.java new file mode 100644 index 00000000..bc42dbd3 --- /dev/null +++ b/numass-main/src/test/java/inr/numass/NumassTest.java @@ -0,0 +1,26 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package inr.numass; + +import hep.dataforge.context.Context; +import hep.dataforge.maths.functions.FunctionLibrary; +import org.junit.Test; + +/** + * + * @author Alexander Nozik + */ +public class NumassTest { + /** + * Test of buildContext method, of class Numass. + */ + @Test + public void testBuildContext() { + Context context = Numass.buildContext(); + FunctionLibrary.Companion.buildFrom(context); + } + +} diff --git a/numass-main/src/test/java/inr/numass/actions/PrepareDataActionTest.java b/numass-main/src/test/java/inr/numass/actions/PrepareDataActionTest.java new file mode 100644 index 00000000..dd144fb5 --- /dev/null +++ b/numass-main/src/test/java/inr/numass/actions/PrepareDataActionTest.java @@ -0,0 +1,32 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package inr.numass.actions; + +import inr.numass.utils.ExpressionUtils; +import org.junit.Assert; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +/** + * + * @author Alexander Nozik + */ +public class PrepareDataActionTest { + + public PrepareDataActionTest() { + } + + @Test + public void testExpression() { + Map exprParams = new HashMap<>(); + exprParams.put("U", 18000d); + double correctionFactor = ExpressionUtils.function("1 + 13.265 * exp(- U / 2343.4)", exprParams); + Assert.assertEquals("Testing expression calculation", 1.006125, correctionFactor, 1e-5); + } + +} diff --git a/numass-main/src/test/java/inr/numass/models/NumassSpectrumTest.java b/numass-main/src/test/java/inr/numass/models/NumassSpectrumTest.java new file mode 100644 index 00000000..77037cde --- /dev/null +++ b/numass-main/src/test/java/inr/numass/models/NumassSpectrumTest.java @@ -0,0 +1,72 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.models; + +import hep.dataforge.exceptions.NamingException; +import hep.dataforge.stat.fit.MINUITPlugin; +import hep.dataforge.stat.fit.ParamSet; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.Locale; + +import static java.util.Locale.setDefault; + +/** + * + * @author Darksnake + */ +public class NumassSpectrumTest { + + /** + * @param args the command line arguments + * @throws java.io.FileNotFoundException + */ + public static void main(String[] args) throws NamingException, FileNotFoundException { + setDefault(Locale.US); + new MINUITPlugin().startGlobal(); + + ParamSet allPars = new ParamSet(); + + allPars.setParValue("N", 3000); + //значение 6е-6 ÑоответÑтвует полной интенÑтивноÑти 6е7 раÑпадов в Ñекунду + //Проблема была в переполнении Ñчетчика Ñобытий в генераторе. Заменил на long. Возможно Ñтоит поÑтавить туда чиÑло Ñ Ð¿Ð»Ð°Ð²Ð°ÑŽÑ‰ÐµÐ¹ точкой + allPars.setParError("N", 6); + allPars.setParDomain("N", 0d, Double.POSITIVE_INFINITY); + allPars.setParValue("bkg", 3); + allPars.setParError("bkg", 0.03); + allPars.setParValue("E0", 18500.0); + allPars.setParError("E0", 2); + allPars.setParValue("mnu2", 0d); + allPars.setParError("mnu2", 1d); + allPars.setParValue("msterile2", 2000 * 2000); + allPars.setParValue("U2", 0); + allPars.setParError("U2", 1e-4); + allPars.setParDomain("U2", -1d, 1d); + allPars.setParValue("X", 0); + allPars.setParError("X", 0.01); + allPars.setParDomain("X", 0d, Double.POSITIVE_INFINITY); + allPars.setParValue("trap", 1); + allPars.setParError("trap", 0.01d); + allPars.setParDomain("trap", 0d, Double.POSITIVE_INFINITY); + + ModularSpectrum betaNew = new ModularSpectrum(new BetaSpectrum(new File("d:\\PlayGround\\FS.txt")), 1e-4, 14390d, 19001d); + betaNew.setCaching(false); + + System.out.println(betaNew.value(17000d, allPars)); + + } +} diff --git a/numass-main/src/test/java/inr/numass/models/PlotScatter.java b/numass-main/src/test/java/inr/numass/models/PlotScatter.java new file mode 100644 index 00000000..2c84ed96 --- /dev/null +++ b/numass-main/src/test/java/inr/numass/models/PlotScatter.java @@ -0,0 +1,42 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.models; + +import hep.dataforge.stat.fit.ParamSet; +import inr.numass.NumassPluginKt; +import inr.numass.models.misc.LossCalculator; + +/** + * + * @author darksnake + */ +public class PlotScatter { + + public static void main(String[] args) { + ParamSet pars = ParamSet.fromString( + "'N' = 2492.87 ± 3.6 (0.00000,Infinity)\n" + + "'bkg' = 5.43 ± 0.16\n" + + "'X' = 0.51534 ± 0.0016\n" + + "'shift' = 0.00842 ± 0.0024\n" + + "'exPos' = 12.870 ± 0.054\n" + + "'ionPos' = 16.63 ± 0.58\n" + + "'exW' = 1.49 ± 0.15\n" + + "'ionW' = 11.33 ± 0.43\n" + + "'exIonRatio' = 4.83 ± 0.36" + ); + LossCalculator.INSTANCE.plotScatter(NumassPluginKt.displayChart("Loss function"), pars); + } +} diff --git a/numass-main/src/test/java/inr/numass/models/TestModels.kt b/numass-main/src/test/java/inr/numass/models/TestModels.kt new file mode 100644 index 00000000..5d25ba65 --- /dev/null +++ b/numass-main/src/test/java/inr/numass/models/TestModels.kt @@ -0,0 +1,93 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package inr.numass.models + +import hep.dataforge.context.Context +import hep.dataforge.maths.functions.FunctionLibrary +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.stat.fit.ParamSet +import hep.dataforge.stat.parametric.ParametricFunction +import hep.dataforge.step +import inr.numass.Numass +import inr.numass.models.sterile.SterileNeutrinoSpectrum +import org.apache.commons.math3.analysis.BivariateFunction + + +/** + * @param args the command line arguments + */ + +fun main() { + val context = Numass.buildContext() + /* + + + + + */ + val meta = MetaBuilder("model") + .putNode(MetaBuilder("resolution") + .putValue("width", 1.22e-4) + .putValue("tailAlpha", 1e-2) + ) + .putNode(MetaBuilder("transmission") +// .putValue("trapping", "function::numass.trap.nominal") + ) + val oldFunc = oldModel(context, meta) + val newFunc = newModel(context, meta) + + val allPars = ParamSet() + .setPar("N", 7e+05, 1.8e+03, 0.0, java.lang.Double.POSITIVE_INFINITY) + .setPar("bkg", 1.0, 0.050) + .setPar("E0", 18575.0, 1.4) + .setPar("mnu2", 0.0, 1.0) + .setPar("msterile2", 1000.0 * 1000.0, 0.0) + .setPar("U2", 0.0, 1e-4, -1.0, 1.0) + .setPar("X", 0.0, 0.01, 0.0, java.lang.Double.POSITIVE_INFINITY) + .setPar("trap", 1.0, 0.01, 0.0, java.lang.Double.POSITIVE_INFINITY) + + for (u in 14000.0..18600.0 step 100.0) { + val oldVal = oldFunc.value(u, allPars); + val newVal = newFunc.value(u, allPars); +// val oldVal = oldFunc.derivValue("trap", u, allPars) +// val newVal = newFunc.derivValue("trap", u, allPars) + System.out.printf("%f\t%g\t%g\t%g%n", u, oldVal, newVal, 1.0 - oldVal / newVal) + } +} + +private fun oldModel(context: Context, meta: Meta): ParametricFunction { + val A = meta.getDouble("resolution", meta.getDouble("resolution.width", 8.3e-5))//8.3e-5 + val from = meta.getDouble("from", 13900.0) + val to = meta.getDouble("to", 18700.0) + context.history.report("Setting up tritium model with real transmission function") + + val resolutionTail: BivariateFunction = if (meta.hasValue("resolution.tailAlpha")) { + ResolutionFunction.getAngledTail(meta.getDouble("resolution.tailAlpha"), meta.getDouble("resolution.tailBeta", 0.0)) + } else { + ResolutionFunction.getRealTail() + } + //RangedNamedSetSpectrum beta = new BetaSpectrum(context.io().getFile("FS.txt")); + val sp = ModularSpectrum(BetaSpectrum(), ResolutionFunction(A, resolutionTail), from, to) + if (meta.getBoolean("caching", false)) { + context.history.report("Caching turned on") + sp.setCaching(true) + } + //Adding trapping energy dependence + + if (meta.hasValue("transmission.trapping")) { + val trap = FunctionLibrary.buildFrom(context).buildBivariateFunction(meta.getString("transmission.trapping")) + sp.setTrappingFunction(trap) + } + + return NBkgSpectrum(sp) +} + +private fun newModel(context: Context, meta: Meta): ParametricFunction { + val sp = SterileNeutrinoSpectrum(context, meta) + return NBkgSpectrum(sp) +} + diff --git a/numass-main/src/test/java/inr/numass/models/TestNeLossParametrisation.java b/numass-main/src/test/java/inr/numass/models/TestNeLossParametrisation.java new file mode 100644 index 00000000..e02f31c0 --- /dev/null +++ b/numass-main/src/test/java/inr/numass/models/TestNeLossParametrisation.java @@ -0,0 +1,71 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.models; + +import hep.dataforge.maths.integration.GaussRuleIntegrator; +import hep.dataforge.plots.PlotFrame; +import hep.dataforge.plots.data.XYFunctionPlot; +import inr.numass.NumassPluginKt; +import inr.numass.models.misc.LossCalculator; +import org.apache.commons.math3.analysis.UnivariateFunction; + +/** + * + * @author Alexander Nozik + */ +@SuppressWarnings("unchecked") +public class TestNeLossParametrisation { + + /** + * @param args the command line arguments + */ + public static void main(String[] args) { + PlotFrame frame = NumassPluginKt.displayChart("Loss parametrisation test"); + //JFreeChartFrame.drawFrame("Loss parametrisation test", null); + UnivariateFunction oldFunction = LossCalculator.INSTANCE.getSingleScatterFunction(); + UnivariateFunction newFunction = getSingleScatterFunction(12.86, 16.78, 1.65, 12.38, 4.79); + + Double norm = new GaussRuleIntegrator(200).integrate(0d, 100d, newFunction); + + System.out.println(norm); + + frame.add(XYFunctionPlot.Companion.plot("old", 0, 30, 300, oldFunction::value)); + frame.add(XYFunctionPlot.Companion.plot("new", 0, 30, 300, newFunction::value)); + } + + public static UnivariateFunction getSingleScatterFunction( + final double exPos, + final double ionPos, + final double exW, + final double ionW, + final double exIonRatio) { + + return (double eps) -> { + if (eps <= 0) { + return 0; + } + double z = eps - exPos; + // ИÑпользуетÑÑ Ð¿Ð¾Ð»Ð½Ð°Ñ ÑˆÐ¸Ñ€Ð¸Ð½Ð°, а не полуширина. + double res = exIonRatio * Math.exp(-2 * z * z / exW / exW) * Math.sqrt(2 / Math.PI) / exW; + + if (eps >= ionPos) { + z = 4 * (eps - ionPos) * (eps - ionPos); + res += 4d / (1 + z / ionW / ionW) / Math.PI / ionW; + } + return res / (1 + exIonRatio); + }; + } +} diff --git a/numass-main/src/test/java/inr/numass/models/TransmissionInterpolatorTest.java b/numass-main/src/test/java/inr/numass/models/TransmissionInterpolatorTest.java new file mode 100644 index 00000000..bd863cf6 --- /dev/null +++ b/numass-main/src/test/java/inr/numass/models/TransmissionInterpolatorTest.java @@ -0,0 +1,41 @@ +/* + * Copyright 2015 Alexander Nozik. + * + * 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 inr.numass.models; + +import hep.dataforge.context.Global; +import hep.dataforge.plots.data.DataPlot; +import hep.dataforge.plots.data.XYFunctionPlot; +import hep.dataforge.plots.jfreechart.JFreeChartFrame; +import inr.numass.NumassPluginKt; + +/** + * + * @author darksnake + */ +public class TransmissionInterpolatorTest { + + public static void main(String[] args) { + JFreeChartFrame frame = NumassPluginKt.displayChart("TransmissionInterpolatorTest"); +//JFreeChartFrame.drawFrame("TransmissionInterpolatorTest", null); + TransmissionInterpolator interpolator = TransmissionInterpolator.fromFile(Global.INSTANCE, + "d:\\sterile-new\\loss2014-11\\.dataforge\\merge\\empty_sum.onComplete", "Uset", "CR", 15, 0.8, 19002d); + frame.add(DataPlot.Companion.plot("data", interpolator.getX(), interpolator.getY())); + frame.add(XYFunctionPlot.Companion.plot("interpolated", interpolator.getXmin(), interpolator.getXmax(), 2000, interpolator::value)); + +// PrintFunction.printFuntionSimple(new PrintWriter(System.onComplete), interpolator, interpolator.getXmin(), interpolator.getXmax(), 500); + } + +} diff --git a/numass-main/src/test/kotlin/inr/numass/NumassPluginTest.kt b/numass-main/src/test/kotlin/inr/numass/NumassPluginTest.kt new file mode 100644 index 00000000..fc45610b --- /dev/null +++ b/numass-main/src/test/kotlin/inr/numass/NumassPluginTest.kt @@ -0,0 +1,30 @@ +package inr.numass + +import hep.dataforge.context.Global +import hep.dataforge.meta.buildMeta +import hep.dataforge.stat.fit.FitManager +import org.junit.Before +import org.junit.Test + +class NumassPluginTest { + @Before + fun setup() { + NumassPlugin().startGlobal() + } + + @Test + fun testModels() { + val meta = buildMeta("model") { + "modelName" to "sterile" + "resolution" to { + "width" to 8.3e-5 + "tail" to "function::numass.resolutionTail.2017.mod" + } + "transmission" to { + "trapping" to "function::numass.trap.nominal" + } + } + val model = Global.load().buildModel(meta) + } +} + diff --git a/numass-viewer/build.gradle b/numass-viewer/build.gradle new file mode 100644 index 00000000..470c5a13 --- /dev/null +++ b/numass-viewer/build.gradle @@ -0,0 +1,30 @@ +apply plugin: 'application' +apply plugin: 'kotlin' + +repositories { + mavenCentral() +} + +//apply plugin: 'org.openjfx.javafxplugin' +// +//javafx { +// modules = [ 'javafx.controls' ] +//} + +if (!hasProperty('mainClass')) { + ext.mainClass = 'inr.numass.viewer.Viewer'//"inr.numass.viewer.test.TestApp" +} + +mainClassName = mainClass + +version = "0.5.6" + +description = "The viewer for numass data" + + +dependencies { + compile project(':numass-core') + compile project(':dataforge-plots:plots-jfc') + compile project(':dataforge-gui') +} + diff --git a/numass-viewer/src/main/kotlin/inr/numass/viewer/AmplitudeView.kt b/numass-viewer/src/main/kotlin/inr/numass/viewer/AmplitudeView.kt new file mode 100644 index 00000000..7f1df961 --- /dev/null +++ b/numass-viewer/src/main/kotlin/inr/numass/viewer/AmplitudeView.kt @@ -0,0 +1,198 @@ +package inr.numass.viewer + +import hep.dataforge.configure +import hep.dataforge.fx.dfIcon +import hep.dataforge.fx.except +import hep.dataforge.fx.plots.PlotContainer +import hep.dataforge.fx.runGoal +import hep.dataforge.fx.ui +import hep.dataforge.goals.Goal +import hep.dataforge.names.Name +import hep.dataforge.plots.PlotGroup +import hep.dataforge.plots.Plottable +import hep.dataforge.plots.data.DataPlot +import hep.dataforge.plots.jfreechart.JFreeChartFrame +import hep.dataforge.tables.Adapters +import inr.numass.data.analyzers.NumassAnalyzer +import inr.numass.data.analyzers.withBinning +import javafx.beans.Observable +import javafx.beans.binding.DoubleBinding +import javafx.beans.property.SimpleBooleanProperty +import javafx.beans.property.SimpleObjectProperty +import javafx.collections.FXCollections +import javafx.collections.ObservableMap +import javafx.scene.control.CheckBox +import javafx.scene.control.ChoiceBox +import javafx.scene.image.ImageView +import tornadofx.* + +class AmplitudeView : View(title = "Numass amplitude spectrum plot", icon = ImageView(dfIcon)) { + + private val frame = JFreeChartFrame().configure { + "title" to "Detector response plot" + node("xAxis") { + "title" to "ADC" + "units" to "channels" + + } + node("yAxis") { + "title" to "count rate" + "units" to "Hz" + } + "legend.showComponent" to false + }.apply { + plots.configure { + "connectionType" to "step" + "thickness" to 2 + "showLine" to true + "showSymbol" to false + "showErrors" to false + }.setType() + } + + val binningProperty = SimpleObjectProperty(20) + var binning by binningProperty + + val normalizeProperty = SimpleBooleanProperty(true) + var normalize by normalizeProperty + + + private val container = PlotContainer(frame).apply { + val binningSelector: ChoiceBox = ChoiceBox(FXCollections.observableArrayList(1, 2, 8, 16, 32, 50)).apply { + minWidth = 0.0 + selectionModel.selectLast() + binningProperty.bind(this.selectionModel.selectedItemProperty()) + } + val normalizeSwitch: CheckBox = CheckBox("Normalize").apply { + minWidth = 0.0 + this.selectedProperty().bindBidirectional(normalizeProperty) + } + addToSideBar(0, binningSelector, normalizeSwitch) + } + + private val data: ObservableMap = FXCollections.observableHashMap() + private val plots: ObservableMap> = FXCollections.observableHashMap() + + val isEmpty = booleanBinding(data) { isEmpty() } + + private val progress = object : DoubleBinding() { + init { + bind(plots) + } + + override fun computeValue(): Double { + return plots.values.count { it.isDone }.toDouble() / data.size; + } + + } + + init { + data.addListener { _: Observable -> + invalidate() + } + + binningProperty.onChange { + frame.plots.clear() + plots.clear() + invalidate() + } + + normalizeProperty.onChange { + frame.plots.clear() + plots.clear() + invalidate() + } + + container.progressProperty.bind(progress) + } + + override val root = borderpane { + center = container.root + } + + /** + * Put or replace current plot with name `key` + */ + operator fun set(key: String, point: CachedPoint) { + data[key] = point + } + + fun addAll(data: Map) { + this.data.putAll(data); + } + + private fun invalidate() { + data.forEach { key, point -> + plots.getOrPut(key) { + runGoal("loadAmplitudeSpectrum_$key") { + val valueAxis = if (normalize) { + NumassAnalyzer.COUNT_RATE_KEY + } else { + NumassAnalyzer.COUNT_KEY + } + val adapter = Adapters.buildXYAdapter(NumassAnalyzer.CHANNEL_KEY, valueAxis) + + val channels = point.channelSpectra.await() + + return@runGoal if (channels.size == 1) { + DataPlot.plot( + key, + channels.values.first().withBinning(binning), + adapter + ) + } else { + val group = PlotGroup.typed(key) + channels.forEach { key, spectrum -> + val plot = DataPlot.plot( + key.toString(), + spectrum.withBinning(binning), + adapter + ) + group.add(plot) + } + group + } + } ui { plot -> + frame.add(plot) + progress.invalidate() + } except { + progress.invalidate() + } + } + plots.keys.filter { !data.containsKey(it) }.forEach { remove(it) } + } + } + + fun clear() { + data.clear() + plots.values.forEach{ + it.cancel() + } + plots.clear() + invalidate() + } + + /** + * Remove the plot and cancel loading task if it is in progress. + */ + fun remove(name: String) { + frame.plots.remove(Name.ofSingle(name)) + plots[name]?.cancel() + plots.remove(name) + data.remove(name) + progress.invalidate() + } + + /** + * Set frame content to the given map. All keys not in the map are removed. + */ + fun setAll(map: Map) { + plots.clear(); + //Remove obsolete keys + data.keys.filter { !map.containsKey(it) }.forEach { + remove(it) + } + this.addAll(map); + } + +} diff --git a/numass-viewer/src/main/kotlin/inr/numass/viewer/Cache.kt b/numass-viewer/src/main/kotlin/inr/numass/viewer/Cache.kt new file mode 100644 index 00000000..dafe9629 --- /dev/null +++ b/numass-viewer/src/main/kotlin/inr/numass/viewer/Cache.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * 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 inr.numass.viewer + +import hep.dataforge.meta.Meta +import hep.dataforge.tables.Table +import inr.numass.data.analyzers.SimpleAnalyzer +import inr.numass.data.api.NumassBlock +import inr.numass.data.api.NumassPoint +import inr.numass.data.api.NumassSet +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.async + +private val analyzer = SimpleAnalyzer() + + +class CachedPoint(val point: NumassPoint) : NumassPoint by point { + + override val blocks: List by lazy { point.blocks } + + override val meta: Meta = point.meta + + val channelSpectra: Deferred> = GlobalScope.async(start = CoroutineStart.LAZY) { + point.channels.mapValues { (_, value) -> analyzer.getAmplitudeSpectrum(value) } + } + + val spectrum: Deferred
= GlobalScope.async(start = CoroutineStart.LAZY) { analyzer.getAmplitudeSpectrum(point) } +} + +class CachedSet(set: NumassSet) : NumassSet by set { + override val points: List by lazy { set.points.map { CachedPoint(it) } } +} diff --git a/numass-viewer/src/main/kotlin/inr/numass/viewer/HVView.kt b/numass-viewer/src/main/kotlin/inr/numass/viewer/HVView.kt new file mode 100644 index 00000000..1bd2b2ff --- /dev/null +++ b/numass-viewer/src/main/kotlin/inr/numass/viewer/HVView.kt @@ -0,0 +1,87 @@ +package inr.numass.viewer + +import hep.dataforge.configure +import hep.dataforge.fx.dfIcon +import hep.dataforge.fx.plots.PlotContainer +import hep.dataforge.fx.runGoal +import hep.dataforge.fx.ui +import hep.dataforge.names.Name +import hep.dataforge.plots.data.DataPlot +import hep.dataforge.plots.data.TimePlot +import hep.dataforge.plots.jfreechart.JFreeChartFrame +import hep.dataforge.tables.Adapters +import inr.numass.data.api.NumassSet +import javafx.collections.FXCollections +import javafx.collections.MapChangeListener +import javafx.collections.ObservableMap +import javafx.scene.image.ImageView +import tornadofx.* + + +/** + * View for hv + */ +class HVView : View(title = "High voltage time plot", icon = ImageView(dfIcon)) { + + private val frame = JFreeChartFrame().configure { + "xAxis.title" to "time" + "xAxis.type" to "time" + "yAxis.title" to "HV" + }.apply { + plots.configure { + "connectionType" to "step" + "thickness" to 2 + "showLine" to true + "showSymbol" to false + "showErrors" to false + } + plots.setType() + } + private val container = PlotContainer(frame); + + override val root = borderpane { + center = PlotContainer(frame).root + } + + private val data: ObservableMap = FXCollections.observableHashMap() + val isEmpty = booleanBinding(data) { data.isEmpty() } + + init { + data.addListener { change: MapChangeListener.Change -> + isEmpty.invalidate() + if (change.wasRemoved()) { + frame.plots.remove(Name.ofSingle(change.key)) + } + if (change.wasAdded()) { + runLater { container.progress = -1.0 } + runGoal("hvData[${change.key}]") { + change.valueAdded.getHvData() + } ui { table -> + if (table != null) { + ((frame[change.key] as? DataPlot) + ?: DataPlot(change.key, adapter = Adapters.buildXYAdapter("timestamp", "value")).also { frame.add(it) }) + .fillData(table) + } + + container.progress = 1.0; + } + } + + } + } + + + operator fun set(id: String, set: NumassSet) { + data[id] = set + } + + fun remove(id: String) { + data.remove(id); + } + + fun clear() { + data.clear() + } + + +} diff --git a/numass-viewer/src/main/kotlin/inr/numass/viewer/MainView.kt b/numass-viewer/src/main/kotlin/inr/numass/viewer/MainView.kt new file mode 100644 index 00000000..c08788d1 --- /dev/null +++ b/numass-viewer/src/main/kotlin/inr/numass/viewer/MainView.kt @@ -0,0 +1,202 @@ +package inr.numass.viewer + +import hep.dataforge.context.Context +import hep.dataforge.context.Global +import hep.dataforge.fx.dfIconView +import hep.dataforge.fx.except +import hep.dataforge.fx.runGoal +import hep.dataforge.fx.ui +import hep.dataforge.storage.Storage +import inr.numass.NumassProperties +import inr.numass.data.NumassDataUtils +import inr.numass.data.NumassFileEnvelope +import inr.numass.data.storage.NumassDataLoader +import inr.numass.data.storage.NumassDirectory +import javafx.beans.property.SimpleObjectProperty +import javafx.geometry.Insets +import javafx.scene.control.Alert +import javafx.scene.layout.Priority +import javafx.scene.text.Font +import javafx.stage.DirectoryChooser +import javafx.stage.FileChooser +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import org.controlsfx.control.StatusBar +import tornadofx.* +import java.io.File +import java.nio.file.Files +import java.nio.file.Path + +class MainView(val context: Context = Global.getContext("viewer")) : View(title = "Numass viewer", icon = dfIconView) { + + private val statusBar = StatusBar(); +// private val logFragment = LogFragment().apply { +// addLogHandler(context.logger) +// } + + private val pathProperty = SimpleObjectProperty() + private var path: Path by pathProperty + + private val contentViewProperty = SimpleObjectProperty() + var contentView: UIComponent? by contentViewProperty + + override val root = borderpane { + prefHeight = 600.0 + prefWidth = 800.0 + top { + toolbar { + prefHeight = 40.0 + button("Load directory") { + action { + val chooser = DirectoryChooser() + chooser.title = "Select directory to view" + val homeDir = NumassProperties.getNumassProperty("numass.viewer.lastPath") + try { + if (homeDir == null) { + chooser.initialDirectory = File(".").absoluteFile + } else { + val file = File(homeDir) + if (file.isDirectory) { + chooser.initialDirectory = file + } else { + chooser.initialDirectory = file.parentFile + } + } + + val rootDir = chooser.showDialog(primaryStage.scene.window) + + if (rootDir != null) { + NumassProperties.setNumassProperty("numass.viewer.lastPath", rootDir.absolutePath) + GlobalScope.launch { + runLater { + path = rootDir.toPath() + } + load(rootDir.toPath()) + } + } + } catch (ex: Exception) { + NumassProperties.setNumassProperty("numass.viewer.lastPath", null) + error("Error", content = "Failed to laod file with message: ${ex.message}") + } + } + } + button("Load file") { + action { + val chooser = FileChooser() + chooser.title = "Select file to view" + val homeDir = NumassProperties.getNumassProperty("numass.viewer.lastPath") + try { + if (homeDir == null) { + chooser.initialDirectory = File(".").absoluteFile + } else { + chooser.initialDirectory = File(homeDir) + } + + + val file = chooser.showOpenDialog(primaryStage.scene.window) + if (file != null) { + NumassProperties.setNumassProperty("numass.viewer.lastPath", file.parentFile.absolutePath) + GlobalScope.launch { + runLater { + path = file.toPath() + } + load(file.toPath()) + } + } + } catch (ex: Exception) { + NumassProperties.setNumassProperty("numass.viewer.lastPath", null) + error("Error", content = "Failed to laod file with message: ${ex.message}") + } + } + } + + label(pathProperty.stringBinding{it?.toString() ?: "NOT LOADED"}) { + padding = Insets(0.0, 0.0, 0.0, 10.0); + font = Font.font("System Bold", 13.0); + } + pane { + hgrow = Priority.ALWAYS + } +// togglebutton("Console") { +// isSelected = false +// logFragment.bindWindow(this@togglebutton) +// } + } + } + bottom = statusBar + } + + init { + contentViewProperty.onChange { + root.center = it?.root + } + } + + private suspend fun load(path: Path) { + runLater { + contentView = null + } + if (Files.isDirectory(path)) { + if (Files.exists(path.resolve(NumassDataLoader.META_FRAGMENT_NAME))) { + //build set view + runGoal("viewer.load.set[$path]") { + title = "Load set ($path)" + message = "Building numass set..." + NumassDataLoader(context, null, path.fileName.toString(), path) + } ui { + contentView = SpectrumView().apply { + clear() + set(it.name, CachedSet(it)) + } + } except { + alert( + type = Alert.AlertType.ERROR, + header = "Error during set loading", + content = it.toString() + ).show() + } + } else { + //build storage + runGoal("viewer.load.storage[$path]") { + title = "Load storage ($path)" + message = "Building numass storage tree..." + NumassDirectory.INSTANCE.read(context, path) + } ui { + contentView = StorageView(it as Storage) + } + } + } else { + //Reading individual file + val envelope = try { + NumassFileEnvelope(path) + } catch (ex: Exception) { + runLater { + alert( + type = Alert.AlertType.ERROR, + header = "Can't load DF envelope from file $path", + content = ex.toString() + ).show() + } + null + } + + envelope?.let { + if (it.meta.hasMeta("external_meta")) { + //try to read as point + val point = NumassDataUtils.read(it) + runLater { + contentView = AmplitudeView().apply { + set(path.fileName.toString(), CachedPoint(point)) + } + } + } else { + alert( + type = Alert.AlertType.ERROR, + header = "Unknown envelope content: $path" + ).show() + } + } + } + } + +} diff --git a/numass-viewer/src/main/kotlin/inr/numass/viewer/PointInfoView.kt b/numass-viewer/src/main/kotlin/inr/numass/viewer/PointInfoView.kt new file mode 100644 index 00000000..0305e726 --- /dev/null +++ b/numass-viewer/src/main/kotlin/inr/numass/viewer/PointInfoView.kt @@ -0,0 +1,54 @@ +package inr.numass.viewer + +import hep.dataforge.fx.meta.MetaViewer +import inr.numass.data.analyzers.NumassAnalyzer +import javafx.beans.property.SimpleIntegerProperty +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import org.controlsfx.glyphfont.FontAwesome +import tornadofx.* +import tornadofx.controlsfx.borders +import tornadofx.controlsfx.toGlyph + +class PointInfoView(val point: CachedPoint) : MetaViewer(point.meta) { + + val countProperty = SimpleIntegerProperty(0) + var count by countProperty + + + override val root = super.root.apply { + top { + gridpane { + borders { + lineBorder().build() + } + row { + button(graphic = FontAwesome.Glyph.REFRESH.toGlyph()) { + action { + GlobalScope.launch { + val res = point.spectrum.await().sumBy { it.getValue(NumassAnalyzer.COUNT_KEY).int } + runLater { count = res } + } + } + } + } + row { + hbox { + label("Total number of events: ") + label { + textProperty().bind(countProperty.asString()) + } + } + } + row { + hbox { + label("Total count rate: ") + label { + textProperty().bind(countProperty.stringBinding { String.format("%.2f", it!!.toDouble() / point.length.toMillis() * 1000) }) + } + } + } + } + } + } +} diff --git a/numass-viewer/src/main/kotlin/inr/numass/viewer/SlowControlView.kt b/numass-viewer/src/main/kotlin/inr/numass/viewer/SlowControlView.kt new file mode 100644 index 00000000..e0307409 --- /dev/null +++ b/numass-viewer/src/main/kotlin/inr/numass/viewer/SlowControlView.kt @@ -0,0 +1,87 @@ +package inr.numass.viewer + +import hep.dataforge.configure +import hep.dataforge.fx.dfIcon +import hep.dataforge.fx.plots.PlotContainer +import hep.dataforge.fx.runGoal +import hep.dataforge.fx.ui +import hep.dataforge.plots.PlotGroup +import hep.dataforge.plots.data.DataPlot +import hep.dataforge.plots.jfreechart.JFreeChartFrame +import hep.dataforge.storage.tables.TableLoader +import hep.dataforge.storage.tables.asTable +import hep.dataforge.tables.Adapters +import hep.dataforge.tables.Table +import javafx.collections.FXCollections +import javafx.collections.MapChangeListener +import javafx.collections.ObservableMap +import javafx.scene.image.ImageView +import tornadofx.* + +/** + * Created by darksnake on 18.06.2017. + */ +class SlowControlView : View(title = "Numass slow control view", icon = ImageView(dfIcon)) { + + private val plot = JFreeChartFrame().configure { + "xAxis.type" to "time" + "yAxis.type" to "log" + } + + override val root = borderpane { + center = PlotContainer(plot).root + } + + val data: ObservableMap = FXCollections.observableHashMap(); + val isEmpty = booleanBinding(data) { + data.isEmpty() + } + + init { + data.addListener { change: MapChangeListener.Change -> + if (change.wasRemoved()) { + plot.remove(change.key) + } + if (change.wasAdded()) { + runGoal("loadTable[${change.key}]") { + val plotData = getData(change.valueAdded) + val names = plotData.format.namesAsArray().filter { it != "timestamp" } + + val group = PlotGroup(change.key) + + names.forEach { + val adapter = Adapters.buildXYAdapter("timestamp", it); + val plot = DataPlot.plot(it, plotData, adapter).configure { + "showLine" to true + "showSymbol" to false + "showErrors" to false + } + group.add(plot) + } + group + } ui { + plot.add(it); + } + } + isEmpty.invalidate() + } + } + + private suspend fun getData(loader: TableLoader): Table { + //TODO add query + return loader.asTable().await() + } + + operator fun set(id: String, loader: TableLoader) { + this.data[id] = loader + } + + fun remove(id: String) { + this.data.remove(id) + } + + fun clear(){ + data.clear() + } + +} diff --git a/numass-viewer/src/main/kotlin/inr/numass/viewer/SpectrumView.kt b/numass-viewer/src/main/kotlin/inr/numass/viewer/SpectrumView.kt new file mode 100644 index 00000000..d37cbf3a --- /dev/null +++ b/numass-viewer/src/main/kotlin/inr/numass/viewer/SpectrumView.kt @@ -0,0 +1,154 @@ +package inr.numass.viewer + +import hep.dataforge.configure +import hep.dataforge.fx.dfIcon +import hep.dataforge.fx.plots.PlotContainer +import hep.dataforge.fx.runGoal +import hep.dataforge.fx.ui +import hep.dataforge.names.Name +import hep.dataforge.plots.data.DataPlot +import hep.dataforge.plots.jfreechart.JFreeChartFrame +import hep.dataforge.tables.Adapters +import inr.numass.data.analyzers.countInWindow +import inr.numass.data.api.NumassSet +import javafx.beans.property.SimpleIntegerProperty +import javafx.collections.FXCollections +import javafx.collections.MapChangeListener +import javafx.collections.ObservableMap +import javafx.geometry.Insets +import javafx.geometry.Orientation +import javafx.scene.image.ImageView +import javafx.util.converter.NumberStringConverter +import org.controlsfx.control.RangeSlider +import tornadofx.* +import java.util.concurrent.atomic.AtomicInteger + +/** + * View for energy spectrum + * @param analyzer + * @param cache - optional global point immutable + */ +class SpectrumView : View(title = "Numass spectrum plot", icon = ImageView(dfIcon)) { + + private val frame = JFreeChartFrame().configure { + "xAxis.title" to "U" + "xAxis.units" to "V" + "yAxis.title" to "count rate" + "yAxis.units" to "Hz" + //"legend.show" to false + } + private val container = PlotContainer(frame); + + + private val loChannelProperty = SimpleIntegerProperty(500).apply { + addListener { _ -> updateView() } + } + private var loChannel by loChannelProperty + + private val upChannelProperty = SimpleIntegerProperty(3100).apply { + addListener { _ -> updateView() } + } + private var upChannel by upChannelProperty + + + private val data: ObservableMap = FXCollections.observableHashMap(); + val isEmpty = booleanBinding(data) { data.isEmpty() } + + override val root = borderpane { + top { + toolbar { + prefHeight = 40.0 + vbox { + label("Lo channel") + textfield { + prefWidth = 60.0 + textProperty().bindBidirectional(loChannelProperty, NumberStringConverter()) + } + } + + items += RangeSlider().apply { + padding = Insets(0.0, 10.0, 0.0, 10.0) + prefWidth = 300.0 + majorTickUnit = 500.0 + minorTickCount = 5 + prefHeight = 38.0 + isShowTickLabels = true + isShowTickMarks = true + + max = 4000.0 + highValueProperty().bindBidirectional(upChannelProperty) + lowValueProperty().bindBidirectional(loChannelProperty) + + lowValue = 500.0 + highValue = 3100.0 + } + + vbox { + label("Up channel") + textfield { + isEditable = true; + prefWidth = 60.0 + textProperty().bindBidirectional(upChannelProperty, NumberStringConverter()) + } + } + separator(Orientation.VERTICAL) + } + } + center = container.root + } + + init { + data.addListener { change: MapChangeListener.Change -> + if (change.wasRemoved()) { + frame.plots.remove(Name.ofSingle(change.key)); + } + + if (change.wasAdded()) { + updateView() + } + isEmpty.invalidate() + } + } + + private fun updateView() { + runLater { container.progress = 0.0 } + val progress = AtomicInteger(0) + val totalProgress = data.values.stream().mapToInt { it.points.size }.sum() + + data.forEach { name, set -> + val plot: DataPlot = frame.plots[Name.ofSingle(name)] as DataPlot? ?: DataPlot(name).apply { frame.add(this) } + + runGoal("spectrumData[$name]") { + set.points.forEach { it.spectrum.start() } + set.points.map { point -> + val count = point.spectrum.await().countInWindow(loChannel.toShort(), upChannel.toShort()); + val seconds = point.length.toMillis() / 1000.0; + runLater { + container.progress = progress.incrementAndGet().toDouble() / totalProgress + } + Adapters.buildXYDataPoint( + point.voltage, + (count / seconds), + Math.sqrt(count.toDouble()) / seconds + ) + } + } ui { points -> + plot.fillData(points) + container.progress = 1.0 + //spectrumExportButton.isDisable = false + } + } + } + + operator fun set(key: String, value: CachedSet) { + data[key] = value + } + + fun remove(key: String) { + data.remove(key) + } + + fun clear() { + data.clear() + } +} diff --git a/numass-viewer/src/main/kotlin/inr/numass/viewer/StorageView.kt b/numass-viewer/src/main/kotlin/inr/numass/viewer/StorageView.kt new file mode 100644 index 00000000..bbc3817e --- /dev/null +++ b/numass-viewer/src/main/kotlin/inr/numass/viewer/StorageView.kt @@ -0,0 +1,211 @@ +package inr.numass.viewer + +import hep.dataforge.fx.dfIconView +import hep.dataforge.fx.meta.MetaViewer +import hep.dataforge.meta.Meta +import hep.dataforge.meta.Metoid +import hep.dataforge.names.AlphanumComparator +import hep.dataforge.storage.Storage +import hep.dataforge.storage.files.FileTableLoader +import hep.dataforge.storage.tables.TableLoader +import inr.numass.data.api.NumassPoint +import inr.numass.data.api.NumassSet +import inr.numass.data.storage.NumassDataLoader +import javafx.beans.property.SimpleBooleanProperty +import javafx.scene.control.ContextMenu +import javafx.scene.control.TreeItem +import kotlinx.coroutines.runBlocking +import tornadofx.* + +class StorageView(val storage: Storage) : View(title = "Numass storage", icon = dfIconView) { + + private val ampView: AmplitudeView by inject() + private val timeView: TimeView by inject() + private val spectrumView: SpectrumView by inject() + private val hvView: HVView by inject() + private val scView: SlowControlView by inject() + + init { + ampView.clear() + timeView.clear() + spectrumView.clear() + hvView.clear() + scView.clear() + } + + private inner class Container(val id: String, val content: Any) { + val checkedProperty = SimpleBooleanProperty(false) + var checked by checkedProperty + + val infoView: UIComponent by lazy { + when (content) { + is CachedPoint -> PointInfoView(content) + is Metoid -> MetaViewer(content.meta, title = "Meta view: $id") + else -> MetaViewer(Meta.empty(), title = "Meta view: $id") + } + } + + init { + checkedProperty.onChange { selected -> + when (content) { + is CachedPoint -> { + if (selected) { + ampView[id] = content + timeView[id] = content + } else { + ampView.remove(id) + timeView.remove(id) + } + } + is CachedSet -> { + if (selected) { + spectrumView[id] = content + hvView[id] = content + } else { + spectrumView.remove(id) + hvView.remove(id) + } + } + is TableLoader -> { + if (selected) { + scView[id] = content + } else { + scView.remove(id) + } + } + } + } + } + + val children: List? by lazy { + when (content) { + is Storage -> runBlocking { content.children }.map { buildContainer(it, this) }.sortedWith( + object : Comparator { + private val alphanumComparator = AlphanumComparator() + override fun compare(o1: Container, o2: Container): Int = alphanumComparator.compare(o1.id, o2.id) + } + ) + is NumassSet -> content.points + .sortedBy { it.index } + .map { buildContainer(it, this) } + .toList() + else -> null + } + } + + val hasChildren: Boolean = (content is Storage) || (content is NumassSet) + } + + + override val root = splitpane { + treeview { + //isShowRoot = false + root = TreeItem(Container(storage.name, storage)) + root.isExpanded = true + lazyPopulate(leafCheck = { !it.value.hasChildren }) { + it.value.children + } + cellFormat { value -> + when (value.content) { + is Storage -> { + text = value.content.name + graphic = null + } + is NumassSet -> { + text = null + graphic = checkbox(value.content.name).apply { + selectedProperty().bindBidirectional(value.checkedProperty) + } + } + is NumassPoint -> { + text = null + graphic = checkbox("${value.content.voltage}[${value.content.index}]").apply { + selectedProperty().bindBidirectional(value.checkedProperty) + } + } + is TableLoader -> { + text = null + graphic = checkbox(value.content.name).apply { + selectedProperty().bindBidirectional(value.checkedProperty) + } + } + else -> { + text = value.id + graphic = null + } + } + contextMenu = ContextMenu().apply { + item("Clear all") { + action { + this@cellFormat.treeItem.uncheckAll() + } + } + item("Info") { + action { + value.infoView.openModal(escapeClosesWindow = true) + } + } + } + } + } + + tabpane { + tab("Amplitude spectra") { + content = ampView.root + isClosable = false + //visibleWhen(ampView.isEmpty.not()) + } + tab("Time spectra") { + content = timeView.root + isClosable = false + //visibleWhen(ampView.isEmpty.not()) + } + tab("HV") { + content = hvView.root + isClosable = false + //visibleWhen(hvView.isEmpty.not()) + } + tab("Numass spectra") { + content = spectrumView.root + isClosable = false + //visibleWhen(spectrumView.isEmpty.not()) + } + tab("Slow control") { + content = scView.root + isClosable = false + //visibleWhen(scView.isEmpty.not()) + } + } + setDividerPosition(0, 0.3); + } + + + private fun TreeItem.uncheckAll() { + this.value.checked = false + this.children.forEach { it.uncheckAll() } + } + + + private fun buildContainer(content: Any, parent: Container): Container = + when (content) { + is Storage -> { + Container(content.fullName.toString(), content) + } + is NumassSet -> { + val id: String = if (content is NumassDataLoader) { + content.fullName.unescaped + } else { + content.name + } + Container(id, content as? CachedSet ?: CachedSet(content)) + } + is NumassPoint -> { + Container("${parent.id}/${content.voltage}[${content.index}]", content as? CachedPoint + ?: CachedPoint(content)) + } + is FileTableLoader -> { + Container(content.path.toString(), content); + } + else -> throw IllegalArgumentException("Unknown content type: ${content::class.java}"); + } +} diff --git a/numass-viewer/src/main/kotlin/inr/numass/viewer/TimeView.kt b/numass-viewer/src/main/kotlin/inr/numass/viewer/TimeView.kt new file mode 100644 index 00000000..10882b68 --- /dev/null +++ b/numass-viewer/src/main/kotlin/inr/numass/viewer/TimeView.kt @@ -0,0 +1,173 @@ +package inr.numass.viewer + +import hep.dataforge.configure +import hep.dataforge.fx.dfIcon +import hep.dataforge.fx.except +import hep.dataforge.fx.plots.PlotContainer +import hep.dataforge.fx.runGoal +import hep.dataforge.fx.ui +import hep.dataforge.goals.Goal +import hep.dataforge.meta.Meta +import hep.dataforge.names.Name +import hep.dataforge.plots.Plottable +import hep.dataforge.plots.data.DataPlot +import hep.dataforge.plots.jfreechart.JFreeChartFrame +import hep.dataforge.tables.Adapters +import hep.dataforge.values.ValueMap +import inr.numass.data.analyzers.TimeAnalyzer +import javafx.beans.Observable +import javafx.beans.binding.DoubleBinding +import javafx.collections.FXCollections +import javafx.collections.ObservableMap +import javafx.scene.image.ImageView +import tornadofx.* + +class TimeView : View(title = "Numass time spectrum plot", icon = ImageView(dfIcon)) { + + private val frame = JFreeChartFrame().configure { + "title" to "Time plot" + node("xAxis") { + "title" to "delay" + "units" to "us" + + } + node("yAxis") { + "title" to "number of events" + "type" to "log" + } + }.apply { + plots.configure { + "connectionType" to "step" + "thickness" to 2 + "showLine" to true + "showSymbol" to false + "showErrors" to false + }.setType() + } + +// val stepProperty = SimpleDoubleProperty() +// var step by stepProperty +// +// private val container = PlotContainer(frame).apply { +// val binningSelector: ChoiceBox = ChoiceBox(FXCollections.observableArrayList(1, 5, 10, 20, 50)).apply { +// minWidth = 0.0 +// selectionModel.selectLast() +// stepProperty.bind(this.selectionModel.selectedItemProperty()) +// } +// addToSideBar(0, binningSelector) +// } + + private val container = PlotContainer(frame) + + private val data: ObservableMap = FXCollections.observableHashMap() + private val plots: ObservableMap> = FXCollections.observableHashMap() + + val isEmpty = booleanBinding(data) { isEmpty() } + + private val progress = object : DoubleBinding() { + init { + bind(plots) + } + + override fun computeValue(): Double { + return plots.values.count { it.isDone }.toDouble() / data.size; + } + + } + + init { + data.addListener { _: Observable -> + invalidate() + } + } + + override val root = borderpane { + center = container.root + } + + /** + * Put or replace current plot with name `key` + */ + operator fun set(key: String, point: CachedPoint) { + data[key] = point + } + + fun addAll(data: Map) { + this.data.putAll(data); + } + + private val analyzer = TimeAnalyzer(); + + + private fun invalidate() { + data.forEach { key, point -> + plots.getOrPut(key) { + runGoal("loadAmplitudeSpectrum_$key") { + + val initialEstimate = analyzer.analyze(point) + val cr = initialEstimate.getDouble("cr") + + val binNum = 200//inputMeta.getInt("binNum", 1000); + val binSize = 1.0 / cr * 10 / binNum * 1e6//inputMeta.getDouble("binSize", 1.0 / cr * 10 / binNum * 1e6) + + val histogram = analyzer.getEventsWithDelay(point, Meta.empty()) + .map { it.second.toDouble() / 1000.0 } + .groupBy { Math.floor(it / binSize) } + .toSortedMap() + .map { + ValueMap.ofPairs("x" to it.key, "count" to it.value.count()) + } + + DataPlot(key, adapter = Adapters.buildXYAdapter("x", "count")) + .configure { + "showLine" to true + "showSymbol" to false + "showErrors" to false + "connectionType" to "step" + }.fillData(histogram) + + } ui { plot -> + frame.add(plot) + progress.invalidate() + } except { + progress.invalidate() + } + } + plots.keys.filter { !data.containsKey(it) }.forEach { remove(it) } + } + } + + fun clear() { + data.clear() + plots.values.forEach { + it.cancel() + } + plots.clear() + invalidate() + } + + /** + * Remove the plot and cancel loading task if it is in progress. + */ + fun remove(name: String) { + frame.plots.remove(Name.ofSingle(name)) + plots[name]?.cancel() + plots.remove(name) + data.remove(name) + progress.invalidate() + } + + /** + * Set frame content to the given map. All keys not in the map are removed. + */ + fun setAll(map: Map) { + plots.clear(); + //Remove obsolete keys + data.keys.filter { !map.containsKey(it) }.forEach { + remove(it) + } + this.addAll(map); + } + +} + diff --git a/numass-viewer/src/main/kotlin/inr/numass/viewer/Viewer.kt b/numass-viewer/src/main/kotlin/inr/numass/viewer/Viewer.kt new file mode 100644 index 00000000..a2922ddf --- /dev/null +++ b/numass-viewer/src/main/kotlin/inr/numass/viewer/Viewer.kt @@ -0,0 +1,28 @@ +package inr.numass.viewer + +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.Logger +import hep.dataforge.context.Global +import hep.dataforge.fx.dfIcon +import javafx.stage.Stage +import org.slf4j.LoggerFactory +import tornadofx.* + +/** + * Created by darksnake on 14-Apr-17. + */ +class Viewer : App(MainView::class) { + init { + (LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME) as Logger).level = Level.INFO + } + + override fun start(stage: Stage) { + stage.icons += dfIcon + super.start(stage) + } + + override fun stop() { + super.stop() + Global.terminate(); + } +} \ No newline at end of file diff --git a/numass-viewer/src/main/kotlin/inr/numass/viewer/test/ComponentTest.kt b/numass-viewer/src/main/kotlin/inr/numass/viewer/test/ComponentTest.kt new file mode 100644 index 00000000..65a5028d --- /dev/null +++ b/numass-viewer/src/main/kotlin/inr/numass/viewer/test/ComponentTest.kt @@ -0,0 +1,66 @@ +package inr.numass.viewer.test + +import hep.dataforge.context.Global +import hep.dataforge.fx.dfIcon +import hep.dataforge.nullable +import hep.dataforge.tables.Table +import inr.numass.data.api.NumassPoint +import inr.numass.data.api.NumassSet +import inr.numass.data.storage.NumassDirectory +import inr.numass.viewer.* +import javafx.application.Application +import javafx.scene.image.ImageView +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import tornadofx.* +import java.io.File +import java.util.concurrent.ConcurrentHashMap + +class ViewerComponentsTestApp : App(ViewerComponentsTest::class) + +class ViewerComponentsTest : View(title = "Numass viewer test", icon = ImageView(dfIcon)) { + + //val rootDir = File("D:\\Work\\Numass\\data\\2017_05\\Fill_2") + + //val set: NumassSet = NumassStorageFactory.buildLocal(rootDir).provide("loader::set_8", NumassSet::class.java).orElseThrow { RuntimeException("err") } + + + private val cache: MutableMap = ConcurrentHashMap(); + + val amp: AmplitudeView by inject(params = mapOf("cache" to cache))//= AmplitudeView(immutable = immutable) + val sp: SpectrumView by inject(params = mapOf("cache" to cache)) + val hv: HVView by inject() + + override val root = borderpane { + top { + button("Click me!") { + action { + GlobalScope.launch { + val set: NumassSet = NumassDirectory.INSTANCE.read(Global, File("D:\\Work\\Numass\\data\\2017_05\\Fill_2").toPath()) + ?.provide("loader::set_2", NumassSet::class.java).nullable + ?: kotlin.error("Error") + update(set); + } + } + } + } + center { + tabpane { + tab("amplitude", amp.root) + tab("spectrum", sp.root) + tab("hv", hv.root) + } + } + } + + fun update(set: NumassSet) { + amp.setAll(set.points.filter { it.voltage != 16000.0 }.associateBy({ "point_${it.voltage}" }) { CachedPoint(it) }); + sp.set("test", CachedSet(set)); + hv.set(set.name, set) + } +} + + +fun main(args: Array) { + Application.launch(ViewerComponentsTestApp::class.java, *args); +} \ No newline at end of file diff --git a/numass-viewer/src/main/kotlin/inr/numass/viewer/test/JFCTest.kt b/numass-viewer/src/main/kotlin/inr/numass/viewer/test/JFCTest.kt new file mode 100644 index 00000000..696e5721 --- /dev/null +++ b/numass-viewer/src/main/kotlin/inr/numass/viewer/test/JFCTest.kt @@ -0,0 +1,35 @@ +package inr.numass.viewer.test + +import hep.dataforge.fx.plots.PlotContainer +import hep.dataforge.plots.data.DataPlot +import hep.dataforge.plots.jfreechart.JFreeChartFrame +import hep.dataforge.tables.Adapters +import tornadofx.* +import java.util.* + +/** + * Created by darksnake on 16-Apr-17. + */ +class JFCTest : View("My View") { + val rnd = Random(); + + val plot = JFreeChartFrame(); + val data = DataPlot("data"); + + val button = button("test") { + action { + + data.fillData( + (1..1000).map { Adapters.buildXYDataPoint(it.toDouble(), rnd.nextDouble()) } + ) + plot.add(data) + } + }; + + override val root = borderpane { + center = PlotContainer(plot).root + bottom { + add(button) + } + } +} diff --git a/numass-viewer/src/main/kotlin/inr/numass/viewer/test/StorageViewTest.kt b/numass-viewer/src/main/kotlin/inr/numass/viewer/test/StorageViewTest.kt new file mode 100644 index 00000000..a71a7a6b --- /dev/null +++ b/numass-viewer/src/main/kotlin/inr/numass/viewer/test/StorageViewTest.kt @@ -0,0 +1,11 @@ +package inr.numass.viewer.test + +import inr.numass.viewer.StorageView +import javafx.application.Application +import tornadofx.* + +class ViewerTestApp : App(StorageView::class) + +fun main(args: Array) { + Application.launch(ViewerTestApp::class.java, *args); +} \ No newline at end of file diff --git a/numass-viewer/src/main/kotlin/inr/numass/viewer/test/TestApp.kt b/numass-viewer/src/main/kotlin/inr/numass/viewer/test/TestApp.kt new file mode 100644 index 00000000..289f6883 --- /dev/null +++ b/numass-viewer/src/main/kotlin/inr/numass/viewer/test/TestApp.kt @@ -0,0 +1,9 @@ +package inr.numass.viewer.test + +import tornadofx.* + +/** + * Created by darksnake on 16-Apr-17. + */ +class TestApp: App(JFCTest::class) { +} \ No newline at end of file diff --git a/numass-viewer/src/main/resources/fxml/MainView.fxml b/numass-viewer/src/main/resources/fxml/MainView.fxml new file mode 100644 index 00000000..0f583043 --- /dev/null +++ b/numass-viewer/src/main/resources/fxml/MainView.fxml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + +