diff --git a/.codespellrc b/.codespellrc new file mode 100644 index 000000000..fecd51a44 --- /dev/null +++ b/.codespellrc @@ -0,0 +1,2 @@ +[codespell] +ignore-words-list = indX, hAx, linIx diff --git a/.github/workflows/coverage-report.yml b/.github/workflows/coverage-report.yml index 08c06b51d..a34e75c8b 100644 --- a/.github/workflows/coverage-report.yml +++ b/.github/workflows/coverage-report.yml @@ -28,6 +28,7 @@ jobs: - name: Upload to Coveralls uses: coverallsapp/github-action@v2 + continue-on-error: true with: github-token: ${{ secrets.GITHUB_TOKEN }} file: coverage/coverage.json @@ -35,6 +36,7 @@ jobs: - name: Upload to Codecov uses: codecov/codecov-action@v5 + continue-on-error: true with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage/coverage.json,coverage/coverage.xml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..ad2aa4231 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,33 @@ +name: Build Sphinx Documentation + +on: + push: + pull_request: + +jobs: + build-docs: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.13' + + - name: Install sphinx and dependencies + run: | + python -m pip install --upgrade pip + pip install -r docs/requirements.txt + + - name: Build documentation + run: sphinx-build docs _doc + + - name: Upload artifacts + uses: actions/upload-artifact@v6 + with: + name: documentation + path: _doc diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml new file mode 100644 index 000000000..4ea743285 --- /dev/null +++ b/.github/workflows/package.yml @@ -0,0 +1,85 @@ +name: Build Standalone + +on: + push: + branches: + - master + - 'rc/**' + - 'devops/**' + tags: + - '**' + workflow_dispatch: + inputs: + isRelease: + description: 'Build as release' + type: boolean + default: false + +env: + MLM_LICENSE_TOKEN: ${{ secrets.MATLAB_BATCH_TOKEN }} + +jobs: + build-standalone: + name: Build Standalone (${{ matrix.platform }}) + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + platform: linux + - os: windows-latest + platform: windows + - os: macos-latest + platform: macos-silicon + - os: macos-15-intel + platform: macos-intel + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + submodules: 'false' + + - name: Determine build type + id: build-type + shell: bash + run: | + if [[ "${{ github.ref_type }}" == "tag" ]]; then + echo "is_release=true" >> "$GITHUB_OUTPUT" + echo "artifact_name=matRad-standalone-${{ matrix.platform }}-${{ github.ref_name }}" >> "$GITHUB_OUTPUT" + elif [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.isRelease }}" == "true" ]]; then + echo "is_release=true" >> "$GITHUB_OUTPUT" + echo "artifact_name=matRad-standalone-${{ matrix.platform }}-manual-release" >> "$GITHUB_OUTPUT" + else + echo "is_release=false" >> "$GITHUB_OUTPUT" + echo "artifact_name=matRad-standalone-${{ matrix.platform }}-dev" >> "$GITHUB_OUTPUT" + fi + + - name: Install MATLAB with Compiler + uses: matlab-actions/setup-matlab@v2 + with: + release: latest + products: MATLAB_Compiler MATLAB_Compiler_SDK Image_Processing_Toolbox Parallel_Computing_Toolbox Optimization_Toolbox Global_Optimization_Toolbox + + - name: Build Standalone + uses: matlab-actions/run-command@v2 + with: + command: | + matRad_buildStandalone('isRelease',${{ steps.build-type.outputs.is_release }},'verbose',true,'json','build_result.json') + + - name: Upload installer artifact + uses: actions/upload-artifact@v7 + with: + name: ${{ steps.build-type.outputs.artifact_name }} + path: build/installer/ + if-no-files-found: error + + - name: Upload build result JSON + if: always() + uses: actions/upload-artifact@v7 + with: + name: ${{ steps.build-type.outputs.artifact_name }}-build-info + path: build_result.json + if-no-files-found: warn diff --git a/.gitignore b/.gitignore index 1e8d53e0a..26c250354 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,7 @@ testresults.xml coverage.xml coverage.json *.asv -build/ .DS_Store +build/ +.doc/ + diff --git a/.miss_hit b/.miss_hit new file mode 100644 index 000000000..5aaf59ab9 --- /dev/null +++ b/.miss_hit @@ -0,0 +1,35 @@ +project_root + +# octave: true + +exclude_dir: "submodules" + +# style guide (https://florianschanda.github.io/miss_hit/style_checker.html) +line_length: 150 + +# --- Naming rules for matRad --- + +# matRad_ + lowerCamelCase +regex_function_name: "^(matRad|test|helper)_([a-z][a-zA-Z0-9]*|[A-Z]{2,}[a-zA-Z0-9]*)$" +regex_script_name: "^matRad_((example[1-9][0-9]*_)?([a-z][a-zA-Z0-9]*|[A-Z]{2,}[a-zA-Z0-9]*))$" + +# methods: plain lowerCamelCase +regex_method_name: "^([a-z][a-zA-Z0-9]*|[A-Z]{2,}[a-zA-Z0-9]*)$" + +# classes: matRad_ + UpperCamelCase +regex_class_name: "^(matRad|test)_([A-Z]{2,}[a-zA-Z0-9]*|[A-Z][a-zA-Z0-9]*)$" + +# parameters / variables: lowerCamelCase +regex_parameter_name: "^(test_functions|test_suite|[a-z][a-zA-Z0-9]*|[A-Z]{2,}[a-zA-Z0-9]*)$" +regex_attribute_name: "^([a-z][a-zA-Z0-9]*|[A-Z]{2,}[a-zA-Z0-9]*)$" + +suppress_rule: "copyright_notice" + +tab_width: 4 +indent_function_file_body: false + +# metrics limit for the code quality (https://florianschanda.github.io/miss_hit/metrics.html) +metric "cnest": limit 6 +metric "file_length": limit 1000 +metric "cyc": limit 25 +metric "parameters": limit 15 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..5a21695af --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,51 @@ +--- +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.0.1 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - id: check-case-conflict + - id: check-merge-conflict + - id: check-symlinks + + - repo: https://github.com/rstcheck/rstcheck + rev: v6.2.5 + hooks: + - id: rstcheck + additional_dependencies: ['rstcheck[sphinx,toml]'] + args: [--ignore-languages=matlab, '--ignore-directives=automodule,autoclass,autofunction,autodata,autoevent,autointerface,autolabel,autosectionlabel,autostatus,autotoclabel'] + + + - repo: https://github.com/codespell-project/codespell + rev: v2.4.1 + hooks: + - id: codespell + + - repo: local + hooks: + # Run the linter. + - id: mmh_style + name: MISS_HIT Style + entry: mh_style + language: python + args: [--fix] + additional_dependencies: [miss_hit_core] + files: \.m$ + + - id: mh_metric + name: MISS_HIT Metrics + entry: mh_metric + args: [--ci] + files: \.m$ + language: python + additional_dependencies: [miss_hit_core] + + - id: mh_lint + name: MISS_HIT Lint + entry: mh_lint + files: \.m$ + language: python + additional_dependencies: [miss_hit] diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 000000000..22cfb7e17 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,22 @@ +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version, and other tools you might need +build: + os: ubuntu-24.04 + tools: + python: "3.13" + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: docs/conf.py + +# Optionally, but recommended, +# declare the Python requirements required to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: docs/requirements.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 069ee9629..1f4884526 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,47 @@ # Changelog +## Patch 3.2.1 +This (arguably large) patch does fix a multitude of issues with the new >=R2025 MATLAB Desktop and reported minor issues import/export, helper, and other minor functions. Apart from that it introduced some new flexibilities under the hood (single precision, raytracer vectorization) that do not expose changes on the outside, and can thus be sustainably tested before changing behavior in the next major release. Further the license has changed to a 3-Clause-BSD. + +### Preliminary new features & Enhancements +- Dose engines can now optionally run calculations in single precision (default remains double). A new `precision` configuration property controls this +- GPU acceleration is available as an opt-in property for optimization. Helper functions for translating matRad data structures to/from GPU arrays were added. +- The Siddon raytracer is now implemented as a class with vectorized ray processing and optional single-precision forcing. +- Pencil beam engines now expose a `traceOnDoseGrid` switch (default `false`) to optionally retain radiological depth cubes on the dose grid. +- Performance improvements in `matRad_cubeIndex2worldCoords`. +- Improvements on the phantom builder to also accept mm coordinates for phantom definition +- Variance calculation from MC statistics can now be computed correctly +- `matRad_plotSlice` input parsing enhancements; fixed empty figure opening due to colormap array request. +- TOPAS now correctly support multiple alpha/beta values. +- Streamlined sequecing and 3D conformal calculations +- FRED interface updated with new test data, improved version compatibility, and the ability to force `ijFormatVersion`. +- DICOM import now imports passively scattered proton beams (gantry/couch angles) +- Optimizer instantiation reworked to allow more configuration options via `propOpt`. +- `finalizeDose` call in DoseEngines moved to `calcDoseForward` and `calcDoseInfluence` + +### Bug Fixes +- Fixed range-shifter handling issues in MC dose calculation interfaces +- Fix typo in RBE model fallback load path. +- Fix typo in `addMUdataFromMachine`. +- Correct DICOM attribute for `SliceLocation`. +- Fix slight dimension interpretation issue in `cubeIndex2worldCoords`. +- Fix scenario listing and the robustness field when serializing objectives to structs or displaying them in the CST. +- Multiple GUI fixes for MATLAB 2025 compatibility. +- GUI: fix missing plot handle; fix empty figure handles returned when GUI is globally disabled; fix `plotSlice` colormap issue; fix scrolling in the viewing widget under Octave (empty `CurrentPoint`). +- Drop `numOfbeams` as a required parameter (it can be inferred). + +### Documentation +- Full Sphinx / ReadTheDocs documentation build pipeline added (readthedocs.yml, GitHub Actions workflow, `docs/` folder with images and rst structure). +- Docstrings updated to be sphinx-napoleon compatible across many files. +- Copyright notices updated to 2026. + +### Development & CI +- Standalone build step added to GitHub Actions workflows with matrix build (windows, linux, macos intel / silicon). +- Preliminary pre-commit hooks configured with `miss_hit` (MATLAB style checker) and `codespell` (not enforced yet) +- GitHub Actions workflow for documentation building (triggered on changes to `docs/`). +- Coverage report workflow made more tolerant to errors. +- MOcov submodule updated to include md5 fix. + ## Minor Update 3.2.0 ### New Features @@ -9,7 +51,7 @@ ### Bug Fixes & Performance - DICOM Import widget allow selection of multiple RTDose files. -- DICOM Import Widget and importer handle selected patient more consistently and robustly. +- DICOM Import Widget and importer handle selected patient more consistently and robustly. - DICOM Exporter writes quantities beyond dose, importer tries to import them correctly. - DICOM Exporter now always writes ReferencedRTPlanSequence. Importer can now survive without it. - DVH widget does not throw a warning in updates, handle scenarios correctly / more robustly and missing xlabel axesHandle parameter. @@ -36,15 +78,15 @@ ### Major Changes and New Features -#### File Structure Overhaul +#### File Structure Overhaul - Major restructuring of files into organized subfolders, such as matRad (core code), thirdParty, examples, etc., to improve clarity and maintainability. - Introduction of userdata folder to maintain custom data #### Scenario Management and robust / 4D optimization - Introduced comprehensive scenario management (scenario models), including support for 4D phase scenarios and automated scenario model instance tests. -- Multiple robust optimization methods (COWC, OWC, VWWC, expected value) +- Multiple robust optimization methods (COWC, OWC, VWWC, expected value) -#### Object-Oriented DoseEngines & new Monte Carlo interfaces +#### Object-Oriented DoseEngines & new Monte Carlo interfaces - Transitioned from procedural dose calculation to an object-oriented approach, significantly improving the structure and maintainability of the dose engines. - Added customizable TOPAS interface for ions (and experimental for photons) - Workflow of the existing Monte Carlo interfaces has been completely overhauled in the new engine format @@ -85,7 +127,7 @@ While we try to keep downwards compatibility (and will provide fixes if breaking - Unit tests now runnig as GitHub Actions on Matlab R2022b, the latest release, and Octave 6 #### Improved Octave Compatibility: -- Compatibility tested for Octave 6 to 9. +- Compatibility tested for Octave 6 to 9. - Octave compatibility not always optimal, and IPOPT needs to be compiled individually. #### Performance Improvements & Code Cleanup @@ -105,8 +147,8 @@ While we try to keep downwards compatibility (and will provide fixes if breaking - **Minor:** Minor releases incldue minor new features (e.g. a new optimizer, objectives, biomodel or dose calculation algorithm). Downwards compatibility (within the major release) is preserved. - **Patch:** Patch versions only fix bugs and do not introduce new features. Exceptions could be the exposure of new, minimal configuration options to mitigate a bug occuring in special circumstances. -## Version 2.10.1 - Patch release for "Blaise" -Release with small updates, clean-ups and bugfixes +## Version 2.10.1 - Patch release for "Blaise" +Release with small updates, clean-ups and bugfixes - Bugfix in 3D view due to inconsistent angles in pln & stf - Bugfix for using incorrect dicom UID's and wrong writing order in the dicom export - Bugfix for weird colormap issue in plotting @@ -129,7 +171,7 @@ The new release contains: - Integration tests using TravisCI (with Octave, so no GUI functionalities are tested) - matRad_rc script to configure matRad paths - matRad version can now be printed with matRad_version, version correctly shown in GUI and when using matRad_rc -- Seven new Matlab example scripts to demonstrate use of matRad +- Seven new Matlab example scripts to demonstrate use of matRad - Added basic interfaces to the open-source photon/proton MC engines ompMC/MCsquare - Overhaul of the optimization interface using OOP and integration of the fmincon optimizer from Mathworks' MATLAB Optimization toolbox - Changes to the cst variable (new script to convert old to new cst structures in tools folder) @@ -150,7 +192,7 @@ The new release contains: - New colormap handling to allow integration of custom colormaps - Modularization of slice display by dedicated functions in plotting folder including generation of 3D views - New global configuration object (matRad_cfg <- MatRad_Config.m) to store default values and with logging interface -- Many bug fixes and many new bugs.. +- Many bug fixes and many new bugs.. ## Version 2.1 "Alan" First official release of matRad including diff --git a/LICENSE.md b/LICENSE.md index 7e4c8ce95..744e59633 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,1003 +1,16 @@ -matRad -with Ipopt MCsquare ompMC +## DISCLAIMER +This project is licensed under the license terms printed below (3-Clause BSD), except for third-party components located in the `thirdParty` and `submodules` directories. Those components are licensed under their respective licenses, as indicated in their individual directories. -This matRad as a compilation with Ipopt, MCsquare and ompMC as separate and independent works (or -modules, components, programs). +## LICENSE +Copyright (c) 2026, the matRad development team within the Radiotherapy Optimization Group, German Cancer Research Center (DKFZ) +All rights reserved. -It is the intention of the copyright owners of matRad and Ipopt that matRad and Ipopt -are each made available, used and further distributed under their own respective license -agreement, as provided below. To the best of our understanding, matRad and Ipopt do not -form a derivative work, a larger work or a collection of works that would be subject to -separate copyrights. Therefore, the compilation as a whole is covered the "Mere Aggregation" -exception in section 5 of the GNU GENERAL PUBLIC LICENSE Version 3 and the exemption -from "Contributions" in section 1. b) ii) of the Eclipse Public License -v 1.0. +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: -Should this interpretation turn out to be not in compliance with the applicable laws -in force, an alternative means is provided to allow the Ipopt to be legally used -together with matRad. For these purposes, please see the "Additional permission under -GNU GPL version 3 section 7" below. +1\. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -The compilation is covered by the following copyright and permission notices. +2\. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +3\. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - -matRad - -matRad is an open source multi-modality radiation treatment planning sytem written -in Matlab. It is meant for educational purposes and supports planning of -intensity-modulated radiation therapy for mutliple modalities. The source code is -maintained by a development team at the German Cancer Reserach Center DKFZ in -Heidelberg, Germany, and other contributors around the world. We are always looking -for more people willing to help improve matRad. Do not hesitate and get in touch. - -The source code and more information on matRad can be found on the project page at -http://e0404.github.io/matRad/; -a wiki documentation is under constant development at -https://github.com/e0404/matRad/wiki. - -Copyright (C) 2020 by the contributors and German Cancer Research Center - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program 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 General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -Additional permission under GNU GPL version 3 section 7 - -If you modify this Program, or any covered work, by linking or combining it with -Ipopt (or a modified version of that library), containing parts covered -by the terms of Eclipse Public License, Version 1.0 (EPL-1.0), the licensors of this -Program grant you additional permission to run, use and convey the resulting work. - -Contact: Niklas Wahl, E040 Research Group Radiotherapy Optimization -e-mail: contact@matRad.org -address: -Division of Medical Physics in Radiation Oncology -German Cancer Research Center -Im Neuenheimer Feld 280 -69120 Heidelberg -Germany - - -* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - -Ipopt - -Ipopt is an open-source solver for large-scale nonlinear continuous optimization. It -can be used from modeling environments, such as AIMMS, AMPL, GAMS, or Matlab, and it -is also available as callable library with interfaces to C++, C, Fortran, Java, and R. -Ipopt uses an interior point method, together with a filter linear search procedure. - -All rights reserved. This program and the accompanying materials -are made available under the terms of the Eclipse Public License v1.0 -which accompanies this distribution, and is available at -http://www.eclipse.org/legal/epl-v10.html. - -The source code and more information on Ipopt can be found on the project page -at https://projects.coin-or.org/Ipopt. - - -* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program 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 General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. - - -* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - -Eclipse Public License -v 1.0 - -THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC -LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM -CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. - -1. DEFINITIONS - -"Contribution" means: - -a) in the case of the initial Contributor, the initial code and documentation -distributed under this Agreement, and - -b) in the case of each subsequent Contributor: - -i) changes to the Program, and - -ii) additions to the Program; - -where such changes and/or additions to the Program originate from and are -distributed by that particular Contributor. A Contribution 'originates' from -a Contributor if it was added to the Program by such Contributor itself or -anyone acting on such Contributor's behalf. Contributions do not include -additions to the Program which: (i) are separate modules of software distributed -in conjunction with the Program under their own license agreement, and (ii) -are not derivative works of the Program. - -"Contributor" means any person or entity that distributes the Program. - -"Licensed Patents " mean patent claims licensable by a Contributor which are -necessarily infringed by the use or sale of its Contribution alone or when -combined with the Program. - -"Program" means the Contributions distributed in accordance with this Agreement. - -"Recipient" means anyone who receives the Program under this Agreement, -including all Contributors. - -2. GRANT OF RIGHTS - -a) Subject to the terms of this Agreement, each Contributor hereby grants -Recipient a non-exclusive, worldwide, royalty-free copyright license to -reproduce, prepare derivative works of, publicly display, publicly perform, -distribute and sublicense the Contribution of such Contributor, if any, and -such derivative works, in source code and object code form. - -b) Subject to the terms of this Agreement, each Contributor hereby grants -Recipient a non-exclusive, worldwide, royalty-free patent license under -Licensed Patents to make, use, sell, offer to sell, import and otherwise -transfer the Contribution of such Contributor, if any, in source code and -object code form. This patent license shall apply to the combination of the -Contribution and the Program if, at the time the Contribution is added by -the Contributor, such addition of the Contribution causes such combination -to be covered by the Licensed Patents. The patent license shall not apply -to any other combinations which include the Contribution. No hardware per -se is licensed hereunder. - -c) Recipient understands that although each Contributor grants the licenses -to its Contributions set forth herein, no assurances are provided by any -Contributor that the Program does not infringe the patent or other -intellectual property rights of any other entity. Each Contributor disclaims -any liability to Recipient for claims brought by any other entity based on -infringement of intellectual property rights or otherwise. As a condition to -exercising the rights and licenses granted hereunder, each Recipient hereby -assumes sole responsibility to secure any other intellectual property rights -needed, if any. For example, if a third party patent license is required to -allow Recipient to distribute the Program, it is Recipient's responsibility -to acquire that license before distributing the Program. - -d) Each Contributor represents that to its knowledge it has sufficient -copyright rights in its Contribution, if any, to grant the copyright license -set forth in this Agreement. - -3. REQUIREMENTS - -A Contributor may choose to distribute the Program in object code form under -its own license agreement, provided that: - -a) it complies with the terms and conditions of this Agreement; and - -b) its license agreement: - -i) effectively disclaims on behalf of all Contributors all warranties and -conditions, express and implied, including warranties or conditions of title -and non-infringement, and implied warranties or conditions of merchantability -and fitness for a particular purpose; - -ii) effectively excludes on behalf of all Contributors all liability for -damages, including direct, indirect, special, incidental and consequential -damages, such as lost profits; - -iii) states that any provisions which differ from this Agreement are offered -by that Contributor alone and not by any other party; and - -iv) states that source code for the Program is available from such Contributor, -and informs licensees how to obtain it in a reasonable manner on or through a -medium customarily used for software exchange. - -When the Program is made available in source code form: - -a) it must be made available under this Agreement; and - -b) a copy of this Agreement must be included with each copy of the Program. - -Contributors may not remove or alter any copyright notices contained within -the Program. - -Each Contributor must identify itself as the originator of its Contribution, -if any, in a manner that reasonably allows subsequent Recipients to identify -the originator of the Contribution. - -4. COMMERCIAL DISTRIBUTION - -Commercial distributors of software may accept certain responsibilities with -respect to end users, business partners and the like. While this license is -intended to facilitate the commercial use of the Program, the Contributor who -includes the Program in a commercial product offering should do so in a manner -which does not create potential liability for other Contributors. Therefore, -if a Contributor includes the Program in a commercial product offering, such -Contributor ("Commercial Contributor") hereby agrees to defend and indemnify -every other Contributor ("Indemnified Contributor") against any losses, damages -and costs (collectively "Losses") arising from claims, lawsuits and other legal -actions brought by a third party against the Indemnified Contributor to the extent -caused by the acts or omissions of such Commercial Contributor in connection -with its distribution of the Program in a commercial product offering. The -obligations in this section do not apply to any claims or Losses relating to -any actual or alleged intellectual property infringement. In order to qualify, -an Indemnified Contributor must: a) promptly notify the Commercial Contributor -in writing of such claim, and b) allow the Commercial Contributor to control, -and cooperate with the Commercial Contributor in, the defense and any related -settlement negotiations. The Indemnified Contributor may participate in any -such claim at its own expense. - -For example, a Contributor might include the Program in a commercial product -offering, Product X. That Contributor is then a Commercial Contributor. If that -Commercial Contributor then makes performance claims, or offers warranties -related to Product X, those performance claims and warranties are such -Commercial Contributor's responsibility alone. Under this section, the -Commercial Contributor would have to defend claims against the other -Contributors related to those performance claims and warranties, and if a -court requires any other Contributor to pay any damages as a result, the -Commercial Contributor must pay those damages. - -5. NO WARRANTY - -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN -"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR -IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, -NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each -Recipient is solely responsible for determining the appropriateness of using -and distributing the Program and assumes all risks associated with its exercise -of rights under this Agreement , including but not limited to the risks and -costs of program errors, compliance with applicable laws, damage to or loss -of data, programs or equipment, and unavailability or interruption of operations. - -6. DISCLAIMER OF LIABILITY - -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY -CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST -PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, -STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS -GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - -7. GENERAL - -If any provision of this Agreement is invalid or unenforceable under applicable -law, it shall not affect the validity or enforceability of the remainder of the -terms of this Agreement, and without further action by the parties hereto, such -provision shall be reformed to the minimum extent necessary to make such -provision valid and enforceable. - -If Recipient institutes patent litigation against any entity (including a -cross-claim or counterclaim in a lawsuit) alleging that the Program itself -(excluding combinations of the Program with other software or hardware) infringes -such Recipient's patent(s), then such Recipient's rights granted under Section -2(b) shall terminate as of the date such litigation is filed. - -All Recipient's rights under this Agreement shall terminate if it fails to comply -with any of the material terms or conditions of this Agreement and does not cure -such failure in a reasonable period of time after becoming aware of such -noncompliance. If all Recipient's rights under this Agreement terminate, -Recipient agrees to cease use and distribution of the Program as soon as -reasonably practicable. However, Recipient's obligations under this Agreement -and any licenses granted by Recipient relating to the Program shall continue and -survive. - -Everyone is permitted to copy and distribute copies of this Agreement, but in -order to avoid inconsistency the Agreement is copyrighted and may only be modified -in the following manner. The Agreement Steward reserves the right to publish new -versions (including revisions) of this Agreement from time to time. No one other -than the Agreement Steward has the right to modify this Agreement. The Eclipse -Foundation is the initial Agreement Steward. The Eclipse Foundation may assign -the responsibility to serve as the Agreement Steward to a suitable separate entity. -Each new version of the Agreement will be given a distinguishing version number. -The Program (including Contributions) may always be distributed subject to the -version of the Agreement under which it was received. In addition, after a new -version of the Agreement is published, Contributor may elect to distribute the -Program (including its Contributions) under the new version. Except as expressly -stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses -to the intellectual property of any Contributor under this Agreement, whether -expressly, by implication, estoppel or otherwise. All rights in the Program not -expressly granted under this Agreement are reserved. - -This Agreement is governed by the laws of the State of New York and the -intellectual property laws of the United States of America. No party to this -Agreement will bring a legal action under this Agreement more than one year -after the cause of action arose. Each party waives its rights to a jury trial -in any resulting litigation. - -* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - -ompMC - -Information and source code of ompMC is available on https://github.com/edoerner/ompMC -Precompiled binaries and reference data of ompMC are used within matRad. -The source code itself is referenced as a git submodule. Contents of the ompMC subfolder -and the submodule are licensed under GNU GPL v3.0 as stated in the respective LICENSE -files located in these folders. - -* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - -MCsquare - -MCsquare is an open-source software developed/maintained at UC Louvain, Belgium. -Information and source code of MCsquare is available on http://openmcsquare.org and -https://https://gitlab.com/openmcsquare/MCsquare -Precompiled binaries and reference data of MCsquare are used within and distributed with -matRad. -The source code itself is referenced as a git submodule. Contents of the MCsquare/bin -subfolder and the submodule are licensed under the Apache-2.0 license as stated in the -respective LICENSE files located in these folders. - -* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * \ No newline at end of file +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/_static/style.css b/docs/_static/style.css new file mode 100644 index 000000000..7e1d9d9aa --- /dev/null +++ b/docs/_static/style.css @@ -0,0 +1,11 @@ +img.matrad-header { + padding-bottom: 0.45ex; + margin-bottom: 0px; + vertical-align: bottom; +} + +img.matrad-text-logo { + padding-bottom: 0.8ex; + margin-bottom: 0px; + vertical-align: bottom; +} \ No newline at end of file diff --git a/docs/api/4D.rst b/docs/api/4D.rst new file mode 100644 index 000000000..3bf3f3546 --- /dev/null +++ b/docs/api/4D.rst @@ -0,0 +1,15 @@ +.. _4d: + +## +4D +## + +Specialized functions for 4D dose calculation and treatment planning + +.. automodule:: matRad.4D + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. \ No newline at end of file diff --git a/docs/api/IO.rst b/docs/api/IO.rst new file mode 100644 index 000000000..01b88bfba --- /dev/null +++ b/docs/api/IO.rst @@ -0,0 +1,15 @@ +.. _io: + +## +IO +## + +Input/output functions for reading and writing matRad-compatible data formats. + +.. automodule:: matRad.IO + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. diff --git a/docs/api/basedata.rst b/docs/api/basedata.rst new file mode 100644 index 000000000..b87ea04d3 --- /dev/null +++ b/docs/api/basedata.rst @@ -0,0 +1,15 @@ +.. _basedata: + +######## +basedata +######## + +Functions for loading and managing machine base data used in dose calculation. + +.. automodule:: matRad.basedata + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. diff --git a/docs/api/bioModels.rst b/docs/api/bioModels.rst new file mode 100644 index 000000000..8b8926481 --- /dev/null +++ b/docs/api/bioModels.rst @@ -0,0 +1,29 @@ +.. _biomodels: + +######### +bioModels +######### + +.. contents:: + :local: + +Biological effect models for computing radiobiological quantities such as LET, RBE, and survival fractions. + +.. automodule:: matRad.bioModels + :members: + :undoc-members: + :show-inheritance: + :private-members: + +LQ-based Models +--------------- + +Linear-quadratic based biological models. + +.. automodule:: matRad.bioModels.LQbasedModels + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. diff --git a/docs/api/brachytherapy.rst b/docs/api/brachytherapy.rst new file mode 100644 index 000000000..9246c53a4 --- /dev/null +++ b/docs/api/brachytherapy.rst @@ -0,0 +1,29 @@ +.. _brachytherapy: + +############# +brachytherapy +############# + +.. contents:: + :local: + +Experimental brachytherapy dose planning functions. + +.. automodule:: matRad.brachytherapy + :members: + :undoc-members: + :show-inheritance: + :private-members: + +Additional Scripts +------------------ + +Additional utility scripts for brachytherapy planning. + +.. automodule:: matRad.brachytherapy.additionalScripts + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. diff --git a/docs/api/dicom.rst b/docs/api/dicom.rst new file mode 100644 index 000000000..cfd3ddcc2 --- /dev/null +++ b/docs/api/dicom.rst @@ -0,0 +1,40 @@ +.. _dicom: + +##### +dicom +##### + +.. contents:: + :local: + +DICOM import and export functionality for reading and writing patient data in DICOM format. + +.. automodule:: matRad.dicom + :members: + :undoc-members: + :show-inheritance: + :private-members: + +DICOM Exporter +-------------- + +Class handling the export of matRad treatment plan data to DICOM format. + +.. autoclass:: matRad.dicom.matRad_DicomExporter + :members: + :undoc-members: + :show-inheritance: + :private-members: + +DICOM Importer +-------------- + +Class handling the import of patient data from DICOM files into matRad. + +.. autoclass:: matRad.dicom.matRad_DicomImporter + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. diff --git a/docs/api/doseCalc.rst b/docs/api/doseCalc.rst new file mode 100644 index 000000000..96521cf10 --- /dev/null +++ b/docs/api/doseCalc.rst @@ -0,0 +1,60 @@ +.. _dosecalc: + +######## +doseCalc +######## + +.. contents:: + :local: + +Dose calculation engines and supporting utilities for photon, proton, and ion therapy. + +.. automodule:: matRad.doseCalc + :members: + :undoc-members: + :show-inheritance: + :private-members: + +Dose Engines +------------ + +.. automodule:: matRad.doseCalc.+DoseEngines + :members: + :undoc-members: + :show-inheritance: + :private-members: + +FRED +---- + +Interface components for the FRED Monte Carlo dose calculation engine. + +.. automodule:: matRad.doseCalc.FRED + :members: + :undoc-members: + :show-inheritance: + :private-members: + +MCsquare +-------- + +Interface components for the MCsquare fast Monte Carlo proton dose calculation engine. + +.. automodule:: matRad.doseCalc.MCsquare + :members: + :undoc-members: + :show-inheritance: + :private-members: + +TOPAS +----- + +Interface componentsfor the TOPAS Monte Carlo dose calculation engine. + +.. automodule:: matRad.doseCalc.topas + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. diff --git a/docs/api/geometry.rst b/docs/api/geometry.rst new file mode 100644 index 000000000..573d5b8cc --- /dev/null +++ b/docs/api/geometry.rst @@ -0,0 +1,14 @@ +.. _geometry: + +geometry +======== + +Contains functions to convert between world coordinates, cube coordinates, and voxel indices, as well as functions to handle gantry and couch rotations. + +.. automodule:: matRad.geometry + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. diff --git a/docs/api/gpu.rst b/docs/api/gpu.rst new file mode 100644 index 000000000..4e2f2ae48 --- /dev/null +++ b/docs/api/gpu.rst @@ -0,0 +1,15 @@ +.. _gpu: + +### +gpu +### + +GPU-accelerated computation functions for dose calculation. + +.. automodule:: matRad.gpu + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. diff --git a/docs/api/gui.rst b/docs/api/gui.rst new file mode 100644 index 000000000..b6cd409b6 --- /dev/null +++ b/docs/api/gui.rst @@ -0,0 +1,43 @@ +.. _gui: + +### +gui +### + +.. contents:: + :local: + + +Main GUI +-------- + +The main graphical user interface of matRad is implemented in the class :class:`matRad_MainGUI`. +It provides access to all functionalities of matRad and is the central hub for visualization and interaction with the treatment planning workflow. + +It is composed of several widgets that are implemented in the :mod:`matRad.gui.widgets` submodule. +The GUI can also be started with :func:`matRadGUI` from the root folder. + +.. autoclass:: matRad.gui.matRad_MainGUI + :members: + :undoc-members: + :show-inheritance: + :private-members: + +Basic GUI functions +------------------- + +.. automodule:: matRad.gui + :members: + :undoc-members: + :show-inheritance: + :private-members: + :exclude-members: matRad_MainGUI + +Widgets +------- + +.. automodule:: matRad.gui.widgets + :members: + :undoc-members: + :show-inheritance: + :private-members: diff --git a/docs/api/hluts.rst b/docs/api/hluts.rst new file mode 100644 index 000000000..4a7509b26 --- /dev/null +++ b/docs/api/hluts.rst @@ -0,0 +1,15 @@ +.. _hluts: + +##### +hluts +##### + +Hounsfield unit (HU) lookup tables for converting CT Hounsfield values to material properties used in dose calculation. + +.. automodule:: matRad.hluts + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. diff --git a/docs/api/index.rst b/docs/api/index.rst new file mode 100644 index 000000000..ce8d8c0e4 --- /dev/null +++ b/docs/api/index.rst @@ -0,0 +1,60 @@ +matRad API Documentation +======================== + +matRad is separted into a top-level API that consists of a few functions to run the treatment planning workflow resting on the top-level of the the matRad folder / module. The core implementation is organized in several subdirectories / submodules that are called by the API functions and can be used for more fine-grained development. +Startup and configuration functions are located in the root of the repository. + +.. note:: + + The root-level scripts (``matRad.m``, ``matRadGUI.m``, ``matRad_rc.m`` etc.) are not auto-documented here. + See the :ref:`run_script` guide for usage information. + +Below, the top-level matRad functions are explained. For more specialized functions, refer to the documentation of the respective `Modules / Subfolders`_. + +.. contents:: + :local: + :depth: 1 + +Global Configuration +-------------------- +.. _config: + +matRad's global configuration class :class:`MatRad_Config` is used to set up the environment and configuration for the matRad application. +It is implemented as a Singleton pattern and thus consistent throughout a matRad session. + +At the core, the class handles user folders, caches the environment (Matlab/Octave), provides matRad's version, and stores default parameters. +The class also provides logging functionality enabling control over output via log levels. + +---- + +.. autoclass:: matRad.MatRad_Config + :members: + :undoc-members: + :show-inheritance: + :private-members: + :noindex: +.. + +Top-level API functions +----------------------- + +.. automodule:: matRad + :members: + :undoc-members: + :show-inheritance: + :private-members: + :exclude-members: MatRad_Config + +Modules / Subfolders +-------------------- + +.. toctree:: + :maxdepth: 5 + :glob: + :includehidden: + :reversed: + + * + optimization/index + +.. diff --git a/docs/api/optimization/basefuncs.rst b/docs/api/optimization/basefuncs.rst new file mode 100644 index 000000000..ee96b0c41 --- /dev/null +++ b/docs/api/optimization/basefuncs.rst @@ -0,0 +1,30 @@ +.. _obj_constr: + +====================== +Optimization Functions +====================== + +Currently, matRad implements objectives and constraints for the optimization of dose distributions. +All Objectives derive from the :class:`matRad_DoseOptimizationFunction` class and differentiate between dose :ref:`objectives ` and :ref:`constraints `. + +.. _objectives: + +Dose Objectives +--------------- + +.. automodule:: matRad.optimization.+DoseObjectives + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. _constraints: + +Dose Constraints +---------------- + +.. automodule:: matRad.optimization.+DoseConstraints + :members: + :undoc-members: + :show-inheritance: + :private-members: \ No newline at end of file diff --git a/docs/api/optimization/index.rst b/docs/api/optimization/index.rst new file mode 100644 index 000000000..363c499c9 --- /dev/null +++ b/docs/api/optimization/index.rst @@ -0,0 +1,21 @@ +############ +optimization +############ + +.. toctree:: + :maxdepth: 4 + + basefuncs + optimizers + projections + +Local functions +--------------- + +.. automodule:: matRad.optimization + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. \ No newline at end of file diff --git a/docs/api/optimization/optimizers.rst b/docs/api/optimization/optimizers.rst new file mode 100644 index 000000000..9d38205f4 --- /dev/null +++ b/docs/api/optimization/optimizers.rst @@ -0,0 +1,11 @@ +.. _optimizers: + +========== +optimizers +========== + +.. automodule:: matRad.optimization.optimizer + :members: + :undoc-members: + :show-inheritance: + :private-members: diff --git a/docs/api/optimization/projections.rst b/docs/api/optimization/projections.rst new file mode 100644 index 000000000..9866811c9 --- /dev/null +++ b/docs/api/optimization/projections.rst @@ -0,0 +1,13 @@ +.. _projections: + +=========== +projections +=========== + +Projections handle the forward calculation and chain derivative of quantities during optimization. + +.. automodule:: matRad.optimization.projections + :members: + :undoc-members: + :show-inheritance: + :private-members: diff --git a/docs/api/phantoms.rst b/docs/api/phantoms.rst new file mode 100644 index 000000000..b8b150fbe --- /dev/null +++ b/docs/api/phantoms.rst @@ -0,0 +1,23 @@ +.. _phantoms: + +######## +phantoms +######## + +.. contents:: + :local: + +Pre-defined phantom data sets for testing and demonstration purposes. + +Phantom Builder +--------------- + +Utility functions for programmatically constructing phantom geometries. + +.. automodule:: matRad.phantoms.builder + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. diff --git a/docs/api/planAnalysis.rst b/docs/api/planAnalysis.rst new file mode 100644 index 000000000..0c5f0c25a --- /dev/null +++ b/docs/api/planAnalysis.rst @@ -0,0 +1,29 @@ +.. _plananalysis: + +############ +planAnalysis +############ + +.. contents:: + :local: + +Functions for evaluating and analyzing treatment plans, including DVH calculation and quality indicators. + +.. automodule:: matRad.planAnalysis + :members: + :undoc-members: + :show-inheritance: + :private-members: + +Sampling Analysis +----------------- + +Sampling-based plan analysis for robustness evaluation. + +.. automodule:: matRad.planAnalysis.samplingAnalysis + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. diff --git a/docs/api/plotting.rst b/docs/api/plotting.rst new file mode 100644 index 000000000..98223099d --- /dev/null +++ b/docs/api/plotting.rst @@ -0,0 +1,29 @@ +.. _plotting: + +######## +plotting +######## + +.. contents:: + :local: + +Visualization and plotting functions for treatment plans and dose distributions. + +.. automodule:: matRad.plotting + :members: + :undoc-members: + :show-inheritance: + :private-members: + +Colormaps +--------- + +Custom colormaps optimized for dose distribution visualization. + +.. automodule:: matRad.plotting.colormaps + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. diff --git a/docs/api/raytracing.rst b/docs/api/raytracing.rst new file mode 100644 index 000000000..9c9a20162 --- /dev/null +++ b/docs/api/raytracing.rst @@ -0,0 +1,12 @@ +.. _raytracing: + +rayTracing +========== + +.. automodule:: matRad.rayTracing + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. diff --git a/docs/api/scenarios.rst b/docs/api/scenarios.rst new file mode 100644 index 000000000..a64ceeb29 --- /dev/null +++ b/docs/api/scenarios.rst @@ -0,0 +1,14 @@ +.. _scenarios: + +scenarios +========= + +Copntains the scenario models used in matRad. + +---- + +.. automodule:: matRad.scenarios + :members: + :undoc-members: + :show-inheritance: + :private-members: \ No newline at end of file diff --git a/docs/api/sequencing.rst b/docs/api/sequencing.rst new file mode 100644 index 000000000..f083e4383 --- /dev/null +++ b/docs/api/sequencing.rst @@ -0,0 +1,15 @@ +.. _sequencing: + +########## +sequencing +########## + +MLC leaf sequencing algorithms for converting fluence maps to deliverable photon IMRT plans. + +.. automodule:: matRad.sequencing + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. diff --git a/docs/api/steering.rst b/docs/api/steering.rst new file mode 100644 index 000000000..92b83c9c7 --- /dev/null +++ b/docs/api/steering.rst @@ -0,0 +1,16 @@ +.. _steering: + +steering +======== + +Contains classes to create "steering information" objects that are used to define the geometry of the treatment plan, such as gantry angles, couch angles, and other parameters that influence the treatment delivery. + +Used within the top-level API function :func:`matRad_generateStf` to create an :ref:`stf ` struct. + +---- + +.. automodule:: matRad.steering + :members: + :undoc-members: + :show-inheritance: + :private-members: \ No newline at end of file diff --git a/docs/api/util.rst b/docs/api/util.rst new file mode 100644 index 000000000..ec7a24855 --- /dev/null +++ b/docs/api/util.rst @@ -0,0 +1,29 @@ +.. _util: + +#### +util +#### + +.. contents:: + :local: + +General utility functions used throughout matRad. + +.. automodule:: matRad.util + :members: + :undoc-members: + :show-inheritance: + + +Octave Compatibility +-------------------- + +Compatibility functions ensuring matRad runs correctly in GNU Octave in addition to MATLAB. + +.. automodule:: matRad.util.octaveCompat + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 000000000..0ed5a3b12 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,89 @@ +import os + +# Compatibility patch: sphinxcontrib-matlabdomain 0.22.1 was written for +# Sphinx <8.x. Sphinx 8.x's autodoc base class now emits ':no-index:' +# (hyphenated) in generated RST, but MatObject/MatModule option_spec only +# register the old 'noindex' name, causing "unknown option" errors. +# Remove this block once sphinxcontrib-matlabdomain is updated upstream. +def _patch_mat_modindex() -> None: + """ + Register 'mat-modindex' as a virtual docname in Sphinx's StandardDomain so + it can be used directly in toctree directives (like genindex/modindex/search). + + Background: sphinxcontrib-matlabdomain generates mat-modindex.html via the + domain index machinery, but the StandardDomain._virtual_doc_names dict only + knows about 'genindex', 'modindex' (→ py-modindex), and 'search'. Any name + in that dict bypasses the normal "does this RST file exist?" check in both + the toctree directive (sphinx/directives/other.py) and the toctree adapter + (sphinx/environment/adapters/toctree.py) and is resolved directly to the + given output filename. Adding 'mat-modindex' here lets you write:: + + .. toctree:: + mat-modindex + + and have it link to the generated mat-modindex.html page with the correct + relative URL regardless of nesting depth. + """ + from sphinx.domains.std import StandardDomain + StandardDomain._virtual_doc_names['mat-modindex'] = ( + 'mat-modindex', 'MATLAB Module Index' + ) + +_patch_mat_modindex() + + +def _patch_matlabdomain_noindex() -> None: + from docutils.parsers.rst import directives as _directives + from sphinxcontrib.matlab import MatObject, MatModule + for cls in (MatObject, MatModule): + if "no-index" not in cls.option_spec: + cls.option_spec["no-index"] = _directives.flag + +_patch_matlabdomain_noindex() + +project = "matRad" +copyright = "2025, e0404" +author = "e0404" + +version = "3.2.1" +release = "3.2.1" + +html_theme = "sphinx_rtd_theme" #pip install sphinx-rtd-theme + +html_static_path = ["_static"] +html_css_files = ["style.css"] +html_logo = "../matRad/gfx/matrad_logo.png" +html_theme_options = { + 'logo_only': True, + 'navigation_depth': 5, + 'collapse_navigation': True, +} + +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.napoleon', + 'sphinx_toolbox.collapse', + 'sphinxcontrib.matlab', + 'sphinxcontrib.youtube', + 'sphinx_togglebutton', + ] + +napoleon_google_docstring = True +napoleon_numpy_docstring = False +napoleon_custom_sections = [ + ('input', 'params_style'), + ('output', 'params_style'), + 'call' +] +primary_domain = "mat" + +matlab_src_dir = os.path.join(os.path.dirname(__file__), '../') +matlab_auto_link = "basic" + +matlab_show_property_default_value = True +matlab_show_property_specs = True +matlab_class_signature = True +matlab_short_links = True + +autoclass_content = "both" +autodoc_member_order = "bysource" diff --git a/docs/datastructures/basedata/particles.rst b/docs/datastructures/basedata/particles.rst new file mode 100644 index 000000000..08425f3f3 --- /dev/null +++ b/docs/datastructures/basedata/particles.rst @@ -0,0 +1,87 @@ +.. _basedata_particles: + +======================== +Particle Base Data File +======================== + +This page describes the format and the variables as part of the particle base data files `protons_Generic.mat `_ and `carbon_Generic.mat `_. +The base data is stored using MATLAB's structure format. +The first sub level of the structure 'machine' contains two fields named 'meta' and 'data' which are explained separately next. + +machine.meta +------------ +Stores relevant isolated meta information describing the actual base data file that does not fit into the machine.data structure. + +machine.meta.machine +^^^^^^^^^^^^^^^^^^^^ +Actual machine name as string to identify the base data set during runtime. + +machine.meta.radiationMode +^^^^^^^^^^^^^^^^^^^^^^^^^^ +The radiationMode stores the radiation modality the base data is describing. Logically, `protons_Generic.mat `_ models protons in water and the corresponding field machine.meta.radiationMode is set to 'protons'. + +machine.meta.SAD +^^^^^^^^^^^^^^^^ +This subfield holds the geometrical source to axis distance in millimeter. In case of the generic base data set, we use a value of 10000 [mm]. + +.. _BAMStoIsoDist: + +machine.meta.BAMStoIsoDist +^^^^^^^^^^^^^^^^^^^^^^^^^^ +This subfield depicts the geometrical distance from the beam application monitoring system/beam nozzle to the isocenter. For the generic base data set we use a value of 2000 [mm]. + +machine.meta.LUT_bxWidthminFWHM +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +This field is a two-dimensional array and holds a look up table which describes a mapping of the lateral spot spacing (bixel width) to the focus index (initial beam width). In the generic proton base data set, we only store one focus index (initial beam width) for which reasons the corresponding LUT_bxWidthminFWHM is set to [1,Inf;5 5]. This means that if the lateral spot spacing is set during treatment planning to a value between 1 and Inf, then a focus index with at least a full width half maximum (FWHM) of 5 mm is consistently used. As we only store one focus index in machine.data.initFocus for `protons_Generic.mat `_ with a FWHM at isocenter for the highest beam energy of ~5.4 [mm], we basically make sure to always use the first (and only) focus index. In principle, the idea behind machine.meta.LUT_bxWidthminFWHM is to allow for different focus indices when using different lateral spot spacings (bixel widths). Users can model the following behavior: E.g. when setting the lateral spot spacing to 5 [mm] then use a foci index with a FWHM greater than 8 [mm]. Or when setting the lateral spot spacing to 3 [mm] then use a focus index with FWHM greater than 6 [mm]. + +machine.data +------------ +This subfield contains an array of structures holding the corresponding depth-dependent physical and (biological) beam properties for each initial beam energy. + +machine.data.energy +^^^^^^^^^^^^^^^^^^^ +Initial beam energy in MeV/u. Internally, this is used mainly as an identifier for the entry and does not necessarily correspond to an exact energy at the nozzle, for example. + +machine.data.depths +^^^^^^^^^^^^^^^^^^^ +Depth values stored on an irregular grid in [mm] (higher resolution around the peak). All depth-dependent data like (Z, sigma) are stored exactly on these depth positions. + +machine.data.peakPos +^^^^^^^^^^^^^^^^^^^^ +Peak position of the pencil beam in [mm]. + +machine.data.offset +^^^^^^^^^^^^^^^^^^^ +This field allows to consider a pencil beam offset caused by passive beam line elements, that have not been modeled during the creation of the base data set. + +machine.data.Z +^^^^^^^^^^^^^^ +This field holds the integrated depth dose profiles of the corresponding radiation modality. Regarding units, we refer to the base data section in the section :ref:`Dose influence matrix calculation `. + +machine.data.sigma +^^^^^^^^^^^^^^^^^^^ +(for a single Gaussian lateral beam model) +Lateral beam broadening of the particle pencil beam in water. Hint: The first sigma value for depth 0 [mm] should be set to 0 [mm] because the initial beam width at the patient surface is modeled via machine.data.initFocus and covered later in this wiki page. + +machine.data.sigma1 +^^^^^^^^^^^^^^^^^^^ +(for double Gaussian lateral beam model) +Lateral beam broadening of the particle pencil beam in water of the narrow Gaussian component. Again, the first sigma1 value for depth 0 [mm] should be set to 0 [mm]. + +machine.data.sigma2 +^^^^^^^^^^^^^^^^^^^ +(for double Gaussian lateral beam model) +Lateral beam broadening of the particle pencil beam in water of the broad Gaussian component. + +machine.data.w +^^^^^^^^^^^^^^ +(for double Gaussian lateral beam model) +Relative weight between the narrow (sigma1) and the broad (sigma) Gaussian component. + +machine.data.initFocus +^^^^^^^^^^^^^^^^^^^^^^ +Let numFoci be the number of available focus indices and machine.data.initFocus hold three subfields named 'dist', 'sigma' and 'SisFWHMAtIso' of the following dimensions numFoci x N, numFoci x N and numFoci x 1 whereas N indicates the number of values used in the look up table. SisFWHMAtIso describes for each focus index the initial FWHM at isocenter. In contrast, 'dist' and 'sigma' depict a look up table to model the particle beam spread in air. In the generic proton and carbon base data set we do not model beam widening in air from the beam nozzle to the patient surface (although the code is capable of). Therefore, machine.data.initFocus(1).sigma is a constant value over a distance from 0 to 20000 [mm]. + +machine.data.energySpectrum +^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Sotres the energyspectrum at the nozzle (whose distance is taken from BAMStoIsoDist_) diff --git a/docs/datastructures/basedata/photons.rst b/docs/datastructures/basedata/photons.rst new file mode 100644 index 000000000..de93d8437 --- /dev/null +++ b/docs/datastructures/basedata/photons.rst @@ -0,0 +1,5 @@ +.. _basedata_photons: + +====================== +Photon Base Data File +====================== \ No newline at end of file diff --git a/docs/datastructures/cort.rst b/docs/datastructures/cort.rst new file mode 100644 index 000000000..90e996cc2 --- /dev/null +++ b/docs/datastructures/cort.rst @@ -0,0 +1,17 @@ +.. _cort: + +================ +The CORT Dataset +================ + +The matRad release includes five image datasets of phantoms and patients. +Consequently, everybody can start treatment planning out of the box without the need for their own data. +Besides a simple cubic box phantom, matRad also features the four datasets that have been published as the `CORT dataset `_. + +The `CORT dataset `_ (common optimization for radiation therapy) is an open dataset intended to be used by researchers when developing and contrasting radiation treatment planning optimization algorithms. It is comprised of datasets for a prostate case, a liver case, a head and neck case, and a standard IMRT phantom (TG119). In matRad, these datasets are already imported and ready to be loaded as MATLAB native ``.mat`` files. +To allow rapid prototyping and reasonable performance in educational settings, the CT images were downsampled at import. + +If you wish to take a look at the original data, you can find it `here `_. +The dataset contains the original Digital Imaging and Communications in Medicine (DICOM) computed tomography (CT) scan, as well as the DICOM structure file. In addition, the dose-influence matrix from a variety of beam/couch angle pairs is provided for each case. + +Besides the pre-configured patient datasets, matRad also features a :ref:`DICOM import `. \ No newline at end of file diff --git a/docs/datastructures/cst.rst b/docs/datastructures/cst.rst new file mode 100644 index 000000000..cfdd4016d --- /dev/null +++ b/docs/datastructures/cst.rst @@ -0,0 +1,133 @@ +.. _cst: + +================== +The cst cell Array +================== + +The constraints of all defined volumes of interest (VOIs) are stored inside the ``cst`` cell array. It is structured as follows: + +.. _cst-cell: + +Screenshot of the cst-cell: + +.. image:: /images/cstCellScreenshot.png + :alt: Screenshot of the cst cell + +.. list-table:: Structure of the cst cell array + :header-rows: 1 + + * - Column + - Content + - Description + * - **1** + - :ref:`VOI index ` + - Number to identify the VOI + * - **2** + - :ref:`VOI name ` + - String describing the VOI + * - **3** + - :ref:`VOI type ` + - Specification whether the VOI is an organ at risk (OAR), a target volume or should be ignored + * - **4** + - :ref:`Voxel indices ` + - Vectors containing the indices of all voxels of the CT that are covered by the VOI. Stored as a cell array of vectors (for enabling handling of multiple scenarios) + * - **5** + - :ref:`Tissue parameters ` + - Structure containing information about the tissue of the VOI and its overlap priority + * - **6** + - :ref:`Dose objectives ` + - Cell array containing information about the functions used to calculate the objective & constraint function value + * - **7** + - Precomputed Contours + - After GUI startup, this column contains precomputed contour data for display + +.. _VolInd: + +VOI index +--------- + +All defined VOIs are enumerated starting with 0. + +.. _VolName: + +VOI name +-------- + +The VOI name is a string containing an organ name or a short description of the volume (e.g. ``BODY``, ``Liver``, ``GTV``, ...). + +.. _VolType: + +VOI type +-------- + +The VOI type specifies how the volume is considered during treatment planning: + +.. list-table:: VOI types and their handling during treatment planning + :header-rows: 1 + + * - VOI type + - Handling during treatment planning + * - **TARGET** + - The VOI will be covered with spot positions (protons / carbon ions) and bixels (photons) as defined in the :ref:`stf struct `. During the fluence optimization, it will be considered according to the defined :ref:`dose objectives `. + * - **OAR** + - The VOI will not be covered with spot positions or bixels. During the fluence optimization, it will be considered according to the defined :ref:`dose objectives `. + * - **IGNORED** + - The VOI will not be considered during the treatment planning. + +.. _VoxInd: + +Voxel indices +------------- + +The indices of all voxels (of the :ref:`CT-cube `) that are covered by the VOI are stored in a vector within a cell array. I.e. we store the segmentation for the VOI as a binary mask, the polygon contour data is not part of matRad's standard data sets. +As the same voxel can be covered by more than one VOI, an overlap priority (see :ref:`tissue parameters `) is defined to handle potential discrepancies when calculating the objective function value and generating the :ref:`stf struct `. + +.. _TissParam: + +Tissue parameters +----------------- + +.. image:: /images/cstCellTissueParametersScreenshot.png + :alt: Screenshot of tissue parameters + +Data can also be stored as in the :ref:`old format (see below) `. + +New constraints or objectives can be implemented by adding a respective class definition to the :mod:`matRad.optimization.+DoseConstraints` or :mod:`matRad.optimization.+DoseObjectives` folder. + +.. _DoseParam: + +Dose Objectives & Constraints since v2.10.0 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +matRad supports inverse planning based on the minimization of a weighted sum of objectives subject to non-linear yet differentiable hard constraints. The following kind of individual objectives are currently supported: + +.. include:: ../includes/objtable.rst + +Constraints are somewhat built around similar goals as obejctives: + +.. include:: ../includes/constrtable.rst + +When generating an objective / constraint from code, we suggest to wrap the instantiation of the objective/constraint in a ``struct()`` call, as shown in the first phantom example: + +.. literalinclude:: ../../examples/matRad_example1_phantom.m + :caption: examples/matRad_example1_phantom.m + :lines: 47-49 + :lineno-match: + :language: matlab + + +This will ensure that, when saving to a mat-file, we don't save the class object, which improves compatibility. + +.. _DoseParamOld: + +Before Version 2.10.0 +~~~~~~~~~~~~~~~~~~~~~ +In the earlier version, matRad stored the objectives and constraints defined for inverse planning as an array of structs. :func:`matRad_convertOldCstToNewCstObjectives` can be used to convert the old definition to the new format. + +.. _defaultValues: + +Default *cst*-values +-------------------- + +The patient data contained within matRad (*ALDERSON, BOXPHANTOM, HEAD_AND_NECK, LIVER, PROSTATE and TG119*) have default values defined within the *cst*-cell. + +These values are chosen to produce a reasonable treatment plan, when using coplanar and equidistant photon beams. They can be used as a reference point for more sophisticated treatment plans. diff --git a/docs/datastructures/ct.rst b/docs/datastructures/ct.rst new file mode 100644 index 000000000..1fa78e756 --- /dev/null +++ b/docs/datastructures/ct.rst @@ -0,0 +1,39 @@ +.. _ct: + +===================== +The ct Data Structure +===================== + +The ct structure contains, among others, the 3D-CT-cube (see `ct.cube`_), obtained from the planning-CT, and the voxel resolution (see `ct.resolution`_). + +Screenshot of the ct-struct: + +.. image:: /images/ctDataScreenshot.png + +.. _ct-cube: + +ct.cube +------- + +The cube is a N\ :sub:`x` × N\ :sub:`y` × N\ :sub:`z` matrix (N\ :sub:`x,y,z` = number of voxels in x-, y- and z-direction) containing the water equivalent thickness of each voxel. We already translate HU to water equivalent electron density according to a look up table upon patient data import. The cube(s) is/are stored within a cell array to support multiple CT phases for 4D data. + +.. _resolution: + +ct.resolution +------------- + +The resolution specifies the size of each voxel in x-, y-, and z-direction in [mm]. + +.. _cubeDim: + +ct.cubeDim +---------- + +Number of voxels in x-, y- and z-direction (N\ :sub:`x`, N\ :sub:`y` and N\ :sub:`z`). + +.. _numOfCtScen: + +ct.numOfCtScen +-------------- + +The number of considered CT scenarios. Usually, this corresponds to one but in a special research mode, it is also possible to handle multiple CTs. \ No newline at end of file diff --git a/docs/datastructures/dij.rst b/docs/datastructures/dij.rst new file mode 100644 index 000000000..4277616b4 --- /dev/null +++ b/docs/datastructures/dij.rst @@ -0,0 +1,70 @@ +.. _dij: + +====================== +The dij Data Structure +====================== + +The dij struct holds pre-calculated dose influence data for inverse planning. The individual fields contain the following information. + +Starting from version 3.1.0, the dose grid has been separated from the CT grid, meaning that each can have a separate resolution depending on the application. Therefore, the two substructures ``doseGrid`` and ``ctGrid`` hold the following information for both the CT and the dose grid. + +**dij.doseGrid.resolution - dij.ctGrid.resolution** + + The resolution of an individual voxel in the dose/ct cube in [mm] in x-, y-, and z-direction (Note that the default value for the doseGrid resolution is 2.5 mm, 2.5 mm and 3 mm in x-, y- and z- directions, and is not assigned equal to the ct resolution). + +**dij.doseGrid.dimensions - dij.ctGrid.dimensions** + + The resulting dose/ct cube dimension in voxels. + +**dij.doseGrid.numOfVoxels - dij.ctGrid.numOfVoxels** + + The total number of voxels in the entire dose/ct cube. + +**dij.numOfBeams** + + Specifies the number of beams used for dose calculation and consequently inverse planning. + +**dij.numOfScenarios** + + Number of scenarios considered during dose calculation. Usually, only one scenario is used. In a special research mode, however, it is possible to compute dose on multiple 4D CT phases, considering isocenter shifts or range uncertainties. + +**dij.numOfRaysPerBeam** + + Specifies the number of rays per beam. For photons, this number also corresponds to the number of bixels per beam. For particles however, it is also possible to have multiple spot positions with different energies at the same lateral spot position. + +**dij.totalNumOfRays** + + Specifies the total number of all rays, i.e. ``dij.totalNumOfRays = sum(dij.numOfRaysPerBeam)``. + +**dij.totalNumOfBixels** + + Specifies the total number of bixels over all beams. + +**dij.bixelNum** + + Lists the bixel number in an individual beam for all columns in the precomputed influence data. Together with ``dij.rayNum`` and ``dij.beamNum`` this information facilitates an easy assignment of columns of the influence data to the :ref:`stf struct `. + +**dij.rayNum** + + Lists the ray number in an individual beam for all columns in the precomputed influence data. Together with ``dij.bixelNum`` and ``dij.beamNum`` this information facilitates an easy assignment of columns of the influence data to the :ref:`stf struct `. + +**dij.beamNum** + + Lists the beam number for all columns in the precomputed influence data. Together with ``dij.bixelNum`` and ``dij.rayNum`` this information facilitates an easy assignment of columns of the influence data to the :ref:`stf struct `. + +**dij.physicalDose** + + Pre-computed dose influence matrix with ``dij.doseGrid.numOfVoxels`` rows and ``dij.totalNumOfBixels`` columns stored within a cell array (Note that starting from Version 3.0, this matrix is built by default based on ``numOfVoxels`` on doseGrid, not the ctGrid). This matrix specifies the dose contribution from every bixel to every voxel, stored with MATLAB's built-in double precision sparse matrix format. + +**dij.mAlphaDose** + + Pre-computed alpha*dose matrix with ``dij.numOfVoxels`` rows and ``dij.totalNumOfBixels`` columns, stored with MATLAB's built-in double precision sparse matrix format within a cell array. This matrix is only computed for biological optimization, where this information is required to compute dose-averaged alpha cubes that are in turn required for three-dimensional RBE modeling. + +**dij.mSqrtBetaDose** + + Pre-computed sqrt(beta)*dose matrix with ``dij.numOfVoxels`` rows and ``dij.totalNumOfBixels`` columns, stored with MATLAB's built-in double precision sparse matrix format within a cell array. This matrix is only computed for biological optimization, where this information is required to compute dose-averaged alpha cubes that are in turn required for three-dimensional RBE modeling. + +Screenshot of the dij-struct: + +.. image:: /images/dij-struct.png + :alt: Screenshot of the dij struct diff --git a/docs/datastructures/pln.rst b/docs/datastructures/pln.rst new file mode 100644 index 000000000..05bf41138 --- /dev/null +++ b/docs/datastructures/pln.rst @@ -0,0 +1,127 @@ +.. _pln: + +====================== +The pln Data Structure +====================== + +The ``pln`` struct holds the meta information about the radiation treatment plan. + +Top-level properties stored in the ``pln`` struct +------------------------------------------------- + +Information relevant for all parts of the treatment planning workflow is stored on the top-level of the struct. +These include the radiation modality, the machine information, the number of fractions, and scenario models as well as biological models. + +**pln.radiationMode** + + Specifies the radiation modality. Can either be *photons*, *protons*, *helium* or *carbon*. + +**pln.machine** + + In order to load the appropriate base data, one can introduce the machine in which the treatment plan has been incorporated and the code will look for the file under ``{pln.radiationMode}_{pln.machine}.mat``. For example, setting ``pln.machine`` to 'Generic' for a photon treatment plan will load the already available ``photons_Generic.mat`` file. + +**pln.numOfFractions** + + Specifies the number of fractions. Note that this parameter only needs to be set for biological treatment planning for carbon ions, where the optimization process is based on the fraction dose and not based on the overall dose. + +**pln.multScen** + Specifies a scenario model for the treatment plan, see :mod:`matRad.scenarios`. + +**pln.bioModel** + Specifies a biological model for the treatment plan, see :mod:`matRad.bioModels`. + +Workflow Step Configuration properties +-------------------------------------- + +Workflow step configuration properties can be stored in the ``pln`` struct. +The Syntax for accessing these properties is ``pln.prop{StepName}.{PropertyName}``. +This results in a nested structure, where the first level is the step name and the second level is the property name. + +Current possible names are ``propStf`` (steering information / geometry), ``propDoseCalc`` (dose calculation), ``propOpt`` (optimization), and ``propSeq`` (sequencing). + + +.. admonition:: Using the ``pln.prop{StepName}.{PropertyName}`` vs direct class instantiation. + :class: note + + matRad distinguishes between a top-level API and low-level programming using the classes defining workflow steps. + Using top-level functions like :func:`matRad_calcDoseInfluence` from the root :mod:`matRad` folder will take pln as an argument, instantiate the appropriate object (e.g. :class:`matRad_ParticleHongPencilBeamEngine`), and try to configure its properties from the ``pln.prop{StepName}.{PropertyName}`` structure. + Alternatively, these classes can be directly used and properties can be explicitly set. + +Here's an overview of the property mapping: + +.. include:: ../includes/planapi.rst + +pln.propStf +^^^^^^^^^^^ + +**pln.propStf.gantryAngles** + + Specifies the gantry angles as MATLAB vector according to the `matRad coordinate system `_. + +**pln.propStf.couchAngles** + + Specifies the couch angles as MATLAB vector according to the `matRad coordinate system `_. + +**pln.propStf.bixelWidth** + + Specifies the width (and height) of quadratic photon bixels (i.e. discrete fluence elements). For particles, this parameter specifies the lateral spot distance. + +**pln.propStf.numOfBeams** + + Specifies the number of beam directions. During the matRad script, this parameter is automatically determined. + +**pln.propStf.isocenter** + + Specifies the isocenter of the treatment plan in voxel coordinates within the ct.cube. By default, the isocenter is calculated as the center of gravity of all voxels belonging to structures that have been modeled as target volume in the `cst cell `_. + +pln.propOpt +----------- + +**pln.propOpt.bioOptimization** + + Specifies the type of biological optimization. *none* corresponds to a conventional optimization based on the physical dose. *effect* corresponds to an effect based optimization according to `Wilkens & Oelfke `_. *RBExD* corresponds to an optimization of the RBE weighted dose according to `Krämer & Scholz `_. + +**pln.propOpt.runDAO** + + Setting this value to ``true`` will enable Direct Aperture Optimization run allowing us to directly optimize aperture shapes and weights. + +**pln.propOpt.runSequencing** + + Setting this value to ``true`` will enable sequencing algorithms run. + +Additional adjustable properties +-------------------------------- + +The following properties of the pln struct can additionally be adjusted. If they are not explicitly set, default values are used. The default values are handled by the `MatRad_Config class `_. + +**pln.propStf.longitudinalSpotSpacing** + + Specifies the longitudinal spot spacing. Default: *3* mm. + +**pln.propStf.addMargin** + + If this property is set to *true*, the target is expanded for beamlet finding. Default: *true*. + +**pln.propDoseCalc.doseGrid.resolution** + + Specifies the resolution for the dose calculation. Default: x direction: *3* mm, y direction: *3* mm, z direction: *3* mm. + +**pln.propDoseCalc.defaultLateralCutOff** + + Specifies the lateral cutoff. Default: *0.995* rel. + +**pln.propDoseCalc.defaultGeometricCutOff** + + Specifies the geometric cutoff. Default: *50* mm. + +**pln.propDoseCalc.ssdDensityThreshold** + + Specifies the ssd density threshold. Default: *0.05* rel. + +**pln.propOpt.defaultMaxIter** + + Specifies the number of maximum iterations. Default: *500*. + +**pln.disableGUI** + + If this value is set to *true*, matRadGUI is disabled. Default: *false*. diff --git a/docs/datastructures/result.rst b/docs/datastructures/result.rst new file mode 100644 index 000000000..2969fee51 --- /dev/null +++ b/docs/datastructures/result.rst @@ -0,0 +1,18 @@ +.. _result: + +============================ +The resultGUI Data Structure +============================ + +The optimization output is declared as ``resultGUI`` and contains at least two fields. The first field ``w`` contains the optimized weights, and the second field, ``physicalDose``, holds the physical dose cube in CT dimensions :math:`N_x \times N_y \times N_z` (:math:`N_x, N_y, N_z` = number of voxels in x-, y-, and z-direction). +Each field in the ``resultGUI`` struct can be easily accessed via MATLAB's dot-operator. For instance, ``resultGUI.w`` outputs the fluence weight vector. + +In case of performing a biological optimization using carbon ions, the ``resultGUI`` struct holds several additional cubes as fields: + +- ``resultGUI.effect`` - represents the biological effect cube +- ``resultGUI.RBExDose`` - contains the biological effective dose = RBE × physical dose +- ``resultGUI.RBE`` - holds the relative biological effectiveness cube +- ``resultGUI.alpha`` - represents the radiosensitivity parameter :math:`\alpha_\mathrm{Particle}` cube from the linear quadratic model +- ``resultGUI.beta`` - represents the radiosensitivity parameter :math:`\beta_\mathrm{Particle}` cube from the linear quadratic model + +Executing ``matRadGUI`` in MATLAB's command window will start the GUI and display all available data of ``resultGUI``. diff --git a/docs/datastructures/stf.rst b/docs/datastructures/stf.rst new file mode 100644 index 000000000..4181f1319 --- /dev/null +++ b/docs/datastructures/stf.rst @@ -0,0 +1,94 @@ +.. _stf: + +====================== +The stf Data Structure +====================== + +The stf struct holds all geometric information about the irradiation. The individual fields contain the following information. + +Screenshot of the stf structure: + +.. image:: /images/stfStructScreenshot.png + :alt: stf structure screenshot + +.. list-table:: + :header-rows: 1 + + * - **Field** + - **Description** + * - **gantry angle** + - Specification of the gantry angle for each beam in °. Range: 0° - 359° + * - **couch angle** + - Specification of the couch angle for each beam in °. Range: 0°- 359° + * - **bixel width** + - *Photons*: lateral width of each bixel. *Particles*: lateral spot spacing. + * - **radiation mode** + - Specification of the radiation mode. Options are: 'photons', 'protons' or 'carbon'. + * - **number of Rays** + - Number of rays for each beam + * - **ray** + - The :ref:`ray substructure ` contains the information about the position and orientation of each ray / bixel for a single beam. + * - **source point** + - Position of the virtual radiation source in :ref:`LPS coordinates `. + * - **number of bixels per ray** + - *Photons*: each ray is a single bixel. *Particles*: :ref:`number of bixels / dose spots along each ray `. + * - **total number of bixels** + - Total number of bixels for each beam. + +.. _ray: + +stf.ray substructure +==================== + +The *stf.ray* substructure contains the information about the position and orientation of each ray/bixel for a single beam. + +Screenshot of the stf.ray substructure: + +.. image:: /images/stfStructRayScreenshot.png + :alt: stf.ray substructure screenshot + +.. list-table:: + :header-rows: 1 + + * - **Field** + - **Code** + - **Description** + * - **ray position** *(beam's eye view)* + - ``rayPos_bev`` + - Point where the ray crosses the isocenter plane in the beam's eye view. + * - **target point** *(beam's eye view)* + - ``targetPoint_bev`` + - Target point of the ray from the beam's eye view. The target point extends the ray from source to rayPos_bev to behind the patient for use in rayTracing. + * - **ray position** *(LPS-coordinates)* + - ``rayPos`` + - rayPos_bev in the :ref:`LPS coordinate system `. + * - **target point** *(LPS-coordinates)* + - ``targetPoint`` + - targetPoint_bev in the :ref:`LPS coordinate system `. + * - :ref:`energy ` + - ``energy`` + - *Photons*: Single value (max. LINAC energy). *Particles*: Energy values of the dose spots along the ray. The number of energies corresponds to the :ref:`number of bixels per ray `. + +.. _energy: + +stf.ray.energy field +==================== + +The *stf.ray.energy* field contains all energy values for the spots along the specified ray. If the radiation mode *'Photons'* was selected, it will be set to *NaN*. + +Screenshot of the stf.ray.energy field: + +.. image:: /images/stfStructRayEnergyScreenshot.png + :alt: stf.ray.energy field screenshot + +.. _numOfBixels: + +stf.numOfBixelsPerRay field +=========================== + +The *stf.numOfBixelsPerRay* field contains the number of bixels / dose spots for every ray of the specified beam. This corresponds to the number of energy values per ray in the :ref:`stf.ray ` substructure. + +Screenshot of the stf.numOfBixelsPerRay field: + +.. image:: /images/stfStructNumOfBixelsScreenshot.png + :alt: stf.numOfBixelsPerRay field screenshot diff --git a/docs/guide/coords.rst b/docs/guide/coords.rst new file mode 100644 index 000000000..f2629bd37 --- /dev/null +++ b/docs/guide/coords.rst @@ -0,0 +1,93 @@ +.. _coords: + +============================ +The matRad Coordinate System +============================ + +.. _LPS: + +LPS Coordinates +--------------- + +matRad uses the LPS (Left, Posterior, Superior) coordinate system. + +.. image:: /images/CoordinateSystem/LPScoordinates.png + :width: 400px + +In this system, the x-axis points towards the left patient-side, the y-axis towards the posterior direction, and the z-axis towards the superior direction (see image below). + +.. image:: /images/CoordinateSystem/LPS2.png + :width: 400px + +3D points, e.g., the isocenter in the ``pln`` struct or source and target points in the ``stf`` struct, directly follow the conventions of the LPS coordinate system: The first coordinate, e.g., ``pln.isoCenter(1)`` corresponds to the x coordinate (right-left direction). +For the dose and CT cubes, which are stored as MATLAB 3D arrays, the second dimension corresponds to the x-coordinate (right-left direction), and the first dimension corresponds to the y-coordinate (anterior-posterior direction). +This permutation is due to MATLAB's standard way of displaying two-dimensional matrices with the `image `_ command, which displays the first array dimension along the vertical direction. + +.. tip:: + + In short this means that coordiantes (x/y/z) correspond to a (j,i,k) indexing. + + This is similar to the difference between MATLAB's `meshgrid `_ and `ndgrid `_ functions, where the first function uses the first dimension for the x-coordinate and the second dimension for the y-coordinate, while the second function uses the first dimension for the y-coordinate and the second dimension for the x-coordinate. + +.. _voxelCoords: + +World vs Cube System +--------------------- + +matRad differentiates between a world system and a cube system. + +Coordinates in the :ref:`ct ` struct as well as the plan isocenter, for example, are always given in world coordinates. + +Cube coordinates are defined on an image cube (e.g. the :ref:`ct.cube `). +To enable fast access, they are defined such that the coordinates of the most right, anterior, inferior voxel center in the cube has the coordinates (``resolution.x`` / ``resolution.y`` / ``resolution.z``). +Consequently, the most right, anterior, inferior corner of the most right, anterior, inferior voxel is **not** located at (0 | 0 | 0) but at (resolution.x/2 | resolution.y/2 | resolution.z/2). + +.. danger:: + + In versions prior to matRAd 3.1.0, the isocenter was given in the cube system. This was changed because during resampling of the dose grid, the isocenter often was different in the dose cubes coordinate system. + +Conversion between world coordinates, cube coordinates and voxel indices +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +matRad provides helper functions in the :mod:`geometry ` folder. + +- :func:`matRad_world2cubeCoords` converts world coordinates to cube coordinates. +- :func:`matRad_cubeCoords2worldCoords` converts cube coordinates to world coordinates. +- :func:`matRad_world2cubeIndex` converts world coordinates to voxel indices. +- :func:`matRad_cubeIndex2worldCoords` converts voxel indices to world coordinates. + +There is no function to convert cube coordinates to voxel indices, as this is a simple operation: + +.. code-block:: matlab + + %Get the indices from coordinates + coords = round(coords ./ [gridStruct.resolution.x gridStruct.resolution.y gridStruct.resolution.z]); + + %Do the permutation + indices = coords(:,[2 1 3]); + +.. _rotation: + +Gantry and Couch Rotation +------------------------- + +The rotation of the gantry (Φ) and the couch (θ) are defined as follows: + +- **Gantry:** Clockwise rotation around the z-axis +- **Couch:** Counter-clockwise rotation around the y-axis + +The rotation matrix can be obtained with :func:`matRad_getRotationMatrix`. + +Simple gantry rotation: + +.. image:: /images/CoordinateSystem/RotatingGantry.gif + :width: 400px + +Simple couch rotation: + +.. image:: /images/CoordinateSystem/RotatingCouch.gif + :width: 400px + +Simultaneous couch and gantry rotation: + +.. image:: /images/CoordinateSystem/RotatingCouch+Gantry.gif + :width: 400px diff --git a/docs/guide/dicomimport.rst b/docs/guide/dicomimport.rst new file mode 100644 index 000000000..ab9cb2a98 --- /dev/null +++ b/docs/guide/dicomimport.rst @@ -0,0 +1,77 @@ +.. _dicomimport: + +================ +The DICOM import +================ + +Disclaimer +========== + +Even though DICOM data is supposed to be highly standardized, it still features substantial variation. Consequently, it is very difficult for us to eliminate all bugs in matRad's DICOM import. As of June 2015, we have a beta version included in matRad which has been thoroughly tested using data from our center(s). We would like to remind all users to take utmost care when running matRad's DICOM import and double-check the imported CT data in the :ref:`CT struct ` and the imported structure sets in the :ref:`cst cell array `. If you do find any bugs, help us improve matRad's DICOM import and `drop us a line `_. + +Importing patient data +====================== + +1. `Preparing patient data for import <#step1>`_ +2. `Starting the import GUI <#step2>`_ +3. `Completing the import <#step3>`_ +4. `Checking the import <#step4>`_ + +.. _step1: + +Step 1: Preparing patient data for import +----------------------------------------- + +As of now, all ``*.dcm`` files need to be located in the same folder (no subfolders) for the import to work. That includes all CT images and the structure file(s). Dose series files enable you to directly import the corresponding dose distribution to your treatment plan. If you wish to include the treatment plan files and dose series files in your import, both need to be located in the same folder as well. It is possible to have DICOM data for multiple patients in this folder, as you are able to specify the patient, CT series, structure set, and treatment plan later during the import. + +.. _step2: + +Step 2: Starting the import GUI +------------------------------- + +To start the import, you first need to add the matRad folder `dicom `_ to your path. You can do so either by *right-click* → *Add to Path* → *Selected Folders and Subfolders* or by typing ``addpath(genpath('dicom'))`` in your Command Window. ``genpath`` adds the subfolder *HLUT library* to your path, too. + +Now you have access to the matRad functions designated for the DICOM import. To start the GUI you can simply type ``matRad_importDicomGUI`` in your Command Window. If you already work with matRad and have matRadGUI open, you can also start the import GUI with the *Load DICOM* button. + +You should see something like this: + +.. image:: /images/dicomImport/dicomImportGUI_empty.png + +.. _step3: + +Step 3: Completing the import +----------------------------- + +Now you can choose your patient directory. Simply click the *Browse* button in the right upper corner and navigate to the desired directory. After choosing a folder, it will directly be analyzed for different patients. + +The result will look something like this: + +.. image:: /images/dicomImport/dicomImportGUI_withPatients.png + +Now you can choose your patient, CT series, structure set, RT plan, and dose series. If there are several dose cubes referring to the same plan, you can decide to import just one, several, or all dose cubes. +In the bottom left corner, you can see the resolution of the chosen image series. You can adjust these values to import an interpolated cube with your specified resolution. Downsampling the CT makes sense in order to restrict matRad's memory usage. If your image series contains non-equally spaced slices, you have to specify a resolution. Another option is to use the resolution of the dose grid by activating the check box *Use RT Dose grid* after choosing a dose series. +Now you can click on *Import* and will see a progress bar pop-up. + +Your command window will show you the following output: + +.. image:: /images/dicomImport/dicomImportGUI_Output1.png +.. image:: /images/dicomImport/dicomImportGUI_Output2.png + +If you import steering information from a proton or carbon treatment plan, you have to specify the base data of your machine as this information is not saved in DICOM files. Therefore, you will see a pop-up window in which you can navigate to and choose the correct base data for the beam quality you use. If you import steering information from a photon treatment plan, the generic photon base data will be selected automatically. + +After the import has finished, a 'Save as'-dialogue will open to save the imported data in a ``*.mat`` file. + +.. image:: /images/dicomImport/dicomImportGUI_savePatient.png + +.. _step4: + +Step 4: Checking the import +--------------------------- + +Objectives and constraints are not imported from DICOM files but default values will be assigned to them. If any structure is marked as *'tv'*, *'target'*, *'gtv'*, *'ctv'*, *'ptv'*, *'boost'*, or *'tumor'*, its VOI type will be set to *'TARGET'*, its priority to 1, its objective to *square deviation* with a penalty of 800 and a dose of 30 Gy. All other structures will be set to organs at risk (*OAR*) with a priority of 2. +The default values for biological planning are set to ``alphaX = 0.1`` and ``betaX = 0.05`` for all structures. +After importing and saving the imported files, you should check and adapt all objectives and constraints for your application. + +If you have imported a photon treatment plan, you can now adjust the base data to your own machine by adapting the :ref:`pln struct ` machine field. + +Now you can use this patient data set just like the ones provided with matRad. diff --git a/docs/guide/gdosecalc.rst b/docs/guide/gdosecalc.rst new file mode 100644 index 000000000..0094d484f --- /dev/null +++ b/docs/guide/gdosecalc.rst @@ -0,0 +1,153 @@ +.. _dosecalc_guide: + +----------------------------------- +Dose (influence matrix) calculation +----------------------------------- + +matRad's dose influence matrix calculation algorithms are split into two parts: First we determine the irradiation geometry by generating the steering information for the desired beam setup. In a second step, we generate dosimetric information by pre-computing dose influence matrices for inverse planning. + +Steering information +-------------------- + +The steering information holds all geometric information about the irradiation within the :ref:`stf struct `. It is generated by calling :func:`matRad_generateStf`. + +The :ref:`stf struct ` uses both the LPS coordinate system and a beam's eye view coordinate system in [mm]. Coordinates in beam's eye view coordinate system are labeled accordingly; coordinates in LPS coordinate system do not have an extra label. + +Ray and bixel concept +~~~~~~~~~~~~~~~~~~~~~ + +The irradiation geometry is organized to a ray and bixel concept, which is schematically shown below. + +*Schematic visualization of the ray and bixel concept* + +.. image:: /images/rayBixelConcept.png + +From a virtual radiation source (yellow) the target volume (red) within the patient (green) is covered by equidistant rays (solid black). Note that only a two-dimensional cut through a three dimensional cone of rays is shown for clarity. In the isocenter plane (not shown) the distance of the individual rays corresponds to the bixel width (compare :ref:`pln struct `). For photons, the term bixel refers to a discrete rectangular fluence element (the limits of the individual bixels are shown in dashed black). Together, all bixels cover the entire target volume. + +For 3D IMPT for particles, we have an additional degree of freedom, namely the particle energy to be considered. This is accounted for during the stf-struct generation by determining the depth of the target volume on individual rays and placing spots (black dots) accordingly. + +More information about the ray and bixel concept (though with slight variations in nomenclature) can be found in sections 2.3 and 2.5 `Nill (2001) `_. + +Dose influence data +------------------- + +Based on the steering information, matRad can compute dose for individual photon bixels or particle ion pencil beams. The required geometric and radiological distances facilitate the same set of matRad functions. + +Dose Calculation algorithms +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Starting with Version 3.1.0, matRad switched to an object-oriented Dose Engine concept. The dose engines are stored :mod:`matRad.doseCalc.+DoseEngines`, and all dose calculation algorithms are implemented as subclasses of the abstract class :class:`DoseEngines.matRad_DoseEngineBase`. +These engines can be then used directly for configuration and dose calculation, or configured in ```pln.propDoseCalc`` to then be instantiated in the top-level functions :func:`matRad_calcDoseInfluence` or :func:`matRad_calcDoseForward` (see the page on the :ref:`pln-struct ` for more details). + +.. code-block:: matlab + + % Example of how to use the dose engine directly + doseEngine = DoseEngines.matRad_PhotonPencilBeamSVDEngine(pln); + doseEngine.enableDijSampling = false; %Configure a property of the dose engine + dij = doseEngine.calcDoseInfluence(ct,cst,stf); + + % Example for a configuration in pln.propDoseCalc + pln.propDoseCalc.engine = 'SVDPB'; %The engine is equivalent to the DoseEngines "shortName" + pln.propDoseCalc.enableDijSampling = false; %Configure the property in pln.propDoseCalc + dij = matRad_calcDoseInfluence(ct, cst, stf, pln); + +Dose Engines implement the abstract property :attr:`DoseEngines.matRad_DoseEngineBase.possibleRadiationModes` to tell their availability for the certain radiation modalities. + +Given a ``pln`` with set ``pln.radiationMode`` (and optionally ``pln.machine``), the static function :meth:`DoseEngines.matRad_DoseEngineBase.getAvailableDoseEngines` can be used to retrieve a list of available dose engines for the given radiation mode and machine. + + +External Beam Therapy +~~~~~~~~~~~~~~~~~~~~~ + +matRad supports dose calculation for photons and charged particles (protons, helium, carbon ions) in external beam therapy. + +matRad's standard algorithms are based on the pencil-beam model, which is a fast and efficient way to compute dose distributions in a patient CT. The pencil beam model assumes that the dose at a certain voxel can be computed as the product of a depth-dependent part and a lateral part, which are computed separately. The model uses a pencil-beam kernel of an infinitely narrow beam, which is convolved with the primary fluence to compute the dose distribution. The pencil beam model is a fast and efficient way to compute dose distributions in a patient CT, but it is not as accurate as Monte Carlo simulations. + +The class :class:`DoseEngines.matRad_PencilBeamEngineAbstract` implements the abstract base class for all pencil beam dose engines, providing functionality for iterating through the "rays and bixels, geometry computations for distances and transformations like rotations, as well as filling of the sparse dose influence matrix. + +Monte Carlo dose calculation algorithms subclass from :class:`DoseEngines.matRad_MonteCarloEngineAbstract`, which provides less functionality than the base class for pencil beam algorithms, since Monte Carlo algorithms are usually integrated as third-party libraries and thus need individual implementations anyway. + +Ray Tracing +^^^^^^^^^^^ + +Especially for pencil-beam algorithms, but also for other things like spot placement, matRad implements a voxel raytracing algorithm in :func:`matRad_siddonRayTracer` according to `Siddon (1985) Medical Physics `_. Dose calculation algorithms can use :func:`matRad_rayTracing` to perform a volumetric tracing to obtain an image of the water equivalent depth from a source,according to `Siggel (2012) Physica Medica `_. + +Geometric distances from the lateral distances to the central ray are computed by standard matrix vector algebra in :meth:`DoseEngines.matRad_PencilBeamEngineAbstract.calcGeoDists`. + +Photons +^^^^^^^ + +Dose Calculation Algorithm +########################## + +matRad's standard algorithm for photon dose calculation is a singular value decomposed pencil beam algorithm implemented according to `Bortfeld et al. (1993) Medical Physics `_. It is implemented in :class:`DoseEngines.matRad_PhotonPencilBeamSVDEngine`. A more detailed description of the inner workings of the algorithm can also be found in the diploma thesis of Martin Siggel "Entwicklung einer schnellen Dosisberechnung auf Basis eines Pencil-Kernel Algorithmus für die Strahlentherapie mit Photonen" (Universität Heidelberg, 2008) which is available for download `here `__. + +The dose delivered to a certain voxel *i* from bixel *j* is stored as dose influence matrix in the :ref:`dij struct ` using MATLAB's built-in double precision sparse matrix format. + +An experimental Monte Carlo dose engine based on a the reimplementation `ompMC `_ of the `EGSnrc `_ Monte Carlo code by Edgardo Doerner is available in :class:`DoseEngines.matRad_PhotonOmpMCEngine` class. It is not yet fully functional, especially the source characteristics are not fully compatible with the machine files / base data. + +Base data / Machine +################### + +The necessary measured base data, namely the kernel functions as described by `Bortfeld et al. (1993) Medical Physics `_ are supplied for a 6MV LINAC and stored in `photons_Generic.mat `_ as MATLAB piecewise polynomial. + +matRad's photon dose engine is calibrated such that a bixel intensity of all ones, i.e., ``w = ones(stf.totalNumOfBixels,1)``, yields a dose of roughly 1Gy in 5cm depth for a 5cm by 5cm field at SSD = 900mm. If you want to reproduce this, you can download a set of stf and pln structures that work with the TG119 phantom `here `_. + +Approximations +############## + +For the photon dose calculation, we assume a uniform primary fluence. While this should not have any impact on the resulting dose distributions, as we allow for intensity-modulation, this reduces the computation time because we only have to perform one convolution and we can compute the individual contributions from different bixels with the same kernel matrices (kernel1Mx, kernel2Mx, kernel3Mx). An extension to inhomogeneous primary fluences is easily possible. + +To model a virtual photon source with finite size, the primary fluence is convolved with a Gaussian filter. The size of this filter is determined by the measured penumbra at isocenter, which is related to the source size as depicted in the following images from `Martin Siggel `_ (`Siggel M. Entwicklung einer schnellen Dosisberechnung auf Basis eines Pencil-Kernel Algorithmus für die Strahlentherapie mit Photonen. Diploma Thesis 2008, Universität Heidelberg `_): + +.. image:: /images/source_penumbra.png +.. image:: /images/penumbra_gaussian_convolution.png + +matRad initially used a hardcoded value of 5mm for the measured penumbra width. From version 2.10.1 on, the value can also be stored in the photon base data file so that it can also be translated to the virtual source size for, for example, Monte Carlo simulations. + +Computational bottlenecks +######################### + +For the photon dose calculation, there are three main possibilities to speed up computations and reduce memory consumption. + +1. It is possible to reduce the spatial resolution of the dose calculation in the patient CT by downsampling the CT data upon import. If you do this as a post-processing step, be aware you need to adjust the binary segmentations in the cst cell array accordingly. +2. You can increase the variable ``pln.bixelWidth`` in the `matRad script `_ which effectively reduces the number of bixels which have to be computed approximately quadratically. +3. You can reduce the radius around the central ray, around which dose is computed by adjusting the variable ``lateralCutOff`` in ``pln.propDoseCalc``. Currently this variable is already set to 50mm. + +Particles +^^^^^^^^^ + +Dose calculation algorithm +########################## + +matRad's default conventional pencil beam model for particle dose calculations is implemented in :class:`DoseEngines.matRad_ParticleHongPencilBeamEngine` and similar to the work of `Hong et al. (1996) `_. The dose at a particular voxel is given as the product of a depth dependent part and a lateral part. For the depth dependent part, matRad uses tabulated depth dose curves for individual particle energies. For lateral beam broadening, matRad uses a depth-dependent sigma of a Gaussian profile, which is also tabulated versus depth for all available beam energies. + +The dose delivered to a certain voxel *i* from bixel *j* is stored as dose influence matrix in the :ref:`dij struct ` using MATLAB's built-in double precision sparse matrix format. + +α- and β-matrix pre-computations +################################ + +For carbon ions, matRad also enables the computation of α- and β-matrices that can be used to compute three-dimensional dose weighted α- and β-distributions, which can in turn be used to compute three-dimensional RBE distributions during inverse planning. + +matRad only models variations of α and β with depth in `matRad_calcLQParameter `_. Potential dependencies in lateral direction are not explicitly modeled. + +Base data +######### + +The base data files `protons_Generic `_ and `carbon_Generic `_ required for particle dose calculation include depth dose curves and tabulated lateral beam widths (as Gaussian sigmas) for a library of different energies. The proton base data has been computed based on an analytical approximation for the Bragg curve (`Bortfeld (1997) `_) and Highland's approximation for multiple Coulomb scattering (`Gottschalk (1993) `_). The carbon ion base data has been Monte Carlo simulated for an idealized beam line without monitoring devices. Besides this physical information, the carbon ion base data also includes α and β tables that have been computed based on LEM IV. Within the \*.mat files, the depth is stored in [mm], α tables are stored in [1/Gy], β tables are stored in [1/Gy^2], and the integrated depth dose distribution is stored in [MeV cm^2 /(g \* primary)]. Upon dose calculation, the integrated depth dose is converted to [Gy mm^2 /(1e6 primaries)] with a linear scaling in the function `matRad_calcParticleDoseBixel.m `_. Given that the lateral components of the dose calculation have the unit [1/mm^2], the weight of the pencil beams in matRad directly corresponds to the number of particles in [1e6] while the dose is given in [Gy]. + +More detailed information on the structure of a particle base data file is given :ref:`here `! + +Approximations +############## + +Besides the standard approximations made by pencil beam algorithms (i.e. factorization of lateral and depth-dependent part, ray tracing only on central ray), we do not make any approximations for particle dose calculation. + +Computational bottlenecks +######################### + +For the particle dose calculation, there are three main possibilities to speed up computations and reduce memory consumption. + +1. It is possible to reduce the spatial resolution of the dose calculation in the patient CT by downsampling the CT data upon import. If you do this as a post-processing step, be aware you need to adjust the binary segmentations in the cst cell array accordingly. +2. You can increase the variable ``pln.propStf.bixelWidth`` in the :scpt:`matRad script ` which effectively reduces the number of pencil beams which have to be computed approximately quadratically. +3. You can reduce the radius around the central ray where dose is computed by adjusting the :attr:`DoseEngines.matRad_PencilBeamEngineAbstract.dosimetricLateralCutOff` property. diff --git a/docs/guide/get.rst b/docs/guide/get.rst new file mode 100644 index 000000000..eb1c9aab0 --- /dev/null +++ b/docs/guide/get.rst @@ -0,0 +1,56 @@ +.. include:: ../includes/logo.rst + +.. _get_code: + +######################################## +Get a local copy of |matRad_logo_header| +######################################## + +To get a local copy of `matRad `_ you have two options: + +.. contents:: + :depth: 1 + :local: + +Cloning the |matRad_logo_header2| Repository +-------------------------------------------- + +We encourage everybody working with the code to get familiar with ``git``. Using the git workflow does not allow more sophisticated code development and collaboration with the |matRad_logo| development team, but also facilitates keeping a history of changes and integrating updates regularly. If you have never worked with a git repository before, you might want to have a look at the `github guides `_ first and rely on a software with a graphical user interface (such as GitHub Desktop) to manage your local |matRad_logo| copy. + +If you follow the instructions on cloning the repository, you will not only get a local copy of the |matRad_logo| source ode, but get a copy (a "clone") of the entire repository, including all its history. This is part of the decentralized philosophy of git, where each copy is its own repository with which you can work independently, and which you can later synchronize with the original repository (or others). The originial remote repository, in this case ``_ (HTTPS) or ``_ (SSH), is usually refered to as "origin" or "upstream" repository. git provides the necessary syncing capabilities as "pulling" and "pushing" from/to a remote repository. + +If you plan to contribute and/or publish your changes on GitHub, follow below instructions to create a fork. If you only plan to pull some updates from the main repository, you can skip the forking step and directly clone the repository. Nothing will be lost then: If you decide later to contribute, you can always delay creation of a fork to contribute to the main repository. + + +Forking |matRad_logo_header3| +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A fork serves as a second personal online copy of the repository within your GitHub Account. Forks are mainly used to propose changes to the original repository, or to use it as a starting point for a derived project. Forking a repository allows you to freely experiment with changes without affecting the original project, and it is a common practice in open source software development. + +.. note:: + + GitHub Forks are usually the only way to contribute to a GitHub project within normal git workflow practices, as most upstream repositories (including the |matRad_logo| repository) will not allow you to push changes directly to the main repository, and there are no other mechanisms to propose changes from other remote repositories. + +Forks can be created directly from the repository main page on GitHub. To create a fork of the |matRad_logo| repository, follow these steps: +Afterwards your fork of |matRad_logo| will be available under ``https://github.com/YOURGITHUBUSERNAME/matRad``. + +Cloning |matRad_logo_header3| +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +You can `clone `_ either the main repository or your fork of matRad. With the command line, you can clone the repository with:: + + git clone --recurse-submodules https://github.com/e0404/matRad.git matRad + +This will clone the main repository in the folder ``matRad``. If you want to clone your fork, replace ``e0404`` in the URL with your GitHub Username. The ``--recurse-submodules`` option is not necessary, but will also clone other dependencies of |matRad_logo| (like our unit testing framework). + +The repository you clone with the command above will be referred to as "origin" by git by default. You can add further remotes (like your fork) with:: + + git remote add upstream https://github.com/myaccount/matRad.git + + +Downloading the matRad.zip folder +--------------------------------- + +To get a copy of |matRad_logo| you can either download the \*.zip folder from the `matRad repository on Github `_ or use `this link `_. + +Once you downloaded the *matRad.zip* folder you can unzip the files to your desired location. diff --git a/docs/guide/guioverview.rst b/docs/guide/guioverview.rst new file mode 100644 index 000000000..4cc86f73d --- /dev/null +++ b/docs/guide/guioverview.rst @@ -0,0 +1,173 @@ +.. _guioverview: + +############################################## +Overview of the graphical user interface (GUI) +############################################## + +The GUI is used for the treatment plan visualization and the adjustment of the plan and optimization parameters. It is possible to execute the matRad script using only the GUI (see :ref:`run_gui`). + +Overview of the GUI +=================== + +The matRad GUI consists of 6 main sections: + +.. list-table:: + :header-rows: 1 + + * - Section + - Content + * - :ref:`Workflow ` + - Includes the main steps that need to be executed for treatment planning. + * - :ref:`Plan ` + - Here the plan parameters like beam direction and radiation mode can be selected. + * - Command Window + - Here the output of the Command Window is displayed inside the GUI. + * - :ref:`Objectives & constraints ` + - Using the optimization parameters the constraints of the VOIs can be adjusted. + * - :ref:`Visualization ` + - Here the visualization can be adjusted to show different planes/slices of plot types. + * - Viewing + - The information specified in the Visualization Parameters is displayed in this section. + +.. image:: /images/GUI-Guide_optimizedGUIScreenshot.png + :width: 650px + +.. _workflow: + +Workflow +======== + +In the Workflow section, the patient data is initially loaded. You can also start the :ref:`dicom import ` from here. After the adjustment of all parameters, the dose calculation and the fluence optimization can be started from here: + +.. image:: /images/GUI-Guide_workflowGUIScreenshot.png + +.. _planParameters: + +Adjustment of the plan parameters +================================= + +In this section, the plan parameters are adjusted before calculating the dose-influence-matrix. + +.. list-table:: + :header-rows: 1 + + * - Parameter + - Description + * - bixel width + - *Photons:* width of a photon bixel. *Particles:* lateral spot distance. Default value: 5 mm + * - Gantry angle + - Here the set of desired gantry angles (in degree) can be specified. For the separation of the values you can either use ',' or a space ' '. + * - Couch angle + - Here the set of desired couch angles (in degree) can be specified. For the separation of the values you can either use ',' or a space ' '. Make sure that you always have the same amount of couch and gantry angles. + * - Radiation mode + - You can choose between photons, protons and carbon ions. + * - Machine + - Actual machine name as string to identify the base data set during runtime. + * - IsoCenter + - Use this to set the isocenter (in mm). + * - # Fractions + - Here the desired number of fractions can be specified. + * - Biological optimization + - For carbon ions, you can apply a biological optimization. You can choose between an optimization of the biological effect (``effect``) or the RBE-weighted dose (``RBExD``). + * - Run Sequencing + - Check this if you want to run a MLC sequencing. The number of stratification levels can be adjusted. + * - Run Direct Aperture Optimization + - Check this if you want to run an additional direct aperture optimization. + +.. image:: /images/GUI-Guide_planParametersGUIScreenshot.png + +.. _optParameters: + +Adjustment of the optimization parameters +========================================= + +The optimization parameters regarding the volumes of interest (VOIs) are stored in the variable ``cst``. For more detailed information about the parameters stored in the cell, please refer to the :ref:`documentation of the cst-cell `. Using the GUI, you can adjust the settings. To add or delete volumes, you can use the "+" and "-" buttons. + +.. list-table:: + :header-rows: 1 + + * - Field + - Description + * - VOI name + - Via a drop-down menu, you can select a VOI by clicking its name. + * - VOI type + - You can specify whether the VOI is an organ at risk (OAR) or a target volume. + * - OP + - *Overlap*. This value defines how overlapping structures are handled during optimization. Consider two structures A and B with priorities :math:`p_A` and :math:`p_B`. If A and B both include voxel *i*, voxel *i* will be treated to belong only to structure A if :math:`p_A < p_B`. If :math:`p_A = p_B` the voxel will be considered for both structures. An extension to more than two structures is trivial. + * - Function + - *Objective Function*. This field allows you to specify how the VOI will be considered during the optimization. You can choose between *Squared Underdosing*, *Squared Overdosing*, *Squared Deviation*, *Mean Dose*, *EUD*, *Max DVH*, *Min DVH*, *DVH constraint*, *Min/Max dose constraint* or *mean dose constraint*. You can find more detailed information about this in the section 'Dose objectives' of the page: :ref:`The cst cell `. + * - p + - *Penalty*. For the objective function value, a weighted sum is calculated. The penalty value corresponds to the weighting factor for this VOI with respect to the defined constraint (e.g. overdosing). By adjusting this value, you can stress the importance of these constraints with respect to each other. + * - Parameters + - For *Squared Underdosing*, *Squared Overdosing* and *Squared Deviation* this value corresponds to the threshold dose above/below which the penalty will apply. For the *Mean Dose* option, this value is not needed, as the mean dose within this VOI will be minimized. For the *EUD* method, the parameter corresponds to the exponent. + +.. _gui-visualization: + +Visualizing treatment plans +=========================== + +After the optimization, the treatment plan can be visualized within the GUI. Using the visualization parameters, you can change the view. The radio buttons can be used to turn on or off, among others, the plotting of contours, dose (isolines), and isoline labels. + +.. image:: /images/doseVisParameter.png + +Display options +--------------- + +.. list-table:: + :header-rows: 1 + + * - Type of plot + - Display option + - Plane + - Resulting image + * - **intensity** + - **Dose** + - axial + - .. image:: /images/doseVisAxialIntensity.png + * - + - + - sagittal + - .. image:: /images/doseVisSagitalIntensity.png + * - + - + - coronal + - .. image:: /images/doseVisCoronalIntensity.png + * - + - **effect** + - axial + - .. image:: /images/doseVisAxialEffect.png + * - + - **RBEWeightedDose** + - axial + - .. image:: /images/doseVisAxialRBExD.png + * - + - **RBE** + - axial + - .. image:: /images/doseVisAxialRBE.png + * - + - **alpha** + - axial + - .. image:: /images/doseVisAxialAlpha.png + * - + - **beta** + - axial + - .. image:: /images/doseVisAxialBeta.png + * - + - **RBETruncated10Perc** + - axial + - .. image:: /images/doseVisAxialRBEtruncated.png + * - **profile** + - + - **lateral** + - .. image:: /images/doseVisLateralProfile.png + * - + - + - **longitudinal** + - .. image:: /images/doseVisLongitudinalProfile.png + +DVH +--- + +To draw a DVH of the current treatment plan and display some quality indicators you can click the *Show DVH/QI* button: + +.. image:: /images/DVHVisScreenshot.png diff --git a/docs/guide/index.rst b/docs/guide/index.rst new file mode 100644 index 000000000..23eceee44 --- /dev/null +++ b/docs/guide/index.rst @@ -0,0 +1,71 @@ +.. |matRad_logo| image:: ../../matRad/gfx/matRad_logo.png + :width: 80 px + :alt: matRad + :target: https://www.matRad.org + +.. _techdoc: + +=============== +Technical Guide +=============== + +|matRad_logo| features a very modular and sequential design which is reflected in the matRad script. +After importing your own data or loading one of the provided cases, you can start working with matRad dose calculation and optimization modules. +The four main parts of the matRad workflow are + +.. image:: /images/matRad_steps.png + +Information about the individual modules is given in the following sections: + +.. toctree:: + :maxdepth: 2 + :hidden: + + plan + gdosecalc + planopt + visualization + +Global configuration with :ref:`MatRad_Config ` + +:ref:`Set treatment plan parameters ` + +:ref:`Dose influence matrix calculation ` + +:ref:`Fluence optimization ` (potentiall followed by sequencing) + +:ref:`Visualization ` + +Important variables and data structures +--------------------------------------- + +.. toctree:: + :maxdepth: 1 + :glob: + + ../datastructures/* + ../datastructures/basedata/* + +Additional information +---------------------- + +.. toctree:: + :maxdepth: 2 + :hidden: + + get + guioverview + coords + dicomimport + octave + + +:ref:`How to run matRad with Octave ` + +:ref:`matRad coordinate system ` + +:ref:`The CORT dataset ` + +:ref:`DICOM import ` + +:ref:`Minimum system requirements ` diff --git a/docs/guide/octave.rst b/docs/guide/octave.rst new file mode 100644 index 000000000..ce596529a --- /dev/null +++ b/docs/guide/octave.rst @@ -0,0 +1,34 @@ +.. _octave: + +============================= +How to run matRad with Octave +============================= + +We try to keep matRad's core compatible with `GNU Octave `_ to allow users to run matRad without a MATLAB license. Even the graphical user interface (GUI) can, with limitations, be used. To maintain compatibility, nearly all tests are currently run on GNU Octave 6 in addition to MATLAB. + +One difficulty with running matRad in GNU Octave is the optimization module relying on `Ipopt `_ with a MEX interface. + +While we provide some GNU Octave mex files for a few Windows versions, the IPOPT mex files usually needs to be compiled from source with Octave. The steps to compile the Ipopt interface with Octave are presented below. + +Installing GNU Octave +--------------------- + +It is recommended to use matRad with the latest stable release of `GNU Octave `_. matRad has been mainly tested with GNU Octave versions 6 in Linux, but also newer versions seem to work. When installing Octave from the package manager of a Linux distribution, it is necessary to also install the development files to provide the command ``mkoctfile`` required to build the interface to Ipopt. + +By default, Octave is distributed with 32-bit indexing on Linux, while for Windows also a 64bit indexing installer is available. GNU Octave can also be compiled from source with 64-bit indexing. However, building Octave with 64-bit indexing is rather laborious requiring the compilation of several external libraries to enable 64-bit indexing. `Click here `_ for further details on how to compile GNU Octave with 64-bit indexing. Notice that the 64-bit indexing option of Octave is still experimental and that special care should be taken when linking the libraries to avoid segmentation faults. + +While matRad can be used with Octave compiled either with 32-bit or 64-bit indexing, some clinically relevant treatment planning scenarios may require an influence matrix, which exceeds the maximum array size available with 32-bit indexing. + +IPOPT +----- + +Since version 3.1.0, the `IPOPT folder `_ in matRad contains precompiled mex files for certain Octave versions in Windows. While in MATLAB, mex files have an extension naming scheme depending on the operating system and are often compatible across multiple MATLAB versions, we have oberserved that this is not true for GNU Octave, where a mex file just always uses "mex" as extension. Thus matRad uses a dedicated naming scheme to store Octave mex files as "mexoct" + version + os + arch. For Octave 6.4.0 on Windows, for example, the filename would be ipopt.mexoct640w64. When running IPOPT from Octave, matRad will copy this file to ipopt.mex to call it. + +The MATLAB interface of Ipopt distributed with matRad is linked against a MATLAB-specific library for the linear solver MA57. An equivalent interface of Ipopt for GNU Octave should be compiled from source. It requires the installation of the `Ipopt `_ library along with the corresponding header files. Since the linear solvers are not distributed along with Ipopt, they should be obtained from third-party software. Different linear solvers libraries can be used including `HSL_MA57 `_, `MUMPS `_ and others. In addition to compiling Ipopt and the linear solvers from source, Ipopt linked to MUMPS is typically available from the package manager of the Linux distributions. Ipopt version 3.12.8 linked with the linear solvers HSL_MA57 and MUMPS has been successfully applied in matRad with GNU Octave. + +Compiling Octave interface of Ipopt +----------------------------------- + +The source code of Ipopt provides the source files to compile the original MATLAB interface of Ipopt distributed with matRad. In order to compile the equivalent interface for GNU Octave, it is recommended to use the `interface rewritten by Enrico Bertolazzi `_. The rewritten interface requires GNU Octave version 4.2 or above. The Octave interface of Ipopt can be compiled using the ``mkoctfile`` command with the option to produce MEX files. + +Since version 3.1.0, matRad provides scripts to help with compilation of the Ipopt interface for GNU Octave. The scripts target Windows platforms and can be executed from within the MinGW environment installed with GNU Octave by using the ``cmdshell.bat`` in Octave's root folder. These scripts are located in the `thirdParty/IPOPT` folder and can also be easily adapted by linux users. \ No newline at end of file diff --git a/docs/guide/plan.rst b/docs/guide/plan.rst new file mode 100644 index 000000000..d2e3dc00b --- /dev/null +++ b/docs/guide/plan.rst @@ -0,0 +1,26 @@ +.. _plan: + +=========================== +How to configure your plan? +=========================== + +Before you can start with dose calculation and inverse planning in matRad you need to set a couple of general treatment plan parameters. + +Within the :scpt:`main matRad script `, this corresponds to the first cell. Within the GUI, these settings can be adjusted interactively. + +.. literalinclude:: ../../matRad.m + :lines: 18-53 + :lineno-match: + :language: matlab + +After import of patient data in matRad's native format, the desired settings are specified in the :ref:`pln struct `. + +Note that besides some basic parameters (like number of fractions or the machine), there are workflow-specific property structures in ``pln.prop*``. These structures contain the parameters for the different workflows, such as the dose influence matrix calculation, optimization, and sequencing. matRad will instatiate the appropriate algorithm based on these configurations and try to override their properties. + +The :scpt:`matRad.m` script uses the :mod:`top-level API `. The top-level functions are designed to take the main data structures as input and configure the corresponding workflow step via the ``pln`` using the attribute dictionaries ``pln.prop*``: + +.. include:: ../includes/planapi.rst + +Please see the corresponding page about the :ref:`pln struct ` for further information. + +Note that matRad comes with sample patient data from the `CORT dataset: common optimization for radiation therapy `_ so you can directly play around with all functionalities. More information can be found on the :ref:`dedicated CORT dataset section `. \ No newline at end of file diff --git a/docs/guide/planopt.rst b/docs/guide/planopt.rst new file mode 100644 index 000000000..fdb85ba7f --- /dev/null +++ b/docs/guide/planopt.rst @@ -0,0 +1,44 @@ +.. _plan_opt: + +==================== +Fluence Optimization +==================== + +The goal of the fluence optimization is to find a set of bixel/spot weights that yield the best possible dose distribution according to the clinical objectives and constraints underlying the radiation treatment. + +For mathematical optimization, these clinical objectives and constraints have to be translated into mathematical objectives and constraints. matRad supports the mathematical optimization of a weighted sum of objectives to help finding an optimal trade-off between adequate target coverage and normal tissue sparing for an individual patient as well as the formulation of constraints. The individual objectives and constraints are defined per structure, can be chosen by the user. + +The overall fluence optimization process is coordinated by the top-level function :func:`matRad_fluenceOptimization`. + +Since Version 2.10.0 +-------------------- + +*Since version 2.10.0 objectives and constraints are implemented with an object oriented approach. For the old format, see further down below.* + +At the moment, matRad allows for objectives and constraints based on dose. Each objective and constraint is defined in a class derived from :class:`matRad_DoseOptimizationFunction`. Objectives and Constraints are distinguished by the abstract subclasses :class:`DoseObjectives.matRad_DoseObjective` & :class:`DoseConstraints.matRad_DoseConstraint` within the respective package folders. This enables the easy implementation of new (dose-based) constraints & objectives by writing your own class, inheriting from those and implementing the therein declared interface, i.e. the parameter definition, and respective objective/constraint function and its gradient/jacobian. +New objectives/constraints are then automatically recognized also in the GUI. + +Currently, the available objectives are found in the :mod:`matRad.optimization.+DoseObjectives` package and the available constraints in the :mod:`matRad.optimization.+DoseConstraints` package. The available objectives and constraints are listed in the following tables: + +.. include:: ../includes/objtable.rst + +.. include:: ../includes/constrtable.rst + +This is extended to the implementation of optimization problems and :mod:`optimizers `. An optimization problem :class:`matRad_OptimizationProblem` combines the single objectives into an objective function and organizes the constraint structure to give a standard optimization problem to the optimizer. matRad implements optimizers as derived classes of :class:`matRad_Optimizer` and defaults to the `IPOPT `_ package for large scale non-linear optimization which is included via a MEX file and interfaced in :class:`matRad_OptimizerIPOPT`. MATLAB's proprietary fmincon support is added in :class:`matRad_OptimizerFmincon`, other optimizers may be added as well. So far, only non-linear constrained optimization is supported by :class:`matRad_OptimizationProblem` and for optimizers. +Optimizers can be changed by setting ``pln.propOpt.optimizer``. +The :class:`matRad_OptimizationProblem` class also enables to implement advanced planning problems as subclasses, like direct aperture optimization as implemented in :class:`matRad_OptimizationProblemDAO`. + +Before Version 2.10.0 +--------------------- + +The objectives and constraints are stored as a :ref:`dose objective struct ` within the :ref:`cst cell `. The objectives and constraints can be set including all necessary parameters via the :ref:`matRad GUI `. All functions involved in the optimization process are located in a subfolder called "optimization" within the matRad root folder. matRad relies on the `IPOPT `_ package for large scale non-linear optimization which is included via a MEX file. IPOPT requires call back functions for objective function, gradient, constraint, and Jacobian evaluation. We use the wrapper functions ``matRad_objFuncWrapper.m``, ``matRad_gradFuncWrapper.m``, ``matRad_constFuncWrapper.m``, and ``matRad_jacobFuncWrapper.m`` to coordinate the evaluation of all defined objectives and constraints. + +Optimization based on dose, effect, and RBE weighted dose +--------------------------------------------------------- + +All optimization functionalities work equally for optimization processes based on physical dose as well as biological effect `Wilkens & Oelfke (2006) `_ and RBE-weighted dose according to `Krämer & Scholz (2006) `_. The biological effect and the RBE-weighted dose are calculated with α and β base data that has been calculated according to the local effect model IV. α and β tables are available as part of the base data set `carbon_Generic `_ which is provided with the matRad release. + +Direct aperture optimization +---------------------------- + +For photons, matRad also features an experimental direct aperture optimization that largely follows the implementation described in `Wild et al. (2015) `_ which is based on `Bzdusek et al. (2009) `_ and (with some modification) `Unkelbach & Cassioli (2012) `_. diff --git a/docs/guide/visualization.rst b/docs/guide/visualization.rst new file mode 100644 index 000000000..30c96ec41 --- /dev/null +++ b/docs/guide/visualization.rst @@ -0,0 +1,30 @@ +.. _visualization: + +============= +Visualization +============= + +Graphical User Interface +------------------------ + +At any point, it is possible to start the graphical user interface by entering + +.. code-block:: matlab + + matRadGUI + +in the command window. The GUI will automatically check your base workspace and recognizes your current planning step. Data will be displayed, according to the detected planning step. Please note that a dose cube will be displayed not until you have finished an optimization. + +It is possible to adjust the plot to your needs in the lower left area of the GUI. For instance, it is possible to switch to a longitudinal profile plot of the central beam axis. + +Plotting tools +-------------- + +There's a number of tools to visualize results from the command line or per script. + +* The top-level function :func:`matRad_planAnalysis` provides a convenient way to visualize the dose distribution and DVHs of a plan. It can be used in the command line or in scripts, and it automatically detects the current planning step and displays the relevant data. +* :class:`matRad_ViewingWidget` only opens the Viewer part of the GUI for quick visualization. +* :func:`matRad_plotSlice` can be used to plot a single slice of the dose distribution with underlying CT and many configuration options. +* :func:`matRad_showDVH` can be used to plot DVHs of a plan. + + diff --git a/docs/images/CoordinateSystem/LPS2.png b/docs/images/CoordinateSystem/LPS2.png new file mode 100644 index 000000000..f821cb6cc Binary files /dev/null and b/docs/images/CoordinateSystem/LPS2.png differ diff --git a/docs/images/CoordinateSystem/LPScoordinates.png b/docs/images/CoordinateSystem/LPScoordinates.png new file mode 100644 index 000000000..06b01244d Binary files /dev/null and b/docs/images/CoordinateSystem/LPScoordinates.png differ diff --git a/docs/images/CoordinateSystem/RotatingCouch+Gantry.gif b/docs/images/CoordinateSystem/RotatingCouch+Gantry.gif new file mode 100644 index 000000000..ba484d0e7 Binary files /dev/null and b/docs/images/CoordinateSystem/RotatingCouch+Gantry.gif differ diff --git a/docs/images/CoordinateSystem/RotatingCouch.gif b/docs/images/CoordinateSystem/RotatingCouch.gif new file mode 100644 index 000000000..b8ceab39d Binary files /dev/null and b/docs/images/CoordinateSystem/RotatingCouch.gif differ diff --git a/docs/images/CoordinateSystem/RotatingGantry.gif b/docs/images/CoordinateSystem/RotatingGantry.gif new file mode 100644 index 000000000..1ffd3e467 Binary files /dev/null and b/docs/images/CoordinateSystem/RotatingGantry.gif differ diff --git a/docs/images/CoordinateSystem/RotatingGantry.webm b/docs/images/CoordinateSystem/RotatingGantry.webm new file mode 100644 index 000000000..560a36bbe Binary files /dev/null and b/docs/images/CoordinateSystem/RotatingGantry.webm differ diff --git a/docs/images/DKFZ_Logo1.png b/docs/images/DKFZ_Logo1.png new file mode 100644 index 000000000..5ce13e042 Binary files /dev/null and b/docs/images/DKFZ_Logo1.png differ diff --git a/docs/images/DVHScreenshot.png b/docs/images/DVHScreenshot.png new file mode 100644 index 000000000..d2fd14cde Binary files /dev/null and b/docs/images/DVHScreenshot.png differ diff --git a/docs/images/DVHVisScreenshot.png b/docs/images/DVHVisScreenshot.png new file mode 100644 index 000000000..2fe0280c5 Binary files /dev/null and b/docs/images/DVHVisScreenshot.png differ diff --git a/docs/images/GUI-Guide_dijOutputScreenshot.png b/docs/images/GUI-Guide_dijOutputScreenshot.png new file mode 100644 index 000000000..20445f8d1 Binary files /dev/null and b/docs/images/GUI-Guide_dijOutputScreenshot.png differ diff --git a/docs/images/GUI-Guide_dijProgressBarScreenshot.png b/docs/images/GUI-Guide_dijProgressBarScreenshot.png new file mode 100644 index 000000000..37ee4ad68 Binary files /dev/null and b/docs/images/GUI-Guide_dijProgressBarScreenshot.png differ diff --git a/docs/images/GUI-Guide_emptyGUIScreenshot.png b/docs/images/GUI-Guide_emptyGUIScreenshot.png new file mode 100644 index 000000000..03ee40770 Binary files /dev/null and b/docs/images/GUI-Guide_emptyGUIScreenshot.png differ diff --git a/docs/images/GUI-Guide_fluenceOptOutputScreenshot.png b/docs/images/GUI-Guide_fluenceOptOutputScreenshot.png new file mode 100644 index 000000000..48eb0f894 Binary files /dev/null and b/docs/images/GUI-Guide_fluenceOptOutputScreenshot.png differ diff --git a/docs/images/GUI-Guide_fluenceOptOutputScreenshot2.PNG b/docs/images/GUI-Guide_fluenceOptOutputScreenshot2.PNG new file mode 100644 index 000000000..b59d2bf58 Binary files /dev/null and b/docs/images/GUI-Guide_fluenceOptOutputScreenshot2.PNG differ diff --git a/docs/images/GUI-Guide_loadDataGUIScreenshot.png b/docs/images/GUI-Guide_loadDataGUIScreenshot.png new file mode 100644 index 000000000..b215449ea Binary files /dev/null and b/docs/images/GUI-Guide_loadDataGUIScreenshot.png differ diff --git a/docs/images/GUI-Guide_loadedGUIScreenshot.png b/docs/images/GUI-Guide_loadedGUIScreenshot.png new file mode 100644 index 000000000..d7b79205a Binary files /dev/null and b/docs/images/GUI-Guide_loadedGUIScreenshot.png differ diff --git a/docs/images/GUI-Guide_loadedGUIScreenshot_liver.PNG b/docs/images/GUI-Guide_loadedGUIScreenshot_liver.PNG new file mode 100644 index 000000000..e0280562e Binary files /dev/null and b/docs/images/GUI-Guide_loadedGUIScreenshot_liver.PNG differ diff --git a/docs/images/GUI-Guide_optimizationParameters2.png b/docs/images/GUI-Guide_optimizationParameters2.png new file mode 100644 index 000000000..80499f0e0 Binary files /dev/null and b/docs/images/GUI-Guide_optimizationParameters2.png differ diff --git a/docs/images/GUI-Guide_optimizationParametersGUIScreenshot.png b/docs/images/GUI-Guide_optimizationParametersGUIScreenshot.png new file mode 100644 index 000000000..22f03e648 Binary files /dev/null and b/docs/images/GUI-Guide_optimizationParametersGUIScreenshot.png differ diff --git a/docs/images/GUI-Guide_optimizedGUIScreenshot.png b/docs/images/GUI-Guide_optimizedGUIScreenshot.png new file mode 100644 index 000000000..fdcc6e23b Binary files /dev/null and b/docs/images/GUI-Guide_optimizedGUIScreenshot.png differ diff --git a/docs/images/GUI-Guide_planParametersGUIScreenshot.png b/docs/images/GUI-Guide_planParametersGUIScreenshot.png new file mode 100644 index 000000000..47f817c3e Binary files /dev/null and b/docs/images/GUI-Guide_planParametersGUIScreenshot.png differ diff --git a/docs/images/GUI-Guide_workflowGUIScreenshot.png b/docs/images/GUI-Guide_workflowGUIScreenshot.png new file mode 100644 index 000000000..9c457d60a Binary files /dev/null and b/docs/images/GUI-Guide_workflowGUIScreenshot.png differ diff --git a/docs/images/Icon-set-Notifications.png b/docs/images/Icon-set-Notifications.png new file mode 100644 index 000000000..359100527 Binary files /dev/null and b/docs/images/Icon-set-Notifications.png differ diff --git a/docs/images/Icon-set-setup.png b/docs/images/Icon-set-setup.png new file mode 100644 index 000000000..fdacdada3 Binary files /dev/null and b/docs/images/Icon-set-setup.png differ diff --git a/docs/images/PracticalTreatmentPlanning_01.png b/docs/images/PracticalTreatmentPlanning_01.png new file mode 100644 index 000000000..42a57e33f Binary files /dev/null and b/docs/images/PracticalTreatmentPlanning_01.png differ diff --git a/docs/images/PracticalTreatmentPlanning_02.png b/docs/images/PracticalTreatmentPlanning_02.png new file mode 100644 index 000000000..2ee973af6 Binary files /dev/null and b/docs/images/PracticalTreatmentPlanning_02.png differ diff --git a/docs/images/PracticalTreatmentPlanning_03.png b/docs/images/PracticalTreatmentPlanning_03.png new file mode 100644 index 000000000..84db32113 Binary files /dev/null and b/docs/images/PracticalTreatmentPlanning_03.png differ diff --git a/docs/images/PracticalTreatmentPlanning_04.png b/docs/images/PracticalTreatmentPlanning_04.png new file mode 100644 index 000000000..379721720 Binary files /dev/null and b/docs/images/PracticalTreatmentPlanning_04.png differ diff --git a/docs/images/PracticalTreatmentPlanning_05.png b/docs/images/PracticalTreatmentPlanning_05.png new file mode 100644 index 000000000..e4b1aa6b7 Binary files /dev/null and b/docs/images/PracticalTreatmentPlanning_05.png differ diff --git a/docs/images/PracticalTreatmentPlanning_06.png b/docs/images/PracticalTreatmentPlanning_06.png new file mode 100644 index 000000000..960e0ca30 Binary files /dev/null and b/docs/images/PracticalTreatmentPlanning_06.png differ diff --git a/docs/images/PracticalTreatmentPlanning_07.png b/docs/images/PracticalTreatmentPlanning_07.png new file mode 100644 index 000000000..fdf5cb47d Binary files /dev/null and b/docs/images/PracticalTreatmentPlanning_07.png differ diff --git a/docs/images/PracticalTreatmentPlanning_08.png b/docs/images/PracticalTreatmentPlanning_08.png new file mode 100644 index 000000000..fbb09b91e Binary files /dev/null and b/docs/images/PracticalTreatmentPlanning_08.png differ diff --git a/docs/images/PracticalTreatmentPlanning_09.png b/docs/images/PracticalTreatmentPlanning_09.png new file mode 100644 index 000000000..214154a6d Binary files /dev/null and b/docs/images/PracticalTreatmentPlanning_09.png differ diff --git a/docs/images/PracticalTreatmentPlanning_10.png b/docs/images/PracticalTreatmentPlanning_10.png new file mode 100644 index 000000000..4d43a07ba Binary files /dev/null and b/docs/images/PracticalTreatmentPlanning_10.png differ diff --git a/docs/images/PracticalTreatmentPlanning_11.png b/docs/images/PracticalTreatmentPlanning_11.png new file mode 100644 index 000000000..c199f87c6 Binary files /dev/null and b/docs/images/PracticalTreatmentPlanning_11.png differ diff --git a/docs/images/PracticalTreatmentPlanning_12.png b/docs/images/PracticalTreatmentPlanning_12.png new file mode 100644 index 000000000..ac8c87bac Binary files /dev/null and b/docs/images/PracticalTreatmentPlanning_12.png differ diff --git a/docs/images/PracticalTreatmentPlanning_13.png b/docs/images/PracticalTreatmentPlanning_13.png new file mode 100644 index 000000000..97e4ec277 Binary files /dev/null and b/docs/images/PracticalTreatmentPlanning_13.png differ diff --git a/docs/images/PracticalTreatmentPlanning_14.png b/docs/images/PracticalTreatmentPlanning_14.png new file mode 100644 index 000000000..234b5ab5f Binary files /dev/null and b/docs/images/PracticalTreatmentPlanning_14.png differ diff --git a/docs/images/STFScreenshot.png b/docs/images/STFScreenshot.png new file mode 100644 index 000000000..a61f8d20b Binary files /dev/null and b/docs/images/STFScreenshot.png differ diff --git a/docs/images/aboutIcon.svg b/docs/images/aboutIcon.svg new file mode 100644 index 000000000..7eab82e28 --- /dev/null +++ b/docs/images/aboutIcon.svg @@ -0,0 +1,85 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + i + + + diff --git a/docs/images/aboutIconOrig.png b/docs/images/aboutIconOrig.png new file mode 100644 index 000000000..92f995feb Binary files /dev/null and b/docs/images/aboutIconOrig.png differ diff --git a/docs/images/aboutIconThick1.png b/docs/images/aboutIconThick1.png new file mode 100644 index 000000000..9c7c5cece Binary files /dev/null and b/docs/images/aboutIconThick1.png differ diff --git a/docs/images/aboutIconThick2(klein).png b/docs/images/aboutIconThick2(klein).png new file mode 100644 index 000000000..22ee6876c Binary files /dev/null and b/docs/images/aboutIconThick2(klein).png differ diff --git a/docs/images/aboutIconThick2.png b/docs/images/aboutIconThick2.png new file mode 100644 index 000000000..f7b562a93 Binary files /dev/null and b/docs/images/aboutIconThick2.png differ diff --git a/docs/images/calcSTFScreenshot.png b/docs/images/calcSTFScreenshot.png new file mode 100644 index 000000000..bb64183f5 Binary files /dev/null and b/docs/images/calcSTFScreenshot.png differ diff --git a/docs/images/cstCellDoseParametersScreenshot.png b/docs/images/cstCellDoseParametersScreenshot.png new file mode 100644 index 000000000..81c0ba2a5 Binary files /dev/null and b/docs/images/cstCellDoseParametersScreenshot.png differ diff --git a/docs/images/cstCellScreenshot.png b/docs/images/cstCellScreenshot.png new file mode 100644 index 000000000..8de360c2f Binary files /dev/null and b/docs/images/cstCellScreenshot.png differ diff --git a/docs/images/cstCellTissueParametersScreenshot.png b/docs/images/cstCellTissueParametersScreenshot.png new file mode 100644 index 000000000..a1177319b Binary files /dev/null and b/docs/images/cstCellTissueParametersScreenshot.png differ diff --git a/docs/images/ctDataScreenshot.png b/docs/images/ctDataScreenshot.png new file mode 100644 index 000000000..d6739f8d3 Binary files /dev/null and b/docs/images/ctDataScreenshot.png differ diff --git a/docs/images/daoScreenshot.png b/docs/images/daoScreenshot.png new file mode 100644 index 000000000..ec1205efa Binary files /dev/null and b/docs/images/daoScreenshot.png differ diff --git a/docs/images/dicomImport/dicomImportGUI_Output1.png b/docs/images/dicomImport/dicomImportGUI_Output1.png new file mode 100644 index 000000000..b4b53b5c6 Binary files /dev/null and b/docs/images/dicomImport/dicomImportGUI_Output1.png differ diff --git a/docs/images/dicomImport/dicomImportGUI_Output2.png b/docs/images/dicomImport/dicomImportGUI_Output2.png new file mode 100644 index 000000000..9a4b8d2c6 Binary files /dev/null and b/docs/images/dicomImport/dicomImportGUI_Output2.png differ diff --git a/docs/images/dicomImport/dicomImportGUI_empty.png b/docs/images/dicomImport/dicomImportGUI_empty.png new file mode 100644 index 000000000..c995b0ad6 Binary files /dev/null and b/docs/images/dicomImport/dicomImportGUI_empty.png differ diff --git a/docs/images/dicomImport/dicomImportGUI_savePatient.png b/docs/images/dicomImport/dicomImportGUI_savePatient.png new file mode 100644 index 000000000..562af6ac1 Binary files /dev/null and b/docs/images/dicomImport/dicomImportGUI_savePatient.png differ diff --git a/docs/images/dicomImport/dicomImportGUI_waitbar.png b/docs/images/dicomImport/dicomImportGUI_waitbar.png new file mode 100644 index 000000000..423203e90 Binary files /dev/null and b/docs/images/dicomImport/dicomImportGUI_waitbar.png differ diff --git a/docs/images/dicomImport/dicomImportGUI_withPatients.png b/docs/images/dicomImport/dicomImportGUI_withPatients.png new file mode 100644 index 000000000..0f68e8256 Binary files /dev/null and b/docs/images/dicomImport/dicomImportGUI_withPatients.png differ diff --git a/docs/images/dij-struct.png b/docs/images/dij-struct.png new file mode 100644 index 000000000..74bad05df Binary files /dev/null and b/docs/images/dij-struct.png differ diff --git a/docs/images/doseCalcProgScreenshot.png b/docs/images/doseCalcProgScreenshot.png new file mode 100644 index 000000000..e96411c02 Binary files /dev/null and b/docs/images/doseCalcProgScreenshot.png differ diff --git a/docs/images/doseCalcScreenshot.png b/docs/images/doseCalcScreenshot.png new file mode 100644 index 000000000..84ddb39cf Binary files /dev/null and b/docs/images/doseCalcScreenshot.png differ diff --git a/docs/images/doseVisAxialAlpha.png b/docs/images/doseVisAxialAlpha.png new file mode 100644 index 000000000..498ba0403 Binary files /dev/null and b/docs/images/doseVisAxialAlpha.png differ diff --git a/docs/images/doseVisAxialBeta.png b/docs/images/doseVisAxialBeta.png new file mode 100644 index 000000000..d3de6a963 Binary files /dev/null and b/docs/images/doseVisAxialBeta.png differ diff --git a/docs/images/doseVisAxialEffect.png b/docs/images/doseVisAxialEffect.png new file mode 100644 index 000000000..ac5668db2 Binary files /dev/null and b/docs/images/doseVisAxialEffect.png differ diff --git a/docs/images/doseVisAxialIntensity.png b/docs/images/doseVisAxialIntensity.png new file mode 100644 index 000000000..2056f26ef Binary files /dev/null and b/docs/images/doseVisAxialIntensity.png differ diff --git a/docs/images/doseVisAxialRBE.png b/docs/images/doseVisAxialRBE.png new file mode 100644 index 000000000..f51b83b33 Binary files /dev/null and b/docs/images/doseVisAxialRBE.png differ diff --git a/docs/images/doseVisAxialRBEtruncated.png b/docs/images/doseVisAxialRBEtruncated.png new file mode 100644 index 000000000..8e8b59e70 Binary files /dev/null and b/docs/images/doseVisAxialRBEtruncated.png differ diff --git a/docs/images/doseVisAxialRBExD.png b/docs/images/doseVisAxialRBExD.png new file mode 100644 index 000000000..667dfe52f Binary files /dev/null and b/docs/images/doseVisAxialRBExD.png differ diff --git a/docs/images/doseVisCoronalIntensity.png b/docs/images/doseVisCoronalIntensity.png new file mode 100644 index 000000000..c2dec16f9 Binary files /dev/null and b/docs/images/doseVisCoronalIntensity.png differ diff --git a/docs/images/doseVisGUIScreenshot.png b/docs/images/doseVisGUIScreenshot.png new file mode 100644 index 000000000..1bdbda978 Binary files /dev/null and b/docs/images/doseVisGUIScreenshot.png differ diff --git a/docs/images/doseVisLateralProfile.png b/docs/images/doseVisLateralProfile.png new file mode 100644 index 000000000..1c0bc4f9e Binary files /dev/null and b/docs/images/doseVisLateralProfile.png differ diff --git a/docs/images/doseVisLongitudinalProfile.png b/docs/images/doseVisLongitudinalProfile.png new file mode 100644 index 000000000..49f369847 Binary files /dev/null and b/docs/images/doseVisLongitudinalProfile.png differ diff --git a/docs/images/doseVisParameter.png b/docs/images/doseVisParameter.png new file mode 100644 index 000000000..48ae636e7 Binary files /dev/null and b/docs/images/doseVisParameter.png differ diff --git a/docs/images/doseVisSagitalIntensity.png b/docs/images/doseVisSagitalIntensity.png new file mode 100644 index 000000000..d1a9dcdf4 Binary files /dev/null and b/docs/images/doseVisSagitalIntensity.png differ diff --git a/docs/images/doseVisScreenshot.png b/docs/images/doseVisScreenshot.png new file mode 100644 index 000000000..68820b63e Binary files /dev/null and b/docs/images/doseVisScreenshot.png differ diff --git a/docs/images/doseVisScreenshot2.png b/docs/images/doseVisScreenshot2.png new file mode 100644 index 000000000..32e852de0 Binary files /dev/null and b/docs/images/doseVisScreenshot2.png differ diff --git a/docs/images/doseVisSequencing.png b/docs/images/doseVisSequencing.png new file mode 100644 index 000000000..ff587e586 Binary files /dev/null and b/docs/images/doseVisSequencing.png differ diff --git a/docs/images/executeGUIScreenshot.png b/docs/images/executeGUIScreenshot.png new file mode 100644 index 000000000..38198da4f Binary files /dev/null and b/docs/images/executeGUIScreenshot.png differ diff --git a/docs/images/initialGUIScreenshot.png b/docs/images/initialGUIScreenshot.png new file mode 100644 index 000000000..65aad07a1 Binary files /dev/null and b/docs/images/initialGUIScreenshot.png differ diff --git a/docs/images/invPlanningProgScreenshot.png b/docs/images/invPlanningProgScreenshot.png new file mode 100644 index 000000000..193b2de71 Binary files /dev/null and b/docs/images/invPlanningProgScreenshot.png differ diff --git a/docs/images/invPlanningScreenshot.png b/docs/images/invPlanningScreenshot.png new file mode 100644 index 000000000..ae2ac663c Binary files /dev/null and b/docs/images/invPlanningScreenshot.png differ diff --git a/docs/images/invPlanningVisScreenshot.png b/docs/images/invPlanningVisScreenshot.png new file mode 100644 index 000000000..13b2fee37 Binary files /dev/null and b/docs/images/invPlanningVisScreenshot.png differ diff --git a/docs/images/matRadPerformanceTable.png b/docs/images/matRadPerformanceTable.png new file mode 100644 index 000000000..04f76dc1d Binary files /dev/null and b/docs/images/matRadPerformanceTable.png differ diff --git a/docs/images/matRad_steps.png b/docs/images/matRad_steps.png new file mode 100644 index 000000000..20ca161a9 Binary files /dev/null and b/docs/images/matRad_steps.png differ diff --git a/docs/images/matRadvalidation.png b/docs/images/matRadvalidation.png new file mode 100644 index 000000000..a0b13f0e7 Binary files /dev/null and b/docs/images/matRadvalidation.png differ diff --git a/docs/images/matrad02.png b/docs/images/matrad02.png new file mode 100644 index 000000000..163953886 Binary files /dev/null and b/docs/images/matrad02.png differ diff --git a/docs/images/matrad03.png b/docs/images/matrad03.png new file mode 100644 index 000000000..965d32a17 Binary files /dev/null and b/docs/images/matrad03.png differ diff --git a/docs/images/matrad_blank.svg b/docs/images/matrad_blank.svg new file mode 100644 index 000000000..d0fc2bd7b --- /dev/null +++ b/docs/images/matrad_blank.svg @@ -0,0 +1,148 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/images/matrad_hat.svg b/docs/images/matrad_hat.svg new file mode 100644 index 000000000..d517100df --- /dev/null +++ b/docs/images/matrad_hat.svg @@ -0,0 +1,160 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/images/parametersLoadedScreenshot.png b/docs/images/parametersLoadedScreenshot.png new file mode 100644 index 000000000..030326380 Binary files /dev/null and b/docs/images/parametersLoadedScreenshot.png differ diff --git a/docs/images/parametersScreenshot.png b/docs/images/parametersScreenshot.png new file mode 100644 index 000000000..0fc0e8ac3 Binary files /dev/null and b/docs/images/parametersScreenshot.png differ diff --git a/docs/images/penumbra_gaussian_convolution.png b/docs/images/penumbra_gaussian_convolution.png new file mode 100644 index 000000000..5b8e286fd Binary files /dev/null and b/docs/images/penumbra_gaussian_convolution.png differ diff --git a/docs/images/pln-struct.png b/docs/images/pln-struct.png new file mode 100644 index 000000000..539d9b6b0 Binary files /dev/null and b/docs/images/pln-struct.png differ diff --git a/docs/images/qualityOutputScreenshot.png b/docs/images/qualityOutputScreenshot.png new file mode 100644 index 000000000..f7a04d246 Binary files /dev/null and b/docs/images/qualityOutputScreenshot.png differ diff --git a/docs/images/qualityScreenshot.png b/docs/images/qualityScreenshot.png new file mode 100644 index 000000000..5b603e31f Binary files /dev/null and b/docs/images/qualityScreenshot.png differ diff --git a/docs/images/rayBixelConcept.ai b/docs/images/rayBixelConcept.ai new file mode 100644 index 000000000..fd93752e1 --- /dev/null +++ b/docs/images/rayBixelConcept.ai @@ -0,0 +1,5210 @@ +%PDF-1.5 % +1 0 obj <>/OCGs[5 0 R 34 0 R 62 0 R 90 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream + + + + + application/pdf + + + Druck + + + + + 2015-05-14T00:09:24+02:00 + 2015-05-14T00:09:24+02:00 + 2015-05-13T23:57:32+02:00 + Adobe Illustrator CS5 + + + + 256 + 104 + JPEG + /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgAaAEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 FXYqxj8xLi5tPLwvYJHj+q3MEspjYqWTnxK1BGx5DMHtAyGOx0IcTWyMcfEOhH3smVlZQymqsKgj uDmaDblt4VdirsVdirsVdirsVdirsVdirsVdirsVdirsVdiqmbmARSShwyRV9Qr8VOIqRRamvtiq E03WrbUWcW8N0iIN5Li2ntlJ8F9dI2b5gUxVaJvMLagU+qWsWnK3+9BuJHnZR/xSIVRa/wDGU4FX ajpt5eSo0WqXNjEoo8Vstv8AGa/tNNFKw/2JGFVS90rTr+0Wz1C3S9thQmK4USqxUUBZWBDfTiqL xV2KuxV2KuxV2KuxV2KuxVK/NVmLzy3qdsRUvbSFB/lKpZf+GAzH1UeLFIeTTqIcWOQ8lnk+7a78 r6XOx5ObeNWbxZBwJ+9cdJLixRPkjTS4scT5BN8yG92KuxV2KuxV2KuxV2KuxV2KuxVZ68JSRw4Z YiRJx+KhUVIIFd/bFUDpmu2upSMttBdKirUzXFrPbId6UX10jLeOwpirvW8wtqBjFpax6crD/SGu JHmZe9IRCqr/AMjT8sCrtR028vJY2i1S5sYlFHitlt/j3/aaWKVh/sSMKql3pOnX1ktlqFul7bLx rHcqJVYqKAsGBDH54qhpdQ8r6Bbrby3NlpNuv2IWeK2QfJSUGBVE+cdEYj6sbm9DUCvZ2lzcxmvT 97FG0X0lsbWnfp3V5TS20C7ofszXMlrDGd6dBLJKPpjxtWuXnWdaenpunN2YvPf9/wCXjY/8SxVx 0bzBOAbnX5YW7rYW9vChHhS4S8f7mxVO8KuxV2KuxV2KuxV2KuxV2KpN5y1GPT/LGpXDmhMDRR/6 8o4L+LZjayYjikfKvm0anJwY5HyVfK1hJYeXdOtJBxligQSL4ORyYfeclpYcOOI8k4IcOOIPQJpl 7c7FXYq7FXYq7FVnrwlJHDhliJEnH4qFRUggV39sVQWm63a6jIyW0N0qItTLPbT2yHelFM6RlvH4 RTFWmm8wtqHppaWqaeGFblriRpmXvSEQhR7fvfowKw/z5+Yuh6TfNZDU9QFzaRtLe2mlRW0hRAOR aeW4R1jVV3PFgR1zDy62EZcAuUu4O60nYWbLi8aRjjxfzpmr9w3J/SkWm/n7+U/mKBdMvhPJCCiu l7AJUemysygsX6VPwfPLZZ+GuKJ+9xMOh8W+CcNuhPCT7uKgfdd+TNNG84eUHha18uwS3EUJAMFh ZTJEpYbfH6ccI/4LLIZIy+k242XT5Mf1gi+/r7keNY8wzilroEkLdjf3NvCp38bdrxvvXJtLinna dQRLpunN3T057/v2fnY/8RxVs6Dq0p5XOv3lD9qG2jtYYz9JikmH/IzGld/g7Q3JNytxeg1LJeXV zcxmvX91LI8Y+QWmNLasll5Y8vWklykFlpFpHT1Z1SK2jWp4jkwCKKk03wqidO1bTNSjaXT7qK7i Q8WkhYOtfZhsfoxVRttaFzeC3isbwR1YNdSwmGJeIP8Av0o5BpQcVPXBau1G61+OXhpunwXK8QfV ubprda7/AA/BDcN4dsKq9+mqSWwWwmgtrkkVknie4jApuOCSW5O/fliqKxV2KuxV2KuxV2KuxV2K uxVhutE+YfN9poiDlp2kFbzUm7NLT91F+O/zPhmtzfvswh/DHc/j8dXBy/vcoh/DDc+/oGZZsnOd iqz1oirsGDCOofj8RBAqRQVNfbFUDpuvWepTNHbQ3YRV5Gee1uLaM7gUVp0j5VrX4ajFWpLjzD9f KraWkemow5XT3MhmKChakIhCj5mX6MCpXrGvaFLcrAnmlbGSMESWdnJayzOT4o8c8vT+QA42l1zq ukajYrYNpN/rMIChop7KSNZCo2ZmvVt4nr41piqrZTa1BbLBpHluDT7ddxb3VzFagdjRbOO8T8cU Kxt/OUxBa+sLNT9qOO2luHG/aVpol++LHdUo80x3OmeW9T1abXb68+qQyPHAjW8CetSiJytoo5RV yB9uuU6jJwYzLuc3s7S+PqIYzsJSF+7r9jGvIXkryTrHk4WGpgapqmrW8k+qXLGSRg9zUnjK3JFk QSdQa1+LK9HjIgDL6judqcntrURyaiQxk+FD0x9RlsNtiSdj08lv5eflJoH5ea1fTabaanrd3dxr avcSrbJDHA5WRlHqPAH3VeRFelAOuZJF7F1YJG4RX5h+SI9FtX82+T7ddM1fTg1xdGBzDDJBH8ci vAqsr1A3Hw+9ds12p0kYDxIekx7v1PT9l9sZs5Glz1mx5PSOM7gnYESqRHyLOdC1S81nyzaanH6N tdX9uJ4R8U8UZkHJOQ/cM/EEchVd675n4sgnESHV53VaeWHLLHL6oSI+SJ0621aIu2oXqXTMBxSK D0I1pWtAXlbevdsm0KZ0SNr765JeXjkPzSD13SFaGoHpx8Ay+z198aVfqWg6JqhQ6lYW976YIRbi NZVAP+S4Iw0qLgght4Y4II1igiUJFEgCoqKKKqqNgANgBiqHutY0i0r9bvre3pWvqyolKdftEdMb VADzt5PY8Y9bsZW/kiuIpG/4FGY4LVx84aL+wt5MP5oLC+mXf3jhYY2rf+JlcVttL1G47j/Rmgr4 f70mDr742mk5wodirsVdirsVdirsVWLNEwcq4b0yVk4mvEgVINO9DiqSXXnHTorC8u0iuRHaQtL6 s9tPbRuwpxRGnSPmWJ241GV5cnDEljklwxJ7mH+S/MS2Vvz9ewup7+YXWpTRXEt1e1f4in1S2gkK sFqAGf7VdsxtFiMIWfqluWjR4jGFn6pblkup6vfaj6R0qLWo41/vPq1tbWzNX+b9KrEQBv8AZAOZ jlNNNq2q236NOl2N1bxqguItQvhJN8PRpYYYJ42qV7uK4qhbFrq1nGkWl9aWKc+BttJ0ifjE+wPK blLAvapZBgVF6npV5D6aTS65rnqVJ+rXFrZcACNmaF9O8dqEnCqpL5S0aSwS5XQre81IBXjh1eT1 nRmI5hrhxfMGAruvKp798aQm+jxX8VuUu7e1tQKCKCzLMirTpyZIq/QoxCpe12LG7+sar5jgjhjL k2pFvbxkAUo5kLyfBWuzjtXvVSgr/WvIepXAd/MSOygL6FjqckW4+KpS1lRuh77YqrahrnlHUbUW t9ZT6na1VhG2l3l5HXopNIJFr88bQ8Z/5yH8/XWi6Jp3l7QdIFhompc5Lpp7OW1VzA6MkcaEQ8dw GbavTp3ry4o5ImMuTlaPWZNNkGXGanHl157dXo2medPO+o/lnFqdh5ZuItcm0wzWvxW31czekTG0 cbTtOVY0KoyV7ZMChQcckk2WB/kN5s/O3VNT1WPW7eS/0+OJWV9W52ISfmBxikS3lLVSvJONBtuO 5Ql3502X59X3na0TSYLwaZNEiWkOlSSy2Ss20guXKRIak7mZQvH2rgMQRRTCZiRIGiHpH5SDzPde SLOKPUrWGGzkmtvTNo7zL6crUDSG44HY7fB02r3zD7PkDiHDyBLuvaLFOOrkZ0ZSEZbChvEeZ/Hc zA6HrT7v5ivIz4QQ2Kr90lvMfxzNdG3/AIaZxS51fUbjxPrrB16/7zJB+GNJtoeUNHrVnvpR/LLq N/Kv/AvOwxpDj5K8oP8A3ujWc5HRp4I5m295AxxpbRlpoWiWYAtNPtrcDYCKGNKAf6oGGlR2Koa7 1TTLNlW7u4bdm3VZZEQke3IjG1df6ppmnWwutQu4bO2JCie4kSJKncDk5A3piqDu9fihlMcUfq8T RmrQV9tjnFdp+2mLT5Tjxw8Th2Juh8Njbm49GZCyaRdhqMN4hKAq6/aQ9q5u+xe3MWvgTD0zjzif xuGjNgOM7uu9V0yzjeW7u4beOOod5JFQAgVINT4HN1bSxey/N3yFf3wsbC/lvJ2DcFt7S6lLFByI RUiZn+GrfCCKDBapR5i/OCfSNWit/wBBztprEM98xdZFjB+MtbmNWSm4+JqjwwcSaU9e896peQwy aTqNlAhI5QadqFnc3ZDb1kiuYURVUdeMoO+JK0vh1G/1fRxaa5dXXpUVTDa6NPOXC0KtK7pqUJYk fEKe9Meaofy/ZOpFu8F9NbIrMLHTzPp55/CBUI2lRU2Io0PzpgCUl892+jpIIvqDaZeyzxIkU50h pyDxLAzetc3fxeNQN9z44+qB4KH6f0NGo3jX6/0M58w3t7FaW66jb2Wl2qELB6muXOmksB9lTbQj mAv7PL6MyW5WtV1efy+yxfo0aSYpeYkN1qYZKtzDep6Ly03qD9rptiq3QYdSl9SHTb2xtoYgOX1f Rbi0XfYKGlnKN03Cio2wqvZ7x776k2u6ytxzEVYdMRYK13b1pbKSKn+V6lKYqrahpcluUWe41rU+ YPJreVIaeNfQ+q0r2p07UxVdJ5d01LD62trqt5KVVhYPqNy8tWoKUuLpYQyg7/H8saQ1p/lfyxeo z3nlWG1dCFUX8NpO7DrVWjkuaAHxIxASr6ZYLBdiOHy9a2FmpK+uhhVgEBKkRxodq9Pi2xVG6hP5 ijnC6dY2lzBxBaS4u5bdw29QES2uBTpvy+jChEXrakLcGySFrioqszMqU7/EqsfwxV51qcmoeZfz G0rQbgwvBoCvqOqiAO0IkcAW8TcqcmBoxG1QT4ZrMsjk1EYjlDcvT6XDHT9m5M0x6sxEIX3D6pfe L72e6dB5gjnJ1G9tLi34ELHb2klu4eooeb3NwKUrtx+nx2bzDri01t7wyQ6hFFa1Urbm35sAAOQM nqDqa/s4FVtRsru6jRLe/m08qavJbrCzsKdP38cyj/gcKsB/LUNpupea/J813LDfQ3sl7aTkR+q1 vcqvGVAyGMlaAt8HGrdM1uikIznj5b2Pd+Pvem7cxnJg0+pB4onHwE7fVEnY0B7h5DfvZ5p2mzWZ cy39zfM9Km5MWxFd1EUcSivyzZPMqZ0K2a+F49xeNIr+osX1qdYQa1p6SOqMv+SwIwUq/UtGs9RK G4kuU9MEKLa7ubUGv8wt5Ig304VVZdNsprEWE0QltAqp6TktUJQrUkkmnEdTirWn6VpunI6WNulu shBcIKVI6VxpUPa+WvLlpeC9tdKs7e8BYi5it4klq4IY81UNuCa740qMksbKSUzSW8bykBTIyKWI HQVIrQVxVW6bDFWC6tp91JDNZrdS2M5IAuoBGZFoQar6ySp8Q23U7HbffPBM2GWk1BjliJGB5Suj 57EGuo3d6JCUbBYBqn5KedNcsLqK1863siMgT6ve8xDIeQJDmFlHav8AdnPTPZjWQ1PFOGnhhA24 o9fL6R8d+512qgY0DIlLPK/5B+f/AC8ZZnuraT4qhbGUtK1FpUfWEjQdfGudaQ4gSXy/5Q8zxeZU l1DQ70Qo0pk+sWbuvEqaNx+raijfEw+zC1D4faUKnvmC6t7HUIVa9eyPpLEtvbwxK4cMxC8FsbE9 GGzRAjxoRQFLMtfu76Ty9BPfNfy2BaPi2qN5dW0LFTxoRHcMGPRfgJySET5Yuol0c+je6lYxSMxW DSrG3vLViQKkyW2lBKnv8PToTiFW6EYrSf8A0m/m0NVQqs0cFpFQClEpPpNmw+VO2Kpf5v1+2muL K2g12LUka/tuU8M2mljRSebLGVf4P8ocfwzF1UeIC9/UO/8AQ0agAgbfxDv/AEf2M4ujriwpLYan dasGpVbOPTaqCKhiZmhWh7UJzKb1aG1197A3cup3luyqzPavbWjTDhXakQkViabcWNcKrNPTWb4u E1W/g4UJNxZQwg1/l5xiuBVL6zffX/qP6Y1L1g/p8/0U/o8vH1/q3pcf8rlT3wqq6g91pzRi98yS wCWvBmtoOO3iwj4j6TgVfK2oW1h+kX8ywx6eVVjdXcEKoA5AU8w0KgMSAK4VW6bcahqkbS6Z5lsb 6NCFd7aCOZVJFQCUmamKr4JNUuJWht/MdnNMpo0cdujMCPECYntiqJ/R/mf/AKvEP/SH/wBfcaKs Q81+bPM+n6pB5e0W8j1bzFcgE2sVsEW3jNP3s8hkcIN6io+fauFqNUYy4IDimfsd52d2P4uM580v DwR6n+I90RtbzH81/wAi/wAwbiysG0KT9MSTyyT6xCkiwu11JQiVmmkUSqu4U7cak0+Jst0+nGMe Z5uJ2l2lPVSF/RAVEVVD3PUrDyn+ZMf5aJosmvxpr36Na2EzRFmWZoyqg3IflVahfU41/a3zIp1z APyD/LL82vK+pavcalLFpNpcJw+rXdL1Z5uYPrKkFxHxKqCORbevTwUIL84/yp/OPzB59sdU02eO +gVIltLu2k+pR2Toas3pyTSOpr8fJCxP3DFWT/nL5D/MO+8vRaro9zDeeYrJVheXTreazvJLYg+o Ff6zKGNf2QoNC1PA1SwRMxMj1BzIa/LHDLAD+7kQSPMJJ+Xfmf8ANPyd5et7XzqJ7TTZ5Ga11K/t Zb94ENFEcxjuYZEXkCVDAtvtt0pz6sYpASB4e9zezuxpazGTjnHxQfoJqRHeOj2OzbzRe2sV3Z63 pdxazKHimjsJmRlPcEX2ZUJCQsGw6rLiljkYzBjIcwVb6p50/wCrrpv/AHDp/wDsuyTW4WnnTvqu m/8AcOn/AOy7FXfVPOn/AFddN/7h0/8A2XYq76p50/6uum/9w6f/ALLsVd9U86f9XXTf+4dP/wBl 2Ku+qedP+rrpv/cOn/7LsVTeSCCWnqxq9OnIA/rzG1Gjw5q8SEZ1/OiD97KMyORpcqqqhVAVR0A2 GXwhGIEYigO5BNt5JDsVS+58u6BdXq31xpttLfIQVu2hQzCnT95Tl28caVKdZ/L7y/qMy3MEY0y8 Wtbuyhtklbw5NJFIdvahwUm1s3k6ez00pol/dx31F5NPdyvHIQKMWW4W8jTl/kx09saW0B5f8o+Y 9KhM8Fxa2d0oZDC8X1uJk2NVFt+iowT0H7nbBS2xjzff65e3Fk0glVY763kliZbm2Qlap8EN5atH vsCUmI/yepzE1cgADL+cPx0/HRxtSQBG/wCcPxzH6fcyHWLO1vBU+VYbaWJ29a4utOtNVQ7V+GO1 uRODWh3X6My3JWwt5IttMJuI7XTr5Vfh6kV3oEDtUlR+9AaNTtypy333x2VH6BpdpqKtKJblAoDJ cWmuXl9C3Ko25yDsP2k/HCqo8Fyl8LJNT1605OIo5lgguYW32PqvbXPEbfacj3xVU1KafRuH13zY lsJOXpvqUVqqkDr8SC1WoxVE2o82zwx3MGr6Xd20qh4mjspgrqR9pZBeSCh61C4oVWm85x7LZ6dc 9fiN1Pb9Ohp9XuOvz298d1UVbWY5knm8u2r3EZPCW1uY5GXkKEq00VsdxscUpJ5vv9PXSr7V9W8l STy2ls5W5vItMuAOIJRTxuJJOPM9AO+QySoW2YYCUwDuOtc66/Y8f/Ir8ydI1fVdT0bW9Ee4lvA1 ++owxT3tw7IwHGVII2bgPU+EqoAPbfKdPpY4r6k8y5/aXauTVGIIEYQFRiOQH6/Nnf5hfnf5b/Ly CysdM0We7lvS8kltIs2niNBQMx9eLmWbtRfevjkusT7yx5+/LYeXY/M0esG2SSzaeewvNSmuZYlU c3QW0s0vxrwIHBKnt1xQg/y//MzyR+YutXqaRNqtlqNsqzyWs88kaSRIRH6kcSTSxhQSocALud69 cVTbzx+ZHkXQtUtdE1jzFJo+pTgOn1deXFXPFGmYxTRotd/j+Z2xKsluprbTNCkl1HWTBBGhMms3 LW0TIHPwuW9NLcU5ALVKeNcKofQls9R015k1keY9KvFKpK4tZYmG6uA1tHGjDsQRkTEEUeTKEzEg xNEMM8h6fPpnnzzLomiTCDQ7GS3nktnRpY+dwpZkirIOBABHLxHTbNfpIiGWcI/SKej7YnkzaXBn yyEsk+IcqNR7z17+XXzegajDrkjIdNu7W2UA+oLm2kuCT24lJ7fj9xzZPNKso1NbACJoZNQCpyd1 eOFmFOdFDSMgO9N2p74q1p7aqUf9IpAj1+AW7O4p781TFUPaz+Y2veF1Y2cVlVv38V5LLLQA8T6T W0a1J6/vNvfFV1/e6vBOBa6d9bgoCXWZEflvtwcAfTywKr313cW1sJYbKa9kJANvA0IcV71mkhSg /wBbCqJxV2KuxV2KuxV2KuxV2KsZ/Me1kn8o3jxf3tsY7hD4em4JP0LXMLtCF4j5OJrok4jXMb/J MLfSfLuptaa4dPtpLyRI54b30k9YVAZaSU57fPMrHISiJd7lRlYBHVEanpt1dlGt9SudPdK1NuIG DV7Ms8Uw+6hyaUBqnlfTbi1WQ6XYajqcQXjPeRJGWYU5N6iRu0ZPWqrtgpUNp/lnjbNLMt1pd0ho sNlqV1cw8VApwSYJHv4eljSVLTpdVuJXgsNbvTcopZo9Y0zilAQDxMcWnh+v7LnFURdweY7q1ls7 u00jXLZjxnilaW2QlWDANEyX6kigNC3XFCzTp20dWhh8rT2cLHlI1ibWSAU6UQSxy/QsWKVG11fQ rS8+sS6jqdjEpZpIdRjuY7Y1BqDLdxUota/BJTYdsVST80NVi1TyNq8+i63Z3VmIEEtvBwuC1ZV3 WSOQca1HY5ia6vCle/L73b9gWNZjIIjud5CwNjz3H3hBflv5i/LS6NxB+X1vpkvmRYkbVIysliZE UBZHWUQSl1ElPsgrXfuK5GMERF86dZnIM5EcrNMk80+TfKvmvT7d/Pek2pktJGFtxupAAXpsk4+q P8dPskU2ybUjrey0nUtFk8sXOhT2GlPam1NlKkYhEHH0+CvBJKgIHSjV7jFWF+Vfym/J7yVqV3DI Vury6Kxga16UioGoyxwF444zWo8W7VxSgvPn/ONflTzP5lt9Wt706LE6ql5p9rDHwlEY6w7oImK9 fhYd6dcUJt+Z35Mx+ZvJ36G0S+uLO6gkiktlvL29ntWEQKiN0kkmCijbMqVFBjSpJ5R/LfV/y2/L HV1v/M8tldkXF05sfSa2WR41ihCm4gaQsWVd14Ek07VyvNkGOJkeQcnR6WeoyxxQFykfx8mL/wDO Pv5Yfmdo+t3WvXlwNL0y+tqq0pW4N4ZGDpIYQ4ZaCrcpKNv03OShISAI5FrzYZYpmEvqiaPX7nuw tfOimp1LTpQP2PqE8ZP+y+uPT/gck1OabznHstnp1z1+I3U9v06Gn1e46/Pb3x3Vsaj5nj/v9Gic 9P8ARbxZP+T0dviloeYNTB/eeXNRjXu/KwcD6EumY/QMbQ5vNdnH/vRZajC2/wAIsLqbcdRW3jmH 479sbVsecNAH97NLbdv9Kt7i238P30ceNppOcKHYq7FXYq7FXYq7FXYqp3NvFc20ttMOUUyNHIvi rihH3HIyiJAg9UEAiixbyDeS20V15avW/wBO0iQpHXrJbsaxuPbf7qZg6GZAOOX1R+5w9HIgHGec Pu6MtzYOa7FXYq7FUvl8vaDNfpqMunWz6hGQyXhiT1gVNR+8py/HGlb1PTLq7KPbanc6c6ClbcQM rf6yzxTD7qH3xVUuhqkVigsvSubxAoY3LGJZKCjEtGj8SfZKe2KpLrflqDzJ5dvLXX9LtY7yWKSN TGRd8DxorxyNHC9a/wCSMhOJINc2zFMRkCRY6jyfP35ZeVPL3kPXr3UfNx1fT7gBoNOmSKe3iCFq l/UjKSyFuNB8HDx3pTCOt4P72PD9v7XfQ7CGo/xTIMu1kEGEh8/Sa8pO/OazuPPt1pEflTU9Q16O 1Eoe1uLeQCMylaPGY4ELVA+IvUjahww7QxyNCz8C15vZzVY48U+CPvnAfpel+S9L/PGz8n2WkFtO s5bCJYIJ7wu0zouy8uHrj4RsKqDQbjJXmny9A89/saTDR4COI+Oa3EbgAe7i6++kf9e/PbTJEElh p+uRf7taJlhIA7qzSRHf/jGflkeHUxOxjL37fc3eJ2ZkG8MuI/0ZRmP9lwlu8/MKynjS185+TbyG MGpeW2W7tFI2J9SVYx9ynJfmZD64V7iP2Fpl2bin/dZoy8pRlE/YJR/2S5fzZ/LOPTX0zSxctEA0 a2Gm280LguSSIzH6XAlid1Yb4J66ERyl8meHsHNkNCeL/lZH9BJ+xA6R5S17zdqMV1rkF3pvlK2k We10W+uZrm4uHWvFpvVd2Rfi3U79vfKBiyaiQMxw4x06n3uylqdP2bjlHTz8XUyFGY+mH9XvPn8f J6KLDWI7wSRakGtC9WtZYEaiV+wjxmIjbYFuXbr32lPJ2qalJriFDplva3AofUS5mkg37UZIp/8A iOFCpNc3kGni4Nm1zdqql7O1eNiXNAyxvObdCFr1bjUdq7Yq1p19Ndxs01jcWDoaGK59IsfcGGSZ CP8AZYqo23mHSbm8FmkrpdEsqwzRSwsxSpbiJUTkKLWo7YLWkYbq1E/1czILigb0eQ50NaHjWvbC qrirsVdirsVdirsVdirsVdirsVYj56sprI23mmwX/TdLYfWVG3q2rGjo3yrX23zX62BjWWPOP3OF q4mNZY84/aGU2tzDdW0NzA3KGdFkjbxVxUH7jmdCQkAR1cyMgRYVckl2KuxV2KuxV2KuxV2Kpe3l 3QGvk1A6dbfX4yCl2IkEwp0/eAcvxwUEkt6lpt3duj22p3OnsgoRbiBlbevxLPFN/wALQ4UKt1+k 47NfqYiubteIP1hzCj0+0S0aScSfZTirVlPqDWjSahbJb3C1rDbym4UgCvwsUhJr/qjFUFHdaDr/ ADtLixeYxrV4L+yljFDsafWI1Vv9jXAqmvk7ywAVs7Y2KoaMunTzWQBG9CLV4vHpjSt/4dv4v95N ev4VHSKT6vcL17tNC8p/5GY0rfoec4SSt5p96o+zE9vNbMd+8qzTj7osd1aGq+Z4QTd6Es25oun3 ccxp2r9aWxFfpxV3+K7aKgvdP1Gzc/staSzgU68pLQXEYHuWxtVW383eVriYW8WrWn1k0/0ZpkSa p6VjYhx92Nqib/RtG1ID6/Y216tPh9eKOUU67cw2FV99ptre2wtpfUSJSCv1eWW3dePTi8LRuPoO KorFXYq7FXYq7FXYq7FXYq7FVK8tYru0ntZhWKeNopB/kuCp/XkZxEokHqiUQRR6sc/Le5lk8rQ2 8xrNYSy2knsY2qB9CsMw+zpXiruNOLoSfDAPONj5MozOct2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV L38vaC9+uotp1t+kFbkt4IkE1f8AjIBy/HGldqemXl26Pbapc6eyClIFt3Ru/wAazxTf8LQ++Kqt 5+lYrJfqKw3V2vEMLl2hRwB8R5RpLxJ/1MVdZ3GoGyabULZILhORaG3ka4BC/wArGOJjXsOOKqem 63ZaizpAlxHJGKulzbXFsaE029ZI+W/da4qipYrS6jaKVI54qlXjcB1qOoINRiqVf4L8rJU22mxW LsatJY8rNyfEvbGJvxwUrQ8sSRALY6zqVoo7esl2TvX7V9HdN+ONKneFXYq7FXYq7FXYq7FXYq7F XYqkvlvQp9Ik1TnKskV9ey3cKrUcBJT4TXvtmNp8JgZXyMraMGHg4v6UiU6zJb3Yq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYq7FXYq7FXYqgP8P6F+kBqQ062Gog8vrixIsxPvIByPXucaV2o6ZeXUqS22 qXNgyChSBbd0fv8AGs0Uv/CkYqq3p1SO0H1BIbm6WlVuZGhRhTf440lKn/YYq//Z + + + + + + uuid:b61aae0a-47f9-43ff-9838-2fa3dbb65c14 + xmp.did:C098B20EBBF9E411A3FDF3D61E7FF9F4 + uuid:5D20892493BFDB11914A8590D31508C8 + proof:pdf + + uuid:f1775086-0703-1641-91a3-3e7a3f492d9d + xmp.did:FB7F11740720681188C6F0AB0253E170 + uuid:5D20892493BFDB11914A8590D31508C8 + proof:pdf + + + + + saved + xmp.iid:C098B20EBBF9E411A3FDF3D61E7FF9F4 + 2015-05-13T23:57:32+02:00 + Adobe Illustrator CS5 + / + + + + + + Document + Print + + + False + False + 1 + + 209.662999 + 81.998268 + Millimeters + + + + Cyan + Magenta + Yellow + Black + + + + + + Standard-Farbfeldgruppe + 0 + + + + Weiß + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 0.000000 + + + Schwarz + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 100.000000 + + + CMYK Rot + CMYK + PROCESS + 0.000000 + 100.000000 + 100.000000 + 0.000000 + + + CMYK Gelb + CMYK + PROCESS + 0.000000 + 0.000000 + 100.000000 + 0.000000 + + + CMYK Grün + CMYK + PROCESS + 100.000000 + 0.000000 + 100.000000 + 0.000000 + + + CMYK Cyan + CMYK + PROCESS + 100.000000 + 0.000000 + 0.000000 + 0.000000 + + + CMYK Blau + CMYK + PROCESS + 100.000000 + 100.000000 + 0.000000 + 0.000000 + + + CMYK Magenta + CMYK + PROCESS + 0.000000 + 100.000000 + 0.000000 + 0.000000 + + + C=15 M=100 Y=90 K=10 + CMYK + PROCESS + 14.999998 + 100.000000 + 90.000004 + 10.000002 + + + C=0 M=90 Y=85 K=0 + CMYK + PROCESS + 0.000000 + 90.000004 + 84.999996 + 0.000000 + + + C=0 M=80 Y=95 K=0 + CMYK + PROCESS + 0.000000 + 80.000001 + 94.999999 + 0.000000 + + + C=0 M=50 Y=100 K=0 + CMYK + PROCESS + 0.000000 + 50.000000 + 100.000000 + 0.000000 + + + C=0 M=35 Y=85 K=0 + CMYK + PROCESS + 0.000000 + 35.000002 + 84.999996 + 0.000000 + + + C=5 M=0 Y=90 K=0 + CMYK + PROCESS + 5.000001 + 0.000000 + 90.000004 + 0.000000 + + + C=20 M=0 Y=100 K=0 + CMYK + PROCESS + 19.999999 + 0.000000 + 100.000000 + 0.000000 + + + C=50 M=0 Y=100 K=0 + CMYK + PROCESS + 50.000000 + 0.000000 + 100.000000 + 0.000000 + + + C=75 M=0 Y=100 K=0 + CMYK + PROCESS + 75.000000 + 0.000000 + 100.000000 + 0.000000 + + + C=85 M=10 Y=100 K=10 + CMYK + PROCESS + 84.999996 + 10.000002 + 100.000000 + 10.000002 + + + C=90 M=30 Y=95 K=30 + CMYK + PROCESS + 90.000004 + 30.000001 + 94.999999 + 30.000001 + + + C=75 M=0 Y=75 K=0 + CMYK + PROCESS + 75.000000 + 0.000000 + 75.000000 + 0.000000 + + + C=80 M=10 Y=45 K=0 + CMYK + PROCESS + 80.000001 + 10.000002 + 44.999999 + 0.000000 + + + C=70 M=15 Y=0 K=0 + CMYK + PROCESS + 69.999999 + 14.999998 + 0.000000 + 0.000000 + + + C=85 M=50 Y=0 K=0 + CMYK + PROCESS + 84.999996 + 50.000000 + 0.000000 + 0.000000 + + + C=100 M=95 Y=5 K=0 + CMYK + PROCESS + 100.000000 + 94.999999 + 5.000001 + 0.000000 + + + C=100 M=100 Y=25 K=25 + CMYK + PROCESS + 100.000000 + 100.000000 + 25.000000 + 25.000000 + + + C=75 M=100 Y=0 K=0 + CMYK + PROCESS + 75.000000 + 100.000000 + 0.000000 + 0.000000 + + + C=50 M=100 Y=0 K=0 + CMYK + PROCESS + 50.000000 + 100.000000 + 0.000000 + 0.000000 + + + C=35 M=100 Y=35 K=10 + CMYK + PROCESS + 35.000002 + 100.000000 + 35.000002 + 10.000002 + + + C=10 M=100 Y=50 K=0 + CMYK + PROCESS + 10.000002 + 100.000000 + 50.000000 + 0.000000 + + + C=0 M=95 Y=20 K=0 + CMYK + PROCESS + 0.000000 + 94.999999 + 19.999999 + 0.000000 + + + C=25 M=25 Y=40 K=0 + CMYK + PROCESS + 25.000000 + 25.000000 + 39.999998 + 0.000000 + + + C=40 M=45 Y=50 K=5 + CMYK + PROCESS + 39.999998 + 44.999999 + 50.000000 + 5.000001 + + + C=50 M=50 Y=60 K=25 + CMYK + PROCESS + 50.000000 + 50.000000 + 60.000002 + 25.000000 + + + C=55 M=60 Y=65 K=40 + CMYK + PROCESS + 55.000001 + 60.000002 + 64.999998 + 39.999998 + + + C=25 M=40 Y=65 K=0 + CMYK + PROCESS + 25.000000 + 39.999998 + 64.999998 + 0.000000 + + + C=30 M=50 Y=75 K=10 + CMYK + PROCESS + 30.000001 + 50.000000 + 75.000000 + 10.000002 + + + C=35 M=60 Y=80 K=25 + CMYK + PROCESS + 35.000002 + 60.000002 + 80.000001 + 25.000000 + + + C=40 M=65 Y=90 K=35 + CMYK + PROCESS + 39.999998 + 64.999998 + 90.000004 + 35.000002 + + + C=40 M=70 Y=100 K=50 + CMYK + PROCESS + 39.999998 + 69.999999 + 100.000000 + 50.000000 + + + C=50 M=70 Y=80 K=70 + CMYK + PROCESS + 50.000000 + 69.999999 + 80.000001 + 69.999999 + + + + + + Grautöne + 1 + + + + C=0 M=0 Y=0 K=100 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 100.000000 + + + C=0 M=0 Y=0 K=90 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 89.999402 + + + C=0 M=0 Y=0 K=80 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 79.998797 + + + C=0 M=0 Y=0 K=70 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 69.999701 + + + C=0 M=0 Y=0 K=60 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 59.999102 + + + C=0 M=0 Y=0 K=50 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 50.000000 + + + C=0 M=0 Y=0 K=40 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 39.999402 + + + C=0 M=0 Y=0 K=30 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 29.998803 + + + C=0 M=0 Y=0 K=20 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 19.999701 + + + C=0 M=0 Y=0 K=10 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 9.999102 + + + C=0 M=0 Y=0 K=5 + CMYK + PROCESS + 0.000000 + 0.000000 + 0.000000 + 4.998803 + + + + + + Leuchtende Farben + 1 + + + + C=0 M=100 Y=100 K=0 + CMYK + PROCESS + 0.000000 + 100.000000 + 100.000000 + 0.000000 + + + C=0 M=75 Y=100 K=0 + CMYK + PROCESS + 0.000000 + 75.000000 + 100.000000 + 0.000000 + + + C=0 M=10 Y=95 K=0 + CMYK + PROCESS + 0.000000 + 10.000002 + 94.999999 + 0.000000 + + + C=85 M=10 Y=100 K=0 + CMYK + PROCESS + 84.999996 + 10.000002 + 100.000000 + 0.000000 + + + C=100 M=90 Y=0 K=0 + CMYK + PROCESS + 100.000000 + 90.000004 + 0.000000 + 0.000000 + + + C=60 M=90 Y=0 K=0 + CMYK + PROCESS + 60.000002 + 90.000004 + 0.003099 + 0.003099 + + + + + + + + + Adobe PDF library 9.90 + + + + + + + + + + + + + + + + + + + + + + + + + +endstream endobj 3 0 obj <> endobj 7 0 obj <>/Resources<>/ExtGState<>/Properties<>>>/Thumb 96 0 R/TrimBox[0.0 0.0 594.32 232.436]/Type/Page>> endobj 92 0 obj <>stream +HW[7 W. >(TN&EL*%Ng"J+ݭx%>χwy!I")I +i˯)DB 鈅O82 O|Z +ׂ2q\NIHL"ۗ㲛\xAzYDb0sLE&V~ hz0ulEX⍆ +M ~%Ԋ s7d<"i#,J!b֞ۥޫfQ%X^IY P/o3ffu=rn}:H=@R_Hhr櫉 +NPAB +&g!M*>Z}j#ڂ^ʥhkxf=fiנ^,fM5$CW^W,naq'K$W;puJ W\^Yܳ)7ggIf~Mߡ +=j9A1 W Y': | oEEDy7U\F-;Qjn3X׺?fAXw.Ph#ͻPZ񒍗%xÿ S!2^^%ظY? tW}9/F  ^P+O&@A^u Ov٨o4gqp̤*7>>`_m# +endstream endobj 96 0 obj <>stream +8;X.-c*[Ws#_hZ$!bR^,k1.Pq)R=BN4&H^25Fe'J-5d6tY+LS>/,NP2mL>nms1'2k +E0-OIn0A^d&1rXkGhiTN;%#r0`8OfH( +4>jmCa"[Bk>26tVkfH01U,S"g6$_u.>43;#kB0*YIi$#0<_Nr=dg;[[s3("='2i)j$mDcl80Ps +e9/is-0^ +endstream endobj 97 0 obj [/Indexed/DeviceRGB 255 98 0 R] endobj 98 0 obj <>stream +8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 +b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` +E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn +6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( +l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> +endstream endobj 90 0 obj <> endobj 99 0 obj [/View/Design] endobj 100 0 obj <>>> endobj 95 0 obj <> endobj 94 0 obj [/ICCBased 101 0 R] endobj 101 0 obj <>stream +HwPS -Ћ!H $!.HZHǂ DAJQDқ4A =3μ;͛y;7g5u HL쌌]\t`-8`nn :\ ñQG܉n@% r KQd yy(D7/2Af)ԛ2 dQNr:@27T.YG3Dd,@/tWG`ϟ +pݑfz9eS+q*8 /?& +1r8jF=(HpE$=n2pjb ^]oMq G-?~:A엂D?0^0*MNQyPp?22W* Pjs5Z4m133u1)ccMcepiL\€*r]p337t4l6|-G,,ZS=MTWߛȔN6O MKkHؾq w"3FjVYvg\.4O0_䭀۷>İԵ,zÇ3!UOIE}ϳ MfրK/s+_ut~yMFp;^>l{A!vT#8jBh1?=1?;12_wt ̱2Kڅu⛰ŭov"v])@s0R%BBM4)t~0)FE.R#,86.'\a܎3.\(O)/?'"%hJ A*YG$FEFO:/ux1Ᏺw '.[$%&'>MN8{Mefޜ\<|[ GPߕg_tR21Uդ'IOKj:j꥟=~8 oh%|վ!ة:B;|m_xCQHcqiSgftgX>_] Z2[^L6~nUbqf6݁xk  LAQ(л;1ɆYyH35VO6U8+|#Ӊ ~Sg/̿)q4$C +Qߎ'JHKJKde+19X\#P^IA\G^iWyIeLOs' +]tub#NcfZ+ 3V֟N ~85`o08zzi/.[{P"ܓKK ȁ)M!}a,(h^bgT_x{q)DaoBT4'WFAKfZވ̺\p]{닧JKe**?sPD-˟ugmFME-}mKtݫ؎΁n7o= +{ /@? J ؏z~$Os9=}s&w6w|B䥸s+~_\l׍on"ߖvwǿM_ U D +2-&6Øa YNL(%#,Vk 3*G g >70^_><@QkbJ$, w\uD65eI(H2HKJee;J\ / УX줢 +W]TkS/?nyB@K׬] =I1!$?P9+F'М$埈s aN_bg8SaKbmk`/lvp:&ғIFGh HCH'8+y ;89E,{P8[R^4V< +Kxl qQW2:HG +r;A;bjJ +߹/3?z(HjU4r6\ttL(ӧ!è^+!d Eeo_87}/tg}KLsurc.p eӎ`qt 3,#|Zh80I5]^K= ǜ;yyC1TxA6G6zW[h +><1]^~|h hO#PT +>$P@%hیoK/{W4yw8 f8e D*RBy;`nQtVy:_@ϨgaJ.?c}z5M7D>XP j2{89 +O +#/a^#.y?FPgq?:K1b4wߘUuF]WqP&:=9ݵ`ez}1mޒpd7lY1-uCrjz19"z/ +2 caZnpHu| +zP1iDhTL.YeP]G&G,,` ]` Aѡ-/s">BUuF.{T}ؗ) N!"GB;dl߅[LpـnF9 ZXcL8xէeW[o@ē1VL-#C&k*K&vV,*p$i;Rxټ_PG?+\ʽMbL6))r>=@x5*fv]^Zduz]X?P41p 9Q~*^ *,0#%8b;Q'p*$LEf"޵wǕSC戽Hah :-"dk>uy7C/)d%x`'qyo SY "&]#6؂5;4uWy +tk/ G &0,#Bv"R9 59oQm@;/D5RIP+-GM#º'uhAt0";=kմ?{-We_]ډ[jA )H>Au%W#Ժ>D.ٜJA1~ S̕V asUB +䪪/j)+68˼=wpmbnv\ߝtGOx;`uKJ ̄wAߕx~Ϟl.ɂQ!h0RȥjEGlsܦ5# J58/G V//"N1J;mh`$?,Wq$L$0lJVzq+ĬЈAaix!dpTP/H_ zZYs-dqN +U)$Yǭ, H -Ϻg,7xeIHh6 <{*M@+{ Pw/fID. O-Me̙mu+M­y!@*D> Ox0LA % v[Flrt@q+ /4r +X8Dm*Q/ZB\@a8~r;E=,7!oM2D[..Reړ7tA};GbsWh q2A];%mi"+g=Uf*l3xݲ?I a~ /&.u +궥 +>'ggUi AN?oO%),z~U޶ 3^Gj)ȀZMۂFiPnQY#TVtgSVD^O˿:JOg+SMnveKlou"̋'gøSzّ[ oIpkк½+/eM=Pm4?EAO =Ф>˭=@tVw {[u)jW>'jcJ|e57R1a7/zu c\V˞ Bb-C~}5ְGU*Z$SmƩૌL "CƼ+g~>dwE͢B`Ѥ*]vQ)TTڒWЬ~E0!U~Jqb `,<7q |?ķz\w5)r-C#n[-V 1/ceSˤpm^/hOC0eծXd#GQ(HJ3`7޼yofq-Eߦ~_ڎOZKW>(C8(8>=%šЦx>ǝ/@'3EHeʹb#ӓ"=ݯQMcOu䅂A~d+ʡ^yk2͈pRtzdswB8u9]7}CꫲQ] )>^6&$Gk{«C\L`ȣnggw +;.ˊr_/C:x)P]aΥ:媀B3-⊗cpcـ=#ӗ=Z"#j%"b7KJːйcle/_>‡*E7@+V6ŸnvQGM]wTnG<2 !_,>P蓧VwN5[,*tgGvR20B'džɫ+ tGj x1jMTn"AKFoUkoȱ*h$e1%#*54N nRΝ7ⶂĎ +V"8%{4G(ȫ*d+~?ɟt#gG9w[wߺ4ݑĈvOrb4鴶ר=yR:p(bHY-Ƞ$D~)5a:GXmr ]c^vzKS8=UĊ3c7o1UNϹu!8EGpҮ69 Esu|?Wx5ɻ3FI a ~۸ߕ +TCY bY#thG:tW5-BG^pw+.xZ t#4r uI_o$郅t/*-]V:SI#< +BqEd_07Ɉp0 e,%(IkLgu=Xin## Ί9W"o;!t%-Izx`to[!֦Z?Q,YjwgW tCOTmW}9Ӓ B$pqL?5LRDH|]gpOhz셠kJ$ۨ +sOk|8C "rզqc (:/k~%Dj[ihH5nߛ-787_A%V"RjxwڶᎣͿ3"汬6EVÄA'X*{G~GZVAZv}&(lݽ6$>Yԟn-ZT3(( D;峰&pۜ :J2gkSIJ}']V +WC;KX_="ŸXʰ #ލ=Omp(e{K  nK 溞b@mfl_#va^Xw4"f0aAҰUҷ`,."4*kĔQ~$pwf k33vRIܞ =|߬oS-KmaRf@8(M_!^3<.ẂXש?{غΏ{h*0I 8G.6hy MBYnW"v vhZn?lL,6XS(i)IfړH|E8Mۀ])>ZɿݳzK=IǴAK)jT|~/|x7L |{R.YXv /hiqM:=cMYCgHZrqqeNfFz.R&)φ,+.Obp.5^W۸&P4^?7H~(+Pjřv$JӇi|zK:mS`Rץ~q1Zo8#π)Ɂg.<}CL(pd,KWJKJo>v܂1%~#6ÜGyuQ`|ƿopCćKc˴WbJ>5&0"J⛰)nÈij||bocv勒WJ=h0Ρ=("hhψБ{oj{;cFVPDJht"=ލ:0"ݜ6׉20}{bnbVEJҒN=~80="ZӋ2z&njub2~U/^IEz=F0fJ"5ƗƄˇ¨zqťm +aUIrÚ=! 0KN"𚙓0qq(ynmi]a PU>?IG<բ038|"5|;"yh!lYf`TڧHϦݤ<30ŝ)#ae^ڼxภl`ST_'HF<{ڤ0iN#({-~~r}f}Z}OGD~ C~7H+)Ѥ݁j~n +rI;fS炜ZKO^?Cm]7{‚+EܣՄ'd+т~@( rf"Zv&NܶCG67^4+(>0N?~/m r7if+ZegN͵ҊC:Y;7R5*g^W΃}̪qwen"Z/NPCF7>n*H2HSՃU}qhʙfe{/Yڹ=NZWBђF7#I*Ɛ3D.r݃},kq;e-,3YVNB'E7 *Xxi<|pķ]dྠZYHMmB}6S+ffOBT;|rp8dX0M@Bp#6񧴜+™@ |r|`p;˯dNSXMpEBPg~6-+- <{$*6aWm: ;^h(zUfb 3`7)GH$I܊賏[:.mjrupTFgʛQ_:= e4V}C?1]@8l^|Eb?,Υkz, +00X m[<?bPAߩ,C] IGe_ ߠ .1^IxO/'`c R.1B8- 梛x*g;ԗ~KtCP^W.T +Dw2EkH|0`W*q"]|P&XbĀC\1=.kCլĊIUl(ha^ȊKJmXpq}P9Vaâo^Q#ŜdD@@φPފ YAɈ~joJ V@ZK.9@Ya΃d˷glĝsg5Ņh7\ |[X̑BAa#7qb̔+z *&3(Vcҗ.' +&1O%D'M DzFHq K0;J5 7GdQX"/! C\8"wv%>+>r'ф1N[Mƿɢuo✔b/HRl+W|[{ṕ)LvŝiC؋pׯ—g߬ +1+KL!Ԙ@|m#>`V(S<{;PXVK~JxY'VV}b{7anWT4[ ҿ\݈lbR%oNCl:^sZEѯZQ-33*HqB6Jwhgڛ:gդ73s=?Wѐ*vn):AНy=liC4X Qע.273ghgq4WZܜtm|>x xRr!P s{P"_PŠyW;f3x^CSW) ((\< 25;} y$RrF|_*@uN$-'?hp +08^`lw`Yll`ײ/lp q28u{lbۇ̝9M;]w[c]|B[v4hL̫%B"HPgp"?es1c\cekE +B7)*ѧ7{>ĭGulk(67aRܶcS|}<()2L/ΤX.`sk$Z<%S0aF~l7'y i^2N@HS&xpyRF}Nք5Qd-!c%٘$cL# + Zx..m}1K橤ybI֜%sSBqrMї8mr0)YͳRܹB wWX8Je3)}U[ )WO{eTS pJJyςG5ꪲ5\E2zP$\ +YFtQwd^Nk+J*6VDy[JB~3{?=cƌifhEJcpn|\VtP rfW,ծB;?Cq9eXc8ß+͋ǚSO붅R.s+ƀ}tຎͰ՝W7۔Av+dol6\9)<:`Z2^M{ר8vWA1it}+4~iD?Q)pC63:>lm~8Q=ӌ*`{3~|wT7Q@AX-Re1CYTL/&Na%づ!o<.ΆD@hLX!V[0|rrɳ$ `M[q>/C +1wEtHt8Eu%UTTl^*!%U,kF܂0!wi %"I(s\׿knu6F rH( &U1?OP|vINI"(* b خ0'kMԸRO3 +KyشvYV:^tFƔ.{od%;V:6C&1nG(y%^eOacD=4ONi6썂; ˭L`f7Qk{2f7PI"ăW(^\ $b1S.%.F6;oAAj[ T>f+(n_I߉^ʻǫbȌ[nq 13ߏBb7C Ӂ|S綵<*/У%ڋDrܱo|RהulsUZ_fת@^uP6T)#:ςlama$Hl^z'1`XPҺ}*%]ibd9&e@՗}@v^HdnKn( +̖{]1;R ;e#ZT"?hpiK Bzޒf1zlRIP<΅R1bm],2;%E7THM>+aOέbw?R HGQK+6f[ev&ŧj|xR4X%kZ% Nhr  HzOޢ[WJk2V\^Jpsκ[GFH'3Yb}O. =דgM!.grWsG @d+Mw8|oA8a +6'GCg5ɇE-h-;^)(pdϔPY u_Te.#!S*0zo#,,~vï:9wՔsU CKWWP)PbE8ʊ w_{zr_ YY{X~ */? 9e!+s<>&.F6һL1jX:U*$ION{y/Yf?ֳb fRowgJ[9T ܹ,$~Z͠I3!MZ)%rKi&yNl!쟢#" ы? p{1(n?%#T3Bym#la ߼oÌW$e-meҡrlwũ ߏ`n9q#l#O#&&ELI +*&/x }vuL7 Z +ڡ>k$G-)1="Y_w: j˱LiYm! ;ʀ}bZFr'^;hͰYնg0hE6AL%eO%wAݞa?UBW24a:Qc᪜~yʦ/ dWm_Z!ÿlq* MlX[͔+l;):|UKeo3W # +)ln>fĆ3 _yL3̀yVii߇k4[p+Z~ɮNW +WB~l^Q%¸k=LzbBw -lƯ* +BD+R7[T*Pu2Gn.(+QO^K#6`.m@6?e÷ Yk7|RO;1KKCXo }Ic?bVx*r7k23{7}M ;=3˚P1YldHv^Jg1͘܇L>Lۡ=mz!b= ,;1) ,%=3|~XEq1o[9^iUSlwzXRkTNP'JeR@݌1p4@ٕK\Ƽ42i|S`D7n.\|"Pm4QX!:# 9Qqb.'$g/jj)?cNF=R%ȟ5TC"/A%Ԡ̟uVmvMI@V#Gxt%CG:T5ɻ5?I'Lwn|ͱ |!\d-CWnd l[ ujoT^6ݖDJ(?X`yt+`˃c8Be^bUvRs-ga>ru{?VK3(r[ܓ|%_#|F|=Iާi m߈0l108 1"+9L(h_UK4wFw#ˏqyZ)vԨ +"kSͲE~fy,pN<<6 eݽXYۻm-; ujp]ɥcRd94M8HjhpMq:)ޞ=3U޶ 5Oq}Ri'!.gEWOL::Le˾FMq.3J\%|G/)^yS +7%Tw#UUCs0}* ڇ(jWA,ӘTR r`]Crz5_VYA9?uayK$mZiin%44Q5Zq%!'Jj$;tр +a,ߛp,=Dշ{nk>Rg,*CcL57iRXZ#EoP ?ҶC_=~vǹ{*N6uW2'{R_irUR(5kx;$d#D^vS:2r[i<WΩ텇wƬw.n4]hlt9/ъx‘;vjjG)Df{׵'8yGWm~MSE"70]˝4Oڑ5|j/@@t'("2h8@$$j$pX]md%WO, 1{O?mShʌ#e3# ($NxX{OZlߢ@Ñ"G\t!nTӺ1~Z%I9zBu-r]f@&\Y˥%RMN:g@W2ݚ $cMSBԶ(~glqIeY8 +LԠ?2o$?9a s}DZ4qlse -X"Lk-?pr2i@$~Ó2Mʉii}ppʬdsƝX3K?P20l,$i|d%|_^pc˧jWSKS>񛎛P1$Nݎ/M,{oi{c6WK>112$6+ X;݉ӅQ8c{ anBzbݮVVJ>W1F$"ӘnD qo3nw9wQwkax_[y)SfVyGqz:{-|}Kŀ`}vػ}j}^~S{~kG%~:k-Ǡ%𑙂GmvEjs`^< RFߨ7:-i Zk/J恌\u9i$^;qRr凋Fp:V>-Y'q4ᐹu;iwrO]ɯR1#FQP:-kBQɘj``t¹Qhⓧ]H$QEp9ݡَr-L I'ar+tLhe\ٮQ=E9D@-2Fg X}F֌r^s˸hf\f$P٩EZ9pO- *7+~s>g[魱APnE97<,- %Yr[~e°Frcg2[{OPD9О,曹 ,ٕ{o$}߼lXrHf["tOĨWsDƦ8ܟx,ԛ\ 2r0ۊwDxlGŬy`*yYUyJz?h{3׫|p't}t~ S%? 8w>~lĭ~`/~~U~Jb~?KQ3Ū'iĢ Ђ?w;8kð:`r:UZLJJ.?873u't⡤ 읠8 ڂv>ks°`4>U>*J<χ?1H3 'C ,aIvsk'ޏp_hU%J ?R3'ģ%U x͏vk͖j6_TmI۬E3R'ˢ׎7Y [Hv B|j/_b$TaΖI>r43u8'ТTꎾ9f#-u¢CjU _T7IC>ԗZ3c%'ڡm#% niu`Gj/^к kS̴Ih>\>3Vc'ǖmH̗ЊXՃuҮi*^S$HѮ碟>:C3Kޝ +':Ғ1)Xtjwi|Ȱ^T5=SV,H?>V3Bs(Ț1V6IȋybtPyuz_v֯zwA{}6xBxvPŬ gcn +7N\VYgM`TfM~o1*:hgKxD:ppqR /Gx-5-Ӡ`_ol ts&+s0K2'AkO* +7UdAjD-Q&!Q B \ 3h(;l 0weX{!i)5$uۙ.mD&WdXQ6l44+\e8\SaOƞ*˞!}S|ILJ\ +h8񜴸x$<~PZ?wJQԔ5 FvS:r]P"6(F^]Se >:1*ٛ>LUh\W-[VLIkIHl t0,x Ԝwe\q$**&6(w$׮|~<NDYo@cuh"hA(w3$ϛvY?qz;*܁ˋ ͔o\/׸, %)gaNh{N*/P0v(= +0?ER.ocqE-輏'l$m'X +<۝|!b_7sD]&1b+r'"ؔ\ާ7-rܕbMxswq >"5OE"ϞGG2/#w5m_QwRrRq<$W`K(ABc>%%D 0oD`D :P˝nYE5e۹+F2Wͪy"9u/pQF$'ߕ>r3&=70N@9 +mUQ)!..U[_ +?KFJ4GĹÄY$ܐ''\]fG63IAv_mfax"C%k4BbT4TՂ"k<\{dq s7/d4V +{lhxZ]b[~=ifig 5Fb(ק9;O٧ViGGX_}n]wӸik6f:,p,@бkbmuFR!s37:!?`CW֧&`':gUDtx4DLؠ²CKh#NLoosR +mRҦUa9B6c.oo18g JVnv[[mFGvzB+J02Nhļɍr/ 䜲Cy5(raDlGx~H?I.ȩ GH;A!A0sARxJ8*ۛD[Ѯ~b +W.Q{!n(1_9W'IlCw'Cp^~u% ^pV& > Mфy6_E%.xA01݇"QC͈ZK+gD2sϑKtOt z nf9 ?Md$n|c-G\zx 1~F;ĚBtB":E&'>"3$vAPrfm#t&pJN866n^yOOMZ,z~ Py$;l*|\7󱇭 ؒvߪUDl?ᐺ,"uy )mܞhqS:wdREZk4cS>nW=kvKPmGYpfo&]*YV.#^LCxTrz )s=\||V+w]mԇZc&r~2fAcS_ he)>t*qQt`1x w֏+I۾:hYK *9I|8Yk^nT| uڿkVܥCA$r*S$s,ౝ+z`;mP걢dbP% @]6 +UUu +nIR:^ar]J& :þҝm)%қFҚp} 2e d';(MdFڶk ]*HДPPCjߙ m> wE2j샽/;Έ?* ,oqS6wniZMfOdI&~Ej*tcHV=֗7<Lk2XF]E=m,6rU蹲P/WSEFlPyzS8Q:/~`~kLS 0RߪnFS+d@wQlf6Vkd`j⻪5)0T5 8+F9yᲹl( ܺ),,&L@!A~VSHIe"3o%4*%yV "_;Sa8iqk¦&m&*F^XbBϓ.$%_d nC^36^$LI|yIB 3Uu ê+z;EQ^}NW%'%3mD35+۞AFI#7'_1m 1˚wEkG eZaV7Uͅf5‚"m/)euw"FYwI׵Xq [8f2n88EOfQk:sXpcuʒ@}5u ?TUf݅]m[-S]Лc<"_.×z;,^Aɝwp36 +i‡WRj|pI%(E:"L{+:eȾgdOژ9ؔgF:fՓ ԙdb:JJ[pdR"QfQPgeGE}bOSZ p0Jd9 ܽIrpGm]$h$0Åzm㍝|Si aYvq%ҞY[iS= z D>x̢L# dsV6S'ɷb]T%/y%7[]X%iLjj#]W=f&0]h=0yx\P ؋ǯ]uG_i?M^.ZRbG*I;Ҙh.}h!BQ0bPEYBtZi+m]xR)^FПf:s.C?!%8^k gbztٱh]3YQǟbFc:y'.!ܖYi3s{Wrp?sBe t`Y|u]NZLvB@w6}x*@z1y{l <1}8${ +uxo﷑yӚRPg΂MyiniƊc]*,X;߉aMAI6:)phgz<όۂx葿mݴbʯeW9.L2A\58)ݛ?htz2uzv{'1wR{Qx|Mlx|X4y}PCzZ}-ez~ z~-qԃ s-t;£9u(iv1}wjwW5xByvl,y6ycpm9+qr󊦡s2t|uivVKw߅B!x,[yiyweoSRp3q𒩠sōt{wu,lhv2Unw&TAzw+xXxZԳntQ gXSТ(LRp*৥U +!!$$wߛ{C I0B{M ED[T~{QfiŌ=~\C¤P/P%`vָ)Y /Y4T?5hMsqjq+:m'E)̐/kby@}{DR39놑>cvvSDžƖM t?65)`o&Z]d?qF4۳}lA(h|}]f;+ʹ @갋.+'n&oA\!R\;u"Q<7!ia2Sp>&ΠڴA2NPKGU;PomBp: oBv:khC8|Zp@_+nPy_@iiyi1&o)y -wSuP$-_gK?fBX/zQ է+v [+Џ3(-]tY!(y e辬9h ro/r%shxu/cP}O:kՏ@9`Ys3Crz 'Xk޸?i1GF)eDK;hز_6AЧ;rl%ۙ:$QFVwp04sf8) w|1-R͓ykx ELуCSL]*R)p̜:Rیh_<.9]QqAJKAmao:Bv/20%!;9)L͓ka{&(}{9lI:RV*҅VG܁`@*a/&]-ad{K͔5-O oۄs kgNO^˟J5YQhl:/=y%qNXD2D^Z@:3sz0ow=+,b{/. R0q 3NyG.:d*K%4U=أ9Wu"gb{3Umg5 g}xFO]RqޗŲfv5gW]eWC;SpFbHy flO0: W띀q\~#?RҤwPw@TzښY0ldӮ㣖e: s/:!g1!w<<>`Eսºshsj-AgAZB#J>ݍmȨtwXTa))A? +wGPXkm +_BIѨ+ ڣ㞠t7tj}7Deߣo]3%j%Hn- E/GXr#3,+b=?^'NC%}Uss6p)n2|0%wiC^0mo#703:|ܞ+¤ sW +TmJocz}a Ҽ؈ WǖSKl$t_@*CTV z7vdr]&4DiOQi lp;#K}&B׉/4"a$+08Mh~TRiu+a2&dg}y j`D/%@3J`<[rO@ojƛiCA F-y\H4tj Ys~T:eK3 +xJкDg&vhh\ +6+5DľGѺHs͉}dU3 ͫV>2  + ]-jxP$#LmNܫyZ44CDcXj66DgmZHSd}Wlɾ[oy@nę&)Ι;-~(.xrZG`znr YoB)`cТzٶmixC,WSeH)5ڗ>B>dS#YZ퐉FyQ R!,-j"~jo**e; +I`h]<nVD¤Hb,q(Ȕ4ZcGP=sQpv/R0!pxe_l&!?F%GNw33䊘""=XC7Βq/߉)˗M9h;F奢.qY8$I-BD/ĵ#L(>f+oZˎL;T +O'{XzE|H>PcU)'ItJ&}>wͧ3dQ%{z ;-'L5CXj>^!#ͪIWhMsfZC&9 +3WʏeL)!acE2'(h*A)?+f7GHtcfի-(U+bІ]Qx[Beb; \h3J0 DMM;Q[9Ԙ8L*fd@kR9rLDg5ٍBuJCa&NOlٯAM :m!vİ̞;('y *1\_M +UH1("D H٠hdC fܛABf"S"Y EQ˧ҴFTɍ +Ϧmߔq;K/G(:'ܷb^# jQtkA}R*SY<~522]t%/pN+t֬ Nh*{]YqxJ<+KKG/4x1%vJ1aKtׄ\&I0GIW)O1)}+% rD"Pc@?CNM_^(6ڔLAʯȿo>-}r7"w:()2I1_vZ4^u)j'6kV6V, Vb# ?/;̱;o5g:nĬ7},4mG6_n) yҰ_2l(󊞎hs`L]x7z.թQһޜ첱nEͣc:6ԥ4ݨ`j(a֍4kɪRM]Լe+-[Zb_Hq6Zن:2+}Ec]ZH&:LQОkdèi_V.36$o^ϰQ-E4<7*,+}3a$}r RHjgKV\zRffmÉl@$]{%qf,h`=Zo:5snX5~ ą(J||5v hGa};WO{p S*8"7xD"U+7euIR +wejwtpO1Yt37z h3HjYh+0`ҕ#ݶb?&ZQ~ )TKty ɘ1r)G^T>Q0ON\PP+8\6zQ-*Ze^Pxe +dP&ub~JY!Y/!]1h>@jVuF'lqV螩m1:UH≢8QEvWb\#ˡyNſ'\u/H$wuψ +MxoI&v}!&yh嫎\0txcL0UA6,*ilȔntmTiy+n^֯pLTqIG#s>jt2١2v&&Xw+Ix +̙+{Sޏ;~sRsi +t^yuSvHw>-xx2!y&.z_| 7~irzhz^{S[{H|=u|2}|&~NE hw GrW?gÀc]{RπHR=l2h=&j BP!qƴ%rgk݆*]ŅRztG77=_285&w3%ח :.q=Z\f߯ \wQ=7G= ׉2z&ff P!9pKfU>\r"QG,<ş:1՛׌t&W-  Œ yhYp,Leέ[_Q' FϢSu<{L1C`&A/; 5Èo}beHq[\P}FkǗQ<*#1g& ; un4dʬZnPM@FOQ;❰12D&Y <Ո6nk̫0d\Z;OE̠b;R1䛨%ꖞr TS_Ahnn^xo{TYcpJWq@WsP6OZt+v[ >?wy9|@%hz=tq^\uTI6uJHv@Niw6N;x+Yy j{$\|h=1~LW4h9zd^%szTzJ*{Z@S׀.IxB@%y6;+Ĥ7  +Z5x+!KVӘЍ ++B eq~[8RtH^ 6>ͧȚ5R*+p!$ +ɑK<ł0e9P[}Q-H.>R254$+a_!8F +feh`Nrj3.s%l5Vt#n#bu oJuqzvs[gzwtTaxv@vyww*yxfzyoѾotK*puH|r/v@sPw0thx"xuzyfPvyS_wz?xd{* x|fy|`m~o#~"p~8q~_Is~wt6~ewfI)wNgxE|k݇%mêo +gpqlqńvPsjd:tRQuu>Dv}))vgẃ~j +OlJrmo^pƋQu!r$c9svPt=u(v'w8i^kdminsx(tOgu@jgyiikȒl 8ԉp"GV_lRȑ!R1>f13fc3B}]ښX߯6ax4Ḫձ쵺9U[sKmyS GP mWlDk#XLꎇ }qK/cH+Mg53Y;+' 2rcz(NwV@87S߾kRn4M5> 6d\t"gE7scLìyCQte;Cop1\mFM9ky+@$z FJ(lSʐߑ44l1/W1cVģz&yOqu]&ЉO(aKv\qX!0͡#,WٖM + r]S~³Km#jc=Bf3>"d',EodbU})XO%yڪ!J@U~e ѼvӋ@qnM!{ n3۲c iwuCcS:dZ7;Kw%7@ks5N'A̝ra{;N93&`qhJۭSיά׺#420$j9CJ9gVK0+H`&~=s* Da%ZCC B l#n#Wϑ+Kd0y WĠFy怙V\0FjLMa o ".3y;XJēj3?$&LmB[Bzth1Cӑ|MƸkXm4LP3Gq!pbM-ܾtw$!2fFz$(b_n-zz1脝[Xdaz=YnQyKU?؎Z'c+U܋]ʅ3QC~"tN~R184֭5Bک+HR.MJ0Q ,; +=PmhbaOZG|mXs̩^4 >?@Uzڽ'a8,Y oۃ<)P [i H60?[^&=NBG 'i]a# +@LA5&i6u3QsN=g6P`f+]i@'RG@=`eU~Qa!:ǶWK_5PK)P>-hΎ7Tհ\W9'i6["%ӡvȘY) +IN0aI !5t9#$1$g]0d 8.{t7MO "nmaa1%T*CJPyj^^ln+T,@ :b̴S6,Ik0JA\,H!̭\R(`H:.pqIx5ͯkR|=m;WRq|)Yf+1~M"%gz6[*eF_gN>OEm{Qy%yI)/O"mr#yBAԐJ#ӖQz9Gz!dA[~2di"nw^XO(Y /"%gbJ'GXmlz@EIDz^9sޯ9̫166j?ǫ֢e,]wrUX>>%{|K@); 4jw@QOZO'8ey`2:<@}]bQ_fy֓hе>"!R Ycݚn0R}˶5\?fU$ 0FuR(` y>| \c]m4ӫ {RiB:okd"*> ~QuIP.|u7 d%T_bB=ֻI<~E=cn~ѣ#ߪ'l͞5u S[ 7\pBEbQL^:l +RV&yrQ՟2G4&sNwԶ3+Ji}/jZx,"d{~yGRX;w)G="q{,xں?U>)!崸tU3Dz.#>8DÁεǰ`G  5T<8 5tvnk0'#O<2PwndO(aY2~ ,⫈o0"fwqrMnHDKC BP.U%XdBfЁ0):}`8M{ğE[KHC沱K_Õ0A#^Z}G>lK0zAK؁7{"LA[ +CA# -nrf,lp%ǜ(l0yL@-yDS~-~e +S4c^z +]5Y&p^G6 +tk +΋@ qѶN6I`a?bꢗ `Pl{i0>#xz)FXہvgڮPxKkn Y^㫳3\~qUv*mI{RXj;,&9N\FD Dwx#YvZ򚹠NۙW)+(YCv:+4CYM,ٸrNF"I~y?)LZiI|`N]{ɿ;fHw2IİeL{Kx`*Q }7c}*72`Sٔ# (%,FS>?cu0 !.VvjnEe6f_^uUM~j~@∋j&}TX/jkB_yzn/a2=ȁ#v u({X/CߘGs ;9!猬/~u읪O8YeIqKLoRQӴfKK.ADcgT4X}nzQ+i=˸BXK}6T {e) rtv}I o.-p`-\񪰸YxM78WmM' :{*p^ʆ8_׾@u+dIWЧ-CRy:((f{3/ X']5ex%] +~%^ At "X]-ig48|GҴ 3" _pd=DSFEK T)[d/JsI.].K.,T *VWgŭu<9>GIy1iAH"M Pq n_}^1E:c:hڶ~*}kK2g%d_o: ~_/l9PKo:~Ty(mmaWHЫͳ/stMDxI,LQ5 SMzt)$"`rSh +K qr#D O[5}yH4&{zEk&Rkd)Y%V2@q3(9w1>h?h FFf &R#`T>e 7kv1ЊȬZ5mMӣЙC^(Vϗ~Z՞Iͼi:XjXG|(=EY6{T 5aȭ@<_P56{>HZV)r}{)owYWc\i#m5Zݑ4ӉUau +XQU<]-}!e \NfQ,u,.|O|f~\Z? έA }sy%*_I)ο`ߞ6_ҷ;I\la Ch)Ha{~ T' 3zIgKw{5czֺUw: bĀvX~fծrCi ˳?qLr(|# +/[cW}BwsZk>KcYpڸ?fr+"4'90Q&OVt\q]s_x>[2Tލ (Z_kN4 +LEF)"bv׸ۊ<.|ԲBT?X  hLcԀ ci.W$-lBB5 +_ƭ$SYo^,^rhT (ʥ )4gɒ?s"z`Gyو=\ϣ_RHO Oq|<  +6J rU>{E10Ipe6V DOy! + +ldRp*<s-qwTGJ*_Y$,';b,^vO'χjtZJԡ' c;l?o7t=-~Ԯui87uԕX.W涐h'y` Syn?wdT [{/\߻یל,yĐBs S-\r77dho߁Ua[ancOIw]}Yf#Z  ZBLmDCᖧnnGrν79 +Ep/dtu]9 _]l .}oz"{D| {~׋lajͯ{a{W%|%M,|C$j}9-}.VM~S"  ڒfJj*H`l`VqLŁvB x8ǝ8.+V"΂ƃ6 +/1(i2_VJLE0Bc8~u-"ٔ' +{ jh_`-RUKJB=8>ˉ-ؘ܉p"ۖ1vވ + <kсhcH^ۨU>K~Ab7--:K"ɕ  +.d!g뫮^Y}TO"KA]“t7]-q" ^ g|g[]٧Z9TLěJlA 8S7g-> g"W Ŏ .fê&]cSTJ]@Ŝȝ57/R-"q蔻H D=F7a_iGW¸RjN56l?D9m;EQo1qz'@sEwtbxvјz~a-oGWpANqUDr;3 +s1KuY'vxLϞyX |`ܹu)WQuMױvpDuwG;x<1 yG'zj柤{/;}ϕ![`oN{VM{QMY{D9x|":쨖|1}`'U~Z~9nK3`MVBMCICm:1c'<72lؚ3~_efVLZLcCJ:/1`Ӆ4'Sb[0(qtU!ɀ_gVL@Cyɉ:Y;1D'zQlqJ x_uU/LrڏC=#:(0l1#]'ό&ˆA0^UgxL&B@9s0/'uÎ˖H[[^YTUXKjBç9£Ζ0Ӡ}'y +5,~^BTѮŠKߞBl9I0'i6f *!^ȁ ne og +pirl(s7n;sGt\pBa|ur0O-vt<wu&xv ywxƹk^nmpx2nq;psQ+qxtrru`ctw+NHgŠ;i؉Ck\/m^no ]~pKr<9vsI%Jsuւtd6wfxhҐCjP}l{mn8W\oފK%q\8rN%rUWu'3cA.e#g蘟 iՖL|klm}[o.Jp8q$r1tb}d壉glif{jklҗ[nIp^85qJ +$q@t2ua߯媧d@fx\hozjX)jl2ZjmSI|o'7pN$qDsfaDceWgziͧj)kYmkIn7p;$}psN/`Jc0EeyO:gnoyii[ik8Y^l@Hn7fo՜u$ep&+sAt1dK#uf2ui`vk}wAmm wo[xqJ <ԉ]*DmW-9:E"%f}` rXJVwmzm,j[Q^KٍHG(َE4kXׁgJ./ w$턔@OxP +y9VlxZk|][$HC?nW3^#4#`'-q%;Q/خf̛`ƭrw O'erϩutKeJ4@x!@/I_!; y@8,Z=lKzQ{T/!G=Jl-x A 3F6rpJǚ;jhi-+ +4OyUVX=oRNwPFV<m1*GP<,yyfC%T/QyM&_Y88$ 8Rt8 +l/E@Cૺx+6]0Bl)q +kd "Ym?7hk$"P!(0 ,V ohJH}nJޣDY"?ޔU=a-\Zq`8g9f\ +Y|7j) +[ݤ^UtsNo׬!(rZ7C5y'gϫA;?JݎBņȐzc*K?]3'7(;\ޮ4*|KNsB!+| pF`OWp"+&ނhĤ=- {/5&2=(yA+=?9SJ} t*DlBU(ѵv"9s>y1zȭV!eQ6_T~7FD]Cw[k>D3 |LEH-'P8YOz2j = ^ u)>QJ$$~=Q Dp7!aa΄E?Z6n;u95~VT^>) +x\q})Q*flfwD,*'}=?BuppQF~)GOFWW/h4^֪͡kUj\uEʌmd_? +ʞouQ) E) Rp!%~^݀mlD+sbW4M +I"Tq`h孁;2-"1̄G ?XxAs~=pQN5F +T-Qjau!MPNi.? B+SO a_BL~nhw%G~y]wJB`E} oe7c[_j$'(߇ |6lTB򴽉6AX+aO.-P5醲=DMHPo5r1O+qkխfIRzjY).A[6FW|{./]Lm lcŻې/13p9紸9.Ï4F>Kع[Gi9—5XkhvY_݃ͩfs?% ixy@v '}̓7ŴG>yC_A^9zM4̠ug(.'z Oӡ }LՔFFO3^3iIfW +ENֆ\$,K*䙊,rBV.ܠeXyIiw[8Z'Uqhc%(>5$ܑj2\gvi,8ɂԂ;^ q8 +N(7O`AlYʿ3WGvRF wy&]=B)+D_7:zrzC%?*!SfS[x?j(S:PKѭt*tV{>9}ޣMJh 5ڙ" +)dfe$)eǵ5ߜJ \iC &)֬ Gb^QQCBe@|%xD2,Z*첄ɳMI}{Hϳ^k +:%(ʩ$?'펠dIW;Fs/K->#n\J5d["tY`L-UӮx["!.vψoKh++쭼 'VxhU]'V'7 |IJl!ԍ. ~;pcn +kT!8+G6|| {H+ƌ+h(4f[  Gg`W': h?|&b(~Rݘ!|_nCRעGL$ EJqtʽL:#sZD,QREIpY͊ŴX.ϰ),vUO_Q ǁ, &˵ +0kěY +0/J}+^,"8xd@1;t$6c-IC S~1L +%d57SE;g65N|v#e!!YH/(~*!)h#j |xT"JJӆ)0nGya OY9^`%*$'LTgw@!!a=>>j>mPbػnX!mSY%#C!ﵼ='4Ut|KaO+aWc%^^[^=3lXqv[^dVo{P fܮ=2spӏ7Bl{B +:ط3&N_=@ 9ˑlW>` =Fx8*>zL[`㛻ͱ%(U;hn頑̯ϛ'@5[\#/N^>-a#J-P +mW +ͲXY}^%=<\7yE0jw\XzZU͈޻g7{BmTtnj U5'_%_hI*OWVωsCU%oT_`-Id$,$5~u>UVjKDj2*u}? +MTC.aB.3j:@c~h$:䧒.u^H^*GAf5\џ] U-Lpt]TцPcY@V%/S%1z$f!h)ȥ,g$:b,6Ь5ex]>9R 'I*krG]ȴP$菜)LZ͈|s-hxVQWtTӧ'1uܭ}[3,n YYшXfU| qȌ5sg;\_Q=oqEI:GW-qU& _` "x{8pfj["PE֗9Ӗ-۔7ރڐ肇nMvz`'pCQe +D[;PCKE.x9Uu,ڄx|njozycp,%eregZiOel&DnX7ɑpY*r1fs u{Dy|io=ikdzC +9\ݔ.ޓ#܌iB  fhS_dU+LnȜB͒9qn.r9#ŏ` '2 +)eS}c\eRhHj&?0lB5Wnf*bphrC\s{w&}#di[XkR'm[H$n>ߠ;p5lr\*t6uw/z~Ud &oZqPQrHs>u 4ݜ"vX*uwFxKz:x} dcjuZIvQ swGx>"y4zl*Op{XOc|M=}]9<W;b| Yh|PV}G0z}=Ŝ}4O~*,]VCxGCbYpPf)Fʞ-=rH4v* v[H, aqXkO#FqÆ=*3ܘ0])𕮆X_oّ!؋pG`ٍgX)ȌO@ь2F|<ޚ93zn)͔#X f (`n!>WaNɟvEBt<3a͎)GOD4ۊr 5W:P_z.W1lN`tEYsרNx 6 x-<|y#z| G}3t&XQ{xP{GX|1>"|5ᣳ}A-&O}#ʝJ~r}<Xi`#5Z-W#ԜE(뙑eWق@4+X(4pOFܨ>.Fn5]-m#ݛcA8hdWծIO: F_=l5l,蝴R#ܚZ2 產9܆WpGN4?FSS=5<;,Ɯ퍐#Йƍ8DՋ <)WŖNvpF$>=˓@5v,A#Ù~P8LKVANDךEޥ=^.4,{#YTO +osP>j _kbӝmSewnhJ} p#jl{qm[|roZItAq~7u\si#ut Ixufihkjmlo]{n&qkForZsqStKIru6sw"t_w ewmxd's@ftEhuNEjv\zFl[wXj'n#xNYoy4HGqmz 6Arz"?s{ vk{a|adj|vf|h}1y +j}~il}Xn~ Gp4~D5q~i!q~U u~P`~Dbe,sg^wiyhkTWmnFo&59pz+!p tγ^}aR ֬:ڕPrXU6_Q8Z}^XpjCNd1dD @Hx*55{he5~,tUs;GyQF.9[YTʼn$fW`60|Di]FJ폌t;g\Ot*hx]/G$m#iZS;ڊ{s7K.>rv:܋¿zv ց h/1!y[D2j䛋 ~P4W?_Ia%lM wO ]mk~絬*Q5K_9]]V0c3KxVHK-/DP>H\/$iv",w ڬ(v ܳ76Ye4T" c.Ɂ\ 3[5]|ޢA +fL2`=t"C_P(2NW>K$ +Iy(¼=鏰5#Eli+qe#3^d]$ h7F2:c/۸# KBOq vJb;\VI; :O̤Ӥ`_ +Atp5X ]}=E-c&%k ~(N[঱) 4⧆x W@F6ȩٰuf#O~ W+ y_#Tw#B>(fbK%,rИ5yS02xbymbE##@ 4>-4zڅhx{]8)9lۜ{dX+$PV?V¸eexoz2u4y/o;I;,QjUO#?w1>5>S{)4\s(nfY' \[gv*!Ag6f˾NsN|r0q4Jby@Y}LN2M2G2+K7W <$?ъ0Gy+)~Ia/G<(z@K8;HҽYLIM )aQD*W3[Q'g4fp;&]It:\/&S1NbXsɣBߓ#A_3qnKnBp4E¦WعK%8\8{BOy$X1K!Cܦb b9!]?z_h8O.ȣmzL{8i H jE^ %g{2ekUsjOW W,X\5h[83] &ӧ~j"GmmnWi{^U_`LfF_~!q%>ߘu  "A6pGK1?noAV7nb֭J2wlk11`O'(S7JiO.v07U筂L,biHb ?D307!iX 4{/#R& soUAaW ^M;ēȋhmSNH>5VzgK'6J~.O]PCKCDkU-e' +TM3U$o;* -(oAnUqpsz?n 46ڝD%.C kXP$,P~ I)ldIN?Apc\\1k?6JCѩҩ9i)Ih_M͒'ږ6syuV*s_WaOUcJlt*! GcP,Iyi&Rae@(v\ RxBڕW/!GU,OdرAaV\ǨT0n|z E:G-X23nEOBQ`EH2ߊ8bc:t-= 0X(x6Ctca$CvnqOIl lO) DZ]%nbJx\>^O ~Z lV+Bܡ|߫8z~ܵ\_jՎxVGR?>4ցa?xN }g wFue>E'qʮvl"h(6xDQJ8PUd +/T$; +E1TIYA:H9>`'rd'G #0k6H,AE7e %a1% J"u|L8>b>_a~is[|3J e_*$6Rv{ݎ +|.'8= Ѹ)Î̥x{*\9RkD.25t{ʍoum xX%+0_NjEG7Ům}*V`JÖ2,fMsқE_g'ڷnejiRP GZ)E[Lёh_i"jeڿgm-&rG?j-< CNIUq-*lβEmS P""ϛMήӈhG#Gx\{"IaԤg@ry  #]3Ý HSGrDDD7]85/bEF"]PuI*2,[VAGBx͖;h/TZ><a(XdLg,hC~DW[޽j}2埶holpQITVE)\`MBU ^r"9vux: ɋ4W.Q26EpOhb۝{UȢ68dC]X=_v5oXk)aYAvKU`/IS9}Z=U4=h(݁{s07dZkNhm"}&\I&˭ !} v0V|bO&8>A 9Ѵ71iK]"]_@3W"CV}(s@pJvJc>a54.QS2ǜ(1#ZoZe]dWP_ֿIe^R-L!VgFz'j+~f]@fgy$#hw̦<{=IնS 9޷Ȯr>.~i-14%Q,{9&mP5&SS-\t94! l HˎrBSfӋV#aSL@Wb.{Qi\B;ыudqw0qecje* #3LMV* D4UA@e#CH `2_򒼑e(T-XQz=Qi-ZkUruz[-Bv )-|-=]󨌖d?PM;+{(h +ؓw8+x*ր!4Eǣ(ܨgè (1[ZBU/Bwߥ/ +h[t]<\,S +;H%JS ' aRWIzBr2ɑSads|b%P&Rdރ |NX9,DaB㬒;܆rKaH(_/DˆlYDzEFs<?;%-:m+ Kk)~xsnRB<Ÿry`^ye!d$_.\ +B>ق)p]4\RYNYenvV>2H5;qз=S$@ Ju"0@Gs j/fXfV!옫*մ /€,+ +]?TsdQ8i9CЦ1xWvWuZLOoeb0;\ hrC.LJM_OՌ5 NY@8oQ9Y7g:m]Řc pcMcu\Oi +h)%L۩Z<ËBMg˚pͣ1ZFx~|I=[BOIOPԧUAq6ڼEyio"Ebg7h04FnQ1b<IT?8=DCUr7? +sv2BԬ%F`w s0/Bw\CM0A& HԆ[p70NEӜ[" [2,2xKȈv@]uCE޷TzȓtST8{C2O%.x{)w95@j 6Pc@ N7a.% ֍.+:>XRP:UH(?˕7O.ȑo%[3ύ^D`S*7_&JapBc:%f1hu)j\m+;o: "Lr 㒐wPE}ReJfBeh9j1l( +npr Uu*Zy~R!jJK1l9BCm9UoB1[[p(trȜt7ěu 䗑xDÏ| QopIکqAr9ss1+u1(؝vxϛuwty 3{U<~WQnvIv:vAh]w99{x1ys(ɜzd{f2|} ~=} Q8{I:{A-.|g9 +W|0q}(›~4~s 3ڋxЀPH@'8X<0ϝy( s( -)PJH@*8a0( Ӕ, jl! UP1H^@{/8~fB0 (ъ  _FȀ/OťϐH &@=Us8N0cʎ|(~ L S`ËK Ï"|ЈvUPOdTGĢ[@ +8'ړX0E!(jk , pd:jkeZڣg^iCaLjdculg3eneiUplDqn3rq 5s0rw wtbdxdBfǒf]ihhk`tjemydlUo~Tn4qiCos52qDtqru qv.*rx +YrxwzRbu]Sdv:fwyhwkbjx]lyMnz\>Fp%{ -q]{-qR{w |ա3`~ b~1e~]xUg6~jJiJ~\kQ~M9m>=n4-fp3Dp)%u-cgfIYhQK WS`e)ꩈ X, (C-(8B {so !܈ WxĞj}V=u=p|?~JuՄ9ҊMnUMI)8eռ`y~~hcԏ.6vI6#e6W|]Agp-U&SMh[LӮ vuUnE![sK]iR:tP+gp6U~U/~(Cg=yhжsP"%/V%5}◄*q0Rœw0g3ƆoLD>>C|F^ 6oO WW.!$Ր OԯjSD-o!Z\))2ege.˟ 8PB$Ũh$v! + +fQojvlK|7%܃(.CDZaM\)>-́˧!ct (Z1n{sBsj*!-QU2!w\h?||s:#^.#LE)!56Fy.[%O:"m].^(-#9UdiҰxiGMF*]As nks!Yv-XsN8MU-ɉF-@k*uOէdB٬3VxeuP<4/۸X'ߘ&@\ٵm'FUpW?<]U4fˏ"VYղAkJ)n +K{vʯl1krȴDnʚS}n8)re]Նl*-QOdW*b@8~(M;H?e>amаcHlBy>ӡ/ʰ%iK:B/ߒI +};p"[՟z.Mfہ1vX˅\^0S}oe4FVZ~gZxe FaL ˮWܯMZ?Z[Xj~ـß++V!+*8X3 +syxhYSYhO>yi*6D `R}䪪p3=]rM>\ ^9#&ăOennfj)Ĩ4_Gg._'HBhwj7zЉ5T>)̵;*lSTmVlؚw +Ԟ64%O5:N '7Mu^.V~yG޵wBuh`4K%?#I>/%O +f c~՟%xoGP*/91KOGcjGc噉X0oNE +>X Jb'(%7^Rg#=X T@NKdAב!i0j-;2nLtO/o]eJl~?'{W x<}G:Eo-<e;Ѽ$Ϳ2H% W!'E~R'~cK2Lf nZlB''x j3i%y15Dȷ"@ 'c8~<F DWxwŇ0C\Tkq ̸ lbLst'FL<$&ec爗I՘=r_c>Cl)1^шnjK/׶PqB?]#-XkFϫ[ЃjϨ9Ruf";!8(ae E;˲Н]BSȨє(C HP +iw1#BmÌxhp;_dRJ I߰!_cΒ#Ղ*MWw[q#`~]\zii&Nw-;e-=aufKP;j WXuXs/zjՉ‹\57&Sj̵4 h3VRf(.wJgܺ5-\S ‚zt.1m͝_n|WX0R:rV4&DYovA^iTm &I5&`0dd(7FؔaXv[sG&?N}kb*7\}O愫V% þ>еK%J>=.w56]-8 +bsl u _7L^c"^hϥQ&RP=FeYs B1*Hյ!,^Xֈ6gv>hEs׷m}NWp:,g~a +gRQi"hū@>"iVV!dKAd2)Y酼8ʟ"/CgGdrǝ--D8cK(>5!LqU +r*d$("eu1Lj9YlhW ߣRUΗ4K !6=%5MQ&3 +s&$&H(QEf-1n%/ش‡i BnKL@h&>W&Ap[Z72(MIIҩl7l'i)3KhAwt-p]t"r"k;)%-[[_px޳:iqDG_LÎ̭q)?5u:gm/H "ϊU + +[Rb'3 )m{7Lm(l,!;k-X5;o)0w#+fk[ԑ<c6[^ h\@6Lzy: VZZQ0!V7߯-i7yOܓ|]lrcmY@x:I{sT3Uy9bWͱ\ȃ :|@Q*x%MnU͜ D ԕ{S˜8QU^{v[>o!za^5<zsr +/i DfhsSNF|QD1_-&AʘppL/lh2t3fQ{ 6 + RxW9)?§Xk%'_nXHG)_3p<Z i1TU<-t9K<TWRA2E  =[JW#ʼnPZw̗O{T[N: )8r.F!>2pĢS2-kBؖZ79=B)ZuRzt+Ey/Gب\rLiqZ-0  o^njCiFy=>2ҹ.Cv '[jxKuJ zTEz\qP]ټv|}%vOiG{l2FEYs7\"eJmA:}K:Al8ڲ65#feBZZޙ DNC@@ăLY+k'kwڬƮ)۲tl6Y{YSuY9[;ܤN iAEOX}]8E2ua<&"v'ĹIpL) +LWҞ3]LUiyZ _|pi#5M>AV@V! + s)lx +"hqblyR%I@SsbO(PIe+{HĊ&ӽ _[OBTF]|}ہEҷ-W 7O ^b:ޝ\TR]SJtæ|9د"cl 1V45.^z^t5{Z\6]͂pEZ[<-V;s{4%Ikp^?]ir{b pS/j8(@+Ԁ` Cr-L>Uduea;-(KOsaU3HEې#ce7gc?ݛ@fXO+ExF3ׯD+jT%nOgkGW%[XWw$g8g=I2N''$JxXI²;co򚑄2r%C[(f18dav1O f!}y︆d`@Xnd.# 9h-e12IaX\a :D&n= ~!mo/p¬鏆=^*KA׋oLdI?$%KncTM?UK<]VT=Nˆ^Ki+y*Q.` @,C7zuBz U8@îRmF$+~P=Vhʍ*zP/UQ/mx/N2Iyj>7K.g:R 0*g\rMY~il_MxĪSrPSwz6c9b Q6@['g(WZ8DmkIz8^":y@N> \ڛPay~+(a]3DŽ 9ݞ +nqwSwVfj92DT1DvPF k8b~;d]~>n.!NyN7NXəC+T8NIQS`Y0?4 x<'Z7̃g!mDmm6ߑ Q쬣?h:il2vNR? wϢEQYN,Vo"~rH-ķzte>P7ϘioΊdXYq[p0VkWUtڰ &K_$k8d0O#VneYH36`e. )2ZNf2LS~]@Ld*Jq7wE1-S &0)xnZ{DeYJ], A-bt?H4/s+5iCD9ڴ$"!Ղ@UDm#r5VY5?bkwp\|i_91:eM4m~2@WꞁlZ틐Vr+S#Z-^[s1;QثemYeֆ$9cքZ˺eM= ""Jy\@ +.lsMY#q3'dy)ZmŴ-IF^hFgmwp97ĤՌ^U,_{ܳ[ǯyTM'Ӗá5<\abcP%:9;ӉK}*WP­3`Ŝ(jJ:vc3%ޫ$(DJi}f~s4G1x"n<rz<"^Ֆux371|;qˁZ +8W8SX.yކ>MXƠjD!E8];SZ]O!ﲶ+'+{_"6@ cpZDN.᧩X3Agis$ 7 U +6ޢ*$EtQpj\ bnIH_xBPe4K˜\FTQ +FעkRrhiq< +CӰ:5տ5<)J)79[ |#{N9X2O=xp?AوQc_;|0,QPm Yůh{CM6gk uZk9>gjc:cY[.[|Uۊ9'a%9IOeUN97%|\-$_[׏Չx"e1/\gUjneji˜n`U[ %b%?dw}Zs,pvH[xr|S%PE9'#.xjy[/jm=jݿ!i<4]lmuS"l% :^hh^ԥj9ض3t"w݉e dzv~6CD],x36H!&V>;[hdtٻH{q d|Y,> c9xD$[B\%xLGԂwYlAMk2 `jP_4 TG2¡p[st?pÑ,4<qqr1g.56W[yJ忢)[HE|ou|4.X|$^s6lE2C#v/^X I/ӧ yD"Am |y̨,̬MHkU]{i%鉪Uo*c`YO.ӊ$&1P/* _l4 +qڞx 03&<@öAoްRӉ p-bѿ*pq&s[n)kŻ뚒ӊ50NC q,1˧ʯEϴ3T֒=Eʳ +_s] ʞDCheG)I oT?NbEܜf+'}#hX<'{NfHs$*'Ofb:JCi+ά֯cXgd2_ +y/\"çs T=u^an`. ґk# ,hs,׭c/K_jsH;s]d72߰Eΰήm{KL41#_myZHTm v3n8r4aᗰ3HeG[v;#6_֔>!IFQG '|6,܍7Xo`+.8aycM}M?E$D%Ւ۞M_M=.ZT@Q(KC[Kk%%ɪos<˲Khhp+PlJn i;YӎLucuj8R%lWf&EUC%X[9ᚲ~$~uۖ0Tq,D]S䀡m{W?Dn:V,QW +N*p}5y HQD lElY  ɳWd2DPpQڏB=ֽjښ‰@(={zW7?rv﬷@c />QhxּĽqqѻ?V*w}Pm0ЀJ:# J|)|)ǿ*RKDžQYk(PԱuA=- sIfXX@x?X<[Kz 'ᖔr"^lQVFJtJĹHqyB+h PE+(@tjp['y `-J9:ޥv]DWY94AK+EC>;z\5G?Ko 9 n-,@շ5^"0u;^=I'aEI挪(Wζ@_s͟a8+ A e5mu݋%a3mN&Rjoس?ϑޭg5hRRXL@,L u`~p),?CSI[O܃|_HKES<5JE)q@Gŀ!/S oz.A9"*Za[)h_1pl@`ڞlHR7x!l~敟 `sYC`OItny)ՎQԦd +AF~+Cc9/NtӯܲavUџ$MWM }ō1hmohF^*; q'-oܤcE[BZ[5)8!j RkIT}):Sה̨]\B`~] +4if$"׵aR?\]i1`qCC6g~zsə³2_D2ܪl~\ܟڀVCf?/f2]INTx9"Wsu.BuN"*Ţ}U4LN_ψU>Ѱ$%&]*_u7q28}Gjr442/%dt2m qS٦#;WC37Y((Z_qN՛oKc~V9iP0" +D729X.i' P# rCpRML޸;a7ʠ+-7c|\LY05|-lŭE0ERJ8:t;)adsNr j,܋KM3 g3l+\do9 c6 ]]7 rPݰ7sƺtl˛NYxnIFaxKX9|ޤpL.6"YƯ\fbl~#W.v ݠly%yrބöuW. SskhnsY'{[̅d6=Q20Ha `Y2}z-j:\Qw(LkK܎u3Q;OX-_Jȵs,ӢHy_al+I1\vX8Ɵ?3E xn>P0p_c4ET~OSؽxz!^߮nYn,9p^ƸiiEyS#Ui1FvHo6T_?SvSthfցS&+O(1;cQ.a";=JDPDcL&aH'lZ}GuyC cfBׯ:-IPS}*}!G{nu`Ev쀟i4i- 蛨I 2@""'t-æ՘Gj1#@ffkB +p͢Lp 8GVms\iiIIWBO2zig?UYng`N,DLs9Vv(ؐN\+f6D)->Èj7zYO3 M3v]@Ĭh~LcptGk4^ZbEJh}]K'eM=ƧYI/ #b Id}H/0,1hg*%X*$v="}U牮ℤu8QE S!ڑʉ!J%x-荺j8FD +q`5zkE!sLFI{ML&ًEt:pbi)8Ϥi/%N`s:Gu Mhp_y4!i2+ +l4V?lT4-w Jj?ƕN @hQϦ`RP1Ȧc+_UߗTB7Ia -P` &h +ަ^YvUYTWv-sVT+Ť`()z:ܚE<Cp BT_ "U/<,V#J PS0uF5E@ Fr(:Z'NWW8\[ P%BI m,3K2i+a) h^VS(9sP.D}ƴ%DfB+҃ +-6 +ڪ K(ڛt[ַ=sWƲə0M9fmS-E5i~[H3NKt-dI:;D&:I4PEJ͛$n3%lO +si9պ11BR:`.nX?/ceDh#-/~xV/FS,jLH|h,FB-AfC"49<jY2hďlBV햎PY}`4щD/P9[_񚏚Ή!3VTSsgO߯!5҃N,MC,RXQjT_Tښfbe_OJ{|\]EäۘɊm+>@3%io}Ԑ{@\ƭW}YM]LLyn(0rQ>bFH<)(K((pHOK P%:HBOTg=/v\e )/HŌމTyjj𔎡bj"!N}ܷ! vG*&)H!J~NN3wA|Rj&}z54E=sWG>Rm#,RQm:{Qưp>mhVɞ/ Gqr +Źd*G>ꈚ!wM/U?mvtKZȴ*;FQ3lI(Qe'NUܳmym˴+[o)>3SF&׺n‹9@-Υ/C>Sc%R_k!xԩBn2d^0Fy㫀h|oڒL6ǍB> 4 +6۳+ wկۓݏ?ݝR,R6dvCSO\e=~|?2}^k[Ez+Q f۶ xޤBB%],]`4M/%Cw)XMiz -J猑R/ܴ łb掲Qp5/_^%reD]}&g{ !e 5"b(,CssnօRї+cE)Jzo^.Mu?% ZN{J1FWś5\.9(*7Ǖ#h5WUL:[%ΑHۼ|R2I@wƷ*X[J:\ר +㉙"r^vۘ[z1:._%DUkv 1kD{$cf4a5_V}ӌCSNp0@jʭ4'Ua([b7$ p& +sn^m7&9都،_cDUPlZ[ʳ>${;^AǑnΔW{CfG Vj<0AP\2{#g? `XH-)7ٲTvTo4n/iL >f=4k=Èkԇ>B +R:Wj??8T5ՁzҸ9Iه8RRhLa<g-Osy:@Ɩ^gqɽӵI%*/9b{V 1]-6v= 1 r}ʌOex-HS\+MBɉWPGIx2^sMGO-V]UD` bOǞ ݓ㜢bZA<((&)<I/Hpq++Mt}B?5XHbcLb\k>Ex@X(eJSq!dUG3OMRiq |-!at.B.ɯR|])P!dHTOk$'QkV:~FnⰖmU W*qCZ\k>E +#ؤ'k#t]6M +^qwQXoM9CPAQ),go! X.zZ,. +ˈe1e*S%^[W5xQav53< 'xJ8:b$W[Tǘ%-HI;Ѭa4²6ܤ8z0҈p̩K{oU/w +K>R,VR;c=eNfMHNI?=)P7hkʔ]i"r:3S8=3n뼝X6&ՖO6O]DRǩGvT[UCay;쩢}.g\ 8^O(+omd)Ke/Ψ +W @4)q&*KJVo''7F)$b4^RuHUO0b +*vc& _'ddEP[+~!nZ}A`a e52$mGhTuxoHAR4F +u.ؽo {A6 +2:JӘ;rZ9C7h]]yz.olcG`z&y^F/~Ƞ_KAƧ fZ:ol#fCd$wg)xp{WJR;%=NH9ﵽFI+dvy ֻ^K0Ϭoo;X2 -G4KAĝuGZ5uQڛAmY@FO+/2vgrZ)*8߬)hR89\` $u,0(Ԗyn5bcr*bc('X=-%@gdM|eODs+' VÓ˶z4oПXS-տl1,ddnc ڌkwDu![M2$sV}@P9x4xKD1QKξ;t0;fN Htݰ 1}-_Yv,}{] inxuQ~LzC;5j_:O᳠Gʗ + *hPV5 ohiT$ǯU{O`[|qmWհ=$|w2gVkiVy4AeLRakpWC4tzEEgvؤ2yA3T8#)RbGaomUCuHa!n@ȍV0OlVZFkSh/9L6A,Q>ceZ(kWaTw)."1ߢnag=c 1 +R&!Ӎd%WS.Y]-㐧qbE($L\J%+49-MQ蜧H}1HRڂQɀ+CJencJcrZ٣}^$?)CL~5cMBɋVJSk`!rjY>E]Ϳ hC5d-~sޚ+2f;%l<)P,l\(N5زM1bǬ揙Lq8!1,ws["mqO~[tY^]Y|_~kMlkd&g~(5M'Vڔsm|2[Z]-j㪭<[鐟5 j|s$N;xGݮ^Zs_Z5PMYԤ_<]q(MO>\_HNW/Z7Buvff{R9VܰK [fISB3E;F<BBF>F97g9 +.V)mvpx"#nɿwqN-U5UqNs\PBQB[9zvfA4!k'VcdBuUWC~&?ܭ8ktJlf!Çx%< +U5D@a-{Q"1uE⁹&)?Gϳ7bɓ2`b<[X*v(4Y%bAi-ߠ54_L1ӑ~Lz e $Vٞ5=[TĺnqA+ +,} Z/xD@'ʻIMPO;k( XnfȇE zX,oau +)T_[# Hc (\8 MҫH7ZkM(K|)G^*QŤbV1Tڂ<(nAHPX)e";F~ 9 ztybTނ4]W@̆y!R[sdz|T&{}0G= +dgn=UTcn`"Ahn~k{/} +}Z[i1Xsɍ PSeWcsoD>/Dk:=*lagzN`CCU+kSiտ< Y6QU/r,gF<'H +IoGi'UQ2PؚEqmђO"ٕ-`#)t˴|7W2c4RruP>v +R*\a|k(㍇Wdngt~_hרZahGHU+]5T:9HcG~MLߩ]B5r4~ΉH օ/Pf +O(,Ϳ~*w&{,;>UϔRؿyXTԊG=d +V^0"@2 o=VL2'\0} Z.D"O! B!悰U )P)sy w" #|8/Fs6 |l9WQ}VөD]3n$3 +O fu[}M:L"C8fXI]jczI<8yS);GwwtƱx3e᫳2fשs6yZwx ԦҩIC_MB?H)]iRKx9e= {|e i,nAspTY꬯jzb1|#~6[(4 [`! + ."2.@HHɗޓ$$0PԖ{VQfHXlSD3l=;92v^µŁ`l'ԱX׊w`5qꨣڌ:~!vbht8x\JpFG[zP9P=V!:[1^Lע6cTDgt)egS:HpK>\S4փ>jo8D|(-@7*i cAx6Rw Dv_+h̔y յKCF'6^8I6k|.,jU +x4>40wPs)x|i5t{.ڰ{KJ :u[p \A Jz!`Q,Fiٌ8D?GHR; +[vq]'UNkB%?2/N,w.i$e~ +֌+3: e.BC1'N39OP;6Mz%s5[2`eSO)YZ6֩2nPDYY^gLGVɞcdIvuxn~ z똝KvPG*KsPydI=X&(I6SC7"g#]V3 NyjFo7e R 2!]$u#?*^M38_O/hPyn6* nen"°D shF0hÚc^'ѽBB-BP ;܄)WNܩߒ(-k!7o<&X7n*,4z%_=^ I6"56`F_T9ܴGsXoM +@FM1 + %Pw9'{l=nZ +ZĪ8njŢx\g}/N0NXe0Bhp]*lbNezW,Q򏐇ҍ*2er<+b"g!L#@rH +H4TGDR*>]&_ e Kk +PyP*Vo致ADZAZC .Bh3U0oPWT}wwI^8QP@_M` jݸb{?0Qe{DYP Ah ::*o=,:IsE3d=XX4?tS땏TIoCC#f \&IdR + C&X({%彈>S©!JT*Լ»gfDWCJPǚ5(ZLϒd(PwȪ5hTwTSo3 E|e8bvmrSe{9#Z?Bu&:f9I2 ,,H(2I0싟"[ȏX>6 bsx杴AWV$|mٛƚE<֋)zf&1HD7c #Xn +؍k"(oh3.w榈gF&r߆t]炻S8:c~_X{}[i}xLJxy_FopAhg,d}'UZ8 ͂Z1v4Ա6I@SؒӁ0R;`m4} +lQic5O򡖶c0g-űZ;wN!\ҝ?l !uK7#ө&n (m~RʝexꟵf>&|Igg 6Ͽc~&BNŜmȓ7`s2tIaunv:c* +zSҩ3 4ɛZmhe7]I&Bߜi$/P:7ji<zՂ O3d{4ycB>"FMA zK@~ ![Ea[mkn>9 |^ *Ř#tÑ}<H:S`9̌lv>h\4pEqyIu}^\2agYs&9~DV =W?8K{Za׷&[]P$`)9GݒFS6ИHzԱJl_Zp7',n)[O:kL8r 5l6rrK ȡ@}Md&pJ]zѺRU*RD_UŃ/y>y:?Z_>;6kfD>o]BnE!6oDYb-*c,{VU DkeQstl5ǕuRly"1 7YZ +RR V=jeپp@ݶ]c 1ij@#-+g5n@&efF}܇)2W + +~U^rViĠؖ?#SNf4sNJjKtrBs1t\ Vn-WT߆|pUa%jq.>!=NϢu HٯڥJ{e^ 0)A !@+b>"I$uAQOe {CrIYlPfh.<*4M_hAtr&Z^W,1薔#cƤ!F1G&yʌEHbh-¿$V屬y|,} XG]ߵeJu{py5w@]lq kAWy+ۇOy>muIMG<[QgD[0nIr+gKs`o)1op؈{JS6G'2rc%ddu*I[=*ƍ +u#jM|G+Tk*+g7Ls0U.lf!9q]gAiӂb1Yrރ^YvqM( Y^;嶧^GFxIZ*)zE b2Kύ R2J7q/8H;2 ((I ˥y]qj)[/V }P3oppsq~ǜP<*432%)m2sx}y971~X|),4)sk>TQf}-37$ö˲xIb4OxPtmNXUiY6^ٗ`K΋Z]I8XԩWsߡt5[ ;|?ݐh1S Q 5#A?0M:Bg/t]ڐfbNB(sdkBEu8$FxM;-Cfr4v0ࣰ@gVW]lB+6~ƈ=Kchۄkz&{nj&92ڌ.ՠq]!T!_2ؚ1SQp,a>~~)nZd|,&-AB "PwWl( ,Uz 5K%!DYE]EDVPOt mQqAXx̛7;s9|1*FJ=#ESkh aJڼ +AojJq%{Zpr?^lTcOVIG9z9{1!RSޏyk\7(I&dca0)>wM V{V+y?JEVH3\~;~FC]I<$m %+CU8S\ +pjƷ +i:T 7u8/ -O_zil,+'u^|,%Sbqz1LFt!&8t -KVo~N4Ғ B'~@ u3j,'Y9 +)qs~n +ͳs䖖')r|fql6s#f!xyu4_^O֐#&rw)6AZ o, [dm +rS VY< SNy>wp +zm9 1\09Maoԝ>jjXd|nY!^1GXYUaV(bpm߱q<'u KŭabwLf#fA +1 @(.$0cdJaFQBZU{c$+h&1Ľ8*cA4S"ӻɺ&nǠ8[E7׸m2{¡ 0\½ȏT7> &Qߥ|> R#` +cjmZҒ՟U**ByN,GQh<*.{KTmI1i!(GAO!A,9pyqX)0qP5T Of@`ͯ8h+a8 @;48:/:|@hx^lʻ$6Xqc<y/# `}ߝxC!`08b1 tT3J||5=sEI~*(8$K6#Z<+ųmָX;{'WI6FjR;#sCyvʺ&+6(7J7-[s~k?P]r]v7ir7Ѿa¿D`"c͟[87S?_MiX\FZ#P#jJt QSKրvB7K/Hoǫ5L +LffSV[uVC-6k!7)!NjZ҉LqrF&Yk{<@\+$;rH?<@0A%BCD +EEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abdef%g0hdۣ/Zߪ ;iǯ0Nlɸ +,Os>lśHq̙4WxԘշ 5I[lzy`F+qO, uO)lF!gD!l%m8c , + V # g;^0{N"qE  !"#p$G%%&'()j*L+/,,-./012p3\4J586(7889:;<=>?@AB~CoDaETFGGx?R@,AABCDqEMF)GGHIJ|K[L;MMNOPQRuS\TEU0VWWXYZ[\]^_`valbacXdNeEfd(؀+ˀ:JXkeVtA,|〟~׉~ˈҦ~ч~󆧨 [56j_M.Ub@p, w$}ߓ}LВ}=~e~#z~H}~tAi8~T~͆@'~2+|םB|"Υ|/}s}F2}||}h,~S~F?~p+~| |0|INK|gϣ||ݕ{m}+g=}S}Ҍ?!~+l~aW{{P{ӧl{|'w|k*zj|_fo}R}q>}Œ+{~{J޲{Y{j{he{n|y|ae|lR}>}r"+}D{ďa{ȯ{δN{0ˠK{lv{_x| e!|kHQ|ȕ>Y}%1+}mzhz8tz)!zU9{.ŋ{q6w{d|EQP|m>-|ڒ4+}3zB z[gzȉzNzv{;w[{d&{ɣQ |> |+}z*z|ōzNz ԉCpu>#%4cV5)jjJ:6.;s~jj2ըVCEJV+ǫ,o]i 6(S{.}_gH]W-Pđڬ%AiM>N{+q&CUro*pHh m7:BO"xQOi +a;|q7bA'):8%Xܯul4 +}D0[~->IpcOYd1XF h3SB4rE:"<;$;:ׯk"2wP\%U:%#y+hAZqgwu" {^!xH_m0ξJʹ(%@h3ҭyx +1@]!],\~HJ_? L@ M)LD"%9)ey% :h~!> n`NN<ٵ/?Ss{=ᰬjxkz^[uIN;3є=&^<仚 o҂WxM<iOC/2ECm*wdJ'ni}gOٲx1X{2M+HIP4#x06fV r]2&n2F߯dV n;ʣxfI;Q5Xm`\sc2EAV ]+< =.ಈKqytHK?dy]D{irn-< %c4VlsNg06Oߎ.'p dwIsmSzWzWc (e4VT8k^ԨxD͉I "ǍLŋ]ؐc?fljj5[c/ +ʯ]1GjÖNgGW,T$=]EN:uİwv E{䱙6Chf()VލLu&oޛ,fR@7W}(\N:__zQ> +}3;/e3ކTgMN2_6Ç=^9ADUѵ,S  ׂT]DA`ATT%L3Lf2i$!  D +b{}z_![JR?&Z;AHQan@({.YOwL/ W qxTd`Tk  +18mL~H nx`,F}/ék?٢ɥ<ܧ8d~G% I=='o\*_!9&(=zN4脹QQy,\e;B*LM//eG9?~?&ʯ -8QWeH. hU61f`@T'@@qˀp =­ ځWTz 6_B6P=)O-e7Po-&%x[A/ɷ.'c叺O( !#9KM?!׵MځZj6wu1g+m:z G ˷ˏY<"ExQtꁷ JJQwCy}yy]Y[B𬋆MGpm=>K;9qw 0\u^>>P5Pޗ4d},..^Z)QQiZokH + 0Q@s4rStvse<:lPYՒ&JȜ^4lM^1>Q#f/d!rE@*vNCkVT4*HyXG0L&8c;2: &w<-xe +3lX9cŬ* +d1zFzii0k + sʮS&0g'ǮSzO8-1vԪ{Xϲ5'&'P% Zݵah~Ffe-:K"D1o{|s6ƌkї2V}Jn-k_Gbʼ 6O+*Υq݋Ot7hgݧLK`z33tA7ʘ")>]UכY~ECh{,}4~O I61~W2U.Wg z wr4)6./s>LwW ^:9ɗ:- $ s\tPN1~C_Eb#^a>녻\6y5U׈Tw<)5>kN_*0`Gk;e[eZuNdO7"&ݵA|wpL: +V^!DL:IT- +˯^O8n,t +p6mreYME%wHҮS&e6Nx&Mƀ 0s`R#4l'2n^M\34\HW҆c 8a8m% #(5&^}?q% |^_S&5c8KS"C#%%{$:w"Y @'\,U֭I+Wfy/J깜3Y#q$tkH #ap%Xҭq_˾Llq~zv3^꥘C>e~)~ +?b#2i7 +T~-u\ŮPC3%yZhWr 0 Ulg8YLGBZ9@sʤCYxmԁw ݺrUO\ATDN8I,~,bFAWPeW3rReZ]< WݕiD`6pG,:l a'["[):ZɞmTu]RrdZ!?QipvJDSyB$#уQke7ֿ-^]N]mcQ{vWQLBc^6IpgMiK?=9`Un뢊g$ܕKVp jҙk<|?wbfĩFi ȫi vX˲&e#Z3Mt "hX xHur@j{)NGoAe4deщYZڣ e"J̴7x;buYMI :A4M +ce7F +XZ1 +Jpa;-6{\JdDӎ +aM'UL7,NnfȺU0*B7K8- +LGA_֍ɏCmɊעsO 3~6lMs8X?נ׉X2c B]UtGݡUJ9ܙJ o>.)Cd%ޠ:V uMt лohܮ=ߢܘ^HZHT5&{ʯ +|D=๝9_B.R?vup=oiHӑVuGU΋270=[Ǟ$2øR>6ǦV\l;Z܊V +Kv6Iz D[BGZO7e֢&[)\|Jd('Քȏrv!^!qKy=#ksbh'9rsrY[klʂ4jY8I{P,YM⑁ PƜEB@J=GLu,Z[1?+4֩}֮ZFOQ-81Tzw UsZf;9-rI7x8.ԜY~R8Ѳ}" %Ռ,GAW"VDD_d5৪x CaB=H~m?xH =EKEWOTv>}-jNH0sQIFa7;&1{'"kz,hV~}Hr[gǓR[[=P>F*M;Ec1߉'ъ~ ؝,r7MfGx[הPf]E;w1X( + }vWqĴufKқ[0\{PptE͞w;sD1݌(,x Y53Yh}qFeӮbZ̨P$nE ;`Α1l(I |άpس.ejZsOEss;Pjh1 m(b U>|+^p~neqZ,1O(EQa;C1(ui9 ڈsZ~v0}jr}` +}U~BKg~ATdi76.%Bdfu +jl_޹&U`K5A0.7ueg.&*%;׍ɂ΁9˂u;jx#_TQU:&K=Aτ7i.,q_%[7)̏#uEmjg؋_U-t|K +BA%7d.1͆ +%w*Ere u`È|j1{_r厱U"J@pO7bS.C%VŅ$Wu ܗQiݼs_%2TǰJéЎ@飹7c._m6%4pڀ2͟ tN i^ⵗ^TgJ/w@֣$7d.xՊ&iAwPGtsˠiMVD^T]җJ}9@Ȣk7kv.J&GN֩Mt2MSiѡq^YuT&?MJ]@7zp.ˍ>&t#qok)shź_i^2SKJA@~7{ .Օ_c&A@sh ]곗USѭ\J+#@7f. Y&o |zo|z̑}3{i\}| )}|~ }/{~K}gn~~*S,~~>~*(~{,x{o{ta|k`|ǂg}zX}lJfB}ł R:~>L~dp*~5yOAzrYzC{b{͈ڌ|4y|e/}Qa}x=}܃*\~;;yTyg}z zsz{f=w{xd4|gP|=4}a*&}׃lxE؋xΞAyJ"y!z5j>zv{M*cQ{܍O|i<|\*}rhwTx(3xy(NRyƉz@HuzՔb{nyOl|<|*(}VwՅwx'ex|,y<yНtzm}a{ +_yȤ!Mz_;{'*`{ދ眃tx؂yvz+9zқ{ˁ|?ur|bAI}O~?;ހ~)lẃ< +D#;큊ZtLsa1dN/xN;SY (5eڋȀ!Fvh$lXP+cu}*C+S,A@OB!@#&2*!Ȟ*RXQkN=%Ȏ &,1j= 6>Ǐ@3X-䬛RK>v'A5#g@x+7w~dCZȟB|s% 1 9ōA䩝SUYY=X3 dgcL3a ͺCnԔyr +lf;~7ޓ~~5f$|ZfDu`2@$\]G\@]zTe 9 4[y#'LY{@m!c>zn:> =.+E)pmp'^2%"qJl?o[:x``:X%>]RC K_ 8mI :u$iG@3zs( PZOr_xs.M"`/WS$nw",8X40m5<ńo~h n}DnaϠ+lQlW^Sl,]{ v`(`R顷Uږ6H['׳W6!EI>_pz)xxcvL0bquf 7?jjh>?^\Y#ʯG)RBQX ~F8F_D<=v~մgԦ4bDFzGv?VBa_݅Jh3Zd+XlJ˵a?wJ3AjVjIgu>LeX<R0EcrCBqf`cǒ02`j;1;/FPcqh4$p/)_uy{Q4hݓj|ۗ2<.LU$i42"L?`RԺR磇@*C!`(`@,2Dv_/{ @ 2]ZEWARSx*dR*WSq!Ѫ`$r"?lߡۨ,+@Ymyon-rI|Ԛ:Oe)Cd>49l'C{ K` HzZiLZsz.fT";ib-2(!-htB  _a' am/ruiP[FUYtc?&%'#'19Nu#7od%o ]#k7ak3RLhUkobJ2m% }01<:E4q2Cl^^)iZJ|jlk;> OP|"Oƪ/kQI +Q<_CUXyF|sM{v*4UWp`H +0 8Jm?qWߞS|lLwy ޙȫ +NL헡, +l~øjb7*&"4+\rT'Vn\5.q`&s S.R kICwv&[r{k\(1b0@5l@|=:s|ESҮ(/c[e4Mty^@8uX!L*<$TJx4k?.3wjnItv& Mҩc ,RHrܥJ]2`8ZDЎziX{!ZM)"pWXtly@@^3cW 3Z+mNƊW O16 HOl> sC +z!(A p!UjIRU:=ح.?\jDtE"5c9!ه=0 /F3h8{24Ulx @ڦ>_L֩l-@]Nk GY$"N (64{/ׯl"eGeu1Y/ìD6J*%F@M#x|hUGZ-*|>'叹5EJw Ty[uG)P*Gb!#a-v~0w[gcZ9w( eU2CKLM+2sZ*»as̉ͦEǟ3pG(]3o2ڴjyoM.}]?ڨ \4 n;b~g?3FG{(fi6-Q y)d2'ߢfX!:=\dLׁz|WcGcIJY)Ų܂FC"]AErrz{0j2[%by$(=]1l _dŶ yBsWܩ*pǤ +,g,{?//\~7&(O7Q +r# +jk|h?|s.jb6P^$@gH4<%"/+0i0 =yA ~'Y +NxR#po=F͵ڞO-t&73<á'$RM»"0XNG^1D>jhQߨM}&ǨpjO|dh2:'x iT+kmc{d4.W}iK?^-H,Q.So2P"rcWz$ajUNsZ2i󺩢qe͑ + hIGO^)uԿf}C&=44>-=m0 ji˹|\QXt:!ac7P{TN[\{$8nh/ymp] ve5"1 ԇ[SN[Z=E1U( +A%RV*Ȇ ^^{B2ણ𫟠pZ'Qw?ⷛK7Մ;wb] +Q.7 f#D(xabaO"Fޒn:`VuɖTUUNK}!oOrJoaxA؇u/PȆ2-6]*[U;/reU^ј +/.TklO"7{77T4^uo ?yi|Wɒ4}Z?K5ukqrt-D tP)j}6N_߳Tڸ4Iq2VQFޒH@&5(AJ)]?E,{zuq yɴQti_nE>JEWT$mT- +bH|apԠ#_RBfxS)yka +ex()S0I'b9F`" {1ϫ>ʑ[oc W[YޙwiP19b$,&ĀGht~In+d7*' W<̓'Hfb k| %c*p } +d>_RW2nqQُu5f%ďܖ5b9YŔg=0^n&kk;7O,5|Wz/é/O앗1/˹x%:Y0 +{XG(U.vabYG錝=2 D^9'rGL^ ;Zc3z7+g EHu`V5 IDI|31vba0fsq>5vGYyf?B,ѽe91x"wԲ`&`h5 f0ظAۣ]SMC4aZQIPD" B`Ic͛DU֒v#(snQqƨhM^{M 7nJ&*^(,Y(}ctS[WU>= ЖRl'[N]@X{χRzm0WIGe!Tht7h=¥T/sp% k2ADotm 4Py3#+: Ͻ؇eHqNC%{YIp؞ M+(z|= PW0aKFT }r=,Es(ŘVE:Ȼ?pT,CV(fiEGArCj$Qa_}$՚&Zu2kX(]Ϗ /&ōt}*vŵSk̯ޟaIVLB8疨/&ጥ\vHk[`H6VM9LV*Bs6+8֖D~/B&ItЇ^mܺ<'uݴk:=`QV 矛L#כrBR?8Ǖ/' "{ry&p6‚ykey[QzeQ_{5GǪ|#>@'}4J~,d#ˀzq~p ~ey~[0~QCGl>1G4i~,-#Ȓuzooe3Z[ނQ.GB>)s4,?#-1z7yGoo#dZFQ";Gi>&|4ޅ,P$3myıo(ǎ9d쌼Z0iPpAG}->%5"&,iP+$R8ׄcy n" +dsAZp|EPGP!>+5n+,$ˆW$ymÃ=nd8AZ-P}G*E=򠍌5 ҋ,"$ď1UfyAndEc"_YLPGG=65C,”vy$󎪈= +Kxˆ nc}YBPDF$]=ٟpu5,%2ވx|mռ*cr#dYl>Ow=Fɤ=ў5)I),%Bω F$x2mJc?ĤY<٠OFJ=ʞH52쑂-1%_c.syzuyv +òz|wkzwٝ#{bx؉{yvx|[zc|{O}i|nz}u6{ja| N|;}<) }v(ҿw w͇xw8y'yڄt zY`{GM{;4|S(}Qt咈uؐvӫ"wp(hx8yNryЈ` zMM{]:|(|t tSBuؖv9wvxTqy-_6zaLz։':i{(|rtsdydtTu6vFv8awƕ4px7^wy7L-zg-:'{?2(| ޝrήsctnuyQv^pfwIpx6]y Kz9zM({)4rdtʝsU\t:Su gwu򤴁pvoRwϛY]#~a&uhbIQ;5)t'VAZẀ|%搾0S#Vuv?aboUsmDցzA6 hׇ +ƻ%LP()HPu'^hvPءlDntYryW+Ŵ6x'&ғw-Sq4¯Th00"7 +R.'|aO&>0HS#ma7CG|3=7fx[:I_ƛ[SK'fAр/ńm8,po R-.\ [&g(ǂ\/{,` vbLT53E7\`NE^'[ϮVvӌ˱:'SQ1;Y!%f&]r}0z[5{rKG-Eؽ%݅!t2!mjPwE5;=~&HRߣ[9t1]8tslF0KꋾѤ[Pl݁P[ \Z\!֫MbtݜGh.3 ݤe t e^VAW7G<{;!5JD-mވdta~g4Mhw Vkٍ<%C  HtHH3@6! peP2箖墦!Hy]'N!zAdUE{ daOAy~W2+yzx5޿S?t7 #mE%Wir=#^+#>^Uu{ޓ&o>Y~<|Hr7%#SRI5(Eΰ_hwPI/6:%[?!.`D\bBnHjc(x8]+6J eAF4b+i8&VxqS:}ۍ<ɾI-w;@&@[Y%sd4(aBTiC0IvRNt>Xp^4$림r25BhBT +xG(=?j<@-h5-Ά1J8)5^6JnyE UD`)r.6 %'IBXEuǏoF_Y2 ;+CitSttKn:Inq;Ԯ +>M144b3fP/b$׌Zp 6NAy$Օ2>XчdmpG|j3'l5X)ی7=a/)JToKOӯGI!S~5{X!FM"U晚!(_'* RKQb%W!6Jӽȶ5T nQ + +*TArNr&f{.qM LqL&0N;580]aBe cJNZP6f[(V2T Z[⢸Fj #ldb RŅZMzjq0* UQ,%b 3!=HB2ʼn +n-ugzTnRkVA^BEp꡺a5+`_iiSDOG@r|12LEjPm,X >^{%(㐇j)~4L@}x!YDLPu&|:41L!oΉނym/4t}x2a\D@n]˽hV$s3UmIX E^a'LeW NmmioQGP}ׄR6GrgIoFUٸ +|Mvb [|(ߖLwud|іx\"qmw(Ta]g@5:^A D{l%Gly/@bjDؔ݀:lK1I%P&Er"=1@cJ:G$x|@෺z'ƛ;NP);09%*P 0[F]^k~m(WmяMQe + g*Vsfdj4!N̩nOGjmA8!#)9ʼֻJ'SaZS eod+ekٓ9CHL(B,6X uznzMA*\_OZw%A}a209Tw"G(>bʷp0D7JלX8e~T.¬0~{=#b[1m0#̇_g=dpbܦv/vӽsa>c|̝6 rKm"V7p}1]"%prږC{+uT @=-)fwPU :@b+[Y_璄 Ϳ8q5-,rXd]]a34sU9T/[y9 )KsPת +Vw5/mUZ /VHTM51R`Ji2Tud\ DxC|եߨ:msރYԵ#ݵ}\jb17U@ZGf&O +UxftpΗ~H7qN(s~;}!^p}x5 GwΡO=@7 +DEDTK)(.UR ɔd&ɤ 7a;;;*SAdmw)k{ Y<]%t@,kM@̑nc}ORUoM%I2;5kۮO22֠0}(d͇7-ӒOg`Av=u2"\7!s799x)gChUI!_V TZ8\ x545fl)w񎤲bNJa}(П\XI\t>b7XYR2~8<T5TpUUK +q}#{+yk|NB[I i B]e$5~kh2"uT'A)bƐ|Ϗ-:+<b,tDyI;Lzۏ77ֳnFt2+J)O *Sis%zagQ{p&%pV#/fCҊ/[AD][@ ]eofj̠O>+i:%4弭'DUeQ"h_-$cFۻ +sֲX;cC%gwb3$s-[r`OϐV i DBƃz+)@*sG,#:jg֗ߒ$|tIC尞yP5uTsYmg˶;K7?5'-qhuw~Iܲ^ƺHiw*}݆<f՝lV3_TUУWB- :v_ oSGgn8}=2oNw3# )jM +. 8c +q2 'vذݮhQ v^uw7ok;mG  ;#Cz/G`&0`CnqMP_@]j/;`'?ciwJ-?/L"=[DQFࡀO$FւKXܲ^kl(72"/4L~[mYeV /(Mo 4+1?("$Ca,HX"E>848o۴NzwuW%Mwz mKKwWl/*?/(B/ag}ADԁ(/W,kvkD嚔xUI5ᨳ-5i-LN?{GjkrtoS]sIMHqWRͥXeF XauOs~q.~,.'.v̼b #%vZDUEiF.mmXJM YKO-Yht3Mp;֛\fT4w;SE.!J~ٜ+ 3Tg8~*`-8Ov4 &+ODչTTBI~ɠ 1\"61zlxbAf9tű\'G7r0:b|zE~fS,:Lm~Z.OJ،%ISַZׇ 1,"n-(RD]uZ4I?f6ϓI2ktpW&ҦIZ$";k +TL "]Q69fܕjCf˴qC,4J}O\E+ ]x?<0»kǗxظ*O2ezuS&2V􈤫+hd& W粗uH/l16Nl7 ]p}|lU$巨djr;_֣s}@}x5} V:QOD4 +aO#%z +(*D[0LBof= B 0Bd6FTR<=\`i8ZjveI3yqƨfnw޼%{'J _X3N-ލ}wElg)i=3pb +&|ײP^"#}hKB8m/򻄙Ebov%"-{Dȡ@!5acNm֓;bEW=EEwQ1_牋}+{OF$m~HXҴX+NMDKzW-I +_[JL%k?zwҾ|B=3R{aHDLk½KȞTK}x*͕}Y>ރVՉ'sPF arʉe-3{yh+gԝ:9nb6$l`-6LK9 2G(k7%u}bLjQM4yWPD- +_CPny~3 +9[1הeʞԦdʤqM1jٛP6ds 3p:2_[(dQzI^M,)Ǫ`1;,Iڅf߰+ BkXnXZV0%+r34ߕ[IK265Eֶa:ҭN"ذ BgBS_5Ӫ8CT=ƕ^Y].@Ǻq0y7\U&64@r9ʥlG>·CP7W7Iֳ!|L} +a Q$<4t +&14$P,4]*@qtZYr_D #nǍc&k&p- +-;\T.Ls^Ӭ=XRcap|n;MȹsÛWR􄾜֢6{:~#x,aGp_1OE -:WhRϙ57e2guc# Ǚ*՟Yí+!{m]& ᜬ>6-/%{c#neې[9L@Cd4S6͚W ZFsϡ|/~ !g'>C'G:dTna#| *J|X8R;H4гg +2~WH0G]&&%bcQO\B6v׀QV#EҨ]4~|3.ZgEv7HT{DJ[; /2E:*?2]u4<"?ȥ:)q=DGaW◃da V5KUB*I%T]A%hXxU؝yd}gDP+.~pLRU< dPhz珪!΋+i{[U^<:jӡO x@{$h"WơD%,tt,ͻcdf`KI" .sϩWڞh{,-c(kyH^EL1 y{d79_9+Yx?|\F%-3Ϭ̟XH)["cf]k\dqcxV?6>Oqn"5f+ +Y䯇w͵[i1BHê'a"_?::_?{p9 ],P/e/Y8*݊o,B & 4_^}$!>ra9ByGOpx Ng5xX\ X InW ă-0WXsd$i4ScbX>uq9i6I"N`ASypCwhxY4F4]6mU(cggxc5:ͅoF R,.aŔQM,i8En?ҝ?!ÚGF$NE76[◘xx1),5G̋)El `#MIȑSAy9=h*2 @0Cay-?uF5הǼkEc"x0%zӽ !yr$z±ݠLlߩEkykxESbQl_/`4H 5%Sn)@$XٮC-guc砑 +zܰaZÅJ(J9WQXDhI+@E LhA|ؽ|*<|D7 ϝa4(DÞ 6ua ?rQ}NdƣT?;)IYArCOB_"O9*ԣۇުeE%V(0Fޝ^ F]*a=RÚ#,&AGlG+`GX̱5ƨV̥c7R!`)rt .5vxAL_bYY'Lb&Jq K : ٙd'{א(yq2a7Kj36)x QfE4WwSԙ.T3r<{W  F= 6}G6ݡgh||9Eu;(ݕɶ{?y/ёkup'᧎}oQocI~ki0ʪhJ[I;n$e]%tHzgtnv&6A%f82N5u754@KgdO~Csi4/9Snا]9y7~GƺѦ$$.lwS"'eQV%TOym+'cE$ʕ$ ^QQB^]-6Lj:l5}YMM. '\̔b=daBeHU +眛 %b5?Б9_m卯Iu.Q)^ƿ+zksm5fGN:JRIi:c}wΦLe}I^6,*-z>%X<=&=f&b$zarw;f@chvp u{v+Х6cF ~9lҙh5H#Rh9}.3/N΃@a.gP;)ADwAnlgr?Q#/NglxeFC-0O[5kV\Tک/X{IϚuxܩ)s.\ ##G3a{q P$[Ġ-xN)OS +!іk-h1@Ou9=R'hLЩfJb($i+ ( bW6Mrd'\r}ȀhZţr*BH߮lHZOqĭ~!*%AC {k!Lnt:򥃕=טOKl$M+E9S R +􁐿VC>uM(4Jv?{AEBHt&@Msp)( %ӜP3Xkez֗"§6s͟Fc{RH."#O7Z1upDp@K>am'fGCAe}cX:|Jx+~WLjm®H,wH@5[UHRPgMǴP 4cMnܙ~ZC> +lE8x?'} :遲;V.Ncj\DthU&ptW +T?Ѥm(G@ENux.ls 6*팵aai85Ts)S-?gOĢt78#@ox7~8D\bQ{U0E;C(~fRH)mс@b~uϫ @ߚVEGSojul6Si?*Bø;UJnv⬭6]g܋l0zWy7=O,8C= 4Na\I[-%T)m($[l(+0dj8J,:wa_5c3"*lcqq^yEc3حm b4"}I@zW* qK z]Vc¤>EB)/A{Y昖)yn__[NQStĮSsts( +Wj;й۩T7AZzd!p^;VmJË%zvM'f^cT{oȲXׇL V7!^tjT/h\'M+9T%$oip8oPW^ OpCϠQE#7V e94|uJ3DReb +^BH(~kάe՟+&IUطƕٗt=)Y ޡwD:%b!$gF<{|5bW;F!QmBd]LDdSwb [b3(gYo:@ƒÀZ4CYF>a剺v[_6%SiiGc^]ap;KF 'd܄1UOg^sN +[iMY_~$? c!EUaExc4ZW9Ab.h,0X.Y>=4<5Q:ݏeoB]5;Kq*V7 +/PW%=ƈһ"aY욑?pIWFn^|r g\9ˉQ䈫F;U1'لׇ L`|1@q[5f-a[GQe>Fd;njډ1'V>)暠{fјxpJeޗ[)ёPF -;kz1jY'TlsVzx9oܚ_e-NZh&P-vEƐ;:1%ч0wMn'mocqPY訉rPJ=tmFv%=vw4 ~y+0B{d" }|~wtAm.OuWcavcYw_P@xFy=Zz4|=+OM}"ߎ"~ v~zRl1zb{oYA{O¢K|F}>=<}4~+kt#RMIWu벼;l.-\bqX᥋uOuWFQ=4؁2+q#QubІ kKbQXwO~FL/<04+J#tk(a4XNŸ׈'E˛_<̗4s+T #7Xk2m񆝬wr`ysɄ+hu7WvFx~6y&z>jflnio(qpxrgtr8VuFPwy5x.&&zrʌjƹkߗ]m ro-݇pwwrKfsڌVHukEv5xk&CziIոLklݜ8npvqfsUUt@Exv^5ex&[yeǁhضjy|l<mZouq3earؕ{U't|Ev&5=w{&pyWGh-i-Tkm_+o +tp_dr`Tt yDu5wj`&yvsuokcqISsZDTu4v菊&x0$xcixkרy=myo݇zIqw zsf^{quSU|wE|x4g}zh$~L|2v3s0vtVwuvjx2vxwuyxeTzWyT{&zDk{{4||$}}tU|du(| u|v}6w}txu}dbyZ~QTzH~C{63|?$}rۅC/s˄^t9nu҃{vuswcx|SaygCkz3{$|q!pr͡stoBuXrvBbw/Rx؆By3fz$|qopدq̔KrstqvHaw&}RxLByo3:z%{ٽosvq r'珊s;QtUpuq6avQwэB3x3z%{8oO+plqqrsÚ|ot+`mv Qw^Ax)2y‰%7z;Qnװooq\^rT~!sANotj<_u&PvAx02yl%Jz89nh oyMprq}MrҦnNs._.u@GPvLAKwؒ@2y#2%[zaFn +o p1qN|rxms^tOv>IAwj2x莔%gz2l~i.b~k^'~mt~o`~qCp~s(`~tPvAI?x1zT#|;~|r +|sTw|t@Sg`ղTcC)ʐ + +ALV aPA9zwXS[Zz**z̃VS ]r7O;lW ~xL/'HhwGH\ŮP<7CUXy96`Ȅ7̨݆}j_, 9II[O@M .hv,-B;,Q9]U a4tNNv3Ƿ0/2&l}R|,sd6*Qe%LYp!=%*:|]|ӿA=b7 +L7+ziD-fU܀#Ogʤe =<}* `<bo4H[ Z%}$t!~WY*ѹ$׺]CsKT`[fr;]|K qm\1!7mFCzaHO8!ۢBgh tʼnPՐr`>$N3WujKB-[$9TdbZgsQ3){甎9iB {_okK]-~wzy.Nė+ڋ2nHZ\;a'fër,}9yqa/~`וֽƹ%1xaIiS>ģ՗y᪼}x2y ϗV؟mn&}1|R^Ǫ܋K:kk?Le(P"[Hs?x6@uӈ;)oogm#0 2׬nu͸ nz+2cœ]zXs^)<(l#Ng2qඉV#.|ouiVէ5Y) Vn9K;!V%NDWji?<kLX}~0U{w+z\JMBGδ;ZA.2؝|ւ;s{r`lj uAV_Q ˄Kh?mezlPg/uxQny|"_?=Ay"IJbSh`@U|QURYp[WЅ.Q^Ut4,N :-feWݏ1 _2-֕t |2CloTlD^)eG>:TXOdet=~ $t4^e}'RG3|w ׹䂜^iHG2Ʉq2ݙ"YA5BV,\{\_Rr><*;*>:θ#{^+f:PCs6d5e8,,XNӏ2EGUHBIn5m/=Л)oN+Gel$Ұ nfBP}ʚ;JRRUŇhvuEvo%{^;0aLr|0^%7ULXP M_e/BLm\D;6dY46otlFQ:ufEO771M)չ5ɂt?j1S]a]LXJDLKi3cOU! !_ +"g>xG=iu#J26|&Ue6'L_Fss*vp}04u v*CIi!Ɗ" .JA̗3^{ օbEzr\Uj{߿Jo=-u ++q>Ou&]=TsV\G;J +F0iYS0yHw +ob<ƑjJB5>;T +3D˞ 3wKHhYiE\6+>UdG0Fym^#:kyQZ`J)3AÊ@H9}F{:RאSOADvZZŵBg pg,VFY:{Kv,奨]{x}wG~ozwo߰^$JhoѠCɇHgItoV,&7D'@sBO8AL2CsʡݤwH1k M,uDK6~\K"r_+?8nB@O^3=&lƨ,_\MaG1,C)ÿA(8 \B~UVHTSb+{$=BL.1Px&1ЋɿwXsz։i;ڤ3ѱ~omȀ-U ׏RCuwqWpbu.o+PX: +2ҦpVv|[2 Y&;] +P 8EvgJhռYjrk}D 'rTs)$JЇI.t +#۵0К(w>n`A +NaX-STNHzN7 +:{t  꽭Yw J] ֦g]_W[Lh s0 A)iSc@ qn)N|bj% }ļE'$8ۂ}g M-4{VO +qt\xNa%+c +f6ҁL uDxI YTn32f8rܣ'L% Tܼ,Th +a| +;S|E\dD\)vmyxgyK's,%y-`#ڈ-Z&ԤtPUu<3T>^^Ӌo] bkҠH-$\wahu4J5`JkJ 9-YٗfVaj}aƃ $=.xy%WW Gj5Keo,J!Җ)ˤM)%k "١xO>W-'fm1`*ڮZ(-ꗉ,ŭlXw:eJUC^="]NFHWpևRܪɕV: B2lPRq(2IDw B 98r;w\5I?vB7jª=l Uit .= ƴҘWOBte.񭹈]hA#0_!sKT@cs|< ip3< hl"җ9t%0=5i6@L({|"l|=ZT--,Y}זTAn/UoL.BF̎@8/#N&%eDže|Ԛ.priK_>lUiKǍAn8=7.,& kӂt| +ħorF0M~5\R,h#׉|)wm!dTсZQ@H}>5e,$˂%vףmAXWcZ4;P͖텺GօB>b5FJ,$&#j@Ovlc/oY^PkSGWE>+W5*r,$D{uU|8kbېYYDʎPōEG=֊?5,@$X=tkSbSX>O?FA =^4Ռ~,o$b؃ԅ.tuYjauߙXDǗ6O.̔FY֒&=j4,X4D$iK}sTj(aWfNݔoF=74ʎ,Fz$n8p)igCk]mT֤okK٠qnCs:;u1zWw)"y!"{*~ pof˫nq][rTsKuzBw:%x1Kz^)N| !t}>;oIufM(v]-,wT%hxuKLy{Bz: {1V|)v~!0wpMnԭ%{de +{\|{S\|J}|B~~9~1sU)!*҇Dn<,2e-D\4&NSQsKJ\B@y9ΕB1) +"3n&/mLdCL[^RퟰfJRB59H1)̍["eJqP +m d5([]IRWJ TxAЗ91c)ą3"#pl2cZR>e=IšA 9koG1wه)<"}2nl"P`cDOZpQܝő*Iq#[AX~9@ꋾ1`W)닿&"Lj'sĄk{bΤÙ^Z-Q:I*A Y9x1K鋭)Ul"هŇG;jrk1pbbKYȠyQ?ɘIH6@󕦒81:0)"uX"fj]=l1TmLYLoDOq<|}4'~-W&9 2Ѐ^dϴ \4SΫ3K̀CܢHL-Ӂ&h w`9d})[鮾PSVKr|C{5m{WX]eGOsCX:}6R|t+jOPHFh}A&|1>SK3m3<]@Μ,_@z.=B1OF?{ۅ@o8gݱ]@Z'j@tڥvAvˍ' +OG7\_ږ}Ny Yx**n=n$^p?NiyҎ?hZCRr-N%MoE0צ,棞nuST_h<#޻!bq^71tq)+l55TXF/?5r0jӏw~5`>2uc)p*K֚fS*W}R#(8s*I SQ ;^KE<|qg79b/a;rcc>ymv +;ks=R\T W=$DE!A$K?弓|K ҥ6At&& ~@" ]ꕆKΏ2VDQ2DqyG+=}r;v;M'4w_un#o/c/pCpfΖQ=kCA3 Lپɘ^M)n{.*#1wبkKʝ28g¸׿J 8ߍ͏F1*Ҫ깵N ebam.P:ZD}`J.#9 AvvC@5ޓǴӼÅBfiMb]/FԏiKQPk#8P7,#.F +×Z{^5JgLHy(n_Mê28Ɋ D#ӆubQ[GD ԠUh4hsTP0RLޤ.>㿰#&Sd fPv>%WfϳhH10I#+&tn{.]+<`29oS7,TQ[Ī֔ZHv +L[(1;*NvA并KMUuk󫿌;.Iw_ݧ Ww?/>&:x҄+I˕e*nN5դNvEiU^\;= g v}*A.n=Exd}"_Q2V8 UnHٵf1)2 %#itM(18IXOd6b*XItS+:JCs~NNq?тT#pLXW?;4NLM A,3xIpOKP.`?n9 xb[6jUrt {eA&ƔdA0<;>X+n3H WD | )Xw~&0:6Ha*!2#bA&H)m.>kXoFMi]aL,~<3̜&e[<qK氀iiѸtQu(q|+t<녥eUH`gr<5 6}gm}*?Ш++8C.LGIدhQ(\цʼ> y1H, +Vݼ?ʄL7 -ղ(U]>PoKi^<(mGq]5:+[g=lצ@Q͞pt_ ʈ 98=a|6Eڐ{=hWhHޒ=]ՔnxTD~(eZ8T?! |oߜŚy&nPZ8SZU2UpP>G$Z=sAf՝Uqv&'jl¥ +3H_$U ]\7;@φ9QՇ~麝ډ=)]\UwS&[K VwkG !.a)BEaA^C.%./ Zs3 +kb3OL +"ԥE<c]DxL;7o Cszq|w/g;6kU$_˵xo!ۉWnx! YY8iaq̰M53%qʂ|NX9>]kO$ds n=  +Z 6dHb@ 论!(Y +lo|,&8neq'[Z:Gû$Ҭ!PhJ;j+6*j"[{@Z#Z0S9Nz?+Ue]E#CҾe=L Qo8y*ˣhGQ8oIQ+ͺd*RMJ4d)-e[U2e+ bUֲ&j1zhB6m &.VWH= @D( T@@@F;$!|L2;@wEݥXEUA-Zz]oxr".l>[nIM[= +Ic#.=+) ԄE>!禔0U8)!R0&lIP^xM*rȸ'#pS)rllB6SWX4 \5'kW9[JoG)y %;LI+&k&V) +xƛؐ@FUmA/Е ˧a'#edS,ڑУk@ks=%O>lLYT% 2#T>[ouʎR=-B'w65\c2>c:lm. ZcʉrA rR/5>O}ǂ72u1Sӥ[Sf[rH/nS*$]dƫ\v +hnMm G]݀w*psJN&O߰oє>QT,P:DWKrԅV7,NS= 0+[XZS=cލWEԴPs[9++f$w=\ٔɡIp1-HEl^)㲾[Fc)#gTcfA2N+V  Tq,GD"1{Q2f/9R0Z0X^P?g|%Ouy|sD&1 ҽRWv`-tATZ2"}+:]q'@Hzu۽#_DX[CNqރ";ޱ;[T7ekHy[>K^H{C9(W?'цc*uT5gl%Β0 hUK._d$j $-4k+}htŝӜ.lfˎ}H7>!Ma;+;VWr'IӨ_?^?8l.+#9"C!c"C$ +%cqvFCSy2ZnL%q`W88Ndbo/uItl,4ޚSzb(v@]巽JFܯU uF˚#ĵ&L[vBϕy6Ur ⪬.?6FPI~o)x.$uyK8,$O]ϫ3jZkzKcuV{vii]sl]gaSŘ+lrqeK %p^tD W/kNoԕ anh՞IhjWrL|+LD?o:wQ>/ʦ{\cHYyA6 [ +UfZeҩ=j+7'2m=ʃWY]+ gY1tGt<@!gM"$MH +1L SaPXW"1)\)|+`E,F0h$EOk!!놚<ZcCgBp/^; .^ډK QWIO姬 KlG+ءC$ DRi>7v7oX7-x[eW%iÓrZ߇S7o䏽.r Ct?Q/_l*/- +Z^f*6OgQ +GeH"|ۉi\#$#XBȭxxOuu{mEeU*G>h:h>Vg'6+^hóG +1pXɢ<σe´pSv1\oMƵ>Sg QYADT@fߕ =Y)JV|U~~x&V5)Niwimbjh#b/#-d_o'HEB (~i|@_ho$*7ATN!@UP?x D9 +5Lͥi=^%;24qw 7$dJ0I`j\#0 _'uwu\(5o>1jJt8wj21L) A"HQ!Nl~GASMN!P֣+H;ޢ%R֑dL-Y/F7w-^9e1{Pj49$. V$$(X=d,D"#WdB Pښ3'KBUʞ h=Hi44GbZpIˬIYcMH^i]swƆZ3 [O֘[ 'QJEEL F᪠\| cN.<0r|dbұJɧ*KvM RezI.t!Jln:u<~Ti>iY0w>7>S5)_,^}_yȌ"KVbNAJ_3tU̧>~z[sfxb{wqY^Ú YFsGd!uaeh'< Tj&˶MyyH0{u +hQ/p=nD3fڛgz485ڭ`,n&l91ˈn ~ FA@ L#ꣶ?aiYG NĶ7n k |vZMH] ̗ +p387G *(O\+ۖى"2.&Jވ]],tES 2/n-x,f,?@/|&:Y+zJ^-^?gcM1UOGpUb (;. i@F7Mtz(8c/xxI{G)b-*wEreL֞5/Lw|/YiIv)#!KwkᢈD"TaP\8,|џK5tWRK;+ J/?Ѳ]V@1&j$)(#|/ <Y®0m)u+G}dUq]&uuVQ'!- +("G_|INVAeUQqբ(X@\-z,-/̓ Im.!u*FU0k^v--,ٖk%4k˸Ymz| jCO0K LDPh5&-,Oc|/Lu:9dցD~` ^ b(ʳD0¸d&va9b9 ̾}_dp3 6XCa뙈FEJlrDhJS7[wJ\63B63!&itBP'.Tg`wVvOcsP?IA<ق2 U.(-&:/doNeNkR.^dr%Xrܕi;qr_pRF ES-I ;MC?f]aa~};C֭O3Da:R(Jugq{YϔPXihp6as>5n&ا YB>ڱ5Ϳ&ΖغZ_^묋)|jeӥ jp$+Z*qQUJ Q씝XPݚ{$n0\Jg'qڋSۃCdC𔺥mfʍU?./ϺԴH8'[i5(r$ُ=z>^" /i,'R΄{~SkeNyx&M_FI:}u')aN 73DOM p6T1ѰF=UJc:ц5콬{Kݏ@v +lDV܊QŘNvo SY@cE A.yKqMX ލ}rܑtanQlg4M)$13:/yXmK1 &œ\IҤ:gcքp.J:T<&%I&er (J:pqŻwJObl2}gn_ɹ +q?ky2ra;`U Ff ')kش-V 0Xd}:QwE4j'C>g,zTs :tLx|-dmf@/&_o©|Wc]bp^K?jJC5I0c.[+hkqGҕcAi 7y{&c)أ{5ћ@)~6s,-8 榃dpůCE*iQ롔QT*'9N{9=Nn f>0WM6j]‡;Dʲ]FtGS7l@eʹF[;eJ$^i8Y*=* +z/o +Հ.fÔW?Xlcgl| Tr}r* kAr:;9^Rm7`xM";Mғ'*wɱ%^jײߕyTsM&MiNf{pZ0bw'4U54 +lI5buT AiUs/8O>O.PNo jX[`ޥʁm.m@=XHt+]"fTNYeI!S:,$9CJ1Aj;+{+i:[4`)V +j2U`OR"v +v)S0>D3$[C fˊ1 샭s~!)FagqL܊ç5A$3z8NXE"4vha. D=B\h_}<kizVb# 4pZ +_Baqx%_DsqףOس"zx [;VQT]5D- +AN?g:5g4D_GdABƒGϞR׎45B"R;P-W 2`4dVAWFH>{bNf+Оn/]n8nj;ՒرreP>ieMwH7ݔA^ع}Q&=;4oL_6vF褭&()?,JEfb_M#Uvi۔GCbZV[`>V^*v40ɨ0l71P0V5Lݡּ[y<݋P%>͗`,]=?l{UC UjS1]ySq+2d0<1ʁNmΣ;mRDZkjT>#@ )qQDHWL2 n^IVف b5T(]"z8-."͸,ûIt|J%(ׄTyFҝ2d>XH=?E1~zh5ᓊ.sS9ĵFi܎~,42d́qz 9b*"e-'hEX>>3e_vSڱ.$>_m8/RH`8ʮGH Ur?4^vYߤ`XJ1y8k|)0NrQ, HZЏ(جd}iO_}! ?a5 ;5$1I&nesjn'1n#ԽD93aP:s"`*V3Wjȫ . C妡h|26aW,'qF`M:~ +/xq.~ͻ#XqmY|\lp&)P,} ~Cv$eIgPeYch];RYPTzc}c.{a̸|}[z_)U]j"{xrG,m2׬|ӏ AM>V(нe"Uh 뗴;u;:mhƔfnqT>|QTf8ÿlG<Rs̯5x(`\vY^[`]RSVb): ?3 #:,ΉvT=#]-@7wz }[Ej< /4hr; GdE']-Mk$W`lJp@{a x1\ub ^V(7i?ubcUd9t&0a7aWT 'zIhkl4a<$ ŘG]&}M~F'F0סqP, +yBy47 Dl~Wg.cř!J#Kb BVƭMl_ԣhba]Yc7/2M{ H< "51wѥaQX5Igq?34;ƪE=Lm:N?"\g4ż%"xM *u`玿rnP/gl2"uyiRC{Ao5_8"(wn+W,?ʻ^W}ڔ.CxM[[|d彁,ɦsUՖtU_.xҹ9YT6A-,4]\3Ͷ4 *%jԧj=*x}f\NHL]_՗lv4n]t7u[fypyNQ%0jIbM >3dEtj6h;_מصU6\i>iL@bd0m_ ԉ_Sy * !*IDp+\\{oH<Npj+xUp" uc[uտ%C R: )Z0v.7m+`D1K$5<LQ[B |W 9ҝ'ikQzGv]L2{fZ6S5# +Aʰ|P +;q`BS0dKG,_SMd5y@+t[cMJ XCag2`Ԁ7EV!l.DGR c"1!G+@'u3X^ O!:,u&W;ymM_}%3M^d G~ W]?@4P<- 5&Gb0Rt}'4:?:UGVjD SveK7 +9!zERdNoSckzrd'BRU'z!*"ӈS$B˧M;bokEӼy|}I{fk("|O"Y ϫ[JK5?O> Qv=7dhi/|ph̕b=qm(k ~5)8Sk[Z57_W= 5bNʖC1MeX"Gո +U4% ͘$du-AqX^_~=ҫ]+SF;,|jVL@Ebq[Jod-Bgl{=%ɱcmDq1}4Bvʔ>J41IW%|Ee_/Vޕu~)ȹ 5<"-s3ߖ>Z2zɎPu1Ҥ;yt(#,"!~Pœp#@kg^0ʈ`7P.'\xi69% źLt;OP0鹪R.w"7p9T=FmM~ikSg-~A+ +ѽCwP|%4=B"Ce6̇>B'p-sџ3шSV{%l"uV+J׏zI%Mx:\6玬rN{Lf˛8aQRC[o^ꥧY"Ѩ-/rDM~0KH팕ϲ=xײi٣XI{HWZn* +a8SkJ!6 +x>.1ц2I|m&!{m4% 6 "RmH&b9Pljkt ֬Qfrܢ4f8?$B,hnx4p<:(yWD[ƨo:9ȾEͦ!WDԉMz69f%!D>g"%ĥ܏ke&i5aHq{ba] zT*6Edn{C\Tk=*@$v1Pn(tl]'|s7*Ζ. }2 +Js*oSC8_Ikϲs_|ØXiyRVt[,JYE̔Wf+0(z35c:W+gQЂM-iʉ*B6Hyy[@!`"T\ +*sUihjVRJSWw3nʨX_33!/RAxB?~q=QJOM`H;ffXT.nYLDVt`FUe[ >YtU}5X86"ژ8vu~|1D2R;%_|"Sh:f m;'Æ JJn wbܼ: 0x2 ]2|N{U {OT qb+FEcH\krމW,$.AP>/瞒HIS 4f4۱WX~>f}( ⿑8/'rzKrC%5[ Zeؿj޲nrKY`*֞}e &U$+@d]g)?(iNG`,Ct*GL~k?oyyzZЬoD|FY_gG8@f"8L{Aƨ*[Ԝ7Uj/0HYb$4삉L6~5HOKL@< `h"I1TdW@m0 `Eޢ\v$TC4?w 3RH<AlS1K-(Lub&uM=:"Tn6ULwv @E6LPԧ$|`>0=]?a!o$ v _N寛)%rz*pQrKG(7AbuiP3 bEG85 uTX ꮘpPxT)R<ڥBalAQ?`ְf.ы>Hqۤ5#Xycny: 5*f"*tP +aKؐ s_areE^EӑNt;F"ϐ/'{{d2J*weTsih{}|H\_=K0;yæočdq[o&_vΩl]/5e*y9Uimkt{G}Mć7hø]]SܼNnx&S6՗fk\|^ijC/lS' ߙF@5HYEѬV 7-BB.{$Aeafd垬Su~,;}\6vaMN {짲kp; U}(\Oq*6A(H- U?eZ!ݤה3둗]X(BU̟q!r! N)dRYITۍyYO!Ecˤyk4~@ne1EZ3'b;) 8F5F-`2oZɽx!Hf^&ƨdت<*HA >1C0A`㌷t[1z& 7.}=Eibΐ٠%]3`:W) \a7Tv,}VBHbSIFb|p4$3 TC֟PBigU#䈰] NC7ja[|r kFJH͑<-/1zBIWVMDm)^ͨ~0"v]AkWF7l1ĸeXI7]҄oNGmp?ޮG7qqԿ`ҷXz4\[*x.aP:] )Quon,T2M^j 5 -Y뚇 IEDD&؎D!&T1-U4)6\*K0MeQO!0}&2Ć\= nX+ v[(rлOGeK^mZ8ʑG4Aj>Sw<5|$1*)-LR#jKwZUּ,.PeLs4r$3 Dc({%BzJccfڬcv!si mV1s fgOfQ ;%)i z,O N#pJ;rSN\Rhbn?|~k@0VŭΕ!R0FdeȔBBwo2ZyQTZUԁu.,ru?~wQT_eg2 .1$^ZHuǴxZH2\6?UJz +3t3dzT5mد7olҏL.68խo,*a$<;U-kf³̳L=Q:_2My!d0NS9 f%Ena?Ժ,| +hpMjʈ HyC@t 1Nր+L#l^s)]|+rt%aP.K?R-pp *]ZXO@5иmC'DQ4+sʽcu?нU8m"Eƍzbe ⊫ذG6Iu3ЇUb Ԯ|E]@SX@Rj!GV'ܖC`P2*_3". Aj03j <^ +o|6XazGrx6oD+Eɥ=h[BkI6쁦 PYt?ҡns󪅰;}I9OԥT򉋁=r/&A儿 @(즄Ę>=m`;Y|b[rw[̓/G#Y 0'+|@`?81lo)ʺk`Jͫ-`#-Lts-DPnfՌ Rh,R{`tgNQ7po]\oNkc?k%/9b!8Ǻǟ=r\A8on:Vٚ}!l_)Kl DYΕ7ck!fL!m|kTr$Wf4 *btfyZ%Zw6sʴ|悒6JMsro`||D]p*3gF'v%S^4nN@8-I^ӈ }(W9X2`[~Tto=elKVIȬoU_S.ڟI~Uϗ7s3r=m3y?|1~sO̤lL7N|6ȃ QL3WIY iё>mZp5@ Ya$7HGW^4x5<*G҆8%}5'd]X(Hfa(3"^ߢ~{ @O3JB"d9'@!ŏ#"y{ɘ?*V\3F`^bda Fw, %&F>_3% A(jiq!0DA#Xbfw9|aĽb 9KR!ԼET\a!!7A'G^=o7dv`u'5c.pBȨ-PCJ-,F> Ny\9CC.o?2Рό"{k%L@x +*U#U̿+̡>Uy/# +ޜ%h(+"+) +sL)k zG) <`Y$NմS]L$N"(/3Ql*Cc:MOmm-@Su_%MUϸ8lu)\3l5I*:VPXnknߛ}T` *@xfɝ:> U?vqŧ;ʰ$]kk]kmU5 ^hȧw=T8 Թ|3V+hinP$YkŖ9ƿOU7 ( +EuK "1 2aa_ͱWAH;a} ,2wcBxV9 Lj =`| +O<fasGjmJ"[^Ƭ?ApuBWV ¨ԌeF^-Gr'q cƘ{>Ljآ-mUzU~ҭձ[Jz}6[7}E'&:ۃd9p xLV0p̵X?Q2UE[+o*~(ʜil77Vo񼆴Vw?T{0ʗtf̷^ 3Ҳ(@SQ&z[ a.ufG2|stLJe;VOXJdn9[iySɦe{֗W(YW-K+O26>Шz`oNh&c ;jaSzO$͈ZIUQ;,@cSI[#nU_+&!?J ]M{&~~ W27ױBE9c@Q?+F4ޛNl$+P|JjxW?"J=bQ"/"k!SnՅH'hoYo7,W{N GV*H7rs4i=_AWj iKԙJ),MiE=C*XS*{Ee t:`d 0O뚁:G + @#AR8 R=܄ +s=b F4j/".ܻvA;H @RNYnh$54ņsӺgj7fv Eߦގqߟkˠ׻ DiuY5a(g92Q^y|V`R#XjJxzV3UBo*6&Ķ/eonnoYэT$./Fgv9l=G6ިQ7Ȫ۔)uj#Nb`Poyd"X̟*>r8P.u6 rwWA~k@\YH-d(2j"BB>/;!JEjkQi[z.T] OXBڐ!vk%^?˟oLc'7-2۴ՙ8Zcya^&Kl >z*d]wlǙlD/e=yXVjP^xacRx{jPD'C&3ZVZHzLH53iwړX1mWmUO577xFKM܇ȴ,Ց?|]ONkXAfHOZhMR'Hcir.NI:?['w&wvKOWu;)wמWOcZJWE i3;qŧ핑30j>uCgI\f>^ +ЎOYVq+, rySpL$#'|*;O*F{uF?H99yN8O/pRšRD|ʫT/s+}(uc@<1u_`F38I"#3^(D*+ܫhXj\ @/ȏJȿwRA]CR#8"N/D/J„e\@P[)UqPNPYu#v'u崳 +9SxSKPQ9UXU0əR a!Y{inqVw릜5;-M1u@θnxO}=>ˋ0]XsL<۠#kF(eueAպn~IS% +3NK `H(BXӃ} f*R@QZJbsUԅ#o# D,&dHϘD3þ +yk-:X|u7\@p䶹N F)q.HJ6F|J˽ ҪjqUUJw=E܅[Lכ6c Fl%F@M,X#]7u,Q]kg%6{8oyZRh J@m]E檖xJWGbj$Z`9wI;u bޭfA2_5Ep$>^1ϬI+ 1π2-K>N~#+;㾭uueV7QJӠR.~<`(aHru0 ^HYɟ#/Pp"V=#/duKU\gQ~X%|(,+fYړu>~NVjfʸ\[ J-P\~Shem^K^ s%vL}W-:SNED)0p,M,LYQ K^{ʬg\XEYZ=Vj܋U_}%Lk܂vKP Ajj)!jӜOu'ѳL:6hQ(*7{6v +ԲW <ff6Ǚ_HcEҋ9dk} 85k *la)sW/]VㆣYšbU5׬;|Cma0Gi +~Эh aZn?" yMP$2lž%fRe0qBm.MWkĆݦ5)yyneKeh |1DA_7>J{Y½iP7)'V? 3UH$󑊇ʕqp@t4ԇ3ݾIiPzWiN4S+o +(U\=purٿJPWy ,Ea\:Aѷt݋ j@7gQy^NKfEk/<;C:fD^0>R &9Dz!L&NG4:ѽh)QP@ͭP+%7ЁUZA.-!}μyXvnJ76-I6\HQKV?+A goE;~ )AeG|d~U(1||D_,@6=a-Cl 7T fkG(%xVøoQn Ǭ\Re݆zwme:kڤ=[`3Fnܜf-k^ +:+7͓q~7y˻,¼6Bm2,*Jo!nٝgZ'yEK{(wzen#FFT^b)ړk$ƱT"KHϸ]t^̸ҩ&f"mdar NӋrc)Ndla\6- Ofƭ>cF4~:* 9R& _!$ָycfa󰼂8|Jz+ѲfqR6w8G] Bi$ҿ >wsHe+;+]=Ίi1hTY-lrl|6FƗ,iFE>roy:QaaͶя=e"o- D-Ԑ/❴_TQ DZxK +h1K g7 ?lN&E-CJN$I%O|tQR^76ѧ0.Sy΄0Vxi𪵃SeW҂Ю8jALmzRS +.*G|\ x'I'ȪP}&-Y>Ht"5"7<%5JQ>RBR20v"7cS0W6l mxvPQGKzchND2Vľ`o$NU:ÏڧtEя zٳ s7GP΢X/?89RK."q5r's R|L6pX߃e[Q qP?`BU~vT{8,S/ k[iQjw|aUѰZ."8*{Jw6 +R8fXFnK]:|7MlELu乱[DW1-rd= oެXjo4AG5^ϷO>[ts:]c +㗢m/%nH*~є:O߱?QvԭVt1gis3$YV M^!ɽ gU|hzR8j:iԝXaɼ2C:s'Aæ !j4՝Uqb'n4:UL βMfN&_37ZCa?T jg}%T2+֫wqL ][/%30e}ML/e +M +iQ/V\V+ #uU}9 ,'C "Qyc=S^kI?+cѱ aөƸ *RZ +d):d/Q~_C7e\Nz1SP}rETp,h0DcX_ļ88V@ەhcBփB:GCK L+$\e,#%  H 0+H&obW;Ƚx|h0Y  $Flқk$&M]|ZCz$$\$~S>8/t $0g qEh3v9Q*!rgL\0d%A?4G/ǘ;!(SH0Bo!bp\{wN./5a5UuofK` cR p1ݷ 71aݴݏQ9EXJXNFx=4# MA;[F`vow2s}p`Xkmӳ-'dmo=uMBD5=)3ٞhު)XmWy'?V]Zvا u=̘-U^qfB?"%6r}nّ~δՠMr0~0@K L%>BW.~Nu Oo8dYw@sGȔL/s~C^_JGT X)z@@ %NS%7$^}wL%IL21T ) Lt&ZAGj=V\gf2,Ө|0E%EJ 2uyq 7&vװp8 tZ׿ CKV !h} =@dvv!*zUKAAm X8Z)N8Qwh`P\Gb44TnEY7i:T(;uWmqƭ7AdTq&IzUr$h|!or,mz3=h멺qv"gNt. +Rݼ.Sy^eLbVNm=ZTdiZ^a76 +x^)^hƥ{|F{ FUk4>,h@DBwB~Q/mGcrOpGuq^L90m5YJ9hIFb\D'l_$0tWdnx FO>FƔRC\;NI|W|"0UJ"Q wdTbPjˊ1|'r +򆟨7LA4QТ-ek7w´_lLPEڠSC1U}NA ͚49h3Cd>ЩTDS1|ּ$0PCA< +BP=xFjU9<8 7 >ʏfLbzH)-O{ pijXڳ 8LpROMXWyb2:4ÿTC`-W@NOT{ +v7&2F1դ`BމY .&=Gt,PG - +|. .ܫCL=k4҄$mŏq1Ec#oڧ a%- {ֈ&W&hWoV._QL?k*H깺-2L }2%ln͈i[A¤S݂FwWn@#ګ陻VTO2wMX&HS{b/.ifL +t5dqQG6֫`yБ;Q`/%Nj$1.ȫׂaf895:Ktg8rܣ~6dtQ}ii4["YIV 6Iֿ Pb5 QMSEHEΒo sҖȇfLvٸQ սQG画!Κ +\ѲӏuKPݤmeI4= b(rdQ0d~ qyOPÒ\5 qԵm7/}-7Th:|X}.O_j\mg@?Rx࠳wiSfZudm,AL?s_ `XnR܀+iPC(wm&mʡ>ĕmBcGOK(<=9!J%>솜6n )RQ8{}]aS&P;y [fs[* +᮪%Œܚ-l3^dSkvfi|W|78qmw";%eg0OJʇkmOEUd%-1En T9|+NeN +*:ݭDaRsK!"՘w 1djuDvI_~3|S^kRZK8oF[f*ƃ"i2f=1NJ# Q+,$fNZµSk-]ewtts:fl8Hc0_l}{Pe$ t~O*lE}d=ML>VG̨:W52ĖqRØJ4Dϣ_!d%q!9vR%-P2w|$D%`,fȐk fʯp1z6_o^`n|z(Ւ/M? s)Ŵ 2D]J3*B]T^ڇ8\\ht%rQEbߐ\t(f'✷34JŔ9.`p^g{y4u-3LjF倐kfUvW+MW +eWL/xNꖻ! + \# XAl>/LjXj\,:@f-ճj_;q6XE%“Yܘ1=ʥTS aM!q#zJ/@=Lb6M[T1 QOTر(c 9qb1m`QDYMl݆P(}KĭhOa/+KK*sk>[Ob5 )6JEE&f8V/tuvAN ɒ~݀<&CpKKtRKI24<B)YK1x# ]rSuVP.ee>W[ҜymHBZ^c7 -aVNQ]WvA":m +v6 ai C!ԋg׬TB1AG.зrBW$&]غ3UMp92lMaVIsuTƔ([8-þRd 3Kmo'a2-\/}|6?m۱,DZ_خe9OO޼쾣p;N`DǕ.͏;&u1\oM&FDU@8$'Aw@PFHH_/Hqq*WG롏ł8N +Q} DۦtcRb"Zͤ-+?b=[#V[cd 4r%>v 7_?=`.^*g{d+Sզ> ?VPg"c4oJ*kHc(b鶎`+{m"fUq-µ+NK`HpeS^dɘ&q^R]p ٱh>\ U鿧O]]y:qNRQᆎ y6ɟZgҾ5=4iD!:+Ѻ/ +[vZb⼄ XI燖՟HYXI\$_;j o'Pz´\K}X{i]~Xc-|Rj䔂݉RY8S+AHGѿ@IJLˮ+PNi]$`ld/B/Xldv~㨞te&T듬7 +3)pmF ih.p0,?rMވ牵^D諻 ;5۲)5 JNF2('FMϢi7H"Afs;Ծs6(̴"idج(TWְy#M Jc^eg!B V ?ls“ٹWh4;Gg;@:2k˔E\I8{R^8^F@Cd(h.%``61~4"^au:u6PWuWsV +Z r]FBM'& B1z>2!hD_cpNq +PG%R|^08V*4;}XbLhOtGD:m3 1CbȔȦqfmU|:MM,uinkAPyX^]Z&2V>BZZ,Nj\@園A) |F嫇I<=Nx7]")Xޓմ2dS]:?&Q{CMFkȨ+B?$(qyu۾t/q V,b H5R&sdSJk>Z*irm +gsS{7]hM7e!͗Pxey+8WqRg)JZsZ6]WMZ9;hVXcZmQ5[F.s*Ū@~sdIy@꺥T*~ O~6t>Y0C7{a 40F"TxLAnQ?#C&67]>I:yI"/e=naix&~ZS~eB,ӰRnўe +iW0wBZ (+zLҴ}*|)1&=ŋh(m Coӆhd0`0=16]B;6nlU3^Xk.19<\bFxTB~`K@}5y'Vz=!0BƋʒ= 3!ȖH og0-Z,Rp#UU)*'N՞ ZW-G<Ǻ[=Z+][~=S`5S);z6 C䃈@M蔷;r +B ]Jm% بDD5b!P>K sxJנg ]%K3$]iuѧҽ'>*fDsT1 YdB +6?'Q7j!,Z-c0k +'kOX[T!f<75?Nd{6zmC_cSus=kei4OmMk!T<2|"Ӧ"s*4{S/kvD1f-.߁GkG盉䗹c/IO6x(9 +:B++쉻~3xy?P󠑪ͭ0'M' hh'}<@tiTox8h쯯oRՙBˡXz +^i b .y:+W6̮·3yhD6/a{CoxwBJ +W.yUIYݫ,Qzp(v烜ޘY_.N޾!a^CxdmA$d',1Usg{cϜ_qb.gm ZKÖeo"._T v6(פΒJ'0c9nȘPx7@c'meZc%7WMEr3WxMe>hF<ԧ0ωnehG.Û-TJά05ᯙ(MGKPyP$uM>q8D$.{(v*rt YOC;ǣᇨ AT_JeA )7D軀DE(y:GI1?qk{ +:^KjAd.##ohQHLxs1>y$cm$%cT上n\pyWt=jZ7Ɨ#뒋[ū ,8Yռg0$ɣtê ˛U^>iuت.rbgоfދ{Դx"}czٯ!3*OםP-j6%+Tݑd8Kg><{Dh+ I#}Qc%aC 3F wMHb'rXq88@Ŋiq⮷w8B*7,8~ 6Gd7kރ1b"S9ExW hǢJ2#ӥLC,¹T\OLXb@0br Ѫլ +iu)Y_Ev֑Z)pgEgYk$~JƊk;ڣ@"!=|]ڂϙ(a8DA-3 x! +OZ3t'X~0p@"J1"E[L;C} X QCTk{ j/kwh0 L  6[6֘,ykp <c>f{'*$g!}ߙCHf;@*~]F:*ZuR8>A`>DnAƑ$A5)Qu#L9l8l9E ;Y3G$ܕ _lq__q6s'd}_[Z,bT⶜4{-ǐ}AI(}Y͚֙m +E?Lߜ+^I[CE  ѡ~ˬ/n{6\fhZ*ͺ0wFqe8<`vUhOq*(.59UZ!襶v5)"'H*%J:cuBcc}!yt% 8KVPBH\8GB<騳X7 5 x C!ϐ("",6PCCW cC|&cX\8 xn]0:7,y,YBc(&!%){̤1oڪX=6աcaeHk*f@Jdt BDmQ\0ԔSWmK(5U1ْ\)DQVN#׋?/wփAL(Q-UHo2@I)SwkA-,޽*R􏺄B:$?Q]%eŚ9$o> :rkM_)b؞Mj_k(ҐR(]ԩxBQ q0 kѹ3EDu!ʹ$fS\-T%iGԢΜY܆YW5@^QM 3YT^jJPdygd-ﯩ7 +37>*N792ѓ/ø IN<,™!5@)ܠڤÖ7䥪`4\)ҬVMM\Q{d~L5bx +&kUC)"듲85_:֜CM$n4e \ +l#0t:|ByFqY4Оn<ظ]1F ű}1Ac+bHq N-?Œ'[="u,5[+LU=ζbO5!o׆_|32BDNn b\JXj4[ "RbUi5R"z +%&PkpZ<7/pLaRv%V/:b)&%;¹WzP*{Tbf%h<7by +$Hx(ԍT)7%k[*qیL]>>b-i);i' lm-OL=W!fcO ri8z}働dFTF3wd +r_Ģ +)))U$7kZ6!  dwNH}+MUjx'5I},# y'd8hٷ_x.O:eS.|8Okm@.A.k~(rĽ/qb=[lUx셤ܱJzTAyIeUIW,;\ b:ZtܖN;)\k1WVi:՝7Gw]HZa錄+i:M?znW=jNr1^a2] ms ^G4(4nm]8 *u'§}_#QʫݪIa%wG؜1G%}ʜ=wB|NR.l( _߮fBzO%9=.4вd@}~NX7/O ZCrB]MPnX8Ҋ?usp^ٯFc68jHi;ZI]#X7<UX ;ZCqHa{wf6xSkq[O?T4+:U4o+Aɚz~o&hñpJ=GpT:ק4Lֵ*VeT}䥪0gܽQg PR7}e6-c18I ;)PT{uּG;#xoZ/ 0NmbWf|Pq2CV2@|dyl]r;%L6Sm`}o53T(mc38ay6N^vj$֙m,\'˻^JuuRKXe|~5x:j3تݗ:MF^"]-?Q<&VrZPP 42WN7l +m1rMȞɬ;I_guҭKjdEAVTujjryF :+7Ab1hO|(SY{}4)])ݨ[Xk@z^:W)HjjIhį`wT\,ƆʋO{x_*<h0Ufפ @!֝쒳+auep 7C'a hO'W}H[E +UZUݲ“ ;P_9z& 6RoMfaa~]+J5y|\;nR$ {,/0 !z}M:I1~j9w㪂eЖ +:&` <yMXԃy!O`}JgS\Z;S9; _$^7pxWcA=]xsj]U*-+pBk̉~e 1tk[-?^\3+t[j1A9w‰괇ڣdNb> +3y`C8y7}"K,/Bhz;]^MIVdkӚ4P% zR͋0-}F_Yk}絃x` ~|i/|#e9-KžpUmo/E q^)tkvvRI2OVP1Y1J殺Q}Ѵ@=^ +XbT! ½onNR + s߳3;'l&e4TKxPLUZp _ׄq*geNLy}qs0Һ+Lo5hٸިvC>6i)PjN:GN4yC)Dn!EHEܗu][BH5SQAkaFvh>la 3Yڦ"ciǰYJ."fibEφiQMn"dib0b".)V XȋD/S|"SaÂj='҄O@V8Er0!f+.-m6h#Ƶ (A0e##B懿hn +oܠǙiQ`aamt.MegH GG_?-1'6sD53J52`9e` >2o'TrtU'ήhU]ijkɸxშnϻA8ď)2ye @ڵ#pG+;t~5DmWhӈH^naMa j+0qn+8&oktNKÈhԩEqnxҌ ~;^gCD2Te~ti`iByT[>H (!8HCOυ"[IaO3Ofnv[|Nc"mƝH~ |>>:B!}w4 =i?ɔ6;?t 4*-t$\x&_ "1]QtoD{2 Z3 ٖ!?9ж: $Ps"Dj ᗂ۰1g%3q$l4V}Ȭel*k)mDb{ ~|XtBs[ hN@Zø_.FxvH=r͙#{EDaq<(t.i`PX:tnӮ!xF g.z vaCkPg.c@5jPuT/"p[B +J*/3rVxuxtc?Țzh9UJd؄،7 @%f cB=h7YrCY6)̈ȭ)7~}ljk%Z,&8eG)FR +Ԭ?Y ܷҧlpʀ}{*Γ=sֻ]+] @ffL- ]NQ'tw$UyqSK% ¢VOm'?4\ᵚ#U}ܙsGuE[{ݯtyru볊GU[$/6]7xڌcyH`o }Ɛ`Y /4>i|*-~A>]{ORnu|3f124r?=BBt" ګt٪ L +&+?_ +ty/ockێρ:v MBTCWQPRqGe%2^aԶDG-%a(!V!ى$g46aD%KOPVd&=V('9&Ǝ8$*#y^?_+y`GI6A#eګB*hboXfaaS͇ +pqA37eR[iV%b;j{xacvYȄӘ,7` 6rSp#+@wS8%XBL$cfka\mɅA@L@6}iUh'Q }@+zPLD .|?[bqLL΢H<D.=yF6%Ʋ4`%\;ͪXQmn~10ZHl7ճK{4?V4Z%GЧOÕ{?\IlxpG|<)g3/Vc{U!Q!G]3HU pV0e$85P&N6SK쉎=kw->sE"pHÄ4M*tq\U:, <0y3 etE>~Fgo*۰&6$N'ҒCX; l๲rRW EM(e{O ö֢Yxb?IԓkdV)TP +B\oW| L^472V2qQT`){N`,&59bFRMz}pOV2օY,gBϑZ5HM +%KP#J +э\f e dyi'-V[WChd&CꘆV3P i`j*Zԡj̷o^e ~r5dd~g|1B LB0blNxDdTtLlܥW^7orה;iMw3=frr¢aQYi峪5/j^7zGK뛷mi`П#_ƿNLJd͓7iE.[Orժx +xW]0̤oqfiesgvSS|!g 9IgQث§|sU"UG>8\ddpga乆^\'t{UovՉ}kJE|VҗddCCT08S&r\cyWS>wdg zK_Ai'nvsWfnfnwSvdcbAw`bbj*Ǝ"ETAkfnoՇn2y ԡt>g~n75a9&V C_%Ͳh=Jmn9&P$hl QdOw/x3MgYsgϸcWm8nQYQ.^qGM,Ml}66.Z ,qŚ7/lToߝd I40>ð}\b6ħa,m13ZGuKyO EifQʇPr?q۶vBنfqR`FQeV5ڔ˂}lNp)'1غEDӈLe)0djZ?pF2^ئ[; >4!p$IpkrR9DIgX)YyJH.%= ӽרmuRzY #AE2 %k[Rmc )gIrnPi%uԬ`6ښ6r<SxHڡ7^m0/a' zQɔW0-؁5LOV{>4@.8 <ڥ5c$ok|~y2dVݣ +QBGlZmmh6;csqDFWcm0oU)kfjriJE!_,.s(]fnۛfj/QG{Q;cЧo1 e#,XvOOe:xQ¨Zj_ti}CHd!%G(wp"k+Jx;% o)a t^S0=AERQ*(\))yD~ Oޏy~)rZ?Ey5AqqO;[]OLAJC0!BAyx16) WIVN5 f J:m]tWw|%+%eVT {UZ:iOªWCk7E}+.} y!@ot0/Ltv~q|/:әNhg;N&N6k1&'<1 +( +""(*^"xcu}ӗ}<3Oއp;R-Xo7qr0:'MjqJ-ҦW)URÚe[ܥpi4 l Ր8Ȕ#N0iVQQʊ/2fjr2fd*"t:E=YˮUjU? ]짶:+ H>nㆼWGޱbUI=Jڠ(cTd5 f2Z3W3WP)8A݅2p/鄇1׸ܥyvBZ+OWh "yz*B;[Ti; +dù2B7x@lCyQEn\.Y2ϼ͚TL)~CQȘm!3f  ag>?Y#DUlX;at|cb6gN&2]σj , !CCWG7DCS@ fkhZ+Ӵ(o ec,Jn&y4c}Q *0XOk2a^+w?lm ,jawW%7Fn6kp˲ 7b^D3. W u? + p$vmȼ&Rs,lx.G<˖G IY\O.<͜!,_`o0IA,t[6 4^yyߊ>]0GoCPP,b(d%kr5AR1ei\< |H%rM 6M.8Ų[ yUv W>&LǪ2Jp +4 +h|fuC9tИ0LTdh(m,<<5C4f=)R aG"dXVH8Y?۞췻ܿ1dBc*޳OAb/˩FX5yҸ +pj$;茙Œ#1!P] ON[j7 6xJc~h=Oaȥ,9sxKc.rkzl W{-ajTŝ_WwT}5ua XiO_'/ڗ'c ϫ#N9PK= +&x֝4A[ܡw[j濾8m+n,}H١]\;~.'q1*6&gɅkre<>Ї-moMʆʀ5tj;Nn(oqʓ"Ċ}ײCcDzseŽ˯+_Ʒ,pǺ v$=6x~uMLǙ[ODئ#e)@ןͿJ+@yh@@D1]|4f-?DLjYHuqƸR1BfH \./U +ui4\~:dBJ8T!0: Ee6)k1 KĖNS2/3`K CB!7y͗ tA&<""; 嘍٫En1#cN8iu֙Ng{؂**!JnrF 39rr@A$A9~}<}~}|k82h!SN"ꦈ)@7.x ˑu[2 C5dzDCQ8uW|Ï]g+xBqzk:$'I,DH?HnmV|00y + hґ84gOY%>~WYTlY3MN2 cl)Sd죈]TQM[ ~p~0<@wh@ŜH L;IDvCL($^%*ʲR{ɅF}f堎, +y&`~4p  @ AFHG24y_#zFeqzBTmfңэRScR&o-ZhB '2#E@By˜댫S%4xUg]ZlgRk +ƪXc,Cd OaM-(M~N]LDטĽV1i7CϪ63yㅌDq{Q& < |+">f?,?a6tLj4j\BR+8fz%eR^2I`1t{ܚ~b`@s؄@SFmmtW 3لyܫv5[5rXjfs +&2xqJ5g,º Ad Bˉ}wnOfjp0?I7Wu h%+:K,$Y! yd.oĚ; vu>Vxx;5y1LZ@zf*[CCagڬ)nJf!k<;ӡx8b7Դ^QeS!|%L 蹯1qA&PϏ3 t;jr}sX-n]u-/rӪop6f%(ʩ usoN6`%)O.r5z )8~sgНgotR9efO-sO@eZ@dg@B'nԑ8ݞ85@|ܱv΅%#/\_q +oރ6a Ɣof&׈8lX)Q.T4/ŲRT߬IlmHik!+~6l70!#PJYŐȉ5Iִ[ %tt~qٙmwәtvtN۪[A G@ BB@ !!w@ B -(" ȯa|k 2]Q%TS)q:@ u~U0w@ +LPF}A*ޭ=) 4ofk$j21_.7bHD%iS$ƞX3!C=dCjlD$hF@:% SWOQ2dɹEB~_oXH$aӵ!Voj 'PjanD8hAA :퐃kVųJZrҤi٪|B}<=Ow=HQw5CTMg_v4P::h<-Ɗkخ!r׋ZZtArNV-2vf){َ 2p~  +8 +K9bh|l.7nIh^J>*T<HFTɒ02:GaR( 41|   :PI>Ws&u>PJL4/gY~U=WnG @}$ %"ҟI$q+BĝgȆKu9&j@4I9il@/Go ̷F5dБ 9l/O}"Z/9iRsMV_tP0$ $CmK @p l @JB KRa(+j\Ce|ՄNAT2$]ZuX4Fjbl(^`:`A2a侮j?]w\"g'cm%H؄7*bg,L& GEw%"+'NcX'gh ;|k}?1yZ=N&"gQQ4eFu2 } ^kk +ٝt@#dh:9h /T:l}Ώ!"G#;6B7v҅-錵 vg 4AcJϺOZܾzʶ2#2xuK9.u:P4chձ*Tqle܄ˏ2/\Vߵof'.^>wgٹ&wn6|KK|8? N(&0QxE$I}?[3U3Ő9h#.w]`غtkկ 6)<|^y><@FK§MEd"gZ%þN77}ߵwjǑ+RnD%Wr7݊Vܫ7G&̜7|CϢx(hRZB(\G>u̱,{貫KtŁKR3J|kvzuĒ; ^@PAPVB$T,3h!w >k'~mqх3B cbdK焴Kĝ˙W"\nmJQkk>՛UՀ*j0Tx%b-^u|б]=}zϯ VP28%1fGRخٝӧ&g9|7!:|u ǒHJx'>x^Ͻ ö|B-*f3oFmF{ܕr>ڽrOBz* +rK N&7) +0p<=NfobЎofῚ,`4{o/9Lp1(8VBN%Eq&i~`=kPݷWp| +K*FSJVz*"^pd&<7ocx5C!4P@٩PyVHwWE-e+xt0$]NIQ1lG5ب1Є4;%#Y)eڹE_N([7+hQ,3m9x)M4Jj^y2%_$ ET\ Zrȣ3bht`::IЀQC=j񆖀sЎaH H.[L|ZaiR6SިZ t i2d>C9ۢPP)S9NsF'pKh9Љa2u[gl57ZWJE*,2Glkh?Zvd@ƋjGMc0hD {5OB{@tc"@8NO2yO[stE&E{ԫS#˅VXUk/Fʬ:CEIo f"l@JB~*—uY -$8EiZ 2UШ/4\*& +lflP{p=tA? yi]J-;IwE7Ы1ZQ7ڨ"FJLơrQC7tIQYo Mjh>g@megJ\`<|NLbRx%}c,ZQc.%mܢ)6+fEհYa2{} ;بHbr!} >P?ɳk{K4\iaTtEQ!6iz .w541-MB69u^ϜӧTf0Jr]#AC԰ d:Lw2vO[e+M,.>fِȔkOk9M}m@\>aX[YlWsMǒw#KⲴ\IЄZPC3j> +;HK]L!.bq$1g.5|̀r#Y<܍R}[q&jؽL2^Sg]Ըkk.OzkxZ~=>G@qmMPt)؞vߝpWigp*R\V xefS=Y^* L6"Jf̓w 9 ߽Ezf8s%DwgMaN] @f~(,`OD. NgX&>sMxȘ2f!S7b$o?߾M+э_\yzrt>2 *Mv/*êyr:r-I1MR钯7hգIѩS:ۮ$hC A|m_7g?~xw_?{xt5ϸ_v4Č8vZq;c׭x, @B @@$$9@B HKDw+GD0AQ-y}yw74R_2MR@9u"D1NЅ?67tGb-鑺4q.zSPu`4Xx7vkn׻buuټCP-x^{ex'J$V'$KMŸ(Oט7CAV1yЖѩc];xu١彻IN/X9.`̢R=%zJ`)+O3g*DWTC;^ց/oRKPk(:sF /[aK /v_HC˃it$d+y/2vzp\ߡ=7_ǎU˽pB \`T܏ihKYQ(>ᵫ00dλ(h68pS0K@DQ"9S@/ {~uY`uR.`+B*XlQiS,*; x[.}C8"&&>+m }i68ݬpdW-X ' +S6N88I_b>ZB90VR|V]ӼiF/їa dnH3Mfw>7xcS } +aEdǀn8K,BZ*lȞ`wp^w^8@:$uU"*h3!H--3S6੆g>z7E{#K{=O{٧ާm5HCҐ4XY eCSj)ytci<~pQ⨺SJrƋ̞ƖFVO6FS]fOF @J@%w3c|ЗgѴr*nYR`!Sc+ ̢fytuKSF[sݬF(kLyH? "P|Ԟv.ˍ&BtS~R26}DFKwjZs8:[RY֦άb7):]mrNW[UfO {w >ǡ}c{zo%'jEK],InfT9֪ +NťrN}{]s]l/v^,^oPpd1yo xQף{ײ88bAB:!@B1( % Ju(4I0"XU0AAΞ=h}xm<>rDVIyMeUٱ3G\W(2$͊tR.V)2]v]Ӑ4dE[c:~G?0lߵ3'Nϻ<):X'n7qEZ"4J隂tX%t%뻒]) +8EHU@*Ґ4dB/pd6FV2_v{َT^ܾ6HE#/68"bIsgruLzT*^T(j/}褚Nfӭow~וրFAVYmhQ~J^WKayЂ +uHa۵|JP2J] i@쌠s}?-~G +kP'T2_=u!.2J **MR rTKJEP3䂪3nedA]Pig҄3p3{|G; x |3_|J[Oz1..w%=.=-jK`fASԭ@:B.~5v?|iu5{emc;;W1xs8u@Y_kkSd4)K|U]u= @&Ґ4nznta8Ťk)_6)Md +osOVBռFOk`1[{!/ɂFvD؋qg8sS<]7;@mhND+-O:A&۩A@`ma#@\8 lA0"WgdXKk%ȬF4 +'y2><ϼ4=48hKDHC84[px+R}w{(?D68*RbH!i9j v&tΣkkWo˙ښv^LSR=_j5*ejGx}( +("( r + ((X'呷ece5~? /2%جƻBPNٛPIPM+eUj(g9f30;|} s3r;:[ t? 9XD*R4=[e4*^i=A[yѝ1eewu̖g}N<@ru9Ёv<)PȐ%w$*W~/E 2p>"xΟKɿt?:NcoUd8D]8:B= z#y}9* y^-XYil|x&A#NJjO%OLnbDqS_&%mCt {t;xGTx^ MBOJ3hyH93d +r,_5KRқV@Z?}\ڃ^e159@K@n0=˃νV*QKҴ>N yoXكU+2ꕝ6AE+~Tќ3l$(#Jg@> }UoA>ϫQh|U|,?5k$6H% $Le[&_}5K׫m65Wm! IC:HmZ:n4.P $^ȇ׺+:ԹE-yO3 +N;,iΥ BDDԡjyZ5yP(T@tO :4j X651y2KWs TjiszMPM6T2 ~䓁ZgKP0W ^qm]g 'YC\+1 s*4\eU1VXȬ/ \.l0;̬^s>ЁTwמFľͷ4}ϰ%kŴjI% +/P +!'Io`0Mt04Q[ +ݍ4FL/78 + :p`; +^#<]Ҿ{'.<Ќ +0G'7%ڴ"*'ʑq::bڈfBi-mtY \f%CB- [ b] f>?vЉfs|ΈtrAu)%hy Y| 5U4Ś&n3=Y3tm*v!Ynqٿ9~>8h?5U4Q}*ϐ4xMhJReMI2EclITt7`TD(rYCkombc'ntS_ Ie +"Yh^Rw$+MERw({$:. OTE2.l;X.hi.{kEݍϺiV ̙DsegO0c(vh l4Yty /L!SzA|k:<Ϳ<0-puoZ`_,m_3 +'YW|Ƛ;.{|QJ_8@L!?z@ͅO? KEu r|W-F޻tpbڏ?n^3Xok/lAOWϽQg> i|q1OϒPOgAE be΢>0Lgtz:ֵAEa HBBH ;T@A nXEq_uwT`P, +XQBM@^z.>7=y9; fd [Oh6 %37DiDý/B\X jG]Un-uuҎxDx#"户, +bAu#v`g9AD%s^&+d AԼJcB̙n*,^*wV X> +Λ=k.p r{+.;∐7I5-o"/i2W +|ec޲|XfX9a+lܜaxl"b`:͂`HX21]r<_op!`E1]$DJ|  #!߮"U:F! +wJ~ѐgl߸IxDG|QlBSTJ@5MHg&O䟃Ze/dN+f'2Bx.b.R\ # +q\' AI5?~Tۧh:Ov)AʱV M>K#kSќ#l[ |Hs4# &Q0 :?z2`3Z3xX3tB=|^~C)j85Cd[kKewl=Wݧ2珆Es&wpjȝe?$  +*Szn8F7zL;_ڏa44G0=qvvWӔЧ5dlH ĶVP;:_BB!d}N?SQJ #+c @+c}x]D}ۨEӽa>ĽԶq- ݺF^Abw9H=ɋf#j)6+llO' x3C&Ҙ*rG{zb겺Y2n;8Ox]'un&cd`l\% #s#[YU,{q5_e]9܎XSN[\M΋ pnqy_s$rCБw%s: fBk[/DwMY1`a{LJ?,NJU$->UTf(Z~TVj:"c:8j:8kQ1Oo2U5Irs?PhLTS[Pкak"(Uav&r*J <TQzP[/rZuR}jzI\rLӰ( ِe1|WTv?KŁ@AmKE,C@{$0!aeʖ=&,% pJXׁ(Tks զZbI}׳n_N*n=g59}h*SZ.,̪?om.ngyu^M:gW5neε6Y4$iAѮoDh:MS 㾎S <&}%*>>[\\-Kk,bʭ=*, 9yӂ'j;'w' KUDWM4k)Ҏw}+݁1!#t{ xK\V{CbIKUjMMYVkYq~{QA(QW( 9E㢜)Qn0/F dHR jh 𭷬tc5t|0!g.IH]a}ˆ7:5!puzKAy SܙVRӝv%JK/t&q&:SK&EBY SMҵ7alaQigQ'#!X~I8ď+:XYܘڜUٞXSEwLekwtIqlŤ(|C/ Yڛr&8fRIŮlz~Q0]2@ 8A?p'"V_&mKjLnIE7EG-∦aD aZpF6Ȗ6joڝg]F/Y(ML\]"bQ\qCb&VH"مǣcl>_ho3< qw-e+ 䨭B:}{Tј/8|;4鶷wo|ƨ:P<(I8U)IhfrZC$-!}Wj 5Tk(ӥrPOބ*1]m~7fa[(^5\Ű/' W :^O;[\8P8Q0:X06Spv?_# 䫮JM%mozx~]&sk4pmwoy+AR'GMaa%,H)?QX7UW7-PgJ|g.kag:Z\n+ɹ5rC>v<"5=tOt-X$.YѿRww;yIy>ws) lol,{^$[(SQc*79&й_ע +KM%/q/ \bO=}z= K#?&YFҋdKQii(q߫C @z?D͝GhOv^+;eGb2| |KY&F{.{y$Hp{>+,wcq(k6nKCyE^C.hT,Z؛jejZk/63IHgtF$b# #JXPDz>N3dDhfW꺯E|vo-Ý`l6,0C+/ROBx9w=AX DѬ f@ ALWO!tBCu#Z Ձb9hmȃ4@W'v2@Fv0v{]]=l ^@y bȦB6 +Pe1ZRLp0{ M16@xF_u7U2ukʛ%!_![UxH(/m'8۟DF )SWV`,i@j &81p<^yjP8 +QqT**"{oV& OHBHR@*R@q^ \(({-V.9ܷwIX E,we 0_(K3o^ܾm@;8|= +>j?GXaUx}֧}2mm6:8B f(Ofi"f=s Co@PK޷v9f .vX@H~)Y_kƯ5_œ 8Ü<=t1z,:1zADLG?aewbg :۲"o轖?ۀpN~`A@aȄ)t c<8:H 匄hc^V -gaӈaV3ӉBQoc=!vHy/8- += pe'((I E : yV #&/0 !qSm84k "7V3[! s)N|>DSHLF퍒D)1Z~ȿſwȯ(>k!j:`X XVa7wlT> GF ϻC%˓~%2 b:IxtCx-l6F#g/S>(uZfi r~ںkVnZ{7?7^ë5gnxxJ)?`W5۲CkמXy[W_KQNmLPc> _Cg4HF^ ';!@*J A:=Q-JhW&<>^AQ!Xm,sHٲ;W.i͟3~Usx~Uo;>O8D4UTf/rҳs2RT)BF& +Ka/Oߞ"ACuBBC zJCږܹ|x*/-h3ѲruCpR|T(*`+lDd T"*4%촚Vbfo,QڣM-283#o^;~__=k&7y|Rp]Y(.#u8Y!3%Uɑ*4YJTRU%dYs1IvWGvk =0` +m +۾|WW xںKVl=/n{TYV.“sNQb3JD [B(y AQSpyZl~S6׫ Yt$__s|>|agX|ymW[\[Kw5ufq@\㾁,JMKB&7II@EEZ`t +ETHG?R(2 +RȢ(xzy~^eKҍ뎭ر} 67l?`,o8t߆C1zck(X3M #8tMC85NCWF]Z~%ҽ[TxI)ȫҐcǮL;|jiF|i.Cl}ɏOdJѕۭ=.YGX㿃S?l>2=97 +ݯ_o1Z4 k"u8wo )ϼ1_'T.lHVݩض:o;Uy$S]y6Cu~BVuy[U2H7NGNۡ*;t0?]l˚|o &mFh\t W55K6jVo&lLDJW֝٠MS?[y&:Ƣ"|y.<4;?󉀹X$%6*L{2AB4,NU[*i ޸{5 ET,}XR +kn*k kMG-zx +[U6UݳCeƠvڙ3^oy Z7uT+lU/[K//LKv$`cKKګb%6޴mȵAOsj{yd:r'euZ:R=O%EcQԥ1 L&~Fc䇬|=D47m-sƚBzKoC?M1~Ӏ| {% , +2ٚ(K&)"oHNq,*GT}IW $C _LTCš]Z"TF)#,0wj*w^^&/Dl10`@.e% IX|/ B-BF9Bt7l~q#a} +8MNg'̝/'aLGR/s!؞!2#Ͳ}>AhO+B>!0jd2>&O 9K +sW, @`O, `:`A $;::IwPZ.42l0zNDLu +sakһy^ t ! W`3A  F@0n3!n dңJ#Kѕa +F~VBN!ހCSX2, Jc0 + :ۋqA@$ +mPBh&h% vV ߔqE۴ +༖s z,. 1"oWW\1)A +$I`ev@U.!}A *-mA~,WfQ~( Qt9 wAz:Ȩ0e HOUw ,Z[H _"O-eՐՠizA:^QT!Uۀ3\A̘JzwгNjy$uRǓ L 7j-!tZ?TU]>OUOP)bPl3&yN? bB|7!FJ}QĒ +Ye|`%_dUNV^~W!yд+4YTe7bPsv~.<" c U~U.੆.,_xVQdx<'QN_wT-ѭ]/o5nq7 (:Πu ;ք0`5w%Yҗ[2Nǽ"ǽ$^'M-TkN8g(PH ܥr+ΆU#7⛑:!2N;cGE~ M7 eސ 7,)}(vC6)r$ +?S5XN,g]"ԳmY1I}m㞦kL8l/z&$zÛbB&C78H~"eDV.y>r)R6]s˂IAUhg*Oz#z@^S/e{ ?ZwOslx:>{Ǣ3n$Ux)%mLҊKZ&ƠĞtK|!M~-J~-`&x@"Xӂk?~|yΞe-WΎrM(r_D>ϰdLfg .MBY-h=C+؏ѓi%l@] EKݡ"x>ԯvO +v-~urvܞLRdW:xFaم"q-E8 ++XrmJX1o+=P*qYAB.ҥ* FENx)>x-?bE nWޞIOlX̚\,CmBLQ,) *T/U TDYXef>VOf I> |5˪ +׻=mwlǂqcCΙZL& +JP1j\Jm&Q >Ջ[tY:N+jŏ4#FABqisa֗.ty[qF׉ʭ-~LUtͷ*rsTOtZNed' JYA!6`1C~6&{I|} + sa v~}k穪5Nk6wΥuW{ӁƆM1KG 6:CU9i1L2dM'Ds`0&Z{~Z`|h48^ Tvp vvҶ/5lÉkC!S ƒ3y*r4+@/0g Od%5k 4 ySڄ)L fx8O6ܩZP۳ʩg낆K+=[P]s1HQYjZNWU̸ + +wʦU6*b+1ϰXۓ \̠j?YDnwt\rnW9_Is{ö黢e'rO*?)مhK=d@5mnE7J[n_I^ɢk_:C\0r tq4ѵ>LJgnR % +!qnBrlDu +""^DO';:u;A<pY:huU7<[+q|g`бW~@F#& QH0q9<)P®t]N:t%1t3K3Cd1c\<@O3(t֩ 'w~Gw}>Gua?؊1 $Ե5Bl߽w޴]I*JpL7`)Àp1Ff34$ #@(FK,?`7ۙ3,|H5*$jM=xjshW,Q7nf2M7-ʦ_VÁlowp;B!YB_!dcΆqdk}o׶OZў-3l -\hխfRMz=r~_KuP7*Nƛ8Dl N^&`-!ԏ![lX١e1$kUL(>##DE``>\ mPp˸әNЙZ*s[M>B.Wr!6![c?S| "´T n &rKN8%$hS`IG^JJSf`ar V&Ec}R$%FbWϰ=XCȞ:Bճo$d3B>#c+0n|||4??h"zzfF&#!*̄8&hb"a+0v` +֪Ho=&!;lOgOl&tu0c>c?ixXr0$Pƒ@u?̡~p0^ALJ8 ( Цg#-BzIiJ~uDSKd^#^ 1x/E#>0X@0$O7ea<HPY +dO!c/idluJFMaa#ދ @ h6/*aq Kh4H-\R-F_u2 8:N +#jXtI$iO^#W4(o ?2Ph3VBiΣ )Cլ ;1|,dt#+kꔁCެ `3XP90ba'Cgq9#T#]~']]>>W^O*]ʛ=E'"PuTE  + stl FN% K1dS̀rWD׭XRlҾQ~}th^_ v5Ui5+뺘:I }=82N Sa +kv02"P 5J̶{LN据LJCfMUЬۡV骦tWܬy`5<;&r# Y@0LTd!?P6Z!,Q&uyŭ{a\`zf\fybW#6S}{#i/Y5;[o-VZ6j>_h>%}1/n +*S-Dt/Ž,ץ7٤O +4mswmͶdmVg;hݰ__گ:kr:CswZ6GA$̍OfL]߿ڹjYN|2PR/5ʭt:]wq^ulr^鼜cnRż\ 6iWAkXPQUWOEvdi;2qr̀UgU-wҵenOEss.ɵkKYv]S' +?/]|EqUaOɡ{񟈊1X?smG3~YiJ[\}RE OV GWQoCG*8P\[boɛ9K*#9`YS|7Lxt{8`nj#tA.ZK !3̘ 3F342.CY\JnIWNw<J-v={|~o݌k}a+8PƱ^ +%᧔̔r;JRO'˩ NjC M2NS<եJ}K:!Z(gˊƚwx^jqՔ7Gùúm4zJ3MmHi46Qʨ&2F%yYA~Q^30')3TAenBOb#RSrsJ;{2C;.CD6NLjJ}Ffz Ǭ +*RV9UFmbRYREv >^$_Ez:Q PUXt*2yiYqg0>OI,z*Kb^"z'OD x,O+VE`]HF x$2T_Xmu{W8xPl-ro( kI2VfferB0!3O1[9~>90`Y.<\䩂V#@MjeP fJsL)mVJc uʼn(fXlDbU 2Y& ="g;`-YrUA} +_7Tٺ0`7,W]t>[k,[]1'NEi|-etn-+"T^(w07U{*H9+s  >K|%(W puƅ5M^;TT[C..%JSRy +)l!)?()E x0`Np?W7JZث4h La7JNUmP48ȕQG$,N=Adlj +cŅaM@,'GKĢ~lj(EK V}+`ZwZaU0m;m嶱פ-Fv:MH';^WTasP]FbRZk*"Ҁ' +l{bJ7=~: [tynr{B2]щ Idrx[zzh7=8-]N hL;q;5c>5m +^< 0څm?lv\YQ3uҫ .{]$f QB Ia1}}TRP_6)Ot,21`h@8#xGU `B 0p,@yۖo9onx9'xŌD]F<4MFFep0k7B}fC|FC~Pe^0]eЂep۟?A}<<ԇGG{ײ:Kvۑ4y6 Pa"&wp#dd){J=}qxz1Ob= \h ǃ:ȝ3g͛$>uN|j:?;4k.spczdz6WC.->wv]E:kXz.apb +^e.m"/M\0^kd>k%7{7>N߿ tv}]e2bGߪksx-[Q%}\ P: x B2>iawQM6ܵqvO;>9Y|~gok/v_ímYleg=UZX&s cn.V`P89 ` @{Ñ>Ӊ@k}_dd̶ۜ=7A.VcCi"D$3nۍQFtj*~ł("-EMr$@Zh IĄQDQdP]`È ϮOT:Î>( ~}>?WlBʻR܂9^!Hjb0*llaXꌁ/W?6ݽs,t;ĸzgXHpPbo";Rߊ w?X/*ZaTXۡ04ܝ=a`>G` N6h @k?f<"~nb@4x_)2o#nHp5(գ^r#(H@yK}Ǿa;_?H2xM|7@LT9~ , +˨_L=$Ȁ]E~& 2GR/Pss]: oW`g;Xb`= + |PWhUa@ۅҪQz-u~ ;2l Y~v7D{8g6Y@2xt6."BY4x28IЁed,e|gnb Ӈ郜)Z?{{HIt +(؟<}$:m3B}@B@<}c rx쯼rV WE+'0sIFwF_N}GQ;}P'F4@2_ 28RN " d] +sHcA)>a'0-,u +s?*vp y\~r6.󕤍&d>pGyOC@8<b{i>@!B m>p7lTx' nCp%82$zLtРNj{J8SNy`wDh3E/F>(>D>><$/ޓ]ݑ5nE݌(6DCe3ח8ځ` 5gꨁ4@'t fUK}ەqVyq!A"ɝ赒GKnl6ތވ^=+mk^{(m $ 7q>"OAvJ#"y +ݭ!z(gmO.ul.=C4-}5'B7W+Mˊ]]K,;u:=>;DpD=fbl@4LnVj gd|Ck^,Zu!27rYY3S*y}.ɤIuc:#gCɟKP}~]_oX5lAY5u +Nv`51JcZ7@o@-o5H<ή"_ϭ)YRݝcTmIl\42l23V* {eڌu%縮^c`XC%M czJ2{F"Ҽ4;bY?~Y#g(TY޶XL-3\n*5g*M;טdNf2].4=)]9ԓ3('aȷ-t"mi︆Jٍe>qJюȪiK?/1/ )\a0[6m\C.`0fȥ%各"Jn$9{vkHK;>~yίuޜŋϚ7wyZo0=_V.ϓU6R+W.˳e2Ҧ2e3ٲ/dsT[S/Dm2-Frn6pՆ霩ZS-v8T8JXU,/1fWNjr^<-GQ-ϨVg(';CkvY,㹜Ș õL[SfJ`R-F}O 7]|X]y[xuSH}[Cwmޙ=)[P( +TF6E*^1S(cȉW<ޖ=59FkhbւZk`A n6/V6l㢞&?fK}`mKeUϞqaY|DNqRBZQjZb~"./OWw07jg*reD9yOT_eͅA}WXn7Sam8΄՚mzm>QT~p׮Qܪ0[Kҳ"U9[Kaŵ9a% +i@[%2ED|*5[)jHq`s! t‰nŽ.e͝ƵBNacIcgSD@v}Lhjm|L|urrDeZtRDZY!<.업T\5*bG0&`b/up'W'+*t +:$n9G|eQ№xi,YҠL n(I n<]x$I؟ts[Pda&AT*QT*i.n2*;MΓSH׃3 e}F)dк㒍I[6 + 눏 iO jW}۾;VQUq:*zĦ·H}R<,#38p``t3[ Ctk_p-:a8+tHrmHDMYa"*4Uؿo<ȯX!DY2׿ +7fG!GHBkFPvR躃+5㪧YKBo=C}"C ~C +|}{|W|*L{? :zdC=I!&ߢQm(Qc<1G&1x$Ix;vvk$wם4{w;qw?u!CWjnR g O졙z GC3-h|oGω(Gd 'KVuw%~T3r#NLazNYeT[7sz]LiΚzKǙ.Ë,wX/ۿ|:5rtz_NwD$=QW_QBfil5r^M\^;ҝ޸0xٿ7{lf>Ĵ`>йhV;L9L8n#\$9.&_qG/f"H=uH uݑalrWڡ 5梛 -0ИWcJ&SX`Dd`y r̯(jjm%KQo#H7B!ֈkLh + (20-˗c.Ks#/Lp?d_x +5,؈As RPCK-q2s\@ MW!Sњd:-AWE譫ej(]JoSy0D, +D M@Cf"t&LdULd dgE-tO︟Lpf>Z`fAEK +D1r]ih@j9Zb9uDZ!͐{ܳL 3Xb|1?K wkd΅tC`QgFEJ3 mL&)lVWD>Ѧ_=-t#tqR26͆i3vlvgEn]uq*su;#{t +}ܯ?ޏ>> _P-Qms?t;qe xcV8[d (?/g"93Wⷠ^ 3{P,N'SGCRǃ!!Ҥ q.  @YBm~] 06L:Nǐ\4W\Q~L0AdHd-~Tp_QDD ϣ$_G%(}'o?c (w/~ `9.\W?m>p068(O,߿)h5fcRv`nTab0iSoQޤ [6> +uxЋC Tf ـH9W,5 AY1rd AG0g\"f& eޤCh25.zW>\/w9b|O y}|7u'©8@삝$c Abk]ī:iۂWJ2$;$Ys= s 5&Yf+4tytyt5q5ɚ:X; DKA{I6}7}˼V#jXUR"#%ekN]lC1vUQv]t'%'N;0${r;}ʝ~ͼ".V@Sg^oq2{0nXqMCqp]c|%313p~c{b!2Z[nZ+v?O+I#x(g4@܅ [l{1j9N&s|-彛ݥb%RAQ}p3EWXGX(cKY/<̬e +jì]7E3DU vB X0@2Г\[^Vŕ< ۢOo!f(1{eJF$ ֱZq)gx7wxJ|[)╋J/ay;_ot@dee[CorA#VНP_墐ltؚЪm؛.4dy*Zū˥%2iTڐd&}/=T,(,JG[@%-̇dRoY@A;j)T1ɡcn_o؜DɐU*etAUX,O. CH5uTzS +^DvYMc/eՙFT}iffG !1j.P(1 +DdFAe5,g>*#xܺu{s&HY-kURܼ=yekխ^wcU\Ì_: f~FϠ!9Jj2D~QW}=!_sToE)q)+ +2Ss22b)8]pcEtA[JT~oJĮ;GSuLЛC?prTVAYDITz_VfgAYϻm9E9(:^y:ii]:6?fiHxMzXhuvfk 46&8jPPBtyK2 Dvqs5ߑz~~u[m0'皻IUo+Z8uX}OH}_P:} lP ho_.YOPr`[n͂Y}GێFiiI;mcrac*2W[kT~66ti*vi>\jeiRxޜT3FzW +KzhOR<I=xt^G G/DmbUDdU:@ۛ>)a 3Cnzv +:C/cdK*Ksӫ$G{{d*^dd({md_Al~D? x@h7 Y:2]Yov$p:fx=zd;j&JG/InJ?Ilj_#ف;] @@T@9'L '\D1%5Smhiw%,PK38p)XBc61fa5u9G$$3'3A};6( SЂbVCt|}3 '$"l rtZG6L(Yհ%$FB: tWѐ_GCy +h~:-zB MGDh:lfCB\ y3Ș(RQA`"6 +SX(bpa ={(:H('T2Т1->K_OZޤOJƤtlPK["CwYR@KK&Zٳʝ@.J(YA6J6rӒ])ikB5!+e);zoU9f7c1Km?1dAkkd8b:JtQNCaB~\gcL0|qAqˠ *0HDICv$ {N%$ +@T\^ю9vuwnj"ppPawE]Жqf I15rbEJ2ݚ.Vg +զ|E`V&AaK_}J"2FTQg.x +N +Z%̫(za{>qe=VkFUdH %Jz.cJ1r5bRKj%4]sIy"MӾi%kEiLPptǹCZ\0TxtZBQH__V+ 2\C2ט*H͒ufyT)Rt +_P)ԺWrgi" *n`FA1 ~g˽`j[Ž2ҷ,ni\bM92qL0ȓ9JXm*Y%sJRB+d)LPp'Sw aJ߯v}v[ $T11%^x^0&;_J2VJFNˌFؘBS%0(ƳH; +[ȿ䉙 f*Dm 0~)8ҸPv~յU5rL+3Tj4O'2ɬ*١ei)<.a_JXI +G|5r?T:Qhvvghcm1jQlr"+S!ϲW̱$lKeiJbYLiA6aK&ip&<[3\ 3h`:`s֭#}KK̇(Gm JQ/! tMfdjL[asN6̤n1h5 J&jED3y {2;; @[v7`Ttb\K:>p!icvʉ,ptFsQN;$QD@lzCo/9- |`+tnjPzB۱Q4o>?B<\к2>wU$Z\PUV:2Yt;+2)9!IEi kBl٪UE I=܍P BN!4B*Zexpx_U#"Bм)(7rݥPF#9eo6-ؖiTt6ΤZߧ#-G-"֊L°RAhG'iNT~wokރ}\6B[3ßE ] dH vs=]'kmo>W*GV=!B+ o s[J@52zx+8a W?'FH4_=j#|_3yұy\f|1#~Mo`U|ߎg <% 7=PG8L w ,`js|+@.\ +Hp! 9쩐B7^vy"3ϸ|Zp ͸!Y: +ASBFzˁŒ&A恒-̸MwqqszwL 4@Z 1 2+ aȭ\~u;q.Х+9o+q&gTfC cyIm@A_  0,&G" ^!f  H6 Ncyc>Gy}y3ewPqӯxU pzcruˁj10FT) Τw>~1 F#U?_Đ1w;Mk.^hZyO4ND>9ĠVP@%(Y:2%}y](~B iW /[)Ao\Ot?!z]$a\x?AŔ~ԇmAn|8 oP;!w!-_CE Ivϔv8VpG 3<F:fUaC +)y AIiYREZMk6ڤ]a-fxI\xvxzz9Hp[8R }S3Hp7 7sf ~Η2\ϧ<ոo21ȢHmOLoI00兌rSD>^Y+2JrIqT \N" _ >ByƟrIp u9c {[+HQZFe*xFq"JQS1)YnV,T1.V[[l+Q?BB!yJr phLy;C̋t[2#-钀M"ZaNF٥*#XeFUg\j0.O5UgPޏ73~TBE Y@7_JXQ E[{[s=qjʠeP &L;ae?XAYw;\'5H% +F>2Ӳ:MH1M8E#VkFIYJgR\^Ҽ$iB9wȻMwvp =aZ*wԗrTa +Ĕ tAeƧfEt2/"RR|f?'jF궀 ^j[^Q*,re4QՊ P,jebE$"а">ElBh*t,oWo O@O;X}OR76Dl5ձ<򪄾& US*QqE /…BX- +;p~Q| p~3~<7xekD 0۶:;@KvVkiK΂&F옶^LI'T)qs*3eMFgǙLq\e.׼[5]*#^q:*>@7vs߂ݠM)Fc'`ݧkTȉRISy lnibqbb؍HV㣈#FE)z PSAO4!pJ~(8s[t])=q݂SAWbH\:<֢g- βtZiѝB-(ˣ`fjDЈ֗4[v%"͟EeE E+FB]?ig8mT|K1~[2;֤heͧ0@% :i$GXaX?f? }J}o=,̣Ϡ;T!0LnK!풷c$e}Dx1v/g^>>6~0mCc^AχG_;َz@{G78 P6= ]IsA1wN6F8o6l3{9> ΨCfQgKRg4{aW썝#`]Wmut)4>4ͮB ft(n!\\N-c/yZ +@_.1/qQDo XN\ojߊŕ2wegF/7[aх箶@r?sͮCn$/!A併@?r Xs&Rj28iS(!@:H0Aİ%V[\=]*(ذV֊um 6EA of?^{>g/z8L0MP9Q_9W([.B1 +墐Ԥ_7`|9^\k3G:0qkQY@]MP΍GH +ұ9lj$z+=}y\gos%VB߈ J ܯCȷ!M#B +@ )K]5~46l +b_Z)=@y@@H0!Uh"<@?r-C 4P.O_r3s3O.5i%ژnVzHA.' l b&3^|gp1dz/-gA>-(½'!Kt] ;D7vQ5*'>[w";$Q t_ }d*,( w 8 !!=ټqŜOVrdN&CnTբ8F5+Q{e%٨#+G;]L#p^:7Pྒྷ# J ,Xp /#EvN?\r[T˹ͪ՜;I]4w1oJ7nG3+tC{UV\sX,B|+bFF<ɵ"}QG%αEd/D=cF5NGu)ʢ@y2N>ގ?-F~ tIi_.g$3Fv#v"yK_D^>p1KqθYqָMqڸ[┱LyxUy|PFp=`> C ɁDEN4U/xQU !;&#[R\sysq]Mk|թN/STV1JM;TL\7.1S\ԣ)2g A88C-"T$C#Н@hɅO'p=i~8ܦ-kSle+,Z~mX}7ײ7RRady?b_oa_#arhKEfB:{cit}Oe$?ZlK*JNoOouuVV뼀[K7Y37Z7f[ Zg=z9xuRuHfRcʤO+ tE("*'wkL"ХЙ^ǦrL(-?eh6?=Ր66ps ٶ١Yakm22mYWr +ϰ/O)X bIJC_R /H "T;!t;?MNРC bLNwʛ8Ty\ou&N.bș+Fe-ZdҴ2 kADA-X@AY$}A@"@$,ְM,BTj\ѱjS;L]PxB/ۙ*93ܿ}y}{Ň(QJOԉԊ_E׊!f%ff6c +*o𝭲b8Q8*n0޹?+O%K mːGP%%1MIcFZ'Hi$'hU q813M-yK-,7MOb/ŮQj p|-p[}1m%mwЧ#7%5;-IU[+UҶIU:ze} %b +S񐩔07to$.fp՘Az\G֐T.ث..eY&mo͈gS[RY XVRɚJY'PQ <Nlu##{̕,}` |?G50T ꭎ`MO;ҶLZ_%'H NiOmk \6P9Xv>vjގHh> l.+8'{=h췅7'Y+.c8>I7ut0 +gt;Ct H0Cwq=]UPѺ^S;mfN#׏o |phܡH۬%iL|}b?>+ A8M?K_XM/%48s?qC%G /72G S쒎9 + +8CՐk(1Ԯ1{F[I1uλEnFkġÇޮ -fs76 c;u c<,`%lȅb4VD.0V[{Cn0u 1^s9!xtfiЩ7NO}gGoI'cy@yE2pLN2ɟL3XE(6xP>Ĕ8x<}<( f]q6O x>xn_,e/\/V/| 뉅/ XL +g@x/,^%{ cM&p A"[?@cNIhΆs \JޮAB*[ .tOs=p았K)@w ,`;1$35Yx ~H>Xz9m~sTjt>LTf5ѨIt W%DJEBTz7"bk/"dx\^g~~3<7> gJQjGG/.&)$#GC +$l">Vl']0 0=wo,B? x "GMWO\C 0 בXE (2%('x;؋s#8uᅧC|yO'"C iCl[LE"DZ=h39?(5w7dwxMVd QGdC1}z؈A_yrwdjGs p"s_.r>X,^E3ͥ<l +J)O6`_AH5#lvx  +w̟,(-Ѻ-'KDY Yr;(Gq0g:o1zE)'VhYXPSSlWQ~wmgm/ݶw+vϨ0ȚCb&9sY݂k͑fW[B;zdR]ub]}"ݱ ]gB!pn_kJ=H=ͥ@;͕6Ϗ,O &(HT5kt ҕ3SӃ3eYp1i!dd=a_Gὀ$qIO;*3ÿ m( +R3R +T$TX@AA LXXk\hƬbI,ѻgx̽ywO~}<\^ c-{ cמZ3ǶM}urY3-VeDd,sZ6=yiwbjj”))sR(f4(RZ)1)71ɃA1ɯc?& }kpȗuRl/4CUbZINA^њб#rbR2&\5obtyl*ELZQptfETnA靁"""# +#%p-,#iO؇KPUbR+LQ),[`U8cyv_=$e9AY_go +>Y>!/}B2 F*kp.w;Mw\TV裴~+&KJ̓gY/s]85 3"wX~Oh~!_7}F!'=+׾3h0Bb >Fg|dľJce+]VTyk/dhcق +blq +/OZmFY{p:7EWEYy 砲+N%CE\nf}by}=yϯ`U12wXagɒOiRFm%&DpB Wf:) ʝvʃ6U'U]4d׆Ov\a Ζ'os?[;k:}z,i=WyZqzv3.2n~+Iߺ>u;,k=fU5kg/̧Ud9i(݇w}<]^K~`Q +6c^D7;#hɛf7Fik՘d6qgchQM<5^4tkԽ]|4,ѓ̿-zMf{ѭlvk;Imjmm +p]8}օmK躴˦mqnۧvD˱o4'<ղmt95FPq@oF+kp; +Ojc4:0Qܡ:vDH:㥓;I;WuvnTԹKպEeb_T&tvKƟ|by⍪wj>Dn欤.=";j?o 0 .LM)1Ply1\<ŢDf]ݵ ]M0:î08 `phoo6I߰qЛ$bKU`܏bR=;Vu;A{:$dB*=50ɞS?7s0tc+N;OB- "H!y`Qߚ@ <A7G\8яY< ѝEw2|SoO]$5gĜwaE 07#Z8߫XT8yGk7`` ҫG1 ~B7KWyOމ1"ɚ|aQXLaJ 7@fX-0Y*SM@Xe"9QJ./ф! x3x,] )|JoA7cl UQacX"L{]LoUu;]c'8~+O=~Ex3QڂĈ111Le !+~Ţ?Bko!no+];M>[046x e K5}MALCО NꂪI8$QL&z*I>~`PRI'/=7g׸/?INknWp<,@}mO$σ'f6OܔݵE྇ ܣׁVpdz w 1p¼%e;~.mAp";$ak)[ <yiȗ<L% o7`3rр3Ns+rه1w#X#,ku +9Ǻ`3^9~hs/Iٸ?P3 F$SSGDI$͇)I´B2lx5iH涐rgȧS!I8y7cww(8%x/)c<d/]x8 _N$WP{p/ld^ $ ?,ts8%,r8)p8!qnr8&lu8"RECAQWKEO ~s#zO#ė|@?%OH ؂hDD؝)щK'ӜuNKM1q-uTlS?oKC{%L.|Kry@ĥOC+]G ėPI,O̟ per0OJ#.#ҜR=mHjVvKi&NitktutԭW:M:#Eأ[ ?0p(LUѤpbmOAYNY._VH!+̞d=Vϭ-e^ݲaNٴwOgM!#3!>P +f`=فt0DFCSdzoҫ'IEY^%˭~mNy|JJ|*Q> [`uW,SބX'6 +"pɃQKw(cW%һ +T[JN@ lFKhemjD[Эu =F'U%V%zeBbU3+PY3y"_V)嶠tUZl-eF18έ 5lL *)99j:/WDJ)*M۸f+JJNP Q09ߑhyPs +([W@UWLkUHiEe ^F4 8iWn+zQ<*YWEratd@V = +^e>,@?gkaH_5|-zoP_""Hk(5uxmeڤ`(CRKxF X`>~qtz)^Wj|ȕ‹-z ?W8= @7}bL([K$N8WuC.% c%UkBכ8s Wjn wƚ3Or֙1?dK*qĦW! +w]Wy +#dg;k m0P:dmZGukEڜԔ f2,yXK {cieJ, e$Hlee<3 9* m&jnzy(r((}八fbZwg_?><ߊiGz$~UٽJw kPϚ!sb%@&dˁG~/f&eĨ'2ڎ]9C]_Ok7/;pi~C}ӠEI5I~]Fa+K[_E]:5XU& dQҁlfaߙžksCJr"1^[;۫ٯ 垜5+)k1kWB,jG|V{B}3]f^v]}Ő-fU{9{|^j-[d(5}z`B{"{.ESQ4(^~0Y\Cbv+xǩo4؂TGCAZݢ  vGE"h>,áKY*r̻ZXy|\L޳T#Y0Ih)k1FGj#way6&Ēmus[=6?%5]f[BnNΖ4m#{R e0r0i|*GDU H!'&KäRkd&Z? Ekk,x[&xVނgcׯbμE\\<$][n >iWxg} <0 Ґнq6M5lmh! 3Vry%3/`YU}c0[4G)6M\n7fd^|AKe6EE\AqK/̵̼9{}098 f+@{.hUCȇ@(5׆·u^m<۸nGCqඦ\d'{3Of1<(oέ_TWjn-zh#;ɽ|Ax)60* N%{*c̉=.0eؒ)$RMHݥ~ɲzOg)@|ʺRUaFZA`` ~lY XFJA&99,E>Y,K)-g;&qsN.bΣgQÙ8Lg8k'_"*QbV}U6cA'D00WqYLE84㬴_IRFj=T82㔪'p\>K8bێF)WP;FqUKM]iM 4uӎpiN8-q8꜉#ι8RFw݉:8v 5nюjnNU%ʼn|/AO 6מ='}F P` \,2TU A +9\K,?=T_)A[.^6_\EsPpgp(xX{Xg_Ž!{B!ZCU]5RyXTQf,aU9^֢~C)1(L)6]R!*!'n4i t&zj]1Wc,[g,622 jaX;!b_@ !B dMl QO"8nxqicם:v4Ӥ[bil̸xF9wyrx-byF϶6|G8f1 +q&KĜ"c$6}7}f~})1@>GuMX<&cUpLk7vU+>yq k +$.A->TےVx$E!MRjl"[*~-z|ɒV},%!W <-\Ex&kTs5ԙn}/ m0rqQvLǑʆ84G,[Tɶ88"-P[ P,E$^ zCMԾ>Z1o9lZ m0ʴפtlo.oQhT Xy)U(RĊ&Ne]Gn ΐ.[N+NZY.{SZw?T9/6YkKyowpow`DNڷgg0 +vlwQ5hISub!9BuSҦRxZjWmTVvFJ׬Ǭ"SVQKĶ]؃+G0sqd3wBw0t@GO;˵뀷*, "EڪZfik#iekzR4cB2Ws2PsYX88I4dM=>{p r^" @7HՃ_I/HSV]( \&@׍qn#Н9}@w/*QtnǓ9=~=Č, ヨ?pWQ/h X]DZv4U2Bx!aX^Y"-Q]<|D~# +Ʊ];=^DQq$f) L4fY u`󠴑{5j@Pwj@G {r) P_]<螿X䕷P擽P嗵 43~CY4:cxғ='ϔٯY3){\zۈ 4>n]hXA}Ce7@faNs]vڿwO_zpe&ۤH1 Lnɦ5$s;'=t_Kʷq+^qـ~ԝǻ:nhXc(:Y f@68ilKCi!-UNS1cw,3,[[؅oݱԎ~oa8vy52*z=<58~y khkDYS!Һ­vt+i[ !Jj!hWco_(n} >y.4̮OCu|qu~v:םs9Nαai)ItoQ [clmfvw/|ޟ~9?Q&[85{9v=aӬu(@yDH n^&G1L#w!RU@ ^XgyIy/0`Fr>H+8t8Qlz!_)6/>mx񠷭u? W_ +^j$Jr-@r5Уq 2g*.l8B/D ^MAqSٗ 9T3wPpNO$8-)4|Z)8fV8ly8(~E)hT֡Au{UgQo}{7rP(`+ +H'x\E +g|%yeo폓&wQq82\gbk"m)ܖc{sQ{z&qMqu1⪰Iaa maIa)2uCux:GR ٬.lRWI +{%ǥ꛲|yjAU V/<}gk oaw0qu~6EV!+ՌmLk7kȋ4 %tzm@N-~dQ֞WPfiQejVjJsw:_Ŝzl0l B[p4\oU>DQVn*SMQfU2jJU.W^h^nfEAOm?fLn]NxlW<:ߜ&lIP9[#E%޲A:E@u65vvvYv+Vۥ/gۧ;,ӗ:,W;.7;.֟qZ)Y)yS׺|MYcl Q饒qDM}9FͪḦa醱N ӜR K S ΋ . y.Ɇb$C<>D)9N'n u? ?wDTb(GXXe}f6$rbxIy)t9c,G^3ӍS|w㍏^*<6+ߜl[X +xbML(sxoYTQS,F$sGy\S,SLSrnSپ S~{L-LT# &š-cX-Mn 9\(J\.haQ^3,#|Y[&MlgY8ђ h,h6x;Gc߹H2 3 +l2 DAvG`aU6dWѸhmh&F5f['O㾆k{}9咟7N7XK\[#dP0'Mtse8)].m~<Ш"9ĬX9 Qb`/wʒfΔK˅i:W5E+JE2!Q#QBg q]~ @X |8r+1uC%h܂$tnȽ|}9vqDC{!Àш>F̡;2G]uQƒxDBxLxG#;܄w7fU\s*Ԁr/s0}@oG $w +|6ecEˇ(\Ɣ?1g>sB^@6l 1Kw8<΢3IWy6qUq Nc+/` D3s}6܀loif3r> ~E?6Ib99zCO^;zԟ1>/ɿ-r#Yl6٘=uyDs= Lt50 & _dqR-ď(3 -x=0P.1ɺſn)}C4݌11+n.^"(z̠OI_ }U6:x ^zezF2m,#\㪮[40` :1X2YJ̈0|8:Sˡ +j詧XKO==\\|}y\dDx꧴ h10}1/ķbLWȤOIO =܆05p{g=j/=\QIi! +"@AAMY*PQPTKTL3-5Ki6u.Դ]LsfͶ{mjgne?>x<jL- sqa`d/Q>0dĜFaPW>fuQu>B|\q8hupw,8_ۀ@?7WfLԩD7Gfi 󘓳?1g`}NWkpc>O 8џ{KSa?GbzKS<ʏ p?Zp\dlX,s$9V8ñ*8Q G"` +f"a:rDN~LN +a) ?%% +KM<;,w1+Ap/DIc1J8phY)L/\Y=Ly`2 u޸>=q{a4iwQoӷG-%">Y!I$&;U*Z,P.KB&޷ϡ-`LX +bwJiߠTE$fZF*Ӻ$UNImC됴K{ۤ;7J[s ҅z@A-c?QOQըG :QD°-bPLٖBݚ%ˡo:dEJmi47Ehzd]z١Z!.B7:ͻė{kzAη?24v0%}YlFBAQ[!kC嵡EG[ǬVYN|XrCI]0_ȏ5r/q@8V!bp=q٩%À BܮjR"![Rԇ(,r)aUJWS^lc;=lrS)G(/Fؔ֬G֬9֬,n3N &sze5ADMyz]z:2:Grj"]]QvruuKMųhxŚ ^(߬7/P/P3@kRy(8ðGQeVs\h1ǑLI>u+ncéW8ԬVej6oQGcEg?*LrWyw(ywKkL"dslg#kAP"dwSj_E-^`*WJ%\϶ڄY֊Lkm- +3l#tۡ( ?zjjyO-~O-.q }>_{D^dJ1*8P^ % Dq״6* +[*~ki~c+/ձ;21Õ"$ ʞE*r_q>,2lGq og k|:ֽMʯKJ5tպLw3mOssR\vԵ#qm`vuv +[ftfo?]3Vr7I:< }fmN޹ |qkN$4'hRGuGE]q;p"*\Df'rknf7D (0(" eVڶ{ilw˲mMr{?y#Mٳ"Dޒ. +gfKBӗ~ñrc՞oEU%ң1 +Ԅhhl~qGȼ;oͻ'8+obkjL=]9!`1p}_ H]A_^ JY~=:xh* +OKh4Pؤ1%aN3eo1QLV{0dF@j\ېl[JkRI%2<9]#y`wk~гs nJ|σ=c4s4fUiSfe={+ՓeJn$y6{7y=ExjS\e_W}+W/)-l\1eWL1^=sc*؋8ieDƲ +Ws;|3+)f4tKCI񌈽- +TB} +*To|dM7S&ތf@0xCu鶯<]JOZ/-'zp7Ii;>-R)Bc E1h7e".{9p)vLhvЌ^Z΢/ˉw!^N~ȨgVr.g 9 . %!X4ĥ@'b$,~_7~U-H >'|_³YGJxs\q _nЛ-tQb}Ρ 6(ie.5yfp +we_̹n1s1@D:J4^?h2g~˜2.e% J9sg?UOUdCW3q~#l>Y/t uBك=(> +w.xR-N. .p TcmU˺v !;>DMu{zG7J1p;ci!lV\A +maUc+%ҌVJ*%XK7!~4ہQAGXe60#*&''V)nalk->ϑ:Spҡkg'/~;6}™ g'qr8 ы09$ j`]z^=vQMmdkїe4A8[|EmJ WMK5mN2m1(0gn<ľ(k.^l^0;j.GyceeMr?9+eQ_ۈGhǿw;{ VͰr&*Fsy9Z|Z8l`LkǭyXW;XW9WV8 mf۲\QMw $#R-($IH@"p5\EejZZvkzyug[w?ekמk%{f${)8*_ɿzz,8[ +}-jyBE E +с,tQaԔ$zRUWZcZ_;lVzy>ސr7o@5WTG=n 1^;ʻ qm>/Ӱ@'PKukY4ь +DE))ja? b|FAMУtb;5c^|\d|f5YУ-lRŻxwq8=hݨ:XV,c +7h\FX2gߧ7zt]8ߡu;:E7* MC ;+.ݚة^q:p~ W%8f|GI4Zeb\^no1TSZZ#ݖ@Tk`sUmѕj~EKilYQk5bOT*%XԠMtSR$$c$| ήmCQ7.0& Lj3DD3AȞGc|$$RbhB$ )(.%AL(24AOAav:&?s:{}ANlE<&R%!{.'$.񈽜H%MFd&hYp,Y^FsKDOagq! +cK`>cМp&Anhb X"J}("0+|l"#:CzFpdu| à+EnP?ѳakgȇ)k " .&ʄ"D1/"Gx `80/'!]):paaS_W0?;Dy}:w=9<ל"*WUD9Ls( +Qt1B!?faN%9ysz&[07!rc7.;BB8uY p "( ! +'^ +Zj6 usZݴ+:Ugk;i7{Y>*k|=|y}؟JL1}ɔ[dN=9XZu:O]rQK:+Ӏ,_edva_㓁9FDSTWдBmqJ#_>g%y?qE Wԟ"m-w8( }1J;FQ(>:YOʙW̙Sµx|CEs&?9 q]-l̺dyS!Q_Ιp<&Dgsk7w3ps}䔒 Y/XF +ZKFa;x/.Er0}ܳwp-#{-'U!d.P74NoWSOf7?C8q;ƌc>~ccJ }/#mOQHF) C9u,a[/Czrx|pg_Sz cWEo,9Ym6Eḍ ȩaԓՌW}^lc 0/VvV6V6ҟ|Z~ra,02K +;9yܹ>!!gÈefFv#IX齂oGy}7O d{zpP>Q1蜁])\lcZ\҄-e,F:lcl7Ɏ`iv-t~R8?o\S%8(W/`#;=]~ }آ&U6JЫ*ScVk 5n^CǸaoas_Qny,[<^z$70~µ Oy|.85؇4Zr}:ViJ4`ʹ(%+Lc:htVaDVqXfA2Y2FO/==o}2 +3Ψ~+%vUKLw1Lj<o'MX2xh̙({ +wl)u4fҨVz5M!9r.L%**c~VaEynXmY=gDأҐRb=R&EB*Qt=@Pbit7u0nf#X &J-c.<>v:rΫIRr)Y)nXX́Z5\jt#TpkIkX' Bkeut[kpv͆=5`^K u١Z +,osƮj"ı8vr#:.$|r4z= ֳh<ӈKt]Bz 57}X9b0#vHuk'~~.:sr8 79߬h(ܞ' +Bzh~qx4V6pn1o1ǼU+^k__{}~@7:>Efh~PTxŕ3"89xKZ 2Tws8OtRtخLT/Z3ܒ`ej{jqA3XȪ#_kb-W_RUO}gqV:豎B|Su&^x{8qp&Ȅa1 F.qLqYg5u]ئFbSj z.up]E|ޞ3^ 0`x#`Icq%FCGzZP-]Qkgz*=DͪF2-wRpȱIJBxCጆ3<F՞ 'NY/ N뿡Mzu ~qքv|7g[sUʐ8['i]P N+,C%aT6Sie" ܥZEK#vkIZmCqu^Q>m`%0 # $@ ]#cK`lu2m:'Iڦm:dLvI;iڸnijy ߽O߽74̻OyY؝F뿉s+/" %vҪRE+Or8hA-(S#1:\>:g{&ivCkz nC7?nw'X0Z)_(b(lL6ʊF ,='FOqx#)3K)K We,:Nc'u.sEaj̝OfQqrN\4[^,oH>R3+ߔi8Bj!1IJX$KhОKv5xwfl~[jְ$dmm=f۰,`kdޚUYSe溼{rr_WrwuV/cQg 2!ܟBl8 +.uYW´m*~ifglyM8q IZd\K +:p;X㿬ÞeoFKCr-zǠtågu4QZ=e4X^Cn6zw+[T +{|ݽiXQVX^RToПXK?/܋߃ucq1 "8½_Fm\ +T0~9o4juz۽aPXjߘW}'Y"[{Q^#34ݕd ,{k``y` 8.\ǂDZ;'oup Z5(rS0& ++CrcZU.H+B/HCߑ?wĺRmL_Yp΃sh>@P I `NBqpے jKd;JHQytI#/ihu7%;:9Q%25&DeI{RQv~<ǰ9p:F `}}DI"lE;&%cT2V@{STғ:e"UA)姂LS^v&(7αM&yv ~B9S2ᷰ!{< 9FLDE3<ʟeIEyiJRnK[hgv}=Fl:AI$=EK$yӷ7KO|H$O hq; vؑaNX iYxCD;f,$53e $Xq7B L~t0 ``#Vr=}-`gP9"a""X\;2Cx(QOq澂Mg)$wI\9,6-,FPߡ) 0< l/U^&uh"9 k<0a{J !uı8hֱlc@r73xe|1 hSzu7Zv3\5Wn{C6GG?f?{]P(`D4}R}Lj=ݭwLokdN +S_)xf^!A/o^WQO1/3?Uv1:T:֣wRk'׏pCm,qMM凓 gBb(sJ85TA=&8mpVa^A>afr_*+qq?kyL!pG僕m0iP5kiVԠvR4ky[jR!XݖKj%ϵvUkzuHa\cv6 &[6؝t$jMdS>0_+i;6pZVk٠EjԬvWjܥE!UB^т!vW;ԧFq}pptm[x7r 2Hk~~:FaZ>|ZlxZFy,5%#0RC>t.ׂkU3rGRUc*."=ͻ_{ Yǣzi?vpoAz׎Q65\"2کj !G^<;iuYcCd(- ʫTUGT5MQ"ܨ2Cy|CYBCit$P8;f;bN b.gżmiV}avim&u梿],?@uP8oUөJga3PL79g88;v(Td*[iќ? 'ZŽe96h/$~u&r<$JǗZMUpm(uJ\dSkЕc.pg,,3Ֆ܄AS$k3m8j x=6kGl5~`g4Z>@ur +" + h7dEeE妨t jL66&4I1jk$Dicl{㼊˿9}B˓3+))F-hu"5gi`nM~:4:w5k})lgO3۳LcE8{w}w}hs^N{JhNiZw3MQK ^52,I!ʆ&Y,欐g9H4)3JJpL2uL3q1:z8zrd(7tFd2Ddhx]a+~;ay s̵Z 7cȖ8c4ڙhE9gisF8m +wPө2 u)ԹSC($,+_Sp Y?%+d=4fCa[9ғX$'B)ȤHWF*8 sMTkRJr*@W]+nCE/˧--s=p=0\)哳v*Ey6 {4Xú,B+| p4/dg=O>%vgfn|ffy-4Ħc Ȓ Fnj] w%Nm=HK]Rᆭ¡QÅ_ReSˡR&5Y0; +q4=mjj] +zgN {69'qk0b06ͼao}b<\<]M C6݇^`a#y&ɍ_f%P"ZGKGa[yi +&iD ~tB<ˏ!y# `GYG1zrd`GK3 f KgB~"a4Ġ gfuz%:.ӓn&n]:عn 9;)goژQa1}l~(x{*1Lvqw{ӣ};q_CF&LgZ'*ҧ*чT7qOQ] 6]eüK~zE_;_up/AUW Ȥ*X_hh0R-E|ܚ ZFJ"WؠprWXG7n Wv &NP'?GoO;`5@(c>xZǼtPJ"~+_M[6BBк@?Jϲu.:?@AMQPf šK0RYsقw%ˈ_ɖ;غt +{Z29zw~}ۗ{tE?TDKpf™O|+ݵ?T.^Rk`T6Wx'?RG=:vExr + +0f!mķsTd?S0JaT¨QOmګV:h=Me^k35ӟa^ OR /\baM3/!20r9 `x#ଅQB%GYi#QANn=B=w~H+BNicg&a$g`,Is)Sc=ycomdXZ ovD޿uYO^p0(xa%I8,lr@\ kp*UtΪ*'2E_ou9cv/kyGM`j0iq꽦iy/P)EզTUkO%o*|kTjsg/͝*2wi߇*.2 *JeͿM۸v<?TuT3p.`*jM"U <ȮҠLZE*\U* ޢf9-g9\Kr,ʶ|/ +W!szǴ^ +/WUサ{ӆ+ͻvw&Gwd7MdC6&ImhhB)BbN BA?# 8((2< u?=s~{4©\ږLVM^ѦR9I4hF+NJq~UC4|Rq4@6`Xtn_lu֖bMTjSOzm,\6viOECJkxZx>Q&`}+]U'Uy#U!,Q76q|>6K_җ\L[[I_ W\a F3F_|)ft1*'fjnk-}hk-6ܻD7}~XQD7VUjkUUfOU]ivUö-Z3ck>`_voqho)/#¼ [~x7ڎ{~mdjгBz]f'hzB6O鱯{!GowqqGwj=f^׬-+y}b]Z~y'6. mw~l@Cg׿DlDcG^14Zߵqk8JQ踶]jwZ$owVu&VX*bFYt֙n$6h͢،Qس(5c_3~Vy]/+wF^ǻf^G{`( $3;?;\Ol T9Tyr tRq *p9ʍ*'VvVek=Z>pI}/ihYߴ 킗fa&P_#6jK]EËU̒3YK9I~i򑈖ꆑ䜮KޡI-`Zp㯵p/@NQvv"HȻ)BF8e 4W T4 @dka7$Q8oBʝNZ:9eK;e9XҼPӼlMi0MKoFG!jlyn9o1! ;1'd&)s}< q!69:c:@OI7?k/K7%AԻvv1kFʇb`bxrpsҙgSyj1}y@Wҭok'lQv/6؍T-Egj1o.X s( A(8ʋ= #Ggq,)4J`w5v5x~m6`d1zB8 +z8N9;MqOi:|9xg~yJHut~TxKa>cRJf`\BW5 {v쵰o%{%0.^<oAP8zXnzҳ0c^`m|9yqtU +x^|RLQK螇[pc%{=z?Ӫ箅%!Lk;~N?~;< id8}ջ& ̣0|7B @/dL&6d,6Y!8I DDTP(TT˩VOjTkuhW[պ)GSlx/_f{}Cw6L_Bo:Iw5]kt;B%E:癰Y~>)?SJg?> wNJȪ'թ۽q@/ q8f7`r{3g1-  F5ch 0ǘ8I7໕S{+f.cXK&:Nm}WGmgx)r~daN jSn$5|k;#FFl%{c63OVst}űv戵q3W o XNp4%Y85O㽕Hv"f3u)rb-|-cܜI(;BpG!J>=(W60LxvU{=iөί`"VÙUZmT8U7@ǵ|W1bg:5઻rh?Uh7ϗr׎mRՌHWjnN6!>;s+8UOܨؼY?oƒjyaEhY1&52wg6z.Rmfʂ4=?]S +4Ԛ +N邅MZ]㋺[<G4S4B-Meh(n#_rL}iOЗzpsۉkf=LdyZdO/T,L+2*5QhFevj4W#`V\ަޜ+ԓ_ݹ*uWGuf2k)}v;) ZtתOY[j*V7jUz6o[ES*rWkU>j/Z޿Xl.epԐ(NBQJbA۪\X Y^*_uˍ[hUtX>6b+]es[wU;d|Gfaꓶ['{dE ]'[-Ɵ*"yep5҆6c4XE+uqioko26m4oxe՟0OZ7 g ~ +4QI*RW$Ӛ*wk\v9*x@@X6dŌF^6#mv۾#l}Y-' +[aMZ+Q7̶Vz.JZZz9r+C|ٻUUPrCM PfhL hI +*.t=gMt[3]6 n5w?%Ҿ$)'CaÕZ+5ܮpJւ—i^xkn߃J{Zs{_Wb?4S] {g nue+]*F6P) -+9R"J(!Ҝy.*4?yA8 4AA@Q~98"P+xIs\d6Ҕ5a붩զn;n5ήl^w00||}U2_% 20ElEB ZFwj{%|uU++dwb+S+lҨ2 ss9ɐ;"'uemX1rf&Eg=xW3Y,CM7T +F&*ПUspp-bY8p05$eTd5BW`D$-Y<|@ȡa8ukż !V&Y@+B t;w,,0-F#&&r̒2`okq]|ԤAk-䢥 lWu.h'^$RPS# 21K:3,R41 ,yrZXc~lNMIr;h_sDz6u'jȆ{&.'s4Zw,Amp@;XӸpGlcA]Ǐ]c7Erwax7d>v98fQ5uvT^ɀ; &΁Vf^ZiPlk& h3VVB< A|7hCM.D~@=OUEmiԀkw];|..>G.=/.qb~_ѫ'M OӐڊÓe9uqg=rE0@>{ܜ|r9o@=nr9ȢzK_&ٗ }!;9QqInrw-~BXK -@!Vs 0FH7XX?ffAxAMз)#'D G@%ktėhhr/h;(?(>CGFdP>a?A}D@[}H򮓬h +:װ.^?V_i@U4|fO>68~\Ǐqu.{z[PI:s7@_?θ ;[8bd<.;ˋ~Ǐs(& PM|\F!ScX{R}?~n]Ww +$:Tgr.ĶVtb۵nrlBk~36> +$љ/\-hp +G,Ӱ?kv!mv`۵kqJ7^jMeNpW7tW^wq\!pE8xi1RVy〣J | +RNuRδD;qf5ȵ5r6l9[9N8 +! 424aܣT dSg֨k +))r=?W^u]۸nscYXkIjT {ʽcU Ol>3U2dphV(o" [l5UY~2.hupO|:ljZ|!0dQSU4S7"U9#f+{d,R(2G-TF22)ԩ}J3U鼦>Trxnb FVV4A%;y^ +/%4\;:L٣;&Fs+sLfYcJ VJPSRM3Ue%Ӕk)Ac$p"YyA5p_ +KCf_eM45tD0ORjHRBR5}\Bs5-ԦJ%5hUq͚lyA,GXY#&*kĄ>4 }lgꄿ|9"0"K6dl-#f T%LI(M Vbx'+>"݈?Ϙ<ؘYa<YoD2ڍQ{ Q/OG1Qֈ=b3\I.×LJjROS&%X͊FhuuE}^Q5"D""XY@E++ + +"ZQ FMR[hjNLFMMQ&ĤL8Q;m6f[XN9>w{ҕe)b#Ti$֘وKm&4bNIhӧxonLԀJPvm>oV̔L RZJ(%d2h4SsEF\#6mdDwQ雌]FDczI]ԴOnc$Ӄ4q# A9(uxm?%[&d QBFfd)6#Y1Eeڌ̹FDf1Zc[5ڮ0kB;5պ_!jJg|) +|clC4'B UJž˚n*fdEeOSdvgnKW-K6Jbs)ض\sVќ.$shЄ75~G +Jw2`dKf[bz9_?US#5`Yڭ +iX 5^q)x\^5:朓_uλܿj .72`!|oIsع#Bq +r+Xs$ic6v#Ǒfpn"D<6k??6[ ߂\l 3H"iZ1ڣи; #(pp'Cu"4 MQ(yWh=߅k)qVhGs@HCA#_8*\&.Abȸ8. +¨ ]jd5"S`B(-dc-A8=pՒ +0x39pʥNd`5F2jkѹu\ln^A@דzpFLrn\[Z-bl)Z[ˋ@9:@>Y4bN@Eo /ci`hro$+E=GЮfSlE/Wї-_h9|5_@mRjJ +oWOYYڰ QZGD/؀.k EQq8v=c'=IOm mc믵:W-&pm!&zoo.XY@.BNJ?ðh=powXJ\avG_`Oq17=C |3{E6Z+t-hZ!cӿ!xC+Th_RK$:?EDg x7VGg~@K|z{-JtYz ur]& +<~?Yx?^y_~<;:ŧ'>7 :&W<vQ0u9/A8 y5`ctt >K`jQ}cXѢwP2Wr\\خ25آP;Waq;K=C|.d0#LV`a.lboCVLM6N7OvIYc7tͷ.1]#؏ƾ k9Q.ဣ va@:u^7A6} p=Q;4G1N#O⺘ ,* G:_J/¶[j"-tZn!9Ď'\8M2PR(7*WYu-QƴڦuGVRi+lL&M>^KP99wHp-1,Rt'XX,nh}C(̂2?N 'C| mR=y(- Sqa + Sdo3+fL9E.nQV e/P}Bo`ViJw\tS)E_(HZb784; AAi̥Iq+ˑ#ê̲Re9^斡ܫ~SrMGNإ}/? s\S3%> +[8G}`r4Z>W{dwU9+#dJPFu YJu+Ydg.%:ە֌)fk)ҹG# +>𪫊"+Կ3 !xz͠T6[>8WXMoHV+S.b]6ŸUEnzL!. 5 Q)~J 3 肫 4PZmx/l;D QLSǠpOB 4Sxn|^3b} @ks jgT6YnjҰImHvrx9} >(G>~zx^LSx1B0({\yuTUj?;XJX7ZOg?P/~̤q~qz1(ݘ1|ypS=FpRo b›e2^ Yz1^ xØ!fb:R”ͺD l=-k{͠PsKj x%\,)Tc.O/rG(F{\a(2 x&1j>uځT]B,OxYZ1"8MMcuXBKc5y =N _z,T.L3썱Gs@+)0aN  +XI\09ֳu4rɯ{g* Jrm͢U "93&, rXJB +8%>7"6Gs 3۸w0c{u'IQ;)[J[X4xk*fb 1˾¹![w;)`VMO^fe]wxh^b/|_ft߾~ֺnpt~6Xc ߳A0/(4'AÈWqc,Qzq^Ϊ.۴^Jd|Ck|g\XNCA@H,0St9˺ca>0`瘋uBryR7{|<Ǿs*~ os2{ul\cߠ[Y787%:>揲3ư˳߇[ +6P:'{2>5.o%{d..ip[2`Vl;v+?YP(;* [eM +: al73[r]]v끏~'+˶('t?6Q*qE)ש M2sj}٪sT9}[Uf{i=ZfVu ?^cq*q{H&Vcx\JͷGz*|ST^ +?xivx5ӖZ9Jl;؀쇱ߡScqOs%K:+A9@U KTے>ۯV 5^cb88ʱ_up4~UX2~,E%Iڋ{jlα#fYYv>_[9s)Y#28Z-aicWtπ5Iγ"K#|;g U?ۍ?[kp2#G1F?ȸD]PU +5i')>&q4("qg PDr'Otqi'9J& fF +k\Up.\A"puA<#cVbdbu;*tv}¹1_f<fl2k҈ѬaCveϪSA}B1v(fUW.u&Ԟ3Hβr+{^ܛj}@ٿWw _%}\>x8?繁\7ϪXC]y.u{՞P$?…q +/ZVKq/[KSVqͧ*u$KA'UIsXŞ27\S +%See7V{26+724UƧ: +ElO#GqA=|qΈ\+$P,sCAr ڣఌe*VNV p}5B6?LIOb-h^s=pE2E*dؔ%(F1Ƌk(ste`"" ma4oymyxppuў!OT0)0a$FQHVzx1c Mcq*ǘ:IBX=q[cB O|v!vSKIG?4q7N@q^@ 34H)E TGN3A|@򽙫j솿ExA!anM`-Vw8@ĎQcQD(bi26OBCH I$*Paւt1kuv[w٫Skwukmf۵ǹiNn~vk[ݭwfؙ&o>|`|Pv:yyK!2 "L,KQMTy+sh?>dnL˿9fE=]T;ټ7ٸl9t)t opBp? }#-Pf-q]uY ;Lo.*~8g4~9xB9I~.e6A;ӣX<YO8>WO2gѺgibq +e}TDȇ?WK4410" udzXm?oShۜi +#\[rBʱv`۽1:V`{ (-VM:?Y7ҿI)LB5 +^J'T%uc7@)4w`YhQ^{viP;Vǣ4hG7f) '{*ph6-=2Nlz@-hv6 +{OZaaoIr(l}Y."]:A\q}2A̞ɖl+ڞiصaץm `;Ǝ'M%c{&e#}whXC5EM_ w),_y y1/Ou¿ixW22r,}`!~D9j21PhWRQ)@QjZTm< eD UQUf}BN!9_(f}?<[5$n(4?C\YT!6l!U4mPb1.cZv8gesqPk2";'tum8&[DYdC"t|׀?!O59 r,SyGβZ9#*-M%!Y+VRqM2gTޥʯxYFyJƲkz.FNz Znx@(w娴^鐭R֪YeTY(7B'or|)˩;WqJW*f4Q~flZ}?OV2UTPS~MPy5Q ` Cy-Cb,FNPi3kZ;\ZM 2'@ h>jx{;ϊkd +d dA! p0Cףˆ0&s1֝'|Ob{ Idbw1M\A-f<YqE<iIIbp0ލH Ar^4MDG2KG"Ѐެ#v'FF {/psļ VaZh.1|񶳩DqUc|h 8!ۘby8DiWV8r]Jq ,1Ui'h4.|3&zXFz؏!}7/ 9A>I8ư?ںhMm .=;,/CCH4b1H,$q%y)~$"ɤ7`b蟣 CuM1c} «})KxIR$XF>LSXQb1FyqX96f2F= CCxc-g}m7\]MW?I;qۉ\'!HJ +)6\` ZXml-viٴ1RvN h?mզMHo* ="<'1ϡ % ^gťMЦ.vXs&sG.rE]8 ="!Cl-ve]:ܞJ9ī VŻ3N@`Ʌc~Ǐ ÙlE{ctz겛F${)|0gr쥭GZhV%Ԃ ҥ~cs-Vzupbt l~EY5DMl.ρ ͩm."1;WS?ǹ(>cAj?vse5]YMzUkB@-q8D.XXSq|[3vZK!6&99̋"`VM[O`qk:wQqn7׸sX2xYJ0 '3q{gs?or?'M]w ~bd ,Rq 'GPBEI6JN^!ofenz%|^RY'jĻ׈bJ +4PRb . +Up-ЯizdyZ>'w?_hl`W]=û4ʯhu3OIUQWH؛$ r5aq_' oum;.{/wug|3_cc4c?~Q=GiM4h :~Z|C|D~W84$ςgɸEo'Sholϳѿ{ ˳(0MAϼD@>'959̯s_)"yFBvt)3|sT-p8ҳɲ}.v_*=sیQp+A٬F&8i1211N4%&GSSv:s3JQSbnEL&Q%I8 j9?Ïqnڅ7*+dq;4"@5OuF8TOmpL>8sM5NmAA.c'uK+\d۹PFQZ\P̯6{Rρg8`ώ=7' Jlϖ/PИ~s.LtE?Wj$C.W33apQ*5{Ŋ=Avll:&8}+6a.ɞ<555V5A](ra8bpᨀʬ^v[dj>*(Ut\yG#ш-\URRZ[`ƾ V)7'Fᩀ{ ̇c1]WIQͦjb0jo|'kp]XoYZgI'w6pv ڸ[9<: . LxWx)_3MZyM#T^!ӧiP(?VAVu++KU󹖕J))Z$aJ.qS\yRLa)k#xZH +80&MxSTԷer3y2B}w@:W콠y+{9W?v2$jIDK撰敐|LZZkmSܶ>qHN~ɶ8iqZnHl#;[ +؂tꔄsEBN4 şo[2,wffV>GrZheXgD˚4;eܔĿv#Q#@5(%X^__3r +; +'|7+`GE2imb*"$3JEК14EB1c7B喊C ܒGoB%Y\YfIEL1-Mgˢ ء˘n mG"FJP7뎬DG^M)s!< g|g%Ts C4@-gpTXUUUR5QƫFT#`j}B%z%Jw>}$ye)v{w%(UFuJEBX Aj-*Ԋt\GEm; aDe2Bޗ3{>bdEQφOC|njIVú8@p ȇT0Ei qƸxnit,18('mͰ> ~|>q5f5ll^ 3x ^A=Cl8l5cNi46@PO-Xp.I#~:} 9We&KLyroǎ8ug|KWrcb]OHrOĒPYkFW?]^2XqQZ!% [~,{Ơ/ѝ_Z6çSB팛 ߵd{.ψ Jwaݩ}EMDœOaW3- ҶxSD't#RRPĭ7O n+*F)j-$j1[omD8oQUqCC[^G9| VFU{V?pxWC*`e#ےсb~p̜_AMyAvmtjx*" +(UTKʡr ձ]u+'$@D!$ $! %vvf 3<೸{ߔi3hg7YG!€! +l/vyfchPIcs2W3ܽx l?`$*CtQ7B3 kK[Z3l } 3L==ztR%\p?';K;޴ 0m3zF@cJ/\5`w;AFjoz7[ < =x0Hb㮙DazNB?gu .a0@ K8e^}-Vq\+N +|9 wʅG; kX4OhӼ]&n{](sY8qEͣfq(OiI")k +KҢ&bxp:˪IїMH\75<:6?tsAB| Kؗǵjz2z]ڕѻG8Kܨiuߔ,qӷD*i-F +:o{nq{opJP{&F[R|֣'ؚ̙t/{#pr][8J!9BR)#EQL[(%9܈ONʿѣO7sTÁkqȡ60_B-lRg(ҁ+M !n]ukO>uᙶ# VKMz{c7o T fE^ǥt.v;CɈ0P8(pݿvR?z_`}~3g^Uϴ75^w?[3p6?>?>$(>?pϰZ z}@Y )T\1ט.j}l*Ri5%IM~pז +W6_ԍI̾4`ik*V4M4$%3pm)]f[ ,fسvehzu鲼)`D,JJb$){& N:4 csg<P`1Rrw631=6^,̣_Θ纪y7G(kqYi\FJ|7Ӟ> O yZCɟ-;+u{#g#xzvU})i%*Y+ɜ*$;uQAEFsQi&%/GεA1B(NYީJV.-7N:~2ؽCە/&?x-f{(-]r8%ȖAYP,R.;%.;NO.5ggqNin;ΎKTP! "r)G@@Hr\ÙH$hp)?ݙ#}}{>{}&=+];_a]@L> K?A*/LA$}.%lݬ!T>Sȋ^y9/^9/Ręd9j˜-v#xm dFXQPP(j3=n~r|e|>q {kS(u!W@P:Ն;4wE|p/>t :!fIM^vωq/ +YOU7&~`  P(rp,oaq^K_m:ld4)c:@Oʊ %e}TB0AU&SLZtݘ[7rWꓹrZxMb2W;7s%1 L``Da6z-zER}MhGG$CfI;EXd#ψ +[%hdn?߮S_oF%Ěb’Bb$t`;ES$]ξAiםa쎩QzkZ2CgTI%%J ƂTOc$&v/:E;g&!l1j.}C+VznCTyH,фHvN`;f܄haO6?\+ב54¢4LSxTǼijP%x-'w#2y2(X.Av QOPmn5?K</=ړL-T%2 8(iu%Nq]S9X+7RЛ/M wۺJ!>CS +ĥ1Yon#R!/քVTQLU( R(\iN/{i#_ђ=:[[;یc]:j?8iqc/uj}ik_;NgjtD*CB$}_ a B$EY\n={7^C*3taݷu=XBw.;},SV ֻ=~iOnl>A[Œ/U{7cX(?'N3HRe7AF<v' 'v\G'kQס};)8:J\*xayr4͟mN=GKpx3c?;ȉs"Hn5iM Cܯֿ]cy N"?p['zYt"iBW] a.oU/)ut^&#W`ahn2>DL O9|x|MG=끇_98~ GgĬD]鲐ؠK>KK.+BaJ1s>~:WrPHX"9۴'3E7  mQ,vCH\$s$NvfgiDkJFLqdDzU)u5{kfV{ 8 ǔmWE7X).~zk% +D']ԶYA}+'pXu@t-0E "a̴BK['qNy4Āk_I"KIcjHh$ +Js-J(w+MM\joaZM\K +k3hq'@ߋw_hAF\Mߑ=GʯSܐ9)yfEcOu휢y&kQZSC, [ZDn/mQ] ʤ;NUz'k=T>n$-.V}<Z{?1A Ґ-Eh0fWDSe4uEy4"R̙ +Y2iHv)MuT[SͭA4"?ײmEpҋ{kQ Ivs"0?G5^۞(lJk!76Rfv͝D >}_ +j^P`fAY8f"YjQg+ +%ΆGUvKdyqm,QJ,S)߽^1~ :J'5ꇜMsy,7(mA컦Di(U1c%1k@wwnD_~ $'ٙ#[ޞk=|8t9JX>_tILR!q$AB@ A 5B A4kseN8vmgg;юn[3WS/- +c;Aubcf=賓)[YC7s)U=ė]//{F5.o}xYEyٝi[;f lWv?{/l.vz1ճr3tܹ7JgåUϷ5ri9M/`_|~,xyI|{gW;B$Q%Xj޷K7[)ya)ElDlbk\ÄRɋ>[W=FQ%^%"KQb0{{#?mY:'5X p5N&x*mՄ縘r]L.8iE k0g`MjY;5%ÇșL8gu&[K'ˆNE=CX8p>}Ӽiމueu8iQ̣_aZ[)aKy:쳎oυAG +r#7=(1IӪmPa"3ʕqqiQ:6a1Rؾ- _a[73G=/q/{8s\eIXt\PDY(& pd[.>'u1<0gFR__R3+3eEr E)efr,n$#/D2"Cp6(gyA|+ 7!p A o8A}; X;Hs$@4I)q9!ZD+Jt: ]k+MEJRi +՚()jcxhbѢdS< d_C.2 C2nE,bZWb8p&)2Ld|Gm$ꌁEu QZSg ֪i ꑰf`N٬ "C2!CAF:!\x%/f'Q3 43Z2$Q=_5LWO('ZA;٤j>nvZl\d!CA C.a<%.3X8HMGG u/&yMEM!M4U(S8US0>͂6 y9𐁋 | H_/b- H \r ;r.7Fk&iiMCHQ&i S놢4XNݏkѡ5 r7 [A |dAkPD"CWt'9C01X嶍ӊ&Huk_>Aږjkfڴ( K6 QْTOѫQ +,Y) "C;8A5 &M^p +gBE&A4?R0^֥KuiRe](9J*{T*( ! +A;xG3lƛ\C\%/*nvʃDq*M+g{lI-Hvɺ, xP Pwn5;mUu+'o:R|wLN/Ҵ:iiUUUE^![SȖv,ࣟ7P P (k>+j"uQ{3b'a9m4T^ʬǶWrS ߩdV +2EOU_OMx`]jvjzr}p+'D@%!6rrJ9Í 0@@A仿죍@2iVRE!盝6 EW@TYB]o x4S g^}->-am9y1D^4X۩%Ci)w?4I(1օ*;8 NfWR{Μ P;:uY5OD bWP4Q<xfZ2]!ڐ$^TcgFB|C(iWNkj~ %67P`]`dEGT:S:Y:Ƚ[&Gr@6H.~7ٚ[m溝dDWxϾ7WĴIH4tvgsx˿-ϯSXVU^R_GeW 6NBPezp7lxkߜo"!aZ"̞Jݛ{^xI-ogV= |X|o!EB m +цݣ[VY[|jyQ6l}nTfa;/;\oP8q%/#&HaJC@?'libK౿UĿA]Yv"6OjB䭦_KN\7mWXWq] RGz( %-d;T!a=a  +yр Ҿ^͟^ g'¯w ޴:\CqkFJ(U+v˱Yš\9U2edTBV5mX~~-ޅ#v$0b) , At6z>֟ V[&,- eeg2£,-wޣM"H h @۰CiM#$8f 'm u xs! r,(`fv(G7} H*ut!#R;do#IcW4h|_} _O  3&eua @m2邙X_6/EP ; F}hDj AzB \|I O_sMPff*BTQp/SI\#5g(Irjg;Zo֣']OVφ4@b556 $ LjNzOF)vfڐ$e)T]ԜJ\r1OʘMyġ =6pAc KgL Out%JLy9+< +HBV(sV(-9r<ƫ+{ʧ7T%Gn C{!mC8fC1LVGoWst29WL)* z½5GG Yo`ٛ;כ; q{v !mԻ\S+q!n^`&VQejBr@&@ȩkEzhё?̟w^ v@<6pц+`jgH!❴)rZ\5Mo4;xDQQw.juUkP<@AGr  B prB"$!X֣2Ңɇ̵\r-R%F%q3jb?0wg?dp;l 6`C׀zT_4@@CE,B{qr[,hZ&1_uS·CakC|zw"l |A7 +Ԡ@5& P#eٸKub\eBLp!S gu3ƲM]A)gP1 +5h ?$@q>jΟ  dP@)}GLbZ2 9 ¹ƞ8~\Z47rD\COArZ6Æ +@ U߁g@7@+&0oiҌ<<4F$#HSHƳs^b뤚| *dp=:@%l {@Wq`l4/Ǿ|SܘHJ.e=—dR{ogBVʕZHVA"-/ʭP (69;VwWШ5~d+^lBG"aN{eydWZZ%/RjaZŇk>e`P A3DzaP(<|E; AR$7kMU|])ATI EVPJ-k#4-Z ;[@ TxzB$a%i9YL3YBCaǎ <D+(%sJ "C+Qnhk"BF֗4 4Ppۺ42jQ }~5.'t_Xnby&:YjdtRLnA:zRK'nG6Tr. p]/B|f)Cd}}XMeLA\˿r%uE$ +y@i1P#rT.3rC Y ` 6aC=`ݞG 8D-?7] nE nIZG+?[Lmו\ ci"tw\'X T*mzhpp]ڐx^S{?1kB;O +>{/fVpjTC稒XtQiW&ƮJX2F Ff̥ro`>uzE#aV'N~ 2E'C?^.#íJM/OSHj}?GM)tmjϮtt-6 _SzX{~bGFs{'ml7y)ʿyP,w*(q0Ӏʍo8r,KK#~Eq}:i7I[ lC[A]nMqfK)'w3ܝ˗!_`<Kş?_5ޅ}&(G>nE=b'LۦAaz@tLG4L4& ~=慦.dj!Mh܂][p͒q7_u\[8w2ᖜvX;9|ދ2W8AF3г \~5zLG *zAAA/*7m's"+_Tn_7p>Nf2p24efĿ;}s(dك^.~P%仔W\.%"W䫎M[b!OX b6n6ƿ?}-p5pr!'O<8Nc;z* ,ř'#A tDD>iĒO n?V`k<Ο aL81p 6NܸTN.o9m{#ߚc;-񉞖DaYm .&Q93|nK3x]"m +H C` 6a[ý@ (x3tC0+"^ԇq . {Q;NaYV[x||X)˶~7V{:/w8a+\vm-:ÏAMS._%H?#3a#1+ 8,E!/VxX lކU[tv'n8JY e v[ ?`\>grDnA,yа.By 3?_jC +h&;@E^v[> {H2i1kTb CƬ>ðg3HC? ,",A`! .U@fT%O]R"Wi6)ކ (^_Dݗ?FѬH|_ mlj"2^@`e ,Q4z9Cc]Q[&!hZ$ BҐD,e<ܑ+(p"+@DBg5ӴӴJfs߇n95ehͶD$/ZioqD}J@b>pK-EЬ!ES©悩 +ӯݗKԭ1PÜ; iȘ?l' A0J;Ajf +5S e$h2x2xR<vhquTˤc-7\8@{{/z"ҐF,hz)ۡ" bzdIq(#d"\7V[:֪`׽)y+۴a`Pp78,dA.8azkCdԋ:ǂd=ؔ4&u'4v%71^L1Oem=F(1%ւ#(Hd: +n_ҧ-8ؚ'3+[mEJegA'UWN)o-Sv*ԤAE䤡K蝾B۲ck~;As|pt9vu GŠq!OZ.د>ԩ՜(\(T_<]t0r6 mVWH d?s%-mv?Cw|tu\h`_{TܕQ^v|y</զ{Ku9ݜ"I*[f`ac3cd86;E"K bZJ)>z߹Ox|?;7JoJݼ)ycSM)RϪZi6LQ`fRcTgoⴟEw~ ͒:d#ԊʨEl-vC\ͺ2Ml~KsvĜSenH<ޘZ ҩT!je6 l~?B;O;|L:^l=+ +jh^S}"lKɱCq œ'=tClޙqΪk !jH1{&fp}/ܧvd̼u*ϣ09i!M=۳"%DL RևU5W;QZ*l[.*_;d"P3a^f6u+uq_b,>sד]W\qI*nLm ٢6*euƑ5AM kj:kmGE!KO;%ck z.ٌ|dOXN@^pC~Ypa.E[؁1miҎ2EZ._*"b6Ȧ?F衆E^/zhszl+ǢK3oݏ}B%uD^](vnJ~aQ@Wހ{WoJطzIZ6تP4Etޝ2!]w ޙ 54AwOע\WMަ59+{;y۶ .ߙ/ro.mC;A Xr>Ott,,v0;4=;:{7UV ~nO&}M:4T]A>!3VZi#W"mD/ i#Nf#.y#+RGV' B"C/Oޛy[>ʶp?myFΗɜ-I_=Wmw WOȕQiHǕڸ6湰O=;*8H([}E37oǷ!zxpjs`sP,s> %!eإKj0Y `i+,G3K+a'ch2r7x)YW2d \IGb6]"JL Z! iMHcBJ0Aod&΍<+%Vc#>}1~L2&xV8fb̵a,L,fѱ9pL=<.K \LlVHhLHޥ?"$D/П㦬_gadʁLh0]3[, l/rGkpi8N d9թ6B$!yO 2@t%_@ AlLp#&ϱOsa/֘3 +Y|!-X%fgPa, s[3ZA>v BLd'^V:/`1X`0@O -W2h +AL钃OG=_|^> (g6p/ o! TkՐ"mjf_ +0`=|ث'3e{>ـ?/`k5TjAH[!Upku*PFQCQG +WU`>TQĈד^@@⥨_ѳr@#c0#CHfObl-!n=;"_j$B{C$QpE9 h0Q`\ۃGk@9 ! >D8GNDp:k$gH|}q028Ydoyaky>t<1G 2zNzN~f FG@s#$_7@8sp . 1ؿ/0ho3xIYo|Y.虿61_u0~ g,NgOh9`'fu?vݎ6 ,!ar26ž/XӸ ţ  q_K?Vs_],̇#Ƣcp;яyޝOklng຺@AHp[z@Ԉ_0)/P|;i(c6T8L1QʛFbfx}q2:0m әәә?R,-!i-Įwڽ Ady|N}Uz"3 @D'bԇ1t9ʄQC7}ž)a{¤%q"1i"n">y"'@gl368B vp}->  MQpQ9m +Agvņ8sM!nÎq1I~,RX.uLR:n,, 0Wց3HvyM}Eǜ$o:+5ML8D~mlVb9Vڈ#<"O ÅUFaiqDZN)0 x{[ L ڵ +mmk(]+C=" xm*%F*Wz]\kSR5^4( ť!Y e 8ΟANG(:mˊ;7;2Y1T +$ +q\2YXMV +jr|Et(K}J9_)+hR`:(7HK- 3HYxWd*(96-jOo}9?Vޫ n%zajfUPThB^]hU1+$r1,ѧʥ+rB +k Pbʯ_W[ssuNy~AxGia)z _."*1_*UvSݩ峻*nS.` }*Sll (Cf(rX~pKQSkgmk[w]]& +]Đ*e'$ :t C5)SKuiijSjl Yr7Yr?tl^\l8wf >(>:X'pKRKR6>+ԣ jjb:!P$& *.)@36Aom/kX>o<`1ݾoký[r|ՍHU$.6ŋh "VD+ҪR-LW,.Vu]Ln:>4/1e pu[rumfconuٲ;7ðHJ__SᔅUᗪamRdESFTхh˭ JWѯE7;!ƻ!כc@#(7g=lk}o t4W*ŏWg[}w+MFKBnCnrzp>3#%F #u#jg6 ʝEbgv`DW>VP)Oo(KGIbTKoPP_ +Nӄ2!?@vѪQq&йJňGU"6&4آހ=*:>U"7ѸRuR73k&` } beTuqC}F,:QHgG ; fv(Y!Ģa3 .}ws N%PIȶ>C0{^bx.KI d/8tBXx$ֆ#&b  dE@S +Fܥor4tP݄T 7=A)Yߍ aZ`*A -CJR! XK.w x!~`iH`LWB2leb0J[ Ȱ 1VJ /`")wL26=15,r .BzhƄ@WDS&XوQ@J~JrҨ&%. +p2pR yS2\jDO^BXJTO5` 5`Sթ{@䗪W ҙ9O(z&U]ꠠ:h[=bfiLӐ#&Nẽz>|HC#HN_i"f0{L+<FyP.[ !Auǭ(Bi% MH{4! &4i)0";$N3٨|خ3r i-̤_Lg}o}-1ft(G1ӡr涩G,-kv4m`7 Ɔ!/)$eP*0AXeѐ`Rp,Bx2,\m$m8iCv4lu? ΢\@\|ABuQ+%1PFFq:V>&ѬENWOJJN;_ /P.Svvk u/Z C%LCM9Ce/˯TB(5oa'ռ +qdJ2k6 im%*)wYޥ>UT{S\ڡ(m38 V|ovT^ k_ry0z/f7rjc{ L֊vEبX ߴ+kumojlګnX7]|WM~_}5g\&0.Bbl]:Y!SQ#;3m2MzYg-=yK=S}O9\xgjoZ6Y[j^?upc0CѩӢm"$%o5n,1ff`̂8"%D!u8Y,u[=N-|DƼLo}@xQGxqu 0JK n[!iLߧaΕ<_]><3u0NP\fod16^ױnEu:Y7cẌ́ꄅ̪ʄEj`44Dmփ);ta>]kb]hvxQe10c&iX6-V[m&TsZr5y^U<4Sqg!P6´:W +)^v܁y#0?;:NLj0kĜZb1 +:_E_&=䫒 +Di\9B4[]i=خ shC_ˊ?/;UsL== +LJg$gPiٕdv*Y T˄ +rrS()iAaL8e6_DCWcHdY۴`nɡuf^U4>iG(2OH jv)!Rsb1XR\:P(/Qa^ꔈ:-Sg + H~zH6Ԇ(إM~ґuҝ4V} += +[4b8&*V%4Q~:[KʳJ9^/gߑgeY )M{TȡM4P gM(ީ8\e~^g5Uoe0|i__egEs"4F*J8<ɐ3 +:SLc(RY] +*AYc1!gLJ2e?*\ Gam-PIgͯsM4osWG Q1W@гidmcFQ3F7\L6^j] TG9cӔ 9%)$TZP"SC- IhB?!NJ54!Yy((+ցh0ގ^S6[:lu0Y]2ѨobM0`Pui} TçJR{aq=Ю4[Towçzw ͻO ,lB&Bj y(E0@-d~? +4MAbDW࡝ gܩQ7/\h$x} +sG.IOtm $eWջ`*PKt9#uk {5+PUh^hgO#\|f=}fxhh'lۼ$Ym;a /Cf>F:}vHp}u S)~o?Pݚ·zv2?vFv_{ +^#;D@=;h@䓞Zg#4gFa#ʑs3gS>+֎+g8{ _ot ~TxZhߤ\|u9mU3rkmuCI +ł[0p> &>qhnm@8ANfC(w0*{x0= !>YHw35 TcixV'C=-ܼ! nwx +0gsVtwXWl!HHVH; P#K{:lźXJkUzjjE(T(twȃ\@G4cq?p]N܃?R^rST( 57Z R'QLak[:hp^ Og\\pu"WtW+X-&IҨk6iB&`"C$.ttr%eû2^\xq# s8..npAυ(GC0A8!`O t$\Me e`ـEs(D&Ⱥe<m |#Azoxa 5r!|&/G@ڙ9[kX`*ɺk#,}!D"l$?D$bZ!tR0M8D>"Mmm3hRHmpCy"b])^2j̰I>rJC'.'y?GM?z)z'̊^gſg܍F&#<+SDYBVH;#>3ɂAx:4vZ:ko-Vcᨡ҇1eҙe4T_F4FRc(LDPY1%K(%c"[auˆ1=q8jGx s\ދ=O7?-)qZq-i:JfӅt1^io<7S]'"ҤܑKin@L?N,U4vIܗqGdeWߕ7a@~'a΋fd#Ylj⛔w6$vꗤN.խ۪nXN}qfHՁȵC>w2Tk:*f:P.xZ Gӫ,HIȼIњۭؒw=eO#yWR]Nλ(MiջΌcGF H$I42FW~Xx!b>̡oMꄐ;ʚ.ecEe{yՎ%g2N^}2WݙR},{`HܑҽĞ&HAܒE0ABZ-@u4u"޵2͚TᵕY]E9gs+JNfYy"x抯sT9Vq$Bš[_彨؛?\];5A6̩ +[anڃ7xer-ұ%.Z6#Ly~VgIhaum5 +v|YpvO݅ݵEn/Z4XhzSPƢ5\[Ɇz(0Xj-4XΛjfVV߶1 lh]nI?Wu8\Y\jzꎒ[궕RrnSɕ u65z`Uk@&P )(֟UK|_s8p׺dp{;ړ'[ҥGW*Z^]q}ٮֲ-eW66kh*{V?4꽍=C,32YN0Dj0h>&k&)JVEQ='-R.%EzNyf^|//߬Ҭ)$MvUL%HhCX2bB}bt]8̮p51[rRHTmr}wDτ۹˶s_MMJ1rr \yXLVuܯ#m6[\ؿuJԵ;zmkm|xGrJmY:WXUXϫTꪳy-znVe=qs˳sd(h e)_~< u­ΥW]9g~@Q"vLLQ<[̭kuiڤ5IO+76 I49L|yHy]hִGtл[Z#qBklL(OȯI/e j˄u kbW8'JH(% +U$Dž`础FX%OPaUvI uc!wQ1OC{;v8!VДmLg-kyYYyTո0Z4.䔏ǖJdi%%0Qx­Tҫ ?aHWn?ܳz ⎘hޡ-܄}Ȧ҈}E͗ac=E5őUa,P+4S:2 +f2ulܛN׃®EƮjFkk%h.η 8|{?&, T 3 pAݤܑݏiP3+%P,~l6f7:ڵCBs"|Gx{?n |t6hff#<ͮd`d7 ;8K1hg4(ґb(rl4^M:ZM:LxLxNFLu6Uj=Vʩ+i$&w3'srſ)@ּ3FJi3⦭ §m+l]Vx}*x.+>pti^_9}}`O$. @0@@'9JW5oJoa,cY묽ʿ\bEm0m<[K\fZ` ֊_%F2K2t"G/6)MM@%(TU FͅkPWI5GKc[dJ3K`ПH3:& n(&M>qs?2+@2`sW_A*eס7.X +QSZd d2ZYOtu1HG9ښD Ա&MU<@2X{ISQoH!Ⱥ#(8|;-F%UQC55RsIqt^JH94ܤ&DE$IIK9v!;,GAPb!dl*(U5PUM-D5E4R!CMmTUʀ $Ay&hZfKuA <  L` QODL4h"ژhxFE&0!Mߙ#9!d 92Քњ@7DdZ ^M 4M9fhШk %DnK-ЬO1noYBYhy_+d)$;訣&ZѵЊl}d2B?Q]L$\BSN_7lfs33cٴflH.S%n)NW!%J?~ޟxޏ lR:`VX7 0Gp8ڃk{}ܜݜlN< , +j%cP<9]bv`;#Ob~uǫty?<02pF V J3pZXq +_D+ ۀ[$\.9_ݎaz\g/n|!t1<14N1"d <ȦK@P` THaxd10G"r/S-Ϟ{I 䋄 qgn#aoxIT ] [!$eff:y=gL/$OҤw{J5y̧Nz{׈_kCO=_y>&?ʽ5k>&F@&ja56J`x;"> <3'PR_6P_<yF;Fyr8:Q1Ꝑi4p&@56)/[ˍ ~$l<4fMeǒ_d0$Ahm཰aûn =菈A"GЧCkDe;'5?1D\R~ԇu_vn+Fu'lhߝH&=;ȂQ W"~gtF\fE__Eœba~><;<ĞcB 6XL2ӃtE қʴ5t^6$!z" +g $ +2.0)鍩bg5αvzbXgb{XX'dwpp:fvkX!Ɔ6Ѓԥ ]MvbވoL?@Iӕ4Ť GpSpS=={8{(2ww<#q7kM%} @X1Y:39tޫ*{TH\r+M<">OeMByN^%';(=359eR=#S)N64ŋ!y1:0ӞR~yg%c*ӫ{S<+#%fPd vm~]zM$8(.5 E5㢪镩4MT 5҆X8.B E:m+1FW,Eլպ^wbqĖ(lĨlaSzcZ!ΗKJ bqH|LV(Od*J6S%EUwU@(6 @yL!勐5 + {槫ydk˾Xj[!=/%ZV*E3PEZR̗T)s%JR.U$0lɈLT6S1ׇyj/@ *uȍȥf܂s'wWPKjU 4W&UdK2UQ%vgH{E;b`NƦa$u6d]G-?hh#n+ZvlwFm+jjPP.M(۔[*̋)ۙS~\,&g 2 wQA^}nfkvP *EM1*5EQPJ!PDѲ F˅5 r}n77}Ss ͹SֵXk ~aMV 澽Q9 +plZЏ>T[-q,QpZˋ?sִsMٳ˴kWٱ"5x)ז$i]}mR͖ :&[ +.PD5?ɇ;y'b5ʒM+Kӣ}xZVFIΜ R^\}ARɡyI%?Iŭ +_M5? vYs-}zpg3}jb:-5g|U'gV'L:9|ƴIJ k˿WQ4- *Z֛5H؃o㊺-> A13)_K[kϋUJ9\)lRdjmzbBMVZlͼI15cjE]w"%l}Rd]Kbë"?L,ժO{pz@m@%"!&t{S7~u^ε̆}j}l`+S#̎_ް)* 2jy0ՖtcY࿻. 2)/ eo3rG&&YM:{F EȻqưtl{KB6 ~P!SSo#Am PD=8] PVG3@]W6X} +? CgHFg(RZ -Ұ"%\КkilWh|;>PywV)_\S^t:PmC=sX@Kȿ5tz8lr7&[<>ەe~Ջ}ze^ rwS䚾o}ԣP+>)>т V F3HEFGS_[YͰxtl`8 K +'zTW` 's .sFXE&-8G3xg/ kȿ3 czG +qcq(`)C%K^cqdc<N8[NEXe7OYp 6uOO'\;@D D0xq8NȲ!߁R'tu~(4 D;k̴?pXv k `B@HX?/a#]F:q icvQ@^yU%8p&cP$e ߝB% yZgd9S3:ei!֛8Gsu %rsg{~6qpiMWY%0PD Ejgzsc&]5XI /|K=nx-61c׷DkURQRe7K 33!ӱ߇L>=Fy F BiI_Rr@Jo[AҶ?CW!~z6' DUgӴ4{d\ n"7FƍqӑÌۑQ'À1]0>phZ>P|FjO-~dnk/ _px!S,1naafAV9qUOd#{%N&rZ6a9InuD8sj=N)BS&zö#JDo~^0 }\^|fp#s˥ Κp:逞U.OHF[ek0 ++TbՂ +!(ؤ-,愅+AosF Yȵoa'g0c=lqtgOVDSIYUr5<ŘT*J6HZ!>$mۤQq*} H'H_$eK^%/w]h.R]w |kW)ϑK-Y-SBh}X3K5 +~R'3RrE^"ϑ;!Y~En[fK3dJ2dϰ dg kEN&@r;8Wo, Y|܆Jܔ +6'K'1Zڨ8TTtU&MUFҫ+ROdW3 E^[wnfp%/t`l{=haWq꣇b71+rE"ZneTH:AjjB۫ku2ZFS*O*3u^]׬ȹ +ؑs/*s8зN Y',t*ѨO1dņ"#f7&T^dxM?`9XX dtw= +sT . Z#>=LsyYܶ’ݖCDs^+"IRf)YDSg1p]megkN4)Y2Ɛ{j:>-UGCٛ>5Q6f3kJ>EVJ%Y|qQT%8fKy)-w^gOqbGbA1UȻP~//Gzw.]Jvti`rPCMBR5jQ\Mz2&?ZS%vckGy;\R|"╀l)$9h&̾h=(F`O{zm>9L=FڤL`Fp۸$FkP(rk.ZOjmŴݦ>jz$ԼMpf/N0ԂܳrК~^ oxaaV*6`/'A>&'{SH^З+'DFGDEzF=ݐujӕn$s9+jp֧fԏԗړ]g?%4 78绂/l?^ŕt8D(!wN2э#U-~qVpEJK.%85Za9 m1美[ztok#n>7hky:zg@[hv #O>OȘ)A:a#;?&vF(]ȮHmأr{: U='۸'AOzdn qm_@kyu Wl2C!SnBcX=Iu+bXj7*t߄l%/%d B=G_m>$n  / ra4"&J-Q I"W,FH } 5CMg2$! rRJHmBGxx/_ZB%T F P B7$ }* fS yˆL+h!^k$$GB\ Elω{ ]AS9o)} _Q[DAS@T@|!$/` _ ]"P&ġ8EQ!f d™R!H b $+$$] >1GΈ"Ǣ>_ 9Q/1%:4r ~VTJ(PQ&= Ň,`ZFfB#[)t'_%} O Bxtr rr0+``W7[C70Lk|PK+/y.꠼rQ,M> +e;-wCAC``J8*FPEjNQ TQ-Pd]z\4xI&UOvoݶZ声FEgX~xw'Wn 67l0  R礈 jD&`?{>Ac>[0b3S]GR݃i_تث1yNs٬nuvhGF A`QFѬrbXX$g1YGRcs^LN,S{\o-6ވ_η9akfSR Gx  T!tZ!FrB'NUgrNbUߚz R93˥-ߧ\Hp>mG@Csi'Ϧ7Lg=ZcIE~=oToK(QQj~Vbe: +#es]\sSFYŌ73B2ߵ͚c; T_e .:6gOxMNh9O'ѻܙAE ry|x5γajs8,CCAt^.ob~{q4^ /q,X_MނC1 *{W a3wC!F$B7Ώq=Rq}Lnv~@'ήsVΉfؓ:6 e^6 XPZ](Ui STzwX73DUrh%Y|$:;5/K$+$II56I|rD<*KV,J5N/I',0bsq\ͅsh!#[k2$"(}|kwyILt}qxυL`7Ρz?ԇ Ԝ(: y^RG- &-хmnf穖!#~>ÑapG=G[Ʊqڸ˭w^Xrz7vk3VNWebg1@y/Rqƍh@}?XI}hM}`no{aI[ ᥎O4qx/c}Gm|@#+>2H)?<@U ;!|&4cs -zN/^l]l?ƗaK6/Y:mE4瘡 \͛w$_Uc@@zW`c1ߠ +])+S n1v1Z=C5 2|6Il\\_YGG.~E+xQ6CG&Acֲ%F:kjRo%YoN6Giz:$LW#Jm Z@;@}FF̾ .)ΟQ@Y7 VV^Aẗ Yk3ɳM 2  0 a!("(-RW]WOZVlݫ.Jz<'9'뾟'9Dt:Xb' +XNG]yh@vb~W}eBԸۄD`ڟ2!D .L 4 3#{D!/(3KQoh>1_?EI`)Rq@j@1mٍZAQ7~ +}D;u8 <;l+E9Ը\- H1QsTutie (; +zTvQv]Fwz~h~#ZV~= +ჾt.s9@|}`,cDv 8{mCt.>o>ioy7|} 0W4Jb3>`j]` h<hA)Y5aod~6KIh`vovV|1xT 'b<`b!Q">Xek;'!X2\$ӨbVZgnbivm;1sSka}~{xAnځp80oNB..cbAn.|+x0qN5 470Ɨ'7p`mGܱwY`.?z?H6~1T^sw* n4nlK!Ao9 /Y/>`q,B|_|a\D#KO1WނaEUy&L-\c\ocHŐf zBzR|>l]Sa-Z +GC3;.\. !E8!u ^L{-cvx{{St'2KK7"Q~L?"1>$.![EkE!<σc$ԌݗP?vo& ;v%yH<ݒ-͉>M _>G=&Dbך@tޫ73uɯWcE<'3CiIN*9$Oݕ\#¿%y6`!`9Iy Qy4pB`^wAN D2܃TB 7E\Υ䛓nyAQ2թcO5=?hs̐MimH[&lmZ>mduZdUyImoW" $+RAR +aU:ܿAA(wk\ó~o!s )496EȴnkAJ&:`CFvIّUZutzC +jQUG/QZ.RA‰1>sx6yxΟ3No8Rf +7i&k*0%MdI&OZ3yFLuflyf|Y +̵E*3Zds\ysee[6 QpB +,:K7|^栳ԇshX 3]LMJ;IR5%'ziN|QV2<"kiRyV]RYVSRfoRhlMoRIbb&Ɇz {>S}us +;ތPAcibSmr\tUBMtE~^|ٔ(.L-]:3wC]SsI-ȹy4A٠@AUr ߃E٧c[@H[>ٷȚl_Iۼ0B*+wXQdNzpŬ iɴtmQzvJ_ӮU淨U.Ӡ(, ozETVE +*; H;(PAqDPhTPp#h .@M&qIJ$*ʢXj~< Ϲw{ҾM֣^f4]9á~dܡ>Dsfnh-)Y`mS]цIY4+3{AxT+fKN(HI$'e$'dθq/!>c0^)%.GUr#_^XTjt I/NU=ofvwvO6{kVmv7ç +=*kzԋר۷Q'^C٫AcaE3&Zmx4Ru$.pSHzr`B嬺 }}=|돺Է9uoG-NNAp~ 82 AG4? 2-.u$ o@b}N%j^F\h\i|ff?[|e38+x&ttWP ב[{28 Y6nc"]mn6o@mmjsn홊i̦WN8dh5r2|@> +s'_ @i+!Wv,0;N~ppxʁ )ckʎ-z'f/L^y}~_/_IrX@+cuk4SG)}_Yp!IM.iCoFz phMBGO#YD6H>3&P6M=H/)0r{]'o~FKǡ>yM +‘nD ytA>'.ʾZ{*wOVҺX9!K(0<_bzoL%AtQC (a'GV(EPJDQLB1a*j*8B'P)mUgO=y{9ZVrdQSo1BBՑ*DȤ:"a;jKԐh"Z ]a^kr z6(-GA ,R (TGձ(*T~dCN1C W73(^xIռ`y"C~$(fr44Tr+J?]:bƈOE.]5-NF{W j~Ot}%>˾_b{%7g{/'! G(x0 + P84T裙245 RF4fTL>\KAqwz1 {zw͟C;%vY޿,Q443})KV_e0@ ąŲ)DH $=!@Id kX ȾCAZV7ZZ:ں[vth7~)hJ.3ZB +ȷb,- `Le?}~ތ_ǭ\dhW]ܺȢ5 QkE;e9av9b:d;`M;Ͳ:cs>ʺì׮m ݧ@"b^ ,K>?p̯sm9ͳkO>町V6p_b=GaC^)}I#=ؿ&09zq(Sf` y+1r>)%G\]I+N +y.q3#LCm>ӂ>a3iRI+ذG4aBøhwLtwDtwn@>@DjDT!E8Z_P*CvkAL^G)-V51Yɖ:&NmK%O̞'d)&Ul})A0֔7&{(@k@~s?b1԰6u6[6Dq^e@}yudfjI +O]%NqqV\?(J O%ߋ |ȗXS^p eG׌ J힠kh VĬ14= +$~9ItuP! +RCDbh BQ_X>+(bbK!_ 1S~Q]@xe0m D/bGhhعѢ7tvUiN&6q]>&+ ꚹ~~2&~ū_bbϙw*cLyp)` yo7z"_Hf`ˢ>^cF7C%UvS417ED3ѹpc\ƴ?Z߅3aDT}=<߈v?4doP= aJF=~4"gfac.!:O8DssC8<8} R +"rz&Fgh.zgty"Gd"g}LwI!gq%ikǝm6bhM1Ԯ~}Sveq +IWk{5Ih{jH/v=ݮm74~ N\+B-5^mowU1gnFluIX5הPصйVsC&[*ڲw>`{@ k.wWiN|@02= ث.r^N55iY{h:v*v*6O١vwګrx#Єڶlm^Iw.39*-|=sBX#L4B&]]˹<7 w~|WxsaPMKXJ`vﻀw|ȡ;x ; M|`j ]SwhPwk6!kfAg"^b3%eC|s=tIuXx_m +Vq$ORH?—V>}s+a1M/>u0=!t;Os_W Cշs"^"F,=I //& MTJ~Pݏ } 5>y- +'A$ Yf$-2 +D H +d(S|" |Ffyk El<ƪ eşxm($+32C3g[#_%f S2& It֢x +`/(M+?6eIBb` :fp2=9&23̤*yy>7?a7?LC&EI#kg^F7B1C"c3-?LJ'/bԂi\BrzVӱMs u~ W*.(*h>a2Kx h'j5UX>[ ]-mkʕն}ʃ}vwa*؄2N* ^"x|IJYք@v|j\!&UuOuP#ԇ:AS0Es0K9}I{hw u%2]}c?UVO23$N^ADKZё{o7(@zݞ\IH QIՇe{ 9ܰ(䏍ۓ'R>7U0U.Oޖ%uwYۧ4ǝV1w]m'E¼*E]@F$Bi>e^z+P zK5 3į<]e ,͜WFLz|:GycرRfYr +Zh .p<Ҍ]ك^drwX +59!/^q&/h٘=ݺCJrƄZ3ٺ*gc-t. +[+p;K×9w/98zDnNKl,2Hnfߠ#f= e("XEv +a  H)Ȣd*u.*{֢S霞Nq9F>~znsi)ݧxS<O1;oWyy^Ca)/w?w/cOWN.q1$ׂO +lz㎛^K-̃dNE;I8â,߾\1`w@2 n +zCZ!Mj"^pZ8&gju ud*\CjV(BU^XpRd3^H2Ӕu3fv戃EV2YX(E賛h]!Zu4HHm|dE֗Y?4Y daH(B]5k)ǨX5P ̔nfTX#?ͧUQZ/)䨢t9U9Jq;\lbO0Kg*XS)[RRDD"k=y&瘃{{0x.%d.8#8v$*S AaG2zE^LZ*VǪb]$=.̵^esزYߙ2 -Q}`/<,cnUNJ;xNUa7_K`8i8΍FCH{7@o+h@NGSdƄ][fWOj0`G^HA1hC MฎA;Xóf F q jz͗8wpvB1= fT#Π%PmEClG`sT,/I6ǚ*YF?'44dn{0W/=iF;]" +}5O DQq}q Vȝt$5f˟`l枈wLbgČI\ccQ {F":>1ΰ.!ސ޿5gF&QjעvϠ3oqTp-DK-´g9o)4sSYf.jE:QB̳o8ݖ_O)b{">E=$I-ZC @Y y @RְT+6ؚhhlX(ֶ֪![_e_l#<%6ak g1ޓQ`aͿiH uo8=sCH dt낔wAMڳPOrY4v/Nع3iXgb*cM㝀n6n%*]]`].}v`%زD%l$F `|/[^q~"#jv%1f&{ 6@G}`5-c'|]6\ + +q9š~|\<{Yԟ9qwv Ƭ˜[w"P_c'_6?*+w + MaMHcԡ " +" QDAE(+ .F#ڢ$h YK4cy99s38sw}޻D?[=v!z s l/e@0?p5 畘"|G ߓUo :}1x1~+׶DoDp }&P|  e>шZK^.杊yG=s/G +5Ut>K? yaL 0\JX)u`dLBF֪\TM1 +0](K@l;j8Ko7+>^_x10DB 1 .#%bH+g)^V}^?h.GX # H1"1b0C  1LF ! pU RZeX݄Zn,U=(C&^|"0iïA ƈ1!)sL3B8J02_I?#}ŏ#+=z<t~;<^d&|L΄jj*d*&0Pe<+@7 %˂g>UIe=' UͥB.,ketETIEkh=]Tk~8B47#ni3:s\q1׺勘g<331зpiO[!t]CRA}g8:gRAgMY2k-tbIF9l{-%d*Le%Sn3߆̎8oƨPhn¹m4zR+$) n]!} C9*a5N Cͼ%_HNKHTwI^ vJpZZ0(r<;Zh%[K:0{jD~sQî\3_t//w_.+]-#6ڤE;AcOfON;|Z}hVhmU*0bl&#7ΏlծЩeZ&^; ~tw/.`k=3K]ώ!篹/td_6dV 6,PP>N>`~]Nڀ5g { V?7hʏȌ*&2!s0#k8^ÉNMEKM6`WAkL90P90`z.Љ=\ԢpV1^sc$0PYMh⨪t/BXV[ ))ZZi,duYH=6!lJ/, ~lSfA. +bVaXFr$z Bj5-q~CzO\dEDi+^1,G?{Y4/h>PgDs"G&DLS ORNQ KPMH}@"`{s޻2V=͹\5(1Dq ֭)_/k"{ղqiYIn 2g1; eij%IE wŧׇƥ mP'Ai"0v]>~W;Wߣy|kj=6k +][#[Xek8|NNGJ,+&d-ZU2)kspl&d|\Feeoys'9| +&BXuBKLH[aZ4jx.SS<'kyOҤeM+)9i\3uyץѭRi^mƣ{5)J6R?YO2}}p}YO:L(q,q8z_jw!ah{P Ğ.9{? 00{P?lOM`fX7GxX5e!,ZaR-5RUsM:F!өHz8{v +lf]Ɛu[/HzxHxÅݳ"> o8^fwϕo`]D& pz}7z " ^< yque〛<{r"Ő7[I/7t\Mwi  p?O3k@z~g^3=<C? >j$?/c)F}N?d^{7[w>wP|r<ЈKp{򜗴y0xOaW3<}W3=+߃N+yLE/+k@x^XE2,xr  +6|:e:F*&3dR@62B%}Z/gzЈ%~"zYP`,Y#kd !O'9zVҡoXGFtU1z ]9Oq-i`kNEت!y։?DJI?k0e Fv5hfMs]ɤ#OPH+k ~a/ً ǧ\&M..o7.m#wA}Pk>X[8ҳGCOF =驢c؏pn7qUuAHAF?cpMOaӘk=idғCO=Z~Bvc +2>×8 ?Ϯ}観tBF!dXf N K8:1zV+43q9|PoB!yfbt{4e"OE<\P$݃riZ!Z3+EY%̫qb/Z-ٲMm89)Y %G5D*>uo-\;J P e3?BM"}65N;.E˰hr)8\1]Am]WB/f5`@H@ 0?B$"@,b0,`ldn/cq8I{z6͌L3Mn&mƎדj?~#w9}7g%t5q mS7%ECySwh8$A} .D2?Abc{$&Iny\OhxJ´/Ycl{x}A:~t*&i#zOcWU6}{0_ ׾}CCODwt !Sz] +3Fia;p{]4}B4}\&rY]Y,)oANf<E&[ރ߻|[.eex>G{R;!6k怣΀UI_d$pY2$tJV$A7IH> ? rp23YC@Fe!d> |v?ţ-)sCׅb%+60VÉ5.by@66f3Szv1b1}1#~f0wFWbH}-`oؽ #E1h]FrZf5GOzevd̨l1fXc+8;Xp+alobkcyxt, +2/a^Ղl)h]iR<y󥒠0Gq΃Xrר>~DoSt% *vJhb-ڢt]O&*b)B<ͷpB #𗰞uxwLu4‚5 +(4E*u`_ZCUiʶ$'K9ܡNnS.XRKSK@o({TOtW%}3ug -BT='*3d;rZW!CSڦiNAFziV;&:N}iTcV>gVK*O<9rܷ݅ ;zֳ~h.M1cB[Mvp_U~ՠk׫i.i֚&mN1jjW~+- [[S*48͗2t=Asy?xl3|X2kFdߟa3 @k4Vj*Od긕ZxVN?ge|9ZY-{\liz3Qluž);_&f6* C`b^#yF@q +gyB1_4hDY0ņu6#%BzĶ=]Y͘ݶљODw +_X^/C۰k691"dt@A3Ѩl&f8LrtTAp$LFf9v;2ݑ"=\X 8N'oM~1,;"4} cnއ߆^#tp`;Aq0gR0*\CKE|3SwVe8́\g0q/n20I;_,Oݼu΂Q`OA Z@D|!9D5rLWĤtWUpizTW5%%hkDڇDʱ?{$3~[n{ XA׶PhWG*9tMX[D_; ϗ# $QA[ +IՈ [QhQQQAE)""TD"QQZU]muluU}@h;;3 {~U=k ,Co$ye Os"<̙p7yU'Zy C\DI1ֵT>AƠ*B+]50(gK_8XE {r Ma42 +H- 4ҁ|sQ75ת%7%;r?&7b7`-G)Qͮ7 -CclXxs-<P~^wjw)5@<15 0}I@ +6괌E8!Y$炁 E&"{\8Q;Ucfvy䦓ƒ;#@p On/S9SKe*mȡPŁvu"wh]^8ѱN372d/?zD ?u#ׁ^!FE1\gSg!ǃgz,D/3(^G1_uj ~=L1amOM2h>wI'T!FZt ͢bYE`1bŊNp㾫jn:]TMQ T23`=^#l!~eě xx Ӭ+(vakȷHh#m y BE7XDR#/|E<{;~FCR}.1T?S%2|.Ii6 nAχӿQhDz*Y1d#+d]yV!݊;mT4Zpm7SA7D>saE;Ћ6*Oއk&YsZH2rVL^spJ>*uhEO:h#I61.[x~q^OȚGb2gdd[ q;%8#zAd3` @3 +}g uPf5ĒORHH#g-99-< GQ(!~sˌ6H5P]e]|[,oO=@?+ie18.ġEGI8"K!Y:ZeaulFM),hE$]F=8IDe*DTiO>|o;M)kף! pe&)&XK+nisn\zc=PRu;UP#J^ +۽Da(xnu_)Yj, +u%Z]6jn.צiA9AAvYvvڛvOWi#iԇm +#[8:c:ڳKk*JFXo֍u}c>!GzB,}:*L}c>1]_F_锦orZkuN}眬{zaKHIaycuvq:G=fdf湮1$R\WEa"PLS\6rkţV.Fp%v{@q&~5k9WЮ-.(W>*;,.3tg\W#S{V'=UKY&"c*~nq-q!=

*pH5#N +iD"e=)8~_C ~vd/lVB#T?+A 2*%vʯL;bi>je9!ɥEaIeCK&6N(]P#(o{%901oϔLh#D*WȜwȬZm^mLoMxՓܦ-꙲0'iL€üx"~ mc 1i*YM#2vHm'!-!}@B}׸1sGWy?77&Nݣ_]w홚#UʀUjF|[ ױpWCmZn'-2%@D$D%sHX>yT8_Qc,q626}:Ҹψe/ _vyXs.MkDY9s׋ԁD%ZZӒ"K'n +q,>6qiawqzGqFG1pjs5,11DJ62fzOxc-"*=q=`/]nb5DlĦ..]\Bp@Γ-{(=I弮ɹSd)Eq@$䠈+"J_{X? \s ;?x't'j`5ȱ|].I_C. +ߖ6le?]W7\ӻD&9B qߋ84؈u9<Ӌ\.rr!|** +_K? k'uyKX8qY^pހ?[v7&0AH7qtn1ѷI7uG׈L)9H%=Op>OR[!"_Dab-q|D>>Ž.Ё{]|35 ߴsî>~r>Pfa[x@,h~Q*Spۃ"\y7},`2!P4O,d<| Qa.N7]R!ȯ/_?"},b`"̕3I9#-SI*R7؛{Kٿ)sG>eʾ?n?~bM vOb@YFOSqf,|sG5S@vKqPk-cSmWy29r$D>`t+OP='1"ۗ#qL™INy3GXiƱ{\)krsm%<""Eùm9x{ PrEn 8)ġ,ۣ-Y *mrQ6xn9FNkDt:Ɍp >s ̽9IU|ɸ#b _Ɋ+eu$@HBlFY&,K@Bb @lFHYl 68vܺn6IdI=4kL&vNI]0{sGsϻa8 +y'[p 71'^ټ&7vFȟ{ޅ]A]1 w==kWBQtDuD?=͂;TM^-F \g`y q; YGkÆ& ka%[8H‰@QOx캏P,O;p+ rZpRX \osm p; +kI8͝U2*xc(9_s<呔 ?,5&cً}.,q8ۡɇD-X%Z|]]rx$'qDx^"fSfP\)¯S N$]$ұ C&ة[X7p1! b%)r"IM]J.K1RRiGRZiNڜNhDөS=&D7<t{|9>L҇$m b{;zv+rY3KQ2{g*=g3Kŕt5-nzE5)vfZZg _e 3ŏ}ff~ 0ӷ +csP1>/l\NI,J`^H=,{NKsjƄT|Z1Y;,ec4.[`{]ҟm?k%mV juo/Ensv +-19RS<c,O;"װ~r]'{C]@|>]d_jɽd}?1iPC?Ŀ^Sl`.bz)Kяst8ɢxS-)0 +>Ua@4[Yܩ4t(-mJǪ(rӨ\W\U3?3!:yd\-'<| ylW[Ĺa\`BN],1\ѯ3z59be`{&ZT֬67"zu/V=5qK\C9=n L2V$9H/pG*lu=Jo\\ |:*`R{^r)˪l.U6hK#J\So,i2hz͐R3%(,tm E}+VHxq6`nd YgP5 F1n5H|uS&P^ί,3F5tZ]Fw0Xw$F[)mEJ(J?(Z(!#2c܇#3%d9j=>LJ𽯖uQO֥x4f19Ep(ܠibzk\@JUbr+ˮ H7_ܬ9"ug#:<-1؜HdҍMR_}C^0TST+Tfst%&#79s̳l)|'5)1}yDVYڀW-l{"BއZZ4C];L46 Ԛ.nQ4k8 +KER˱4en2oXVY69Yf6}<$д:VCVrBFv7j` }o$S=bzQgufB6#GbkdٺBŶL J`Z@ZS;~'j{2 Lj&ܼkpYFfۺho FAnB5Fsr־RL?fKEv+~7~'~co3.IݼՆudFz0݂n ޹Fzt2! O$$ )Lw*Rc$:::8'±˞ coxď=ō|;3HzG 7зه#B:bph? @\ rC+Ht){\9T_<.:9` ]tԱ4L%%2[3[Q 24vm2ˢCu=Hzo^<}~Q2W2|KI{DwHԗo$ ່c7C`}ᣅ C%Խ$~ߝcP ;| ]@jޏnz??#:H@9 /ZPRw= c0:BG ._{?Q}@w)FQtDZu#vQ4SЋ +t>N[nJoG~x*\^;E.tp_&i<Ѯu +N!/b\~ <үaPgCyܰ V<׏7A0,l{UuY*A5 /X7poZ1͐hNBg:g-^^STN X>ŕaP3D1+~)G;Y{!hrE}hM@#Cuk zwui,ИFϠڳNolOS&Fc3JF Tr= +udZux ~)s|,YR0g~JenJev~KN䳾3~}7K|bp3xG Əz"WU+E8-P״,$-ݒk>o}BdLys ggˬ5FkŚZiiwHTO<)^zgPƏtz׫1lEE;)PwÒA}O:.t89Y801ccb@## njssNwNs +pl + a+!熰Cvr8=0у~ x r|`?(ZU`+; +%+X Gk_.(+ & jPE4DDa(01UMms4$͢M$M,IkCHN[9p~NZ9ͯ1cv+=eh5#ƚ+*Li i̥iⴭ´}GyͶyͶl)׍P[|MyVA,ko-P,joX7շ>71&gЪlkHEvɑe7f:"3 -̞ȜYGfdaM*jfydMR +u-U@IJk_KP4P_4٧0Ҟ4`^pI~0// Ēk}D>*ikJmf3"%%䜳"d] O3=aK?p&=plbi54G F-pܤj$~ف%sslم {y^joN7HYnLm?cU9191cN5tvH^lV-eKu؄᪬RyU2Χ2?bNPNyZH4GADaIvXn+[lIrF#bcWxP̲㦄K4_1yL +x~D~>a54Vj-Pqbrˮ5 &e̐dg)Yjta3M ΝӝW>aS+6JX'z rt_pIE _dTnHe7N0d4y.J4!9 e >UkcrJ-Cec,֐2'%oFs|sv@\sa'7bW RAIM[˃F7xsݰaڍhW57"Y'IJt(b5ٝs;w7=wggLzѭۼoj=5jUW#y|,-~X,$\7>]v9UR߷Վ&tԸYkaTW!Umt-Qdg;{qTB^RhGZX + +]Mޥh笓歗ֱ^= +5-Sx{*w{S57WA[+4g|{vg6_{Ut{~jnu]AMRvuR]q/ݡꋒ_7x`cSQ><(8Fr}!mcv~n $7%ԻS*!lla25~4q4jWU9C~F68H-A-,;93=Aȴ^Lİ{`=hvnEO&мyN*Aa`Y#xG A8a{MvHw+pM;YІn%A 7nqJHKK')o" c`t2A:źGrgI,q>V/o9Is/4=iiO:/K0!: FWzy>hC;=9^>JY,E/Mp&P&A4tXn G31^9C&'?Ƌ[;sC@s5;g?0h u]U׽{ /ַڄW۪hZ,\sw}ʓ>}xy !0 +ba(ѩI +D l@g :(nw!n=]'|LA';@Z1h&W +yJ^SգehFc-,FWY 5T/PcNiQ&8}$2*랓hc+ +h.Ztگ8@~z$ :ʍ>'5&`4G9fM" S|\tO~];Y>9uqNby+v}_c|BUx)8ORA Z_E=ZOQYtXHY~=Qdp%vsQyI +r茢NX鄲+X\;^ZJфQ:ͩViVN3kVݥ)[4zƷ}JcX:B*ẏ DHCѠx6zNoE'vhXwJZLfOvl4Mgi"t(,RH"ߠ!/'́8dLb9\h1(q/!wCmtF7vHx&( RI}$5yN["iɜEc35euBi.*mӱ֫eeb-ec̥1[>W-lfVkpc`LUFl_áZu;vǑphDP].sTUv{[_n[c8~EiØBb ++>WB[PX8xpe4Jݴڝ{l^SSAD]Q\PmTT)q'/v⌎9eؽ1E<y^?yU vV}{ S`{/О +-mЦ8ZUB[9Bfv*L.WlSY5FcYE<ϵ)9HHr~/n,:e=A:<`m!0h;k:yT/j=TIc*=\>Y,4QhIVvݽ2{$Z>,uq_gw(/ԴN8JsT+<۰'2|3 ANDqm쉢ܓɘzx"W﵋tq]eDiƻ-H~?"ԝ0͊r)pL:n7.W17H`*smP |MNlS +79pMnL}fg߹;zX+ePq0 "g/ܭpكDf\ Db +œ&2BҞP!*eԡjNR034l]":ŏܤQ{)LlX \I2?ƒNrhwXBN Ru0mp9)un'C$ /x vFE}^q7 3,`QXe ʨ@W" AM FlR66m6ƚZIr1q9>;}{oqE'dYg}y?עhZ#ew +$#JERdZ*`2}|S>:#eqL9I' r6y < r keHKM8% +)\ +c\ dDR#uxQgIa>lh8(6!ߝl& +o0xlXu ĸx?x+ApJA i_Baf-ر&dmȚnŸ73nbȀE.0P.*cF4J`vّ1պ6 +z5[ hwҔ`G .].6ul<7M}fs>1y( +;[D44;h7zuS  +1""G9QlG]a|Uz>"S7I~0=6Y@:#ˠE`NА!GΑq!Rpu q2z8 I;EQ0CHOa2)t A{09G݃FDnҨ}tܸNoqE2bǾfi|=nQɎo=avfm㴶ͺ>BeH{]wϷ-ia} +>E?hAo0iY7z4zbH4l:3Y2cbf&r}+'j)$Ml5%~JC!f_`= n1~4Y0Xc0*pZW0MLg%a>"hy[Yݭ^?6Ĝ#sw3Wk"?[ )_2c^T>J|akm$5-4.7A>//a?a 1D֟i"0J`T} 1qBVvI)Kۆ}XF|k^$X8fű0&Y)04F^ƎJ9=D,6rluX~4a=VG"Z9v'''pxXqpc<f:je-Hn:Xc;QCռ.}u7UIU.ocGIe-a%y8O* UcXjcRUVҴVm*3 + .㚖YY75n\God5k֛{:(ZnU>EU!K~UW+T_bF-YKw+MŽwg]"AͼB+i+F?o漢NʧU%Ae]GkE-HVq@e<-)V%ZlB[~\qvA/nhvs2tYC\\sMa"u񵁴;6lO{OWIO0h +*?hS88] drC +RaYc +l e>f!-af 15erS҆6žV-!~*]46,`X6ژd jO5>87<ה^`/5 +5dJ iv4-=sr/S1O uyLy>{"Jz}=>jD5|*Ŷ~̞}}?@"B4?1>Ȕmjβ0gzd'y̶xgyEf[R#,#[E$[wX'Z'D&j{~.븾(emytպ աh|_>ЪQT֠0lGG#:1:Ñ5͑X5Q5QX=ޣNx;qmHw9iui6ZTA VBQ-/7ڬ'+͎}Ƙk7ψl339f1>b&.)z\11K}GGWnw C][.ށ\>/^ڞ@jT +n1,ɈRzREY&xMO7wl\R;K?<3 7H;RE,*dQ`XCP +AYq%**(`eܠZ"S9k[w˨ڱ3gTghmi3p{~3} I |j%NLᝐءxROS㜚8t @\g͖"2ijO,CebzK^z*'M`Hզ|R "]B`7,3<kr@;:&: M;qNmTJtj"tF Ir ;g4~N{fĬޒ=XrCYY#]'dqKLLH$edx'tcuzc.hc\mlFD_i<1gDmT&܀Rh4daB4X<1$'Fs"b5./=4^7MĘrMѦRmJ3T5Ԩ0m {}x?e8^ӖM/eD&勘m/1I*//U$G[<# rF6T ;XD;oH"@"O-qe/3xqxqrrjZv!^=W-3..l9P%5" ") w _*BJ+35o6ȣ`y4q5qֱDҍۆE`"/e^\d,~/m&Qpa{6x>!i&Dcylf=0G .V[+J݌x^d74=NYC5h*f {I{F ife+yPr=Ev=CA6ٓv{жُ]w +4ֱRS7n&zfm"jJP=r2 _@;CKM:dr?1)qW.X)RAͅo ho2..йk%Q0aaLnL3OF\}ġx <Ϣ/_zrv=t?9t>@#+f +K<%)&~5̒ǻvV5: ʵ\Mg||l''r!w v\\NsunIHB@nJ-v6UueĺJ6mZM6iNl|;Gw<9aǙq?m߻lIDkzYk}Y5ӂS +N*XVyq\ǒ؉cb M1La^ǜt `JSLH!{qO!K +58MI}N$plS%`1 [Z1̤!naS#L[DY]Ø5l`]D D8?N` =ekE\-y9Gˌxf$2Ոd:tc:ˋCY~Lfwb"rb8HγA8*r^`wП?BolYz_ [E>OmY$YءlV+T~T!VXF(jhQzH.> O ŋB)St[*zS=WtK^ +]rnX({>WvQ[y2FLb#B"&bU s2dP+{̓.2h^QU*[VӏU-%Uޝ ]AIUIW= y0)O+0VK"baZڕ!kڨ궶u+aYzBk=/z/]r_[]OmྍH)JbgK|F*0d߆~{&Eݦ;[l7kvf^m=m6״ڪk+$9+?O$G*K,#Nz5'hc<'Stt"XS許vVjn6;[uMΠٯw;Gu)}3w:q^v~MWx_km:]٪duPqТy snAw*7 eJo}vJ z׺:]IW$ik2:T:鬮7{ڿk˪?ӕ9'_!z=mF1ND=Dt*<|͛ݎo<^kUhjGWylpRgBo,vyV%umMIoĝ ?nYktɺ7}|r܎ڥҰH + +QG^ԄhrNdGa[(ң0DHހ~gErHFr,Ls["_~&܍Тi-Xb0L:QZl")PbEc4i?eC("Cq\iKob.uE(PNkRL}HAI8 L1r(,6q^8/R/3óI&=4} xd\" "1N0uH= `W&Eh3A`O  Zɜ&$"/4 4sGL8=~|(1{x{a>7cޏvQ}"kbkre 6ScMfoC]BV8vnr>2 %Òyb=65W&q+_%sqq#$fxuN[Z^!+yɹ\ tT7,B2ed$dM2!@$@E@6AieX"XA?0Qp/N|a&rR|z'oeyyȥctSM'fzX-cSa9?c+HH>>H:.wpuu 4dAr??F` `2(EUh҆6씿e/?A)usJC(֟H8wo^q;,xٯy; /љ$Inn1 8ߎ.B]Ъ+-D^D߽2{{xdW /8>| nαF|2 K?NUq?N~~&z|\sC4=_#m\p%ÕWvvYC#IF>*q*60N##d#TyoqGi<W +\YG zNG;Cw,~rq\DO5pWsVýS_8>T>a#A-#Y6ӧdbW<)*xji3Z鄧_c .$ r71_*8]p)@䔑Q<&=2x\Lץv+FDjHe=X<[λ>|Y:*P2ɢEH*']1-Ai .-Ylj٦y`Z"3M 5=*5Rm:&UAoTӧR|{#Y f7p5^gg+ɂ2α*b}bKsh-rV%3.YjJMxTRTR^WJBn+U3sJO-wON~4JZ#)2J2+.Nv)3KfRmQ̍Ts1/TW(eCÚW5M^mm^w:$& DoV( q&$8ؔjKʒj=r4muڦu[Ee:.ײMHN.3ߺ0³tkF?+r _A}Af$PJ6+][fsJm.V*[ekOuslKN[>ӶYk5-iHMMzMMI$|Oz7lw :88tf5K-Z6Z< RRESi j#W(2:* َ, \ñ`w6m~O'_U֔6!!k< + zyқE@+@-,8MR.ٱ˙ul}, HsV؝ )Ύdg!ѹʐܠd=ePc3[zk0y ;gϐ%+e94z0gHRwNЌ\U#rwMb$O$xb$(q&Ɠj#= MEe}gQE]aYDDFVHpv\A0PD״QcDMt14wu5%q4vlTin?kN{3 5"xd"k@2\9<ۭ7`ZેXgK2Q5x@c5;HP4rh.HKޮ +:ޙW +IWsZb-3dTN,ⷣr^ySQR3NQ2~JɕS +v.RP*7) o復n9*qM{2f{p5kM*9Y$JcT}JdrV{zWhuwTF)*C)%J W*F%5]_KλR ;wB> y4HdDddᲯ᢭ba0j|VgUJDc©E7[\} .Wb N{ +ўV+XӀ\Ȣ=׈mRiib5qК(n#f @՟"0$^-k8@ +~J)^ &$ḵ0242m3|K$C.VjYkPRVr:gԣe#"$\/ZJt=H:)ށ og`x<VG~Ə5= P Eܶkк?ۛ^MZJk8oT $,d+.uSRx`7:8l[HVrс/uJbvJz} 5 Jg\8 /efeRrUc? I>8oϩKsi#9 U`_` +#?-#,JGbwR A'NC|.*h2HkEv8C2 L N?o ",}r^.."wHIpNe;\c3=|qi&~߂}p1K>K\W&׹ kwC"@x\ߣ%CEBD_@)T7f¹ {FL|RED>ׇ>}ԅWs}6.:F7QѺ(+Q/U[>Ï ~|"B~~i>{~޷@'8?>ߪ1'x r9&y/~_EoC^l)AxN6гqp~mp\TOb g0<#ᙠz%ku9PG0j5AjrCƽ|뻩.an,b9\o4x5 KOqfsWНfkUh'tzs ܏pEn +"D3KᙈSᘁ};󴖺?Vӧmj*i3x*%Sg>?{CbM69M\k*SN +kSFFLf:> +١އ8 (V +լ-ʎ+uoJYTdUy*/|rç)'bʎR%_R,J,V9ͶWefDSbYM`fjCXφxVN~WZV/ES,gYQV &{heY'(:EDbԘJ)֬j͌YcVjz CSG5;%D_6$D1$ Cl3Ŭ>_onQ0e TF\lCG*m8 7et}(BnrMm#mJ۔Ph-,R@PBk (B(yȔL M(l,{Bs?53֧%M[©1g~f:%?O2\HdE)!R:X*Sc˜,e Ԙڍ68\+2UjLc-rMslR-ӸZj/jioƔUcw)ɧ > N]d{m7T#e Rs͉Rl1),Yj%O˳C-2:(Ri-stdyBK5oђ/hIjb - FOӊ4YLBL@-p#߇gRh"6kYS\YͱZ,knʰV[STl=ٺX7XWi j|56-%&S%:mf> ϣ9H!+R Y0 5`$1T$[/_ɵ [dْ-MͰekihتdK7&k YZET U/د ?*yHjxO -8Kt$PI> +"[AH.qVI1I=R b{dS%jRUc jQ/T"+dp&XOТ?KX5 8;\dpM ;sԂlJ+HR`IxG:2$a(G(! wLA2@B+ڥF|+{S+oKHO@=LYRrOXk@%({#=Hd'5"ᵺC{뎒A%m"vՅr!\ZDXDIi-mkV9; >7m-2݁T _Tâ0Ca=9Ap + ާnI\r<UnYcEnh/L5%b7coC xz8apX<8OQSx +s/|O<{4vb&^y ~`_ׯ7ax#R Fh:>#_Ы9.2EɕWz4`B-hTAu\EA|˕ : +* +K< }$rNNX:xD[@[]G\EK\E|," ɧ1~Ï ~fxN24'w@5Pc$(-߄k[:KLc>i3b# Fb-:wbE= Fn +ձ ؃7s*up-:݁Kovћ;~8 GlBD)+N?c uYc8zw5Y'+BѴS6: :0FGo>H_X,a#"8F1v=&rt4LrYDW.f_qYg5'HbHO6Q/euz7 r(#0311p rȱ +Fu5nS][n鱶vm?zMH4Mvlz2O}SO3mpXwkYZҜQE~]y-:c,/},Oc_B#‰)xR"|x{<5䃣Ȼ 1iOj(_jHnU1x8/{H%d<$#oq3Gc;fc34Ɨ;J] LlV{bԚ4"_Ҵʛt\IU>lp?#Ք(o6f6PU\FcTښ (  Ķa;j8lBkF^[Fn9UX"1f*p̙GLFs~,'Kal䭚$`Y:x|_k5UodK2TQnp: T38[MySsĔ5YF|VҊUjoRUk?S92 ɻ@+h + WJ+"UJP,۪Bwņ2rܻfa#=mlsVR\WuKIUBXw%>2O}2|{x+j|Ɏ4!K Ѳ4$*k6UixKX-J);XxU".)LuߔQSv}pϿfU^o 쇯4אZ4e6=|JnU?Y m*P\~>E(uTF+ub&R_}.5q?^no{H~2+JQm)*S 4 +``x.[fЁkG-"P'\${]NKÝ?&)!d/AW +P?AUpFǼ QI쟤Oc}w > +lb-K膟%Hwt02࢛OT141h4r < =۝&àmൊK9W o5b ~BC,p} i Ce!3vsx|O=fV3i>TO.:ɒ x {n6L;h3DX` v},d"/%8L=9L"qcه%Q:sE̎QN +[\SŐ+kG(K| +1q=]݀yC'#ipH~B?dOˇ6wc?Yؾ xc۫ƑK)9~?Ci چz;>9z4F"ӱ|"XaM.c'~Gc;~l?BadO6CވD,) ֡4)-ODJnbء,w)je?6XO~4~a 7#@[ +pRXN> ;P NV7>ӱ=]Y+?G>`קSt.u#WN _8|Ueb`d)'QwhY}b +v%"ٝeT5#zG>D_а9tg,qZ#i}b@qe\E`U:X! +oyT%DgNvh?mҏhݖ@;tZf9˩/ ^kë`mchVCꌚjZ$j5G y )tQ솻ke5UZUZTe,e8!奿!=Gଁ8ȿ;Q7;h?| dՐ,r +TcWRN[*PinJrg8wl+UIydTv~UY7QH{Y\2Vc2A3_>!WA ,Pea Tj+SFvPdk5l=tCm@Vre6*ӶKd.846ݔ9/LjFl7`f11@Ԋ|J{ re/VqI9*tT)`une;)1OoXԒJ)yRIP|R EX% iua^x\tԎQ#m 9QY9^AE*4)ҢKcwS^e|Rbq| _Z q;FrB;-Rpt SBdB`5uY,h]`/KC<pwT|5乂=Ĝ˩;W"8ڦN>/4gN!V.>T}|l-FP#&2b@.G[ ^ap|bVgG Ar!v>} I>@`Vgt/x""5s5.!ĜHG|}bǷ{\}\`XNK1zy̍s;@.0z^U5.ƅ: c׼шe7~jgq .hσi@*9y?`n\&9PoRKaY.;ܳ2FL;8;jqXHYӷ^75HO^g]c_?J 6 AE?a~ZSt jST8sXӴ؂B^eCz6+ =%}ɋ^Եo||>9:ghO#>R>d;x?Nu?.qH_/и)l/ל):S4=x[5'`jF8Ē O!>;߰ќÏlYg؂NI/t_ [c|EJzW^܆ ^5FW48ZM˕bqXCK61bb&x86{w~]`e))XGavggAwc3hYR'c7~ďLw0@&mrlb[x7zpF6~=b7}]q%c-/dbGUحgЎ.p?#tT/VQR^?+ {1#"#9H~>0x Gu؝{XxrZ%tA'좫;N#>8d嵜^rJNfa?ʻ1 د#N[3 Oc̶i YoNu1}ܾ1͸_ +]ϱp$a=l2_d-ܶ-G3oyPfT_G?#E.L3 +xl8I'$JUKQ%ʧx֏! M7ykw"v/[Ffg=YGMjI Qqp5Z 4*RW BM%RQTPbzT>εCHlB#jiN@|!YT\q0%N\YƃR&z9φHN' +x>=Gq"%j[ D@~4:\[?A{H"5=iM LV y$h&yTdJojSiƛzmڦ̠:*Wy]\ hqoA`(A1{>MCL ֔2Z%(wX&e0@3)| gn˼\iJ1)ɼ_Ǖ[9B*>s p_,n5` <%YO}A-Ej1 2nPs9x&36PaJTj]c,u(Q\1-rtX%K#߫q{^aW>1^m!l/I^JA(9= @*;)1)BcSө1 ٝy9')9]Ut6+¹XÝ= +snVHn Kਃ\ PN88Dz䎏<=@E16F JhcIMjkڔmdB6hgҦNFљdj\N<>pT#A13&d=O)0IFKM/zcǫaaaf>ΐlTcu#'-c%p"^yfszH2WKKN#\E=ìnE#?j> m= я2QluG>xu~Rk+)'y1V%%G.3[vm v V+Xd[7A S  v1. +^bNa=o Kڅ0{4Z/C@=x MMtS'D,w;jxA-"`!fƌ.1VPfAq h=#187t)CWi9\u}?51H] TH;\ G+ف۹@HфOyTE!2VcvImLvPPbt;Fۨ,h)-Փg/14P<IkʹlTވ*8G!Q$, "{81t?&4(A^mM]!I+8c;1go2q*k*<{*ƕe0r=&U1>h7!x p_+4)H)~x}޶k.7!uP8WcSVE8c&(D,!>J%WB]Y,,q?.rPoS3o/Nʅ;cNOkRhipZRs @ze#%Q> ʹ\6W W Va BgA B)e} zc#CObũ |m,΀S"}Kŗ)EO\e;*(1>dX?>aǏQe-9~%gU~u*1pT._gpYb}G.c>5~Ǐǯ5%~J>NӸl/4I + ^c|gS`_d|jmvG} دj(_F9L:i%>) /JD.KKFo oC__@~ ̀iPn/bÝ5E ^a5HF$PMi vFݾi0 LzBI9],jPQlcZ? :UOq.N:GXmgj4ي//iwgGCn b8Z -th3?xV%mkWzV\b6o1@Z5v\B):G̅rbvskv<bnm$6kPr(wm˝1@$ .~fT?Cb9_,sP.3G4![b:tfc .ahO'#S.XzgXCs^LIu;r6{}O}r:wae6VZgQ-_?7odx+3w`&Ynzc:(i:sD ; e_@NXS#67(6'qU|]؞ዧyL:xh= w0sMjWS Lޣ!hʙ?<WMM5I:o y^ec7| F@a[\}o +=v]@'%u߫eK</4p''8?׿DC;m'mϯۙQIJP4%Yz\ '%.#me1˽]i6> ^%x-q/^p˼:x =0tu=#q>9,Gq$Gһ Si*НhA1_ˏMd=Ow2Wj)ݶ<,.pe=*Vue{%ghIŭ1|XGv٭dwhf)_-׳m Z޸Ndn%{)ˬ}xw.Nz2Md(r'ĵ8Dkq*wՕrd?=.>׍.3,&[nIߚ6.m vxy $t2B!]EvUC 9Xgshpflb Du^i_+'  Bȇ+#*uq9RWNn1vٍ8|Y{qw_ IֺzFU2TM8(&X%OϢ +z,nqd K!sU| Rܒ@ʟ Lgl/|=xY\Jan3y-ƵgE;ϵhJl&Q3wYj;Y6س:uB@LrC;ťU/ew3},uePxԨjF%u|%N8zr&,ӻ%+\BEk sGUk2r\b|ZW"15Uzh*WESd}6EoAѶ*b~my3}/\686epD%Z|quExLUUM1K3*HW+2 +ţPnٻq>Ū\X`Զymzg5>79C*gDTᛀoRLIuLi9FQ򘐳u+DK*6'x]8Ν8uGU_a$dwIvͽwwsV·5a!O;٘goWo$h2Kh_CFQLqeIYRhiTLk%L{ŚqT,b(T-k#~J ˊ +|dJ5]k-9JTVZ%ɵvXl+dmmѶW2G%rR Y$5sZ  `0wg45 B +KAI^8Rg|Cɳ{%^!6GXM_2Hc:+ a 9 9luEp1P S(%wInQ؜buISYr19Òl6Iu!)NE'ñw y !<:sوa8zśc m>2HVYlpK%+)J%?`9Uv7 AN$*e8t%?}jXJJe<+Tg3f5G"V ` 6><]K7蠰o릌?>r15 Ľ(8]W7ܤKά,0 3tU;`y! @]䖬ƮAz=PGxx憳s;K~L5TиhbavZ*C83ʇu4uģ#,M,5,*5a0:T2Aȿ ]nd- +Ssp3 p0S"4:vhPXQNse׬g2gQad$!#|އ]m^ >tq"xf1"65 + jiPiɵdXyс/:uptpghaQlj~QMИAE~3թaA8={;Ϭצ$yASNpĤ> C2g2)tuĢ9:U5.nZ<(5"l]ߏ豄e,З/#C%1SE8+E>l5x>Hܡr&0<# +=Nƈ4aQ~bލ (=b)(ʯ6z8k"gތn;gpbÜ6PE1Њ'ODs3A݄/6bⱎ>vl3ܻ:A>Ow6U3F}۪NxMLv؝$cx<2JNl%/|&95|\fyWƹASmp7@|r8;@lG8POB>AG<&?vr+F-YSC{6-CVJ- /JPO($qWg%Xtm:|25fI x]U]~KEjrOcUx!G~'+ϲ[ !6)8OǓ O~&:? .i*h`;aݣU ik M%d e=2ܣńlE‡5ȍ95RFx i#p QZj|m<9ܠ xKrޖ6%[kW;($ɼjda\( rWRdD=^#E݌+8O{ڋ"Bk|HL߬+ɩѩ L蟎L8r(@~ ː 3+!" o&K:h}Iz"3XOh_ۯ6:6S+h&&q$.<8ʑUG=5+pT!*+HO+%Ul|0;9:6c.݉y%&Qr#JߨVj/p 7]3Zu^2C}(TB+tT( v6REBq?4, +gduatX\qD$ \1#MѪScVciS5.uZS[MҴq[5uS퓏yf?sy{Ϲi >kpЃFVxى bf`FvQ)vm4ߪ j:pxxeOJ[ڃ3hZ4(3i7DJ2!^"Rk] .$ӧQm'i>[qpXX\ݰ`Y2@HM,gNƈ8Ab5?6h(cDntݘ.6[ ]$D'V@ 2CsĤ{p xd*KY@@F`|A +h_bA4ϣ#~n9DjX$Ȝ),3ɯ@|ޏQd`n _{dc?a0d@cgnK(!b 2KPP-~D1YRג!>Nb_JY8ug(w|M&O~6v  R@">6r + 55GC|?$A#[mNۓDgdz٩(;&N:6< +#9+1/"~>1` Xt;%$(6)W~bâ#4TAA;(jL@᳝![2ߓX4qPC$ HK:q< +:⨾Xn9Dsc;Αѝ$?İd$KXN,)Z'X[_8z񒘭sM5Duȉn{_JYob=>48yM><+93C<9hb̘M"IP:_Lg?c:?sВL8$rH&yng$ qpyP3#C'Tshnrpݜ 77{H+ a'/"|;isyX>a 3޸\$ -:EgKg3ijH4/e1[eq&8&]!M$Lfɼd4 C\;ςxfjJv#R+g#[5z +=Cy%\0ۓ3 ]{κ@Yhky8V0ehg{2]ݵKQV@>џ=H#&@pOL6z1"{R9&bqcUڴ>3WvLD6yS) ødJٓR>󣡸bnB곀!Ap="N&G馐OlkٯqkfniFH3|T3c!Nl'hQƢe7yxh\Hj ݐ$fsNT{WT1K4USp ]ž#<6vZBqi`kyΙpNap{(2Vf15|Mʀ(CuH` "m@b_RKٓň^>GF'ۜOX髸y(ps+;(,m@c}|=V#:Dd8ڞNд8*]@RC5(Aqy'f9DO?N=JDl]$RS%v]i=I;CrbփR?,J^h=XVa9 NZ\'\|[\A=]~j<3}~A֐0{xD#:&и+9%iZz2Z۴mסc]>x=sd~ ISK 6tӣnj7aeS=;_sΫ|i _YoK._Uj 7}_fع={Cg<'?/_Sq^~ۛsk#+ƾr- ֑v$ż|{> $(@"%`dt()~3?SjG+=^S,FeJw&kWJȲYU 1|٭ُ8WS}P (.( 5-%ӽ#˕O{0?rD #455B"Qh(,0J4<"W镯JJ*T2d؈ƌ8Ͻ,\ڛ,[r5B$Kh0*\Ϝx Ɨ\W]oo޼u6ܽ{1Oug]+SƳW}ɰʫ;F-s;&|U;eG}T3~EC 94АCC 94АúGrؐܯW|gWQ=C*+]~Slb߸}v?vʗ[=Wis&䰉o}eWT^:2lũφ?qQ{w:趺'^د4㨝=gztN;A;HDa $,aMT6u\lk @ le kH$@ bلgpIny+k_V+?m0 VM0-o`L)E`Է:7T4vPk=Ӫcd (@2\Xc\jl;׃Z?0v:ZKkۭY(Ϙmh[_f.tH澣訾QlnU/C!85h rԺgz +e [Aɯ3tufy7ޘ MG$ eѹE]Wmv|Sk$ۥ;5 1\aꜛo -EF}o"' +1<,-ǡ)Wxѩ=ޭ[ր +B@WCYI7Jcx͛7j[0 [&Pm0y.{FXCm` VM֠1!"ej7ifƗ9=ѧ,b3G_% ْcjD8 p`y9۹A?ZbhrBׯx/Ι1{rY!P şkK"*kEvٽ^Ah@D״zÊE9k)&qqtRej*[`P<},fjϗ*T3~Yzh.7~0$IT,y:m2وKrKS4)ktA0A-j/q{͚x)m[γxڧnYU7†RJR=x>)mO yZP`Z̈́-.%C-FߎnuE(;'lyzDm1E,~22Yn@,`8qPSiW+ɳΗ[ L&$LG?6Đzh¾ab2e<Ƌݽv:Fkf_:G[/~PPNOMC6bB dPǏz :͝%|76\!6>ñܒvIvD02V&J'kRhk4>b48a.# +:UΜZb]0,X 1 lĐ-h~Gޥb1aXb;eS?I+Ssquq͎-݁,7?+ +Id1Đg5]U^34.iFGWڦPv{K8[ M6zbOnR9!1*~*Ǘfگ^g;X +ǬICg<=_[q 6<[ܛH"<ݐU^81֧{/Cu7/̙FŖOӢa\;=2N*GfsAѝ@ }˕Mim`DKc*/_K'c"A[|;S܋+!{}(GwMs(:>!_HDS*]Lfs&SAIG,S=DV$gmp9XS]vxeǽo://da<:svf,((JoRB I&CE&H ЂD MRB(BJ" |3gū=hQMiy &hǯ躇/#iח L8N|AHŏ05vԧ=2S}9r9%2P>gfI쿢21i,:8c{}M|mŭlA];m]ӌ +;pH*TePPPAݚNN:WmWI&>Vh_% ?|-}<%+Sr`s>2X8v|PY%`-{KsĀ9;d dP Gwni8ؿᨊ״P65 Ա&3m h&$@qw`}!IIR ]U !H#eA YNƿ aMGDOO.W>VRE"Wƙ_'m_ _ _ _ φmp10 +@Y56IM bOEGǔ=J ` +7Y1~~02đQ*ɹO p[_'6 #&b#YyVc}a8.'pYi!EIՃ}ݡ:BJfo{6in8{ڧE=[1l>|쀒4?13*Gr#u2=fgxgwsڥ X% B +b {ZHJ,$J}B[kG8ϷYnVx[󝨞;%C_Vɐ Bb(1ԅG.l…>- s_zrʄ9Pȫ_Ra %%njE'F ` J۟ J;!1i*Vh3-4aɳbTMhͭQ^W.t1soENMX[CJbVKt'>l|RJeù, =Yܰ0sԏJg JgC EY(PݷVy|Y1'1\Ɔ ='m%ڼ&r Σ#ZO+= d r>Y} 6b((Poл0+Lh8.Y4H--ka495lj\i5^~>/Afg1[zD @/Wݳ\Y䠑+c׾tHӤUR3BV~7l~lRo[^MFt]ctPDviLA ~ϫމ4U75ri}geofx RcǔJKCĹ1^xWꀳc=dmbBEv!SzfECgq 9e& rov  WȱŭALً#.cC}h3:'Ty\r;OKFԜHER<`uJm: l=! r{e'gʉg.Y2);}9Z:6Xap\۾K>{#Km[¦c?HUU^L7McUJܒ_[pY yn,h:}ԽFE9ͣHӖ1)|իK-_9ܶxu"u VsD'/|+\8e)%:(kD) ԉHCv`etJ\4 -bA ;`;`}1 %۷Ȇ{AA! +Ղᔞ7w+C8z֪QQZnh(PDM D CF !@ B F$`,D=ǧ΅^svt?=&b2|:d䊺- +2ɧ +wm]Rt ! \,~M; q0KMLlXgCu1{nT=ܬ \fS ;Sb4WҲ_u-@ԍ26As t1zo cqQ]!wux'L@Dv"YAdW"_d-^K.fᨥ8f,˙ ʧ-+J4}4|4|41d#tĐqHȏC5-H_z@Y $EdgHPztj*qp}FAgÝ6 y>; Ce? Tl,~bh‹xSBjddIxpnnߚF/Asw]= !1d"|u%(XFRK\λ,3d*1?A8~6x 꽸=վJA3&߷up[e + ke=VJ0"sd;F=Q4I$GҊH1'} M@` ",@U c +'_ fg=4נ5(+B!b(A \PeY{r _&?+NFpG e񝢤Ik\9..1%2jGLDhZ7CbU EjTϯ՘c +_ +\jSfC+2}؎DImR@uuFs{Dr{1MG(> UVk1fo)BQ>+-vżv +QHaȉ<#T~#FӺj?@ mUbel{DOPrq%z{uA4'q0#sY<~>H` &!1wS{j%wM&/ۭܞw:৻]S: 4rv.#7Š 6C9oF-zg fؐq$Ɉ3E0[Ȫ4yD A05pݏ&کo{vJRYޓǦ8MBǸ#vĵ-4u-W7EZSdti@ ~@9G&(@ E+`Ugvjv%f+j*)";M@|=7 P9{yzja mn[Pϗ&˘h%MEX8[OBpbx,lJN{[ӻ76>$8ϱ3NX;U[*"#@M=$@@! !%aHHpDQGiժHeQos{\[ZAkO5:z٠kg`x) L"5;;څ(5hUw܅U/?a5gIiBxUW~ƃC[kn +=Z'plX/\c0v+_G;pж?$ttKEE>Qe`*mmw;@@ֈp -@ ۃ-:YE],ܐ<>q[T:fS ؒv$}X {]g0pƶszp -Y X<>)(m!Ukd\Sm(5|7{p  bR3F^GЊVkv2FP-b$e!RQL\#P? / $Đ?CaC j~^ťd+XRëKYp~bx7fyHluƴX儏lPm#b o4Đ !P\ C1OXKN^!dQqEDV05//ºPqtQ;$W6 #Đ5dhAY;Be(Ƽ'EWri R =)Zu0;.ĒRPB?'ڨ*S E1"~6,u߷Y庺/lJ|ք%VTOӈ-T8-[ܝT$JfJ;q\i+ cDʆ uA`[gg6v%q |W5Ɉo$wud򯷤8鍲dƵ :z (1"b͝ Fc&8o?qBDUq艝r +;{."ɪWdd"[O (GUhFCZ-N5[vry70P2%M1͢ڵQz.i4%u#;5HjR dPOmگE6 ^p4M`S9f Qra֍<USXը )e*b}ni jOPv+}6&/NO]]N 2D,oGe$^*IF!LVN]-lPY\15ӻ{U␵[Wf<2qP9 91W̤ #G,W+&qlWlP_CVM:;?kreyq#(shz8VՓKx99\6̖19aee[7  +b`j| :;: ;lJYۏe]f^>1F05⁅-|6g]q< iR*j2 c2&3rS,@-j=!=X󏎯4.L/t0sT ē8ԜoZ,26$w63=O#lv5t%wծY6`K1`!A`.h?/@b!,<6NQ GUB{2bhx9BehW6lD'&p, ;X:=8ق-xۀrV⦪:ԋa +lژ[4@kW؏CϦpL,mล[YX8[w%/}Jh͐vpDkCU)2O@Zb$M8Z`YSI\'-a.bEs)z:K} UwmHW6dևb !l|f$C"WDZ-,b؛yoA0~{{!T+7Іܣ{nuΦ@ a3$&KB k.p6x)s|otuOѷc#%wBGJ 4͵l244"ou`DG-&ffљٜҙ ӉӉ]oOcxUO*9Ro ?t4hLtR8GmQ<,n6@.pl*+~ռ.MՍE+oG(.D D o eYgx[RGmT7-JHaH97xF \qB4ID4-{Q ؛UlXeU+kb?;v;w!"r:UJyM<˦2ư+ՒߒDєΖ -])W|%n^p] +`~ylPfxG_>#ޗޔP& +qrNhM|WzW.N^OkmDy(EX4Xiv( Vu^ӵo1IǜY5|/+tt!MK)b>_ +`) 4#mJPcSTCHQT E]94i;᜼Pْ%7g2O9?\-nR))'tבZymti 8YPl`jP.iLgQhg0jT2&!ߩM(*W,b[-'xd^_$+8+c::.*j`(zH.i/4yThDvOMjUk9AnbJ~L) +hW+NJ\o ,eCFiȏPos^l{`OgObﴣIq^NW k.f +X4#xQLr`&'ѡ +(&Bs˝L^ x8<ᏺNiaNN8mxmVlʊj;DŽ=cqߌ+5dC_TRʆMϣo]mTʸkEI729=ބB^_|Q}oT;Yљm !h!7fmạ-3VƂ.C}A;[/*՜SnK%uGeF )mm knDC̗ xߠFyUmm{8N.z^ fy ܆Q1=!էzk.todV4N +DhJጢ.*o\l`} !qQV}K^9z|JڙZ/'CCR;XJYL~ F]k+I(\BDV +p'6o_K 21n){AҜ>ƥ[,>=ё,jjnNkɧ]h%EQ$#2Y X+IV|1qx`o֍S E 2\9#g<똃!׼|pehB}c\Q{uo-+^[s_٦M9N8" ~kN}+Yg풾VgcEYIa 㙳3nosqyoSsؿ;{n>[}-_7~Kz/N;Ώ$VUdj#g&F0c-sIvs$۹$Dx0p+bi ҽbοkZUŏ2<(J:4C< 0QW;`dLY ^a_ #VV R)FN" ~AzԴc}-I_a6BfĀ96N0pVP-i7c&y!j)Nxc2!B%5-Ri1vH`TA8Ba7(:@݄msK[)(&acc G#)`B1B"uHu_Q,%9;[@^5{ GLih ]#CLևYd]P:'kX B!< IQ|q !RX:a<CN +*F8vJzPԁ6t4C-u` +K-PDIqIK!|V0܃P5yDQB1mDڛ fP* в@+ 2K!i +OG(w@ [y7M>QʔC:31 h!Rq'Uwi&H!ŝx?*VBXىSOK_gg͚a eO xޣ +24p1Ι f2>0 +30b+ІJR)INYZB4$m݀X!^qTv᤮<(:`]M,VW(']& [|c'2_3~ept%[ ÍPo#A6{6?C#Z"Ӕ rZJU%/w_#\Ziy=$S.&g FՁ+_!p7Ď\=oNݭ|>~1u[[Pәln &z(jI9-)-Kz%DUO󵁨j e.m:NzhRfTq'bhbKic0j2[/JHDIaaXĐxҜif^oo6A.:@zڷ[ͷ, + % OzM†p<72ZkĊRF U^ҟZv]]L.Ek(2\5Fuh ݧv|mgGoC}]s2f)ST.\UA?[$KvK)8 :_lY dõ@7ZCk? +m#vL9sw %t]awGЄv,МܑҘq=üfuD0#rQduj(T5Huu]7wj6Lۚ(+CË)(jAWLc ==EՖh17D üUuVi\5X k-z#=b|/'&flL?nf ̬ -Rn^]j$v`$zaRfGL*j  M@J(V5P VA4?vk |з,F^d˚FERsƃ(кHe8"%D|9N='hL8d 6HWJU %zN_xV.H4Xqwyy֜.^?13p(BcҨGhjFhtF=I1˵]}0IVʵ5 :#/ȽySmقiNվv(sq-6gz,*9 +lUT+br9V\g)jgwe7i@F[db; {ӵs2*o{)G/||y y$-5k9ܢ;^Z ]&| +ߣ)Z6YY>O_>~lq#1!ˮ%$ԧs.c:Hv:#H8 H[I=-2m)gw fp +N1[gwk{wgd N$:YA%`, U+s:)%ŒJ$BBQZS>13f1f u2#X"#"-*=I=.O7]y?>s>?|!(` 02 -UXeVú-?F׽y.pپ=|6vo7ps)2  $C:5f(0Yq ua`6{8:lgv.N.py.E@fk"F{@R4.BCpRgA"BPzaMw!Paw)w[C·Hk :XAV; pۀxt7 0^c LFp࿠Ө71xȗ"' c<[nDi P\l!{rVn{H ZLTPEр@i&bH;~4f2~M RLCߑ&4D! 1W^g+0@H"1L"3,&~D+cD}:/a;E1^1CGoO4bu%0s==2hL&)3|tD1Ww_\w +> +sn]}edtsc Ym +nY ea,8Cɉqb!^)x/Kצ_ģ!l/Ԁ Rp;ixHC@^kL; 8Y$fR'>pɄ&m,#Äb+|UA6kz$w1:OmK+>h4ˌvpA= NԱ=&Eq.>L +Hbs,)AP&Y+DlF.P~PJxuTKԀ1j0E,a!ĭY +Iv@d@/yGwOeHÏn75S{Ǫ<?ѠzTQ9"K9 +Jxr ',WUUW~R:*嬎|Vwi.Gú_5-A v0b5ޖ۳~D{u@{|snu$F!ۜGO c˫%ɢ3I$EMF. k29!:5RsP AhilA`j%Vپp~p~mXPG>ب184aE^|VrV\Hj8VKE@Ҽkf)Ɛml2l5}_e>ְ{`Gvݮ lnDJbS+ <*/_!jSSeԪzAگ\QNr{ Wة +k[A(7Tb2Yl䢯k^>tCl&25d]dqE2.ѱ2#C_kOk;5 ?iX.|PhU6m=҉͍(B]>:&"TN)PrɹH-Ց.4r[cNՒ2:*r}@h6F?^r_G]vnz?{8\JW'(DFȠqaoa%Bӑ6-ZNRZHqԱsJ:ՅtuU.TVy?p>_v%\Tj煮 ;N&)K+M?Yxdo#+:zZU|FuT؂*l#}6C݉ ++ݾZ[ݮzGVV;u + 9U/,CrݵO'ϪJ.I5d*]U_+5ѕ 'oG9]\ 6Q 9TC.|~J;mmjw3LK-I^vbSŞM):<62S ǥgKUwH+Ziz:bBc3ivVW y14i?9;|+ۙ%9‚zmaFMɦԦݪe>HRXܰHx?  +/^x rߡx$T"wHsVcԋ5v/ R D B?xI! CDn\yrA=6B +?񇄃>;?ʣp}* A*P8Bx6kMB!8,I&Ւosq5tT&}oeυ#g `*CD5D[́ I˭xbˁ4M8!Z/EDW(FgÒ!ɍ/1$ToDU/ѠxX  C5͍4A<-@R˱8g\,*Q1,U}UJ Қ>^YC'ݕdϓoRzR?%ޗ5ˆ j"F1җAB!+k"GWE U_ƤO> +Sˎ*W]Fv5[v;S֑T6یه[ޕM`mYH1Y&v5rc~ʒI?&{b֥uGC7}lfCȑsq1cs`f`f3r5UMzPv?=Km{|}{%G3g#6Tde6AxI\셽D-c:W(  vn[ux+hgi=o9!+_0(<1 dOyX6o41# nyW.c(s8$l]6 -,] ݎ/r^aB6GLD>GcH"Ueb89` 'Yi +1Gm_jlZfkǶvUSoAYx1&ez; |NWӑ'%Fs& 9Ic ʹ\=vk=pO&a%RjCέ,b˘Yjz:d9a)D@!s)gLAƁlg +L`Ǫ#f']lnw|}>ȣK& U|t "znJa()5Y9U @#T4̧%@Zm),73lL}aw.ךpdQ!BQ%6!0I2<-6=LIRZT^":,ESF>o06 o1H4|9g +(_.:/:1_^<{'g8b;!|7-`27 '0JaBy{ps73ү.y-4=߅:Jzt[Յ:G +:wN6w'o"|]>\r +[r OR`,k99 +)[V;1sx啥mdt@CPT×&`;$8]?} +r.r3h킿G׺_zh1a>RB/On~ +rs:5+k>|YTW?sD@D@ }C' l,A NDQqߩ˸TCt,V¸pZZwQDE@73xcF{ ny(uqBwB%AT<1tXt8IpHK*NV㰑}8ez :Lw n& ޏ_m #kl +^Y&"@Gb| RO<>y([ca VZ&J.ę];A4 1\;8:dUwO6)#0(D/F\x(Rax 2QM/P&:38{ Z~%}a! f3J7\̲asppMܼ"j0|H?*b/|_wȨnPdїw.>^X Ɇb3Al63lSyps(a?\ܩXo|)QNq3< c>N%F|;${CB3 +:#ɢ$E#U 4$! h0H$u/AjH#Q`OFM5A/ ,? _W)`-"5%tAH^XwƂq Sul'uĀ  짤4pFhӜI$qҧ/lH'R<^;ң|vz$Y1`%QfC"R%՜QRk"8!]L&n'L'Ly c<$ >#dX[8 O'!w$@s#͌KvM +^H;I:M}:6}28 c?O2` F$Sim-y9AŞȈ,<9Wu* l6pəN?șJ?ΙLq| +_s? + '/E`=i +X@H6!G3=pq1!N 'k[cf_YѰhH%?Iފ$/c AtP +)&C6TTT'CB#)E6s\VHFīC⦌AC%-wɅ~ɍ7ҞWWʧAe쓢GY~W>%)ȱ dv_h2hC\&%hG+d ?dTeV(^K6U==?^Q?+镾(푿/1ߕ9sC1y5kJf +dlrU vB$ܨ}(_:WQ\R.Q>ʭ*}hXv[Ew+k;+UsΏʛGNe?ն}(=7j7y T +u^֨6F:}ZnMjpA6JsG)ίYv35zμ3  tGnln6zOVGGƧtQLfm^_9P_ɩȼS_J]-(go1?gE1Nzfˊ`!!Sܦ%sYq'\H:PZCⒺK[utZUeRZyS1Am.k.Ⱦ՜O[ng=g>+ͣnq R++QӡI|ThsH⠳L%#}52l:14Zmcֲr:YaV&+#!+#w^A<'.Ib/#U05WCZc$.vwbϵ4Sk *YM m *),vr ;? E +NoD0UB eX+W|/qKfG8#~}]Qg˸/ԥS+3KsJJ:bK:be{m%M̵`<7C0K `>—.ywóIYx4)p)1#g:|#zbRsy٭%)iuu٭̶ƶְɖІк2^0QT|/[d`W +6iaXQ#4L[ _u68qk߻v8$d02ψLojig4;z:'j:+Z6n2y$n98W;83^+)\?Y}M~ fLgV_!M>~WW$5Mce+'\=|o{GD&|M;=5OU9MX~{1}e)ӿ%|_jֻܾ7`{"\ =Bdm4SfnXQRלbV\{F{/{,G$=NNr['V%uŻ0缾xɋXv"ybVG_\"U"\|#z)\,|k|8)Y+>/\_x s~$Mkۡ@w߯۽g_^~X{`v +s?G"nW降2udrJ|41`nb飻vשO?T|pt3˺#/<=e\н0T%'P(.K 55>kfK`CA[K =QjctVY! >i3}Wn\ i|G%_<P` jF-ӳC#C +[ S79n&܂v Oaqb#26ǀ#,ppzP@tG pG*6yuMk$C4'Z$]$p˚Fd i#SK kr*[=}!( vE$]QBX>S6?G{X!ܖCZml&yB@h[kAD;N'.'?EC|A C%6 ;M Zi.PzDy5B0 xb 3\\Ҍ9'njcFCoaFBcC1_1ac#H4.!WB$OM$&YcE::ܷ}VPxku 6q 4l7_Slex!|<9@|`8@r``FDOPFHa$}&m ' Dj!~$% '_!o"ސ?G AS*=AC` +d?(v4g 큏^ +5Poq1~4vHd%;7uSꢻ(PZc^Q~}I@'.Y+Gѣ1q'oF,f@ -֐$b_aA*/!+zM_D%<,a>a>ֳKma=c=>;&n3[5K :Y(KS Z{S&]fsAf?8 ǯF384惘,ν_1۱;xbx7譼qqxWx= Ӊ '$ AHC%WZc1!7pUW&㹌{(Hʼοz9^+Ux>al~ģS3̗ۢΤcIm!xA]Ym9pM@TKAts}O1v Qvr-xr:)SK#=*M֑W>$;>.;|U֖D(i +Cw^SFfbm +9^2W n0+G{Sۓ44 "!.9͑q\ w_kWWW6˨O3e ?JbM DVs n6.p@]akBafG;&&A~V4{`>A;jg)Ujj}OiA`0EE 2@0srO5k jQ&O|mV]:#tbTѧ4_͍mf閳 v.qF_꙯] ogmӥjʦAǴLA ݐ*SJ5gj |#Cȴ & \4q~r#~$Th[xa6WRLeYBYW.W(e(fF*^K2%,}45%&jl~vʃ7n2?x#ѥm7կAUbTWr%Bqn:]+QUU:JӤKroTsU/ Y]̷9@g[Nh:iᔻG]is7=u(`Q{K}={am"_*dZIPYhUUI :ValWXY;(EJ!$T#U)ǎ aUlbY,Xб8Vڊeul˨ 2θ*` s>_=oAjfqf٭ AX:Fѳf܌VHG\,9u*LPrX}2.8S֗>`Q2rm-ӊ +6'M[1~r/}e?2 Ըw)5̘1ڒR=rE+埏O\{*q)sM[R:MBɒ%rJcKfŖRv}US~x"&XolG!CDN-~W:uoZzC>j5 6D2wSOOK=P>{YE1Yy+/:{fi乺őEgD0?aXF&[^՗fݯuwwͨȝwU6[h]oz>9hqI gTJZQfAx'מmҙeȨ '] \Ct1ԛgЭOe9OԎ`ͬ #:[S!uӂ_&?r/.~=gwp(эsDDgo=0}Oh+kNCr^yg3}4i01**qzL91Mˣ7F5l1\Rmu=,/4 dwPiuDlDf$;|Em"mR +q{2NԦibS;2t5Z͇m~h>T\GKeng`;Owv<#mb=tYgcT-Sףizu +u[-ۻ/[їտF՟'K2x\$CAwX*.U CvDV15ZrD2ļc}\ᢆL T <<. ; t Pw'W\5+WN OT(ha 1r"c \Oxܠvrdq\r1Uvb-Rye:,~!vD(L@fQ0!` +-e'x!'і;.8lXVm`X>nVT2P2e`/Fbd64L )(O 0t,܊+k;Xۀ3 +*\>Pv; L[ CfP9>r.PD(|;V +QEAyPu |9C}yeL.$E:τ3LbB+8̅sp _9}n ҆,~_偈w8SIYvOR5BR#\}6,9b>`w2')9ږ +'S!@| }5+f}Y;m7aعI햊HR{9b9vfj7+#ɶ!vTJ3LD8V-34sgR/4] y>P9yc󾠻y1ew f$쑤iDuiyb*aQx:kz7a=RR\ +Ev-\o^ <>{.ZdCf!#,Fdwf +,=E$A£g^rlSN*ݮU*%RY*ɴH +fMS iKGk kYP/NwA.2x@CsQ xc T/#unkC9Wan1'E;Lj}:T+Y2|Fcreymv%G'?_(4W*|J){'tRSe6TAA1dd/4RkD9%qgy:`Z;`|;–tqZI匨Ē,B-ϒi5F&$iLS&v 5":D3dN]I>>Pޘix\=&Op[߲}V}صMkkhᶊdh,FY $:!ØsYiK67 ϳXLVFKxX-n`wnh|pUCa{Z˯fӲRGxAH)TyfK\k`-}94H6R8Z0<`&3Geܳp8c!sgЀȕ5-ͤc4l#sjDV!P&:zYvv͒`Q4>5b$Z1NIַ"W x,XxP0=/ݯ>]:gvu-ub7ZNBmHm #Nv LOBk))Y*rEEr*H6'?:"!:&$4T3%;vNp/p?S߷ͷgQkMgcv;)1V;Mk㦓Z%V0,s tᝓ|G6'+0`pA#\_^*lZiʬ1޸ 8ڹ]QMiѩCBBH I,@a ""KtXc[[;j設N]c((:V*(ygI?]b.&S l Zϩ , dqxWC^Mdg Zm !*q2>N1<gtsqCKC+_5ûeRHD=FzZB@ȆS|x Yx~{+A7 $\3#bE:"40Kb*Q"G*r,ɰB(Z_I/A#Gf4' Q̹IHc2/ˁo:6CQCS!&L0ZDH ᅡ<f>L&2QC3fB>>GۭToREHϤ$37@`%BB0$0EPZf( 3ꇲ_TOx+BoӳC_~ %>Q2J=ăLԀLV0_ L!0Mg78੐OBtxER AjA6П`To  9Qrb4 + q##BD9} + <ȶp!UkؐeCq6&o1ǀ1i3:y`A%@# .,&1Li0+_Q0.UbZ-6}99r0qIe~iCe}KPcteN2Db\J. H`͉~j3l}bY3`YȺJWFm{;}}^!ݔfeOJNm-COSy2oHa"`xՅm83)/KYu;u+uvfi;tҪ7r6;[ Ο tm#Kn/s>Jo5#baJ!(ְG +Eez=L7_OZx͵*ӵ&JFsG6s{ns>nyܒyr&פS# ͹04BאGS zpo0EfΎ ퟕ-Ko䨯$tfWX۳[\Zhq6{L&ǩ펓9UZGcnsj}ywGm|0NG 14._0/2&I'd2^y&Wyft{tr\ggQFgYFgDcGmq]7mx恂Kw28b$-5$~SP7/L+^O|br¸p~!m=M4sDqEƑ9uKr.ϫ)\W]]ї}E_{WyFMDg":vFcd[Tgdk7QÑMя + :3 Q@< 3 R3^Lb*I^LU#)>*"\cot[J72MFVYzvkiv_t-{(=]yŨ-Q1TC C?΁V +SheR5[ swi>e05='撚"ڙӼv Wͩs\=w[sOqu ~(⣰B>"--f5<_F#Wi=7^7-ܩ= +UB)\3% A$)7#|H7_f~:ev+%nk{ msCs"OԤG%s(EV.dK I,M/OTUFy\!'H%c%DD6$)MsYK>~*1rS +z{㫻Nmo) +9XGqT3uܔ,H * qDy\BW;"uP>'W&Dq CP2$O*#V\pFMpvYo핾֍}d|OI2Rb*LT(b*PSTs*JKy/|#&sc At -%tR)X%/ą*daf d@Z2*$L JqI,R$Gycçn6e-.;[Bg)^z0IưR>ZZTCjZxĚKQvp X G#V~d@9LT> p{X#V}2z|w)v?N"rh!-etPSlmPZG#NP)X ncG?@=ހ׭@7hk{"ڱIW=.폹tI F +N&JC}G}^,\@~t7V a@77] Vpw7A=IÎd߹lNm_{P[$4ߛ<_[Lo/FOϡG<^pt}+JC߆twGA5a _KM8aw2|mwИXa1Q148sD׉.  v \`=bf yG;ވf9uj* 'lzz|۔^)ݱg4cv{Nk>:4Ӿȋ_~gl0p!b^7WX^1!kYCs;afc?/~rہw[8ef6-.l065ap W:j"Ȗ}#!E($! ["HDq_jUZR9*ڊZ;(U;j]ں`U(Àsf~{: %HT1a`pDHa#OB@|9Vr#-,)Hq='?pɯ%WO +)*r01k dC +H +r桘 f*LT3PKg4F2I:#_J'ge\@kAyF;^~` `H e +drd"AC<&Odab%PzϾdRz_~c2wPTK%r&5AyAW?.i,0H?\/4OhLIrHd^r bA<iB|<1Yp z[ [& Cr4HA/RHO}uU*a|HcLAbAlBWR_B?7#BİJGb=qQbaRITMr+Ao;׍WLP]6Aq)) +z~d"G?lL+ݨ0e>6z4~FȻ呷M_D4m=6zk5WR[5^v;BZGԹ?mFhS2C Aɷx#b +߷%a]1g6aE}ǜ=9z+Kc/gTj/fTi6hϛiϙwkZtg,ͺ6KԄkǵf:8% QDzy4Xdy'eo>Q[k956' ?ZW'K895.=C٭˾xJ0o\h#)B +H;fp-vh9Ny<3Uzq0dnxc9s>3~t8gPӁܝMܣ);7FʞƆEcwC7|}5\nQw2j) v8ҭ ֯@PNZ,3#)Wz05KZ"ʐ֊ӥ-b8M:$J'ĩOSENe.D#Cy9|S;WR;Bl^uVw~k>ƪu]D%SH%99Jibv4![s!=6'I o<<xXF3IaR#]ѽf U5($(-) C'IeeQ#NiDioPd>̀zM򏜠ʄN9~9vgmmcMH51ʄ)yzjDIxI$A&僩1ahL2t>>LV^L=DYa4^v~rrsNv}qEUqKiK(Ԇи䨈I":/%ZRӐ\ӑT3T;X;T3/Y\a4_h?VLnw vS: ~F٤pǪ:/?黡3>$jL8 hN +7b1M QM}3aM!A0p9o2~. _={tl謃kK.Ct׺.AG9 ;[X<ݐ S oqͷwwػL횬5yb;6:߹it籥kKޭ%+޷(# ?MTA06m8m;1ekt:;~NYǛ]˛)5um4>{h+O|?࡯TCp@5x~D>SxYJ{z7nqo0h% "Ly~ :,q/s{E5X[ˀ~j !8tPȯbk_J͞jIOHzo%=ewC>GH|FHWIw|'nnvo4*2?..@B!BO b!Rk" 4{~|_3-XNTX bu+pտX]9hz< WoZ_ ~js1˳r/ꇣk \2$ql5_ǹ=c= +]ͳg;Ĩr ktx9ln}‡΄8F\s!Ɉi_2{& G2@Ԅ%UUV" E3B+Ntl?cXcQ|KtyZ3ROEmq?qKŞ+<5.GELx$>us)?i꿏#=SNo:Y \^ +u2׽D26^l]((.;"cΎuܟ]-M%J H kDlG "[-͂f~f`(j\T`"^2^9^A"G&@Ye(s.n*`WLrgc:Fu&xvf&C$&QIL:AW'j ˪ E#J vJJ 0*-1"0Y1.\7ѿv!/s1V B*C>[ð({lÆ7oR 0 V\PNQ*Ked'gfdg53 ӳziYCBiE$AJjĴEbB(c4?zF iBY l%fZmXYSWV"+(Hȗ效ya^$E%%ɲsblRϟ ~ [ +L6=,ưWQsͨ(sb}[,}[(J>:[CM6mgUr*Cde4('TP%M,,p + gDEw E˛;y]Ng-ںx3?ɘpv殌Ĭ4cV|cv[jbV_rb,4&eb»:a y4veP\)($+ |?nsQنU{RBNȘ<9)TcNVjBήr̋=67.iN܎>c\7,R{(WK;h!Pp؅_P"`YW$'K6o˜e  +g2*,-)l)l)苊cORG6"}A]U_VQ-5vƇY=Y*|ue#qI)_LI*O+[:3l]TLYVTtٮȨQ3χGO, <0Y!c; MTaG*5OiS'(8RxlaVMjPT5!37̨ ^S4ؤj ӎ«+񿸑E~%e@ ]z28M~ןe`m-l~h/8w&BglriHOGgׯZwjCbІjІs! Sw>=t +=r52CȽ9g6."rhgzH[5QӬ#ÚB;7/snԲqr>I-UvZj;M}pq6ԃ Pu{e=wȼvŴ\$8-;Ə0CMApDSmE@"5Zl_GڷJ/ iUx^<F5yiiLj6Rl?R `lͅ{*Vh wRп[6ȼ=s=dnJl>,<Gf,DsHR }%e 06@}"Ṛx:2nbǗA%PBׅp_3 w#O8C_A=hH/f" r,0+NءK`4(1D((!s`(0H4&ʈ(Kȿ)U0ep} `?,kAU Ą補! -Fnb*T-#67+QD +.*xmP"+rt)*Rm!lcv |D5YuD̐m"~ xFoQ?YZ{%S*POg W"?A4rVHQ"4c[(D^ȯ8s\dZ˿F^3y͌rwAheCO[:z5*C(UPYbPA~!˅8"@1Ugy/F?qշC3-ɯTm*y*H3h12P52~&EmQ]7{E#0VVGO/l ~?]?utG7!P5!FrtVnjt Ԣc퍚FV(^m"t!}/p*?u*?q=vfz71\/1]ۄݮ]»Ln&=o +tW[.E_ldDB͇=b_hܗ{SylV<.[~߻H}@S!S-uIiQzG*EoߓvLodW}Qv<tUQ/Ɖ1QA^W~-z_ بcآmءXxP>?diPgīj99}'nb@8DH8D"7"hPK]A-ꈺVP+:uwN;ڭgvߤ/>|3tg??>g/)-LR$Cg~;7$M A7$|,YU+2!pD<pp4pt:p<x<x|'pdͥ B(luzow4_žkމyCkkFcst"5{]>hV|V|||r>2p|ʘ ̘ ĸ:X|:|rβI1= )`-C+Tnr;No2׮'m\yjuvއsҰ˜Y5OGLFL#F Cw9'\F^h6gj?8s*w-|Y$ (=/4Dq܄ wZԉGND;DS'E1# 1ӿJ{F""0 Uϵ.+;x< vYx]Ӑ|gUi8|Jĝƴ>ށO8&`Xk֓8 %`؅>l>Ɇ{'vK.)BuҁLjo!s+XwvE8KaX,zDIRҔ~YUj>&:GevBA萝#ˮ[eI-ffLD5)`3A\2&K?Ki % +c: 7Q%sRU.ܡ4)(JҤZFeEyfV>UHLڐ $g {A14yB+́Hs֟*sC +{ml UּfUWu iVw2YS:Evzmm#ʹ?d@p% @pyo5/:aLX8)LPo(Hߢ$X9sv`V.[3eY榠\DXm~Gȁ(5<-`yZ!.nwyH۫XۏVZ˰F2PRcUy +m\{$T-.юsBuq~JFAܟEy9E6rNԃGȹ7!ALZ&̡1xVl7w943>U^P (/X":cݤ4K7'IO.StJjOP M5z1Np܊[c?z&}O}4RE*7hXEy¼b,[_)TL}\?-W/d2ywU1fC3P +>CwͿ҄2hFFZ@fwjajF뱸A_VUbJA./UH˛C32 RaI"XM* +ȕT@3?C]Ps?vp1l ^Mm-m5G+Ÿ,YwPEeq5cDl4IӍG$"S/&2tiQ$0= M9Bd9h'GF9L0\mn¦.nhY裂IB%Q٨a`<\*M"*[l|e˳pE5q͐mgVѻgEtK|r~:ׁcA2^?]el)c( r3U]rCMm%Lni286:}jIc/m$fk"R z>C]Dwp";5ky ']@c+uV,?A\8LqC2"XQ&g"n$2Vޛ°$})ַO}O8j"|'G;0c(ԟA7 QI9g>Smq1q$gTcf8::CQFGFgG>"9V#I'DzЊ2@_Dώ.2jL{s7l!!@Bʎ([ViGNksU;u +UC@\PY QW*ZAyO99Ϲ'y~~r?/XFgyNO1Q'L03S+byT)e6c3;`=sses%9;dsysMPgaչ?`J*mK_iux^eu@*Jr0j)1ɘDը**wRU1zwB^q5[uCj*<Wqjw930*r|5Wd/2ZiKS<{ F5 1H!$5b34D4@^@οLr S 1Y}'iyFiH,2(',gzy_&5Vw|zD!X89 9 rGI ;X@CtxE@/>f>z!wApQP)Sd %D drS 6Df( Q =jF(ƒj '# ,${b(#*)NH\_.^$OD/ fbHQ!Pr2ٔcXB9VQD>e($~$)q!i>h'G'CgyԒݛ0()GhajdTL.S_Pycr8l"}\TgzLtON%>! C== "&R765*)Q3]P>G֧X(}\*T{U|G,䞉{nmT{<ў<ҶJv0wz%nvG'roiOF?w5h>CD4ikn y,.|Y'Ba{{+߸X3};>MlMwݯbB Kŷ@?Th+1LT`AAjD}A5Oǫ:|^\Kem-͐epR+4[K.>6B4qE`Za2F.a&0uwd(|x7]M[UK둳W*oZ~)zbw 1b)ڭyPqřz({OձX_*E՛`?u$VT9#xL5JbTmC4k&6&$MP;)R8gO7 5v|H3~p|ٱ |#9Gu\8qTiTgoX( , +B aIB[ D’Bl ;"QhѺDbX8n9gTڙ)N[[7~yyw[:]I'TD4Xsiطdعr .I},Rm L>-s(5k=)E>Au@!G#w ȝaJE'8&l< jI%9_s +)18vNdǾm9+lIyQ+$ρtbrwZ~`g2C +nօ [BZf>jp4Ax&T+>+.4"T~Mx . +|)TiLA)ߟ^7 ΢eIBZ2 B22µ 4MF6c:c$*TDeƵHUHUM$PS\ s ;;kT\L ȎRbsOcL%/ ΡjeQqD"RTFWv3v0JER 8>X:Z&”D؅bʰk0=epr["̦ة亶%4ȅZYu$^+.JU1 +q#H[(b.YQs :*TD+b[VfPuլU:EeaFyNbY(-͗2'/9Q~?Kr%#̖Q92n&s>{Gj jz5K;j"wjqʓ}Kӂ +=(%)(DLyk|?>M~8^(:.E>O7FX@0 +HS]9T AG״7:(Vtz ˩VQ裬L+2vY%9iJe\bg@'*OJ& %/bKޱIJ"ToNzx3/.cat5̦Jq)O,%5TQUvR*&ԼxU#/NˉS'bx,9#*f%M.\C:ps`ų{)uu]!*wEٗk6J[d7 “xMQxF&WhFqt9c-^c x'6zl0P7E*룭(tp]r6w%wd¸텴vn ,D0Kw;<6Oa.-Df>3fp "'0={U5daw(fQL6ee[dt?N.9SQ} F/7rKds>>={`w +~'y CQg"I-EmI#5 lЃ:P{F=z&~=\?~k;nA} p.b![@vcXOm k}'WpN0S!y-͐> [j([jЬ:ׄ Vn;26H>ȘŘ٨':[ی'Vo^xٖs+|.~}5uFi;{p +MU^FU7I6S xӌ3k:˒ >Z MuyG=V\u d'`O ,aC{p\y\QHG=>I'yOrzZ `p=߃ݓ 3SG)~ }w +@5SA6;fּZNm38HK}K%ټ!-,R:c# =x7':N̻з}![l!l +9`-8,֒=L @=?*@ʖ𨝬@'/bGM'H*`ٞRK!)AB! x#Ne#|B(ddn"nRK'C#|yOSU<nT7*F#DGH:ҩTlaYBed5YOgL(y~ ܃#흼.h%p%E0ՠ^|IJ5R/TG>PD%GX| +ʭ픹yKpmT3zJ~!ONІhk/A%X!7J~ bՐS )`e0L;/-𜩆62'O +-7pLRtZJ2[1:Vh.F8'ɼ7t+D Lv$TZj2OG +桴i k1e4=[FGd$ | H%&W^!)F`{蕳VdFs9<}k'VK1ru F|;|vvipod껵z/{"'rϓs_C(FX|)a=w> +c/gkwIFߙ9qnpWrWpTq^ +<x{>L=ɠcA p +v1xhR^2N['%8Xp70F}aтkYa‹a φ/((X/:UtTOT8*>">-ޯxjO^GH";; pʏƳwjaZ]YMQQ}{ mNζ>j]c}H@bژ256d7fnAj9۝Fv[ToThWBM@<)ce;/HA\VJ%PoR?.{y;;Ŏ+d[5͚*>& Nh8Ӵ;ՠl?+`E=Kh3РNyu/$$9Lu{B˖LתI 3\7&̕.-Ӗj7VkW%\q_^}LQ^E-P li'Wg3pjGYJR;lR9`zH +ϵuY^+uVy{/Uxy/MYxgq⏾Eϼ>xЃt#PE=^z\&iLv#-b6.k IL+,etR}RTbE  +U O[ֿZyzuxJ=hN;hk{K>H=m)aT6kWx1,0GabÌE=V66T2 a6lQ4ûc '~>>E;'e_WL13hHÎtL]UteFV2AaA9w<9Y3ӍƊ\IQף?Gf߄eŞGg-4f@;߿)ڷh贈tW6ga}VN^VjO!Asɡ3DL3eDN1l{i^Sq,V8erL21#H( 6i4fMq[66t9"YvcpO5;82,tȑcz7g)3͹>>c+c2WŎ6ZFhJ +@:4P.wcղ.W+sͼ }d&zLHf 9vt2-%Oeb|ez@=R6[6[YΪZnd=܂HTC=\{Xǿ{ڵ*2)R)$]uHiJdKmM)#HrefJ3gʐNf$saϳ]ou:k!]l^]h.C%Yʂy*̨͉?gVD3RRB>IJL^6!8dMMau䫣S:ƥ "R[p}5,}(6Xg|GyfZf-=R3&xM3?qiqiť},,6m㨱{CcOD_ I?&M<~_4y}(axsY}PT.+a8 ,#?6-7oJv$fMI7-CujyљQWQoR9z^C@+v.LCsm@g5Ky/4|.-p ,(g*KTV)QI %DQ̭D0<,K>L5TP ΍T5бGUze]m]Uf9jWUٰCwc[bw*e x 啴fj L} DbT-B\0SW# VxՎ6ZoY;ģv,unu ]kw =n0R8F+?Ǿn-,S}H&j:(0k?lG#$d.(=g]nI7[˓:K?mKz%v'#!Fz~-p;8vH*z<8 ̤wq 8 8 A +6DfGX7ê-a0o$(/΅B(ZVðe7- ϠN@ucݷsܕtZ7;x4.̀% +`|C +[FݲV?n<@.7»۸}GS-zֻGJ?sg |kLz^ xdEbw6靷Tӿc_^НqJ@y/Jum^N/3sbg7ϑK_p O ߲{1f͓9#p瘝v?f>$u'w&;Ă8/ L)F+ּED_HP`@@L`T`D@E"fi+ G:t J?R{1;Л98*`UOw0~ dhw%NٟI'*aL005R #!g7sP180Y3%ϑIoEt-GؕZo~IC3pwf~{&\a$ +aT3;CaP 1 =NL

  • UBO9+-ng\5I#%=Vm\@%γX&c!9ka02{\[;ngf~&q&I{TQqS"\Ur% l;Zkp-dلַ8m%9i'i7{tF5ueq\H!򰠼IDH <HH $ \LH0;1;"*TԂJe`0ejkGl5:vf:egvZl?ʇ:~sw7BPID= ͍ןGk差WnYC=zi@yѧޭOvۧ{[n@V)Ku:B}-^X.^ 6APHYl %{AxBem_bi nyr-t#t=EZf$-1HטEIU,22yJ|-Y?Z !g,zzVЫ(qwYL*v7$m&Z WY/EZ/FZ_^losmso39ϙN۞^.Nsn=ܗvǸo즸$; ,HV4oR?rѝh/cy16$~1bgcg='cDž' w.:;Mv4*iX@qhYG0 t?"ZKpCK1vN$v8'Ju}H|BT|Llp97p\Fœ;Fgw w Ww}}"-. "2Z(/Aw)hA`q>%Tp29y*I$m\s{WR>"t%.CC%cf)^ɊWsN3zK$!6 BV<'We8t4IVG\FSS݇RpYwLcɪ|{d2_ϯC6.;F1f)mkV]Z)IoF x7J @{}wJ S\q.d٢:fF2ғ͊=>=iJJW!/Mڀ6yK@3Y>(? W['j*9Ae@>7xs\aN@9A8}DTv<֖hIћE:E{p\cV*N3 yV㐒= +)Q *Qa(@ `AdBt}PM廢|`^Y'pڗӤy{uAոYr]BDX>~1\G?2sq"('j,Eu>e] l9xh/bo.ok<ɩnbrBUEʈ|ecdShSlyF{n!l`iTsrȅ}9ޫ}K59as;C +-!(ZN}A{u^_NA/*YtzVS1F5&JSj25<\K|;'Cs8!@ϠA~Rqݫ:X*;W:-*96cMnWCԧ0-@3ypu~FNk"ߣx)ϣSt/8"9D_(@hFн܄-ේo +u~#kVQ%v)HԗiҌb/rGj&C'ndb%T_es3_C ށ<PPA0jvek?>uE7Qq^J׳vP"e꧃1uZ.+NCx}s/BU620}o8Yߚ%r;%i jI(61DrFg5 M4i"0܏ڏJP[~3x55%3FCꆵ~:# e,UVmC ۓ*<*Xs\cn᛻OGy=|ύܕ=p"<6 yw +yvL#tx|P {=E{'swsowK Z8S$^Y~=V*˪u̳1'f}N~W_NWQ>d߅B`BڀEbO *(q\ǥwE6=6B>6Qo}n-reFۊt] k] +j:|ʿ'\# WK%kībJ^U,^^eE\e'twW{T]I(NTOTyr=M`/P٫R*oB+Xo9e +`.}SJUj~stD;G4B&8,R# A9;X_G.[h2~Kjd7<\@tAbw{I 7;t hpqB sd(( 9|$`j%U\rzk +$;k@zOo~ `8ShwaD!B9l /_w {tcwFA? F}K: <<8opɏMq8N.}_StO{,k$5whwnt. J%AseOQ +s׽ d>#ȚYsfZ]T\wH~"%w} x CE8ڈ/-4@0QHP! 3T2, )$!kQG؊px3L*g&{  9C[Ѝ9aH>'1Aițc!)tѱ? +Tp{krv0+t7*OPү߃}d̡RItCx0 :XLr\JzYGG!c*\ڛp8'1b_yg!tZ\ZjJHCHw15Q>g,nϹ-p{':QʸvzD41#aळx'TW- /Yx(tR$;1tG4gqZ1 qRtCrr4kШއ͗ا=nwQpxҫd "D觸&]sE]_4i81G>aLd7 Cbag@5vA] };RU*b xCZ#%n+h ǩ8зmg>fKuHJ5iҶ5RuxTQ"UFTJTqHQyA)$\)d[P"^"X$^$$4xh4J_D%( Uu6VY*z\3]3GY(oIˌ+ԸAUlܬ*Q66~Ώ%(49$;%_Yؗc 4cT]=:+jLq\{֔z_S?QS(~0~vivi6ߔ]gӮ5rL[u٦5S뮫[[3Vi>+,|R- |YX2}X-e~,sEy;G|I+{pu7xxyg7uؘ¤7uYCy<'mpCu2븀 . \`MoMg c-m3ۺ߬3C2j ;K@X H%l {XKTAEE +PG2괂ӪxslSO3N0<' {kWq&2N56?cuWYLSaPfEzNq}OjSwԥ#9mKrfQ{ΣM*lNٔMc׋8YNWGvqn:1CUYsmie\΢aa;-&9%nmi9-bIҊ5^bwXFܳJ<Ոp+g|JSRV*a/"g1 y(D3'c-X->E鶼V[vBfi63S!q5r^ַB[&SIa~䊿B2|WH y<<@ ܁s3'ڌIp!%hW'wEX6%8˓ݪs9ܲ|_T/jҭA +i_P`\z"8Gz)8[ypy`-?[E Go!Uf`ⱻ̒zݨHWfޢn(.HrUyR"*zAL'ȒmHe Mv< N H1|=j[uȃyh$:oخ1NmZSmEUuY]J,I,,s + +RAF~MXz~[XZ~OXb04E10CO g9z S-5Ўu7 tZM 5U16*MCae{nywviD(V*)%U$e`r_P9JP +W~__/;ϗm}3D`=#ځ|K&kjh&*mԲuI5b*'jMP9(AoLjn6|SÍ)śU=At 硓kykovhT&[k"t44fp6iem/A٠ݻ*F{;Z{Ɏ>\WYǰ#k"seE- =D-D.#RuSq)YKӷ/O"rnNwxlcth]gcn:QH9k;Eyu+¸.rg63AcG==;#c~RQV/+/D3"iG҄屽ѽYz "{*hv8Dۯ9"]Xa +_مt2+;E>Ely$cD[@,sЈC+(eCICp(}V#2mmlBl[C, 8bwjI,wl,cyfPោΣ}0`ī1>ǍZP̨#EzSh +btqh`DhJgiP~GQނ琟ޟxpWmd0bFJeNO <0+cE'A wU$R"JÈDSD~07&g-9CZaXO dkȠe2Ta3Y'd2}͑d2L&s bp 1 }mvuNy D9c"ә%Ěq \ijgQ($6s:׸&=w?pj,5\$}Hu9h  2Egfh>mu% ͠K%zCQŗ>^-p +1?}%w3›m"?Y}7'zhfĝ#)gg(g(gܳGi{=O><89K 9,IY4 +hKj:zAzCoz}HN#5Wk 47=[xgB/:9(F5oi-kV'zx,(cv-d9ʇ/0 9ƒ57&KXм&ނX>qYK5a^3w$D.Blj%!&#Gt +:k2F'}\L0GmS1?B9/_`5qXgb^` ;SKzv(G7(7Ri,.kE}lBg,7GVtjE'qq:j;IGlШWt^02o`];1/p=qっǞGw- f#IE4ᢦt@MtңtLPOG=::IzCAwz ϟie9;փq=țAtW-!v>]x҄ :?NF1aJaF3~QWT͌b>Y 3B̠xrsz,o57!A\}t`::=w}3=3g-휕vHyvjVI OѬWkE2dy٧uIN(gAļM׃uSW/#kJ$zŸR;:4ן͝l(,ʼ?Lk-L 3{r=aEaM:.iFziE߲M:bN:f*j޵Z#}lں!?q~; +W0ۋ11Ϩ̂.򦝋s皿/[Ȣ6;e)2Md&+Y'im5ۮmm5ʆVˎحMn;ʾvh_wڟb;2}G;i{E1ۼ+rՆVyCJ$ojNsTF8kn1{1됃Y>=xA>6SK5ɴ*<(WSZ7h ˵&a,(E[׶'j{D ]xQQvR ϧX-~S|uS9r yi5rJF!SmxW<̢(5Δ钩W &$uiAlNW,Ոcukt1!ZwX$V>Qt0: S:\3F'1sF w k>ۑs<{(=sSB,CSzR`mrX/U+BɝKwD&_YWђo_ #l`d:\Bh?~}D둏UGU {SQ˛meo>%;5!S1TCRCXaX!44,1lEѠ "pÿ_/gj4b?RZE-a?ui) +J)42KgK)E N*ug)1 27]k +rLNp49 G *ej0.5CD0  b* y,SAvt5P#wUk/ϙ~=|3)9{b+.sBJGliULu-}FDU9Yvn~#BPswz:[x0?s}zv41+,aD)S3_˘r\2etǨ쑑Ͷ'mƦ_S ].dh:՜FW[\ @g ۣ%|F H^Ř[&XGkg)"oڄkڌjcY/i[j+B-7m&To-Zg/ pspo=@i .!B$3+E\䄉E>P,+Eƙ-bVhR8O[l[e4W붚~&߂22m?xo7E4'tf@s!a'PP4~N.~%S.ˣ%哤~hlm(r~S/qח +Υy=_P*E̱{Pͳq$YM!l͢j5۳Q48 7C\ Qp1La:iX: lr:XG!sòJC73z(#w/k@Rdzk6>̀s+0hFaj8ӝKڏG yZ0S $ş:V|ID_+~}NjYnNza=aw[^Oo +]@tzc!gOe@Rðㅘ[m/wY8/CY7}>&ǢDzoc^:'Ή7Ћ7| @.76#S ExaNE"Eag&†] @;*AǑidYDTI6|R@ xJ` V1X12$9я)lZ>:c=wq?4+_,#L[/@/c0c 182o>CD< ӵ>2rɣcn]chTq] 5:aF c a CTf3t%ӵutlcnrNnpN92߰_W 9nStm:k`l~8Fϼ忘-XCzYܖqsr :\tY/Єqg85N24~J:I0̟ro\= f~tNcNJg).`%KYӳ\zSDG)ǯl@;rU`$_<~)ɹ̹$`5#XxH%T B{BY@P! $| KvTH +{%6v6U]Y_M󐯺[_a3~G1h rE]ZTKs5}q$3$$*p$H p$r 3J(Z +U[vnw1n;q:|Cb朴yPZ˜60{SN椬咍eXdYc3Uaُ<eg> ^{b 8B? DKLz-Y@ a9+Og k\^On7zۼASWz)}z|w}O:_/I~ɳ^˲=6rr<-33qt{Z2wXY;,PV+w*mϿS9Ʊ+g8{9ʥV孀f_MFۘh1GEh9nz[1t\Y[]ٙYZ^ߢ16kLAMKp9^[3 h͙j[a&̓JVW@N z]sDŽE9 70z +X|w>cix9,Vk Va'ܤ iOJW%E߁e@*G o<{^ٍy 32YH=(lh+Nj.L/YA|ADX7q&1"EHDdrdH7KNXs@*E7Z3(bB\C"j]\.+M5fp*tbCPY`h7tD ha*:pJ>hw9x{#`NՉ6X`׌ElGSݵѥUvj9)d{/^K۸2{wԾ#a?o m|ĶV9\G$]m33b2&ez026R`#u0LLrjy>RgO'u`;psm֦b6`ov{w}Vy׎y/&?O_!}|'\L4!ĉM$w)mw[]XWkj`ntuzl]5~v][ 1c`F_xFa7k0pF͢a>L4 :1D% (96m 9 mL5" \C&Zl$IN |x^8?`E 1x)5X{#gבsDB..3c9F"q@MN ,_nxK(%L|L'X|5'hAc4h\"e%6Av(-k0י9 +m !YA7q`a 70Æk\B(&/b-@i3u2xsB$EĻCp +6oАߗ4F{a-&6~q^ tZhCSdh%/e7 Yx4?H !% ", +qAPUq츔:h:Q8uZe7 =?w}D/ +޿c8+FNκGN.]Ht%.' l@7&בk%pwuwm%Mf ,Ò}nҧtQW +2F/s,83!B1;:/3 K O>}6a.FVbx 9ڎ |W08F +wOsΎȫp2p +VGs0xi+Oh'V :oH +g0 5rF?9 +s Bጆ/ ɀ'bT[9<̃xڑp³X-+ V;[^s!Ruj8LpYϔO><%̄ +W/oD'\=p-ُom}<c0k8C1 Ml`W=ozI4Ci]PJ[fj&amz.Z縆j=Z|z\P+rcX>KlŖ4cJl /rUрGmigmJOsiw!Π5 +Z-^Y#Rl)u{KwR!T:|Q%)YVh6hQ3>5Y Htds9m%Yjns`wa`+wArnCP>ho~>^m׼炪@VP9̜k8VN?Cµ# w;e Le&pBxTQVߢT6UQUWTZu`I0W)R:T7;TfOgo +OE3Tr$щXA0`ס5ɊŴD3is[4/Ԅ;4hu6* U犪DsEjYFrMcǩTѩD۹XsԹPsC\y,~X;vCPnOFn^cn%Q[&4DL׆G:VjNYTrm TqvD[RRȥ@嚯]alU,W07\u4~8u`C? 9@]Mϣzo˩*u +gE;EK1Z +|=)ejrm,Z`K qLvMFKqޱ<ƒ᣷LU,3%ciQNNHW'.K>#( Mz!Ib!,BœZVl|^T*Zv%/ǝrrFRF`QBfؔs7zmIHM1ٖ*jEXz<4\{Teǿ r%njP IZQAaDKɠDR*ʬLUw;Y[ڳіi(?g y=m N4~ H% uLG-=z}AK]˰ahd 6i5YDMr477Dԃq.sə9㱻|gM1?){nls`zvAx64sTA)^d}/&>zcP`䟐?A  =)l[ >_H]2 z @s +#toaafASO1ݙ?gBqy~y~)yK|+ޑ'1wB~W|iS\eǯ{u ]q'-UR\u2b8TӊGjjqaT,X3%YJ<`3[yY*{r.z8qԟ{,txF8LNJaźV XÆ*fVJijLSJ54@K)4F)wJ]S⬳cXk1ƺ8Z6Z6Z:ºe]"V2DhVc2SF/TӏR&f%]]sM76lcTmmehmnc3Dr CmE0jVjHe*(`/\k)aXpqz?XfXM#kfKa4C|v/ۃjT=Vd')>C5Ⱦ@򱯐iz m{M6.P{zws;̶&Eje6ߌ{$(Iޭ&yɣ%R2&˵Nbs-4Z3D2M e#p! o w-mR !3@P*0f[Gro7"bp\NoiWhlmlv>we&6bhU]vofm^i9ZvHЄ}R3Rd%"]~6!.Ciѷ/`=\/Ӱ_>'"'};9d `s x `Q/\tIpéӠ.}B-CqGtg?v7lJ"p0SXc$2p6КvcOh- +t9ctW^Go_1|'#\rA%œ5|2h,E90(":IoQFXzMͬK/1"8D<;;@ފ|i09Gp9͆ C.9CQU +\AGݦgաNӬqS~R;=hw]>SMjqZ/8dyN!gUk +Xh_l翷SZf: S[N;@c_=oEMCFM9#Xڧd&EP +a-zt;=i-En1tpX]N3ٍWU~S5m+'}oc sQ3ҭuū#]{LNj3= m.VU}֪n`T;pj|;UwXNj_SeChS}(Fm~+#ח9rWmj 38~T]m +QMH6.QuhևVU: kڰ,VҡWr +S? c9-l WCdꆧv4mq6De*jlQ"*Wk +T>rh&3~# V$I$ ApI!\& )jhmԶ*X^:Sgv<;;uu[na9_|=}=/wN.Q5hH3\eE4X/Y}?irz5 W8B[;̆@ h;I+i4ڛړ^O/aݬZkT[ֶAm;k v.՝.su/yBZ< q݂2"&Sڗvg)qv8z7r~nMw~^~ש>o>f_fdL؜Ga_F=#lZė|Ǎ@ *<>4.ܛϦqv步tvĘ7ZxF?l$2T +j&a=Vhq\l< j4&ϋ덷%u%'E2/ʑ*{5T5,(tLAn<$`J/Hedq,yM4ZnqZl4H΄z@f<)KY%Vo&U?M5`i ѻ۵9XͰ1 1g_Naվ,Z6JlJk55$=ڶMViU=#r &[ +c[ieF"} q:B(tTQk:Mz^CI&9U +{kWmrGXYR{/'Nv_O.r|\x,t]YhgEE~0̶_zc=l +ŽݩLn&"R%+sV(g٥".U%u*u5z_irFir `E|ޅvxl:-hRzUY*20ʼROQy6O٪x̞d2;0xߓz-Uf9GnA_F0n{1g!hQM[GuZ=U',J%V_)L2׮4L?(O'e$&ff$eT12@?7Qc~%h=6h7sۤ w**kJe6ep-P MRcCURnCCRNC();ПĤY$#pE1c.H^Da3#Mcj{[ž/lCo܎z(LCK/ .U %Pqh9C)d iY`6'?hڊ9m.RV/h 7DP< <)HkOm{{W<͖ofFĈxyz8y^\w@=F1xO氒ëNp&K6r2Vp)O7iV^j|xt^}뗬՝x[W- +FЛ^@=":E`г6AG(#tu@ilJX-gk-50kUd22A+"Ҳ t=R }CK=ez6_pOC86o݃ zJƉ11ڱz,VŴ\,V +hY*I Prl+)bQHұ$9G;$9 Dă ]<8fycؑP!01yfh2M.'d +%LjIWAf14΂ $ d@+g o+΁Hye  h|P H00I"D7`yB~.a(0@gOh`7[ +n>fw=T$Oqtaw ]n@ = ݓ=@>FG#DxN g (2@UϴαcD}!|~ǻM7niHX ]M.7 =EB:(O0*/ i 9F `,c J߉ Ŗlq]F-.]@~ tmһ_y @0czTF5`4FYIҋ{8KC8.Gt +EFt /yNB~د/,30'@1V,G|:8N!8zQHi+=FWOX}:jߴw<1ݟh[c-!a,xz,r_AErpz:WkbFvPvTz;NhD aѣ }|y[^*X#x&"څ={*s+͈eK蚍vcڋy\$u**f4:z9x@-cql`u +༂!^t ZSQuCp5i:<\œ|PC %? +fxz2h3'6q i#<4z7Sz+GG[Y.v)KmL\߸N;f?Ɓ]0%3i46hc~&9wV?3V=SJݓiFZ>y-޽ὑڼwP[xIjr}>#_^ĕ~;xrs_4fYxQM2x֏L biմ$f.LZKK}]WLm~eį5SvZ8}%->@MӷQ5/PM]x© +`9U," اP=u-Ǝ8QEC3iHZ@m)A%(fSSURCP#/.NMp?*dS&<(4'$1o^˛Gu5}q3z {`f|duϚLF-ᱜ:p!ʭ sk s܊ZnYBnQ+Q+V)(?仕_ \ʟV8pVx 9 170XQAkP.ı$JF-ӨIL(NJíRy4~**QT%bUH$.tǬb^| rU VlקxRπyĎ}Gh+zRıP-jfP&[yZA(,Ύ D"qL\[%n]^y+K˦>'oJ-RK<;84AF}3v,8V6m1O:o*O'O,\]4oV#piE&q^U'riFmYu]2n,]]nEnҝ7DvL <=wf" .ıqb_JSg"cǕVOdӥ6}̢/g苙4}%c6,`R  ۘd[L 3|h etz58qmK C<qT"t/rOIS3Ŕ,I7fMjcRLszS"4_3w(>EyBcǨͧxuy>gbq"*bLE{O)rc>+Upg )?s +92);sǖK MV8Ś*[,$Z܊kBcmPL cmC24vMeAe`t9 bxN؀ݰE[я\X n|.eN"[/e9W5:bzGHgO$3j{,_e*G4+r('Ꭻ0_Y @;Џ}50V#ĒpIJF һ8:N(֙&V9%N$(]uqG, ur>23""Q\q սU*8ʀ #2( .ȦR%BdQc +Ԙi$5165*&j=`=was}}L3n9%{vw}VSK[` x/s~*.f 0E# C##_rcqm6;28M=[B{wAsn:~kww8FW=[sϠ&eeIIR,%r4sz⦁_EX[}, , aFe12b3Y ,CgPzuK|[ w &<2$My8w*KP>KsG\Y4[$S +T򷎔u[5V>օf5_V#o5:\ۢ'd7!52 r-X 'z4"K_?|l]eWIz6TrM-F.yrі+cv  mXZ,eܑ˰Įج +Rꑟ؊"znd!Ct,(/ d(` + (|8#Kyʕ\t +@-Q-+H9~xz K(^\`+e(e(.oidno|,N?Am67™ b⊃燙GE'<o?,=@!39K; q*}r!gҌW0t3½%(Md9`8Grh?dҟ+z::sz&}KgQzm[(H@K*t` &WZ1;-C@+Qh5uuut25 ./gpMo>C+k}3X~t=y$\c'Xbc)%;:"~\PPHlx^vr9M6R>R~Ï`r#] WeT8fr_"S)t/g)IW]p=ҀFs2z<n+r8shO4L$c6=Yr*JT*ߠվo2+ORZ(%` - \AEhnPOiv%5 +gcX&]χqs>9]&F(P-9JI J4Ei)ZM*4WDܰ4C\0;|aV&*ÌCƘ8D쎭x8=jŊӺ2n;#HekY6]Є&4M($mJ i)4Ȗ*[,@Uѩɢ"2:0(BuX93z 2B0y{S,Nnb:$ь2-#Cɰ*"fb٣55栲\oVˌ(+3Uc3TŪժҬ%Y;6Q_vfFڔRkDV[cU)rdKL lEbS WUZ +K:d)UY415ܹ@QUbko}AWd=^ {s~?/blx_74Ll#:6>i  c?i*ieՄi&ԁr!i=cڛ]x6lWM 63Ð] XTnwijwxXZSA%~8쌝H"FBM`8l:MC!!: !67I "kcB!j'8-ξ _9vS?w!npޣNNb ļ|@Cyoseq81{8չuvG;up7MYaϧK3g+_<-(@2~暄q "pJ r-NW8Kx/qB)) $So}|'a交bP|$pL/0n 4n]bqs}61Mq(0UZ/+a).4=}?[*zX +\C,mA|FX ĩ>^N98MN=yd^fF ew?hdH>&qԟD\G- [21xj5qL:G!bkQ$vz;Ǚ&czwX,b@~O6cy_`})-KQ֬y\♉UNKSOzUX-06k1-NcS,QaBZt1~k 봇1c}6de ,-H{8R20ǶlYLGzLEu"ՇuQX5g1dK  (c4-N)m6{3>g3>3lO-}[b16Sq&L.a2'k1# NcMB07#IK:N~]ɿBW?љrɚAy#Na_ܴ'[,m>NSNDJFSJ`(ӗc ҽdt`eF/2ѓ9iteDF*|EЬL^&z35.~79\n{ù#V5mV2WHV4R0` 9˩DON 2N_h +m#/wb.xsBh"6덿댟Jo\M5h9K?p =M9ʑcNL:?g ].T :ItM+!cM[:Ӭ.*,//zC2} ;MSg8 4uE۸ku09n(L3c<-*ciNCـIKf1zG3nK\cT[&jiݣTZ)KzK-\Y,,̬ޡ_#59~g)"!\D--d4ٲ u6XSZ&Jj[RekV+m],v&fZ-QKUulZlD-.JWl"Krz#j/kׁ)}?WXG VFG]ejBHt:J%!;JcjЕT묕#:sܭ9JJ}%%+]CUho;=K۫ WrL*줽jT\vҕ +WP沊rr)G-vE5+USͰZޠw*y}N6_r?KY/嚚Uޠop~c!~c3W +baQɃc=eS-x仉D]$ ͟ ^MoҤWiĿ0d}*cs\Fȥv*5w0v-+ ]z[@)&$@-!ֿ1X_wDwK5aI&ܤpb#r23 _Ϝ /?VyVK!X@iH >`² *<#*@dp [ B R r?~G X/lfNld2CZ4@._K.6r56#"J@d(BFBf +!7 ]ӔcT.- %>:=Üż>o$@fS:aa60o/臙a$;I~Xr8'1N#_P +?ϵ{s}Yoz/1yہ]QQW}ஸ@Fdf%숀QDA\qjDtbpE+1ĚQQS۴6Mml&i1'<|}>~)P,.pqֹ..Z4U-\KkHpMMd bn1l!l5yvA ߮K\mt0].7sx-F[ _@ Kv mC`4phX˫wrޣl+f#?$=p|^@փEmM$Ky72?6m~8u0d4f f& F6pm'~iU|C$$oϱLexWoflŏVgq @rFR[,=Tڱ25ʽ(y Ĵ݋.M >d>"z>=Bu- x%ɏc;89%p8[E)A:v uC8 +"68Nzp +,B\/Czr?bAHM.q/\x(-𭃫Jx8,X>.g9p\C-Nw-Vsvoܦ?=].w.*3N|Qx\1D +R>p#(5-EHҫGx-ЗR.z[~D HChݧR*Od&p|) Q҅;o((HB~N e kup |oF"U_u2X&uD=D9}ǏOP?P^~⼏μ%o.H$}No?Qo |Abp&a{|v?nnn0~!MplL)We;E u~A 9i%7΃/ 01ȥĤ_^I<)" /$RyWfUṗc1XDތ^Zǥ(Y165 ycy &# +qS0R1|%Y*i2G'UW+_*eF2ޣJz,=Ȏ).tOܐdR')}/3ϰ׍jw e,->en@꥗12E%e}ejl)7AJHRAπ?ItA4e[E6֓:곀uҷiCtH ⡉R44U +2e_L(vb_.y$:yidZddސL2VH2tπSUb?Ϻ |n6r +ʚ:ud0) ÆKA@>,$ox<,A$+hd/I"cBkFդ\$|I~$; rX">ֿԁutd^Wô2)dCt*Qrha)9b&#,GI ^I __J](#?Wjw02ki Kmg%ߔ^]+|(7ҳy刡2."P2#%=(c"4I4eTx%9ʮ$^U 3UKBJMޤGQcOk1ShdpjMQN/ךuCvVO־Hh2?G>~dXI)TM$cj(S:2&S5Ej\l6&n}׬ԨjD{>U/>$'(`&!Z§%QSN2-ZII'6$Ē&lVbj9C5F,,ejeaYMjUa=ZUS-_f IEV%#hD,dŏ +jS MH|Rd3hm([( rߑKCbxԓU + A\Aŵ(-56D<Ɠjb2&1iDMNMV63eĎKt7y@l(|HJ~C_vC!!xCaqBu9r9EḷUw,.[/8I"52"]C5(&DW Nj8oh2M2IeO*1%-WhRz%5($鰂.*(㻜&t9 MD!5x.q4je16Y1)S}/5Z}R+.= e,yg#'\J|1l1m1˯f.l0Cл;Bj%oÜGj&h:ɆHOǕLlIč+,;*и +.r[N,,erA++l: +!q|ԫpmeF=l,<ԃ9=BVrRe_ť_%SɅSp*YBk!!r2xv[ǛuIj2y?( +I@v䀍EԆ6B[/ȉF,l =8z»C C=oeYx/w2۞6ѺSu,&\ E'u>[Vr v5UJf'j$E`&80Á/@*tfUd7̒Lm''(w3y{mZ[K.VYZYΐ Y?s@}pm O2<##s A+` 0t\mej= Z*=rUùb&e=OIna?o. S`0w!/oyy D-}P& -f*{wAFu-&$:r7ѓ'tw+P4wwUߡuo)qvB|R5u*4hkhNK69p :u & 2z}TUTv\v츌kcp:{@,@usoZa:qpaC'`r%§3# ).s9]٥R_S軓zI ms\~~X>SWJ +bǍp$s~-2s9{6gЪ%_ +uoӫ +G?H! SD^;9`[p(m{[mU/o0\)ر4QNh Z i-@U ok ӊmx_ 51D7S? /YӉz<S +2ήR5_X'i1] +/Us0} urO| ã4_?“OcR(>ʴ +&HlIxUD `d.ׂuaBܣ\w-\K͓;^'qZbHZH},cRs|љT *.ȡoG;YN2J=gy1_͊֕T_,\CaOF2e+óA6c ǕihSNwRkY֯X}є[blVŞ*qBwe\ihpgeqi1]yXY,gҽ5ɫI&cJjDJ:3\\{є Hbk, ;@پ7\~cT{LT&g?KEX*%*!h⃎i|PA_@]:ur+ /ك{ {Pq{/`p;b_AW0_tT3MB3Q RaHCR5<ԡ +VvX'^̈:#+ݲJJa I8c"wS/vfac| / g1-kȾ* K,ʱ*˒(G]9Gc +#5z3Hi6l+؍F\lOSLL)6Hx<)guEHltLAG9Zl3kV_٭QJ+ŚaFR\a/56ք9Fш-7lo>#v]QSx|Gk~]{*G_"QJ2jq${)-qR![UII&gyFLrRnDTYFDB#,mA洷zRzJo +J8̚s"8s125h{5)V>}g*=T1hEmd+"#ie!2g*ȱ@ uc|'4$|3o/k1tQpnmM'R+l!9X&^I9l_EdМxsRB䎕hHsi@^Wܿ;{ Ⱦ'c?<oco-YWei4Z dzRA +*P@A bW!\8B PX!WT/YF} +wTD77Gyoe&l *hAK5Ur5=K`B+AqRgp˙y8]2&W_F'f{7er,EyخhMdzi*GK!#Ġ%Zqy5H@.Sąs.ĺX*aܞ8}H}vz| 4af-cJy)-hcyOL9@! +pTqWqTQ̕l1N&%L"SH]KV-AR lHM_bd8̲L\ \ԐrQ[C԰y3=MN_TM Mez^Lݬw+^lha^@^밅S2-їBn,êyIhbo xzz.$h:6x[|YuulYFOlXM_b~rRg^= b}hf.E C"{h&4^3DR!XKuX=k3#:8QJIh +_! @O6J>Zrt,gOZ9LZE+~,#-[C G&O&i> F%-Z'(>/X8<h>ڨ6.6zd=uN.H:$8R]{ +y`'mXA bكql'';wEfOpؽIӿI;O6Ky<LX]y4`l=p{[n{oNtٮj.z8U?Ojp$;;݆w`?8v.1Q.`:Ef=P{r.Y|7?<^YM` p xLG8۬? W o8op |t|^Zp!IYY8?g ;oo|ゾƅ}Jq\a.Pt IiG xqC}\)p'q'hwi(ů0lA 5 ȫXXy/:Ul]л~ Vg%߅=(Wd &=HTa_`sdKB)|/h:EML}q.ju8eX` oT`Snzn~%#R#:6G";qTa8pJ<=ĥW;.+S;pJ[1mMeZPF bnf~K+͝ܣJjj+i;k Om‹h7P t뱇˥{VF-*6è;wigV;e ,/X`UfZa~suDZKNTUd`%Yg'>Md! rg"/3S 62^`UjְYUr1J|h /gy#71?}h2%W;!jz-NQ͒LU/YJ+Vש$Y* +'pXAG2A7@AkEW>tȮ#\ƐCaF?U"8N!* IZ7PRW+?Qer-Rn~D*G)eG^Wf#~'{ʈx9}\<`.y\Dz!x ?F{UQ%Z%OrDYe;)WtrW)'RΘz9b*+v2e7M*tR)kWrgJ6}%ksYc q_$yYC.2[Yr ;gܦpLZaiJQv|]\\ts6+%al}JL(,weS7Zn+`N3|o}u xtzXt4fJRFbhWJRlIJN.UF֍ضl۫1RRbl+kE۾T!:gܼzAzdUn~{\ Gj+ {jRRɚTSS9ͭŊOޤX{31i-˸hW +Ea' +O3Wa9LM`4H1')%dʜ,L6fg*&ۥhG"p4*٦PnRPΔPK*0{N0#OƷ1YWI;7.E͉O} +Rtn"]&E+,/GK +W +pݭ\F nS$?gۘǰvը ^piBW' $0{bߓ*?C+-['vayE,k^/܃ +7 8wn U4|b'ZR,(-*H(rU2\ʷczIXp/aJC]G#6k:#s6Me $Yi Zv;ObV555I"YQ ;Vߥ5O.wk>8@064@oໍp7%f;ߍ~dlf43liM4&Ip=ai T=:c~{wXÎ i` + s |.с/p vHFѷ8C[kmRz܈mzK,'G0I=|jҩ.4Ҽ2{xI!1EEwA|'܁"~ǂ 78?ӅCp T'Пxd}q\Jq +' +~ooĈg6D~>O^6=Dp&:>O5|V'gLp +Gq1?C0xqD` _Sc0=)dA|>Ai }[̨7~s`xa7a/GN!c2EL3)i:Ń'/COћ(WRlNS|˫WF|1Kg^e,1HDR_$3G<N,!Uzx݂UcX*\$0ۥOj]ȍ ;].˃ӫBS;ƙ}َüX!9pcbL2rga)>H{D2?f?aZ}H<"GO5 ;FJrc~O +c_,/ur `a@`F"2TDToiF-oi^G-]M4m۳g3vsMۭsZ[]O{ǵ6=?f<}W|U,FiYAx{{\a9޸ʬ^cISl?CE-t7h3 3lgw5sjT G(Pi)؟Hֻ{Mq oo W?b/ogn1?Q-^C64{>~ѫ=Z*My= N?)yUs旤l@|'~ B3DW4A]FGG..ɭP&Poc~x<ΒWqUv} +2Ct~W/dGCس>qbK_B62Y@ .Wo$N|Zu{|C&xYX#=Tv7ێ}_bkרit)rq8"w WF|Ow#ۉYڋn.tji+o|.8볍$Ũw!N/ ?S)`LK$ѫ3z YGkjbѮm*6@ }^yߘ!c?M.3#(20_ Z]]᱊b&eVlb2Mz>s>#_O6QJ,QMpƁN+8xXd>ey.g6@W3-?\5xn' +n,%8S*il2 3AaRxjkn'SIt$i7&8BUA&U',$]#BrtH(Vq*a3+e,q2"N)-o(#UT3&VpY*5ڴQFVѬe*T1^d9"3BFdVveL-.մ\)MJ6SRIc>tK0Z5g[s5{ fc22F80)YqʌRF\K5%'NSyeJ*޼W&/k qw!4[#7b +?1~i@2TfI&ghlfS%I)IJNU -9y.S\Ŧ.UtFE1S&cwLω }h B1ިȌ6msnQh R *(cg|!:Fur3w_[zjSANLƜ=DqYQJPtv"3e+^0{Bs*$UAd]eeQaaK%z?;![Z9ѳ+axK9iK=\ʩ[9Icn#Ђ +)P#OGn;WX8-b.ZBV؇8ckɿ#Ӥ{.5N \Y}XWqѸS5m%MS9ϼ䆼' 0ywe/gK(}%s{ +In=|6M5*(Yqk"'!GhL}ACK1u4UAnpf0 +aP>Nqip9DwE;,p1RҬ i.f.f&&nxbx4X6-Aqx}[s FH%& .& >QY;ڲ 3\ w18Sh)4['k&!"'-\^v(>󉒭uzx&u|.b!/#o\4hJ/}1m}E_tquq̤g2\3 =@t?"owD}acFATT((@E +V.(֋jUmlScS趱m665kjulݮiibɆ<<{}{>xsO'I*ޯ2{*~G!1/9cNc;: h /E=SOXttsuqw+Wڅ;ORx&}_ihepBl oA>'g 0 \Ѓw~r:6,&Msx3g\ 8M~Zp1B~4sy< L=ڍY [ +13 MF]Șk!:Q/H/HLS#'lc-('sٽkrw.<.%'X#%|Y&>^V?o7q?+?6p>%;-&?3EIs߱Y|F 'l!S,>bq}>`%/. W8x | d%3|1bk.< Y Oح{lSWhVﰁ-tLAAb\y`I6I\F/@WpvkwW*E-f96+9G56f-aʙ](qp)ttCzMl$'<?:#|1%ÓDvK[ح"ErJ6bhDzG%!n^'#0Qu7!c$ۈKZ~ـuxQNxxTSmUTj.]b$(2usknι᠝:mFd!6$%sEZ?֡c-QC5߬GE%V`?L+'eTB)NƸ^NRz-\pq#B \1)3p&lʅ%((pnvANxS)ETp!_H< +4\b `:?8x,kmngǪ$77at!i)K[/Eb$`\[J+,f×"J~ f2'9"9nYƭaF%h5080_'ݕl`4-ZnJwOR{fyd*#OIJ4+P)K1^z*kLS>,^[=YVH=n/N-kWɛfҼdofX5.O5[(_V)¿V)sL?  +:ǘug\`=b=n[JȗM92$觘&Y+:ЦDE*;bD1"US,w5iZص8"+*I'Y"aR`d"c$ sm-BZybc~1Yy[R s}k@-[ѲV_|;#%]FcgWMDyƧݞ#7;δc2;d:C/FgzG{yb_7z'h)B|̢vb3>1&sFܒح)d(eC2IuLndn*i}c=3 6.Ѳ +-%Z29Z:" |bpp8hu$Ee0f'C{:qe*f)=A3z_zT3 gI(G̡g6ͦg'Y0ٵy>-e"%iٮફ; +I 1*(K,A6AEUEDDTDaPZwetڎ3utNt*N;M?!ϩ:swFu Kp麈 2\ʦj#[Ʀ)c1]zVifG( Z!‘Lj':fMjNKj9R8J˕E%AɌK¥;I=&D'&*O[5Ǭ0g+#֬ғbrseUx:[\s DHtUrQ%U*4=1M3M@[u`k}jvw5~w-3xqch%ۢI4'Llx"Of؎qqTo3 }qBOpS_;br{kC[]rͤ="[f\`.oow=C}ʜrV:Dw47/T﹄8$~EcHOr@' ۱oi7 { W}r|R _'Q/_ŧў +8~K M3CQcyx!/:^e~O 'xAJcxGg!ESÿ]gM;AJHM|s^OO''4r_em/OxH40?K`'NO ^q/%kIgcL3ׄǣ\^U#Ӷp>-\_ȷVn9mkq9 M^5yۅCxe9vޮǣN]ƪآ7ɓ`Q&M5|zĝ+x&Fo/+rPwܯ&uVy`PlYƎf hUʓ-g\O&]Wnuzt?8ePǐN]ZuE`] eO<9+b^.ÓLood3ݮusJ~L^g:[."X= k*aMu%8ǞiG +'AOR5,~gG˖<E9` 5$.5/Gf'+b{*NNAhdn$ 5]BPV``v|XEz:rPu&|Q'sNn5}߮yWJ-IŤ\9AM!MEfe7x=ΆU kO>9}Ul= ԗ8mKLdӱYqnS^k=$Z-w.""7dXR*L9́^ >PkorN^I#?>u1d7_,6q7w.@N%Ƒ#ȑtJR.'z+^ɥQ7ڗEQaDwy)"'薺$EVZ]d6Gc1d~,{زȋƑB/( e /lEiFZgE^gENs[Aixtθ422YՑH6:t4Er#)뗑Q2Mrngo!Ǘu=~3f~7B9gvr#';-"K^ddD~H64RsD)H5]Dy.Ku'zz9y샿k\̢x]&.d.aEizUꑡrr~\{DznfuE'(,洐Y-dz:mzotdS-tCK ōqv#(;;ڋ,+-H[0)ǷrCKoᲞ]v?uC_a/GxptC73w[fU/'̓䂼H`/.΋=9܋֋eыeɓŠ q+&%£D,f 4~:nrG ˕-KIsL +2(a =ը7tpҘCUэٕՕǥ @hpV$?!SP^B^bj~%b .f4R7OtI*[w|זܗ a9 c-}.zEo#cK6r'r` f Kewny]T,b ⛩CgL@'@]o 6&R%4(9.͍Me֟ //%L'}YTBQLtRh#z`qr ŧؽ:WudAGp$Fr'#pd8y1Q$P(9@!,s2:+5|#Ĭd?}fT+|iObdPc,&xƏhq4q $cb,EaE>GӧFm3}[-|"ek/M.8/QM:ݤӤ2q/qq4p1>bqUO$OsM2\$4VO Id=8`ɉYLzDe=dcB` <~9l&MSPߧI~dhE\,B 2mN,evCm6AkJre\.6/1-(< ">GNð +.V+/gZvRG{o~6{Xf:gIL \EAФkt0.KeIj> f;yH\熣}N8ױwMg%k4,[tr&3,J'FW􎃼G<'HBPh)UV^=]>W٥ 5~i-ZѪWv}:ԩ@nt+>}a9)R3s-ygUkֽGjJR!)dV$Q&q(^Hv\BW⟈[ {˃>DM4D_#c/c[h&OܿPj߾-wK=%9Enc&hJGv&'ڱ_v &O_Dc̚hɯo4};懻t#=D޽ǧT avfD'Pm[|ps/ב_6D6n:Sn(g^=9Ke{)#]ʀ}edM+گj(wE$*F6D+v˝75ZU3*QŢI6_QMckbv6ܟIe(ߎە%%KoqŊ ݘPQ=*WR+˿%@@%jHhsNм`#ƕ֕K +G7FsRewdBFf_tN 6hb!?Yv%?+a@U;5d/Xz%Y`_He]L-FfP|6W ;mC5߳ Yhc_l ]s,"~ +zx[n}6Sաv#/41#9i+6^pZp>y ``,PX\ b$q/b8 u֫)iĜGTXG'Ds "j"@D0:削-J N8}??<;B( VzQ +~T 7_l;oIRYW~WqWq#|G@.Dp((n+Gǟ/2v.;k0~6v>go1Ij p5If;pd| PMu>(YN$V|k.'.g`aN{+!8ژT +l H9"~s +Dۜiы)VD$f6>d (sb nck-_gMxE7m~@e9mբ֍̌y UoQ6=-;iբyǫXͶV+*K y6Hs5H07d6H|L{b+EF^S03!-sK275UجK{;'_/%2$nMHP`@`m1]V»tKu$^ ء٦"r`.Cl۠f P8r$`1Au5 <*ur$@2glY+vJCܟvhC$f]8}`=vFt:6-۶nָq&f[&4'9 b1Wq=)\Lڜ5R1]iMNY񾛏ҭmXͤ}ϡS}^*2d9mhůϟ>{.IXt񂹳= `q-=U᧨|Elw` $̀=KY+f ,@~o5:x" c0`y -g0W`;2 +endstream endobj 93 0 obj <> endobj 102 0 obj <> endobj 103 0 obj <>stream +%!PS-Adobe-3.0 +%%Creator: Adobe Illustrator(R) 15.0 +%%AI8_CreatorVersion: 15.0.0 +%%For: (bangertm) () +%%Title: (rayBixelConcept.ai) +%%CreationDate: 5/14/2015 12:09 AM +%%Canvassize: 16383 +%%BoundingBox: 10 -431 605 -195 +%%HiResBoundingBox: 10.002 -430.3477 604.5195 -195.9507 +%%DocumentProcessColors: Cyan Magenta Yellow Black +%AI5_FileFormat 11.0 +%AI12_BuildNumber: 399 +%AI3_ColorUsage: Color +%AI7_ImageSettings: 0 +%%CMYKProcessColor: 1 1 1 1 ([Passermarken]) +%AI3_Cropmarks: 10.002 -429.3672 604.3223 -196.9312 +%AI3_TemplateBox: 298.5 -421.5 298.5 -421.5 +%AI3_TileBox: -101.7378 -598.749 716.0625 -27.5493 +%AI3_DocumentPreview: None +%AI5_ArtSize: 14400 14400 +%AI5_RulerUnits: 1 +%AI9_ColorModel: 2 +%AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 +%AI5_TargetResolution: 800 +%AI5_NumLayers: 1 +%AI9_OpenToView: -453 177 1 1503 728 18 0 0 48 119 0 0 0 1 1 0 1 1 0 1 +%AI5_OpenViewLayers: 7 +%%PageOrigin:-8 -817 +%AI7_GridSettings: 72 8 72 8 1 0 0.8 0.8 0.8 0.9 0.9 0.9 +%AI9_Flatten: 1 +%AI12_CMSettings: 00.MS +%%EndComments + +endstream endobj 104 0 obj <>stream +%%BoundingBox: 10 -431 605 -195 +%%HiResBoundingBox: 10.002 -430.3477 604.5195 -195.9507 +%AI7_Thumbnail: 128 52 8 +%%BeginData: 7083 Hex Bytes +%0000330000660000990000CC0033000033330033660033990033CC0033FF +%0066000066330066660066990066CC0066FF009900009933009966009999 +%0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66 +%00FF9900FFCC3300003300333300663300993300CC3300FF333300333333 +%3333663333993333CC3333FF3366003366333366663366993366CC3366FF +%3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99 +%33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033 +%6600666600996600CC6600FF6633006633336633666633996633CC6633FF +%6666006666336666666666996666CC6666FF669900669933669966669999 +%6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33 +%66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF +%9933009933339933669933999933CC9933FF996600996633996666996699 +%9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33 +%99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF +%CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399 +%CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933 +%CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF +%CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC +%FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699 +%FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33 +%FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100 +%000011111111220000002200000022222222440000004400000044444444 +%550000005500000055555555770000007700000077777777880000008800 +%000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB +%DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF +%00FF0000FFFFFF0000FF00FFFFFF00FFFFFF +%524C45FD26FFCFA7FD53FFA8FFFFFF7DFD26FF835EFD4EFFA8FFFFFF7DA8 +%A8FD29FFCF5DFD49FFA8FFFFFF7DA8FD2FFFA85DA8FD41FFA8FFA8FFFFFF +%7DA8A8FD34FF5DAEFD3EFFA8FFFFFFA1A8FD12FFA8FD27FF8282FD39FFA8 +%FFFFFF7DA8A8FD0FFFA8CFA8A87DA87DA8FD27FF8382FD34FFA8FFFFFF7D +%A8CAFD0FFFA8A8A7A87DA8A8CAA8FD2BFFAE57FD2FFFA8FFFFFF7DA8A8FD +%0DFFA8CAA7A87DA87DA8A8FFA8FD30FFA782FD2AFFA8FFFFFFA1A8FD0EFF +%A8A8A7A8A1A8A8FFA8FD37FFA757FD25FFA8FFFFFF7DA8CAFD0BFFA8A8A1 +%A87DA87DA8A8FFCFFD0FFFA8FFFFFFA8A8FD27FF5EA7FD20FFA8FFFFFF7D +%A8FFFFFFC9BCA1A1C9CAFFA8FFA8A87DA8A7A8A8FFCFFD11FFA8A8FFFFA8 +%A8A7FFFFFFA8FD26FFA85DA8FD19FFA8FFA8FFFFFF7DA8A8FD05FFCAB58C +%76F8276F9A75A07DA8A8FFA8FD0BFFA8FFA8FFA8A87DA8FFFFA8A8A8FD2F +%FFAD5EFD17FFA8FFFFFFA7A8FD08FFA8FFA8A099A85227277DFFC3FCB5CA +%FD09FFA8A8CAFFFFA8A1FFFFFFCFFD36FF33AEFD12FFA8FFFFFF7DA8A8FD +%05FFA8FFA8A87DA87DA8A7C992FFFFFF527CFD04FF99BBA8FFFFCF7DA8A8 +%FFA8A8A8FD3EFF825EFD0EFFA8FFFFFFA1A8CAFD05FFA8A8A7A8A7A8A1A8 +%A8FD05FFBBBCFD04FFA8A8A8FFFFA875A1FFFFCAFD1BFFA8FFA8A8A8FD24 +%FFA839A7FD09FFA8FFFFFF7DA8A8FFFFFFA8FFA8A87DA87DA8A8CAA8FD07 +%FFA8FFFFB576A8A8FFFFCFA8FD04FFBBB5FD0FFFA8FFA8FFFD04A87DA87D +%A87DA87DA87DA8A8A8FD24FFAD5EFD05FFA8FFFFFFA8A8FD05FFA8A1FD04 +%A8FD09FFA8A8A7FFFFFF7DA8FFFFB4CFFFFFFFCFA8FFFFFFA87DA0BBFD06 +%FFA8FFA8CAFD04A8A7A8A8A8A7A8A8CFA8FFCFFD2EFF33AEA8FFFFFFA8A8 +%A8FFA8FFA8A8A1A87DA8A8FFA8FFFFFFA8FFFFFFA8A8A1FFFFFFA8FD07FF +%5227A7FFFF7DF8277DFFA852F82775A8A1A87DA8A1A87DA8A8CAA8FFA8FD +%36FFA8FF835EA8A8A8FFFD04A8A7A8A8FFA8FD04FFA8A7FFFFFFA8A8A8FD +%0BFFA8FFCFFF7D272727A7A85227F87DA8A827272799C9FD3EFFA8FFFFFF +%7D7DA8827CA8A8A87DA8A8CAA8A8FFFFA8A87DFFFFFFA8FD07FFA8FFA8A8 +%A7A87DA87DA87DA87DA8A7A8272752FFA8CA5252A8FFFFFF76A8B4C9FD12 +%FFA8FFFFFFA8FFA8FFFFFFA1A8CAFFA8A8FD17FFA8FFFFFF7DA1A8A8A7A8 +%A7A758CFFFCAA8CAFFFFCFA8A8FFFFFFA8FFFFFFA8FFA8A8A7A8A8A8A7FD +%04A8FFA8FFA8FD08FFC2A0FD0CFFBCC2FFFFFFA8FFFFFFA8A8CAFFFFA8A1 +%FFFFFFA8A8A8FFFFA8A1A8FFFFA8FFA8FFFFFFCFFFFFCFFD0FFFA8FFFFA8 +%7D7D7DA87DA87D7DA1FFFFA87D82FFFFFD08A87DA8A1A87DA8A7A8A8FFA8 +%FFA8FD0BFFA8FFFFFFA8CFA8CAFCCAA7A7A8FFFFA87DA8FFFFA8A89AC2FF +%CAA1A8A8FFA8A8A8FFFFFFA8FD16FFCDCCC6CCCFFD07FFCFFFFFFF76527C +%A8A8A87D7DA8FFA8A8A7FD04A8A758A8A7FD04A8CFA8FFA8FD08FFCAA8FF +%FFFFA7A8A8FFFFCFA1A8FFFFA8A8A1FFFFFFA8CAC2B5FFFFA8FD0AFFC3B4 +%FD22FFC8FEC6FEC6FECFFFA8FFA8A85152527D7D7D52527DA87D7D7CA8A7 +%A87DA7A7FFA8A8587CFFFFA8A8A7FFFFFF7DA8FFFFA8A87DFFFFFFA8A8A8 +%FFFFFFA8FFFFFFA8FD0BFFCAFCC2FD05FFA8FD05FFA8FFCAB4C2FD21FFFE +%FEFEA5A67C7D4B27277D7D7C52527DA87D7D7DA8A8A87DA8A8FFA8A8A8CF +%FFFFA8A77CFFFFFFA8FD24FFBCB55227A8FFFFA82727FFFFFF7D2752FFC3 +%B4527DFD1FFFC6FEC67C4B512727F827277D525252A77D7D527D7DA8A1A7 +%7DA8A7A87DA8A1A87DA87C7C7DA8A8A8A7A8A1A8A7A8A1A87DA8A7A87DA8 +%A1A87DA8A1A87DA8A1A87DA8A1A87DA8A1A87DA8A1A0937DF82727A8A852 +%F82752A87D27F8277CA85227F852A8A87DA8A1A87DA8A1A87DA8A1A87DA8 +%A1A87DA8A1A87DA8A1A87DA8A1A8CCC6FEFEFEC6CFA77D7DA87D5227527D +%A87D7D7DA8A8A87D7DA8FFCFA87DFFFFFFA88382FFFFCFA7A8FFFFFFCFA8 +%FFFFFFA8FD18FFBCFCCFA852277DFFFFA82727CAFFFF7D2752FFFFA82727 +%75BBCAFFA8FFCFFFA8FFCFFFA8FFCFFFA8FFCFFFA8FFCFFFA8FFCFFFA8FF +%CFCFFEC6FEC6CDFD06FFA17D7DA8A17D27527DA87D7D7DA8A1A87DA87DA8 +%7DA8587CFD05A8FFFFFFA8A8FFFFA8A87DFFFFFF7DA8A8FFFFA87DA8FFFF +%A8A8A8FFFFFFA8FFFFBB8CCAA8FFA8FD05FFA8FD05FFA8FD04FFA87DCFB5 +%FCC2FD1CFFCFCECECFFD0CFFA8A8A8FFA17D7D7DA1FFFFA87DFFFFFFA8A7 +%7CCFFD04A8A7FD06A8FFA8FD0FFFA8FFFFFFA8A8A8BCFCFFA7A8A8FFFFA8 +%7DFFFFFFA8A8A8FFFFFFA8FD07FFC9B4BBFD30FFA8A7A8FFA77D7DA17DA8 +%A8A85883FFFFA8A8A8FFFFFFA8CFA8CFA8A8A1A87DA87DA87DA87DA8A8CA +%A8FFA8FD06FFCA93A1FD06FFCFFFA8FFFFFFA8FFFFFFA8A8A1FFFFCF7DA8 +%A8FFA8997CFFFFFF7DA8A8FFFFCAA8FFFFFFA8FFA8FFFFFFA8FD20FFA8A8 +%A8FFA8A8A1837CFD05A8FFFFA8A8A8FFFFFFA8FD07FFA8FFA8FFA8A8A1A8 +%A7A8A1A8A7A87627277DFFFFCF52F8A8FFFFFF7D7DFD0FFFB4CAFD04FFA8 +%FFFFFFA8A8A8FFFFCFA1A8FFFFA8A8A1FFFFFF7DFD20FFA7A88282A8A8A7 +%A87DA87DA8A7A8A8FFA8CFA7FFFFFFA1A8A8FFFFFFA8FD07FFA8FFA876F8 +%2727A8A17DF82727A8A852F8277DFFA87DF852A8FFFF7D76FFFFFFA8B5CA +%FD15FFA8FFA8FD23FFAE5DFFA8A8A8FFFFCAA8FFA8A8A7FD04A8FD07FFA8 +%A8A8FFFFFFA8FD05FFB57D52FD04FF5227A8FFFF5227277DA8A82727277D +%A87D272752FFA85227A8FD3BFFA85DA8FD05FFA8A7A8FFFFFFA8FFA8A87D +%A8A1A87DA8A8FD05FFA8A8A8FFFFCF7D9AC9FFFFCFA8FD07FF7DA8FFFFFF +%A7F852A8FF7D27F87CA876F82727A87DA8A1A8A8CFA8FFA8FD32FF5DCFFD +%0AFFA8A8A8FFFFFFCFFFFFFFA8A8A7A87DA8A8FFCAFD05FFB5C9FFFFA7A8 +%A8FFFFA8A8FD0DFF7D7DFFFFC94B27A8FFFFA8FFFD04A8A7A8A1A8A7A8A1 +%FD04A8FFCFFFCFFD25FF8282FD0FFFA1A1A8FFCFCFA8FFFFFFA8A8A1A87D +%A87DA8A8BC27277DFFFFFFFD05A8FFFFA87DA8FFFFA8FFA8FD06FFB4BCFD +%0DFFA8FFA8FFA8CFA8A87DA87DA87DA8FD23FFA75EFD14FFA8A8A8FD09FF +%A8CA52272752A8FFA852277DFD0AFFA7A8FFFFA8A8A8C2B4FD40FF33AEFD +%18FFA1A7A8FFA8FFA8FFFFC927277DFFA87DF82727A19ABB93BBBBC9CAFD +%07FFA89A8CCFA8A87DFFFFFFA8FFCFFD37FF8282FD1DFFA7A8A8FFCABB93 +%C9CAFFC976279ABBA1A1A7A09A93BB99C3C3CACACAC2B5B4CFFD07FFA87D +%FFFFFFA7A8A8FD32FFA839A7FD21FF7DA193FC8CB593C2C9FD05FFA8CFA1 +%A1769A6F9A93B59AFD0FFFA8FFFFFFA7A8A1FFFFCFA8FFA8FD28FFA75EFD +%24FFCAFFA8A8A8FFFFFFCAFD09FFCACFA8A8A7A8A7A8A8FD13FFCFA8A7FF +%FFFFA1FD26FF5782FD2AFFA1A7A8FFA8CAA8FD09FFA8FFA8A87DA87DA8A7 +%A8A8FD39FFCF33FD2FFFA8A8A8FFFFFFA8FD0BFFA8FFA8A87DA8A7A8A8FD +%35FF5E83FD33FFA1A7A8FFA8A8A8FD0BFFA8FFA8A87DA87DA77DA8A8FFA8 +%FD2CFFA782FD38FFA7A8CAFD11FFA8FFA8A8A1A8A7A8A8FD29FFCF33FD3D +%FF7DA8A8FFA8FFA8FD0FFFA8A87DA8FD28FF5EAEFD41FF7DA8CAFFCAFD38 +%FF57A7FD46FF7DA8A8FFA8CFA8FD31FF8382FD4BFFA8A8CFFFFFFFCFFD2C +%FF8282FD50FF7DA8A8FFA8CAFD29FFA8FD55FFA7FD7FFFFF +%%EndData + +endstream endobj 105 0 obj <>stream +k|.1[ kn +sv\X|_O7ujgϿ ]\L LEZTx4kR&UoֹiUk:fńKj{ҀKgo|@S5loPX(<_u|f}¼Okgd 7,wyx٦hAlqp:\ahZl0&b:-miVcڽ8o1:]гPT.""!57gi1}^Vg4@cHSs(~0sq3A_f+۸[e[+PXWK䉜0BIsDZ\fqt0bihj כ#q +SsH96HhJNdHU玪uVP\Y< +kř 01 pXnYe:a8F9edQ*Ppz ZVsRlT\iCPJ tRz,bƒd +d! EƅE+vyڲç5gIh&CK026  +(XZwȕzVJ?.,U*ծYd7V!BpD{ +Ѻ5E*b#cn!fXl-\?c}?]9V>'aδzMnʅ]*3Qgh8y;%>riȸ vRThRRS2N1t~R,PF +P6tBPI6lG)^w8]_TwqW\Ͱd$60>+KǯQu>j2|.=E5 `Cb^R2  +%DSjm9m  +gFK1nj최nW*Jp,e!\B/3*7ח\d@7q xm 0ЀaM`P74ZX.K +pbN;%C4Fn_G}x~v_,l~:5%G +;xH_);f6EXjQ'F4Uvt:^eJ&Ӿqޤtn O)OW}|^8L7߀7eM l3 ;DfK +oo,Mkſݧ?t# >OaqYږOns y+uEvW:k1$Z>8"fDcX]~lTAw-/'i1h69y^޵Mp3uE@ +u¥@*έNAbOTaUO[0Vv1jw +usvv[W$_y[K9{Oʉ~]aA㲥 YJM7Gv>?utpg[`vakv 4bN8uO/ٯ8wmi~Q*(|=D咺kNfV\xf<ŒZ?ץo=[n ?̲Zxkc??*O;=[;cb!+|⋛]dO(>mkΚ&l1-DJ7)J&Ь2{">86wy`{x+NG~E /#1OS{.^xy`yɂ]73]}&M=+p|4"73 +M|Y]]Ê"Ec|@~}=¹+fz+w92Oz!@m}gR>΂]YSn$v|na.QD3*U/$"7#4sӃ6q!(8yڥsmK(] ڡ,\G 䛕y'+{9[ev_h߉"r xx᪷2S]w#EbphU~9`N@[bo{ѫ9+Bzh)ek&Ρ,Лrcq<[qyft&.>9 R$\ ;hq\Kg랽\櫪Dorߡk՟mpRALlnw +6:,j6Eݗ雕FΩ ~HOsAWnJj ܌ ssaֹR=66z'L~F[.[FoI[eNڃEa=펋IOP~4g=Cˊ0vl '$6?gÉ%15Yq7#qB8[%QV|e{-6xi9ꩱTo o#"]٪D4œ.1=v)Y6wP;ְҎ-r`K)qabia\7$Wpt'jaգLPpn'U:ՌnK,-*Yٰn֔vwlk~yp~o)7UE Pܓͼ3ک/Q[m/K\)/L=a_|ֿb3*nsiuK'22OiE>݃\H~̐-5 +S㍧ðQVpM +শisi|ݐzGPNv],n5lg/*8l5P:I xM[PS߶JfN xWT&ϻ>YT +_E!ܠq5@^a]a/HJ/ wp+jnj',k$3D]|D93?qk3\q6@!;r8R~,RAӨ^oXGY[4?2_TџJ8 +'trQ֌滛j Xٌj\pۙpcmRmФϿN7EUlBUo,՘ŸX37]NiMRiij6R )(.) oHXPB%Teyun'zn̅՘v`gGL0L|\|B>X&x0Я/*-*]Fy{ 3\~׭UrENdRlϗ<-I݅8ݺ{a.B*=yaܨpX^dI$o1]K?yu~JIUg2>%e %ސ15b?yQn;2lUjtD4:3Neᝡ)gPʟڱpX WGn,?L/w+ЃQoSnߢ2-~Bds:Q␨1Q7|B+$;Oal yfrjkWa+RBq:}S#i&^,ev .j4JGldD)l92˝!T65T50 y4$9l I=D7a8K[Y#T?ӷMf^V'7t#&lͦXWR7b)0Ѱ[w(D¡[<:FA> b!Y))q8gnyԠ>i Pp蹠~},8/7<2mpǧ6=!5ăOHB_s 'Nz<.6LX2ay?T1y’~::rsnNra\ҁ̖e)c묕hX K1a( {Mf5M,m+Ƽ~s7he⨎K%v G1J@pAx΅)iʛ{@ <@gE-PS |b8E;=fZ!ftibԥV +7͉=dc::M+-yϓDE')E AƧ/]9kn ^ LVp6EGOhS^qj;0 T1v 5+7=W9m{[]e6K!v +*j 0>QY/^'k?:tv#r؋:z CxB#B8HhܙyAg pգwXt=RZꈻ%x. +D'ME/ +T.Kn^>>@(H/iAFU dUI $^n/XY@iojzĹlv5hF'g=fND֜/9"r dCPK밹1m[Ʃ_;q"9ǀεfyГ{u&S hQ?"@WW@WG碑m7sWCX?;< },gZ.St?e;O0_V m!p d\KR.p9wܧF[<\qÌܺ{B*8Z OQxI> ?xY{_}_ 4} $+vm{gybpIq|RS'| +LJ?/O܌c%j_UKY2.H;ȴX;QVȮ>&r ?ٯ[7|仙&v5g][6rksl|3> }䟨$b EjL͊Jˇ[}]nvm9&k/:/ dYhٯsr_}O5R ,ѽcX :kKp:ָ.iVzg_Gyy6ߎ`qsg٧[5fفNuP9N ueZ +.o-m_o\,ُ-#dkd8gf; v{ͅ?p%LfBZ%&Xv8{ث8X|pJΣ|7_w_B>eKH#_ v~'yI-ςݨŹpPkӄd~5m>wb| Fg Fԑomu< *zHx^ރϐJf-I4'uK)ZI^"9b;V'з K6κm7.r׃C0nm/r~֍.LNH$ފ d H]0CPy 8,ۃ|uU.E}&]/C=~;7?RC ƱVl.R ZuOwoHIyCTZ$\F ;IB<3tAeA{$e:ܱ} Kva6rsh}1@ZɈn).JNSJ=o/<UW>U؆CfMwzƅբX< jQH o<¹{N1^5N ȝIsg>-ngN?fBM<6ޞDR; vkt~B.Q/T뮾p2ihfV01Hm6VlLA;j~ũtu:h7KWg7 ء6.Ω>:W,N0 w=7{cffu ȯ_lKl~h GV,,>85þb[P/-D֤^d=ۡFu)M {[7=+gzXOTXq38ܓʖ:t/17mÆ- 9? a3_Kj7ժkM3gZ+e +yT{{' >ïܽ*N~GTadžF~,iX'ֺTs"˨͋+#WDA>"Z9esSlg,QfjԬ j2_ڷȹ @~h6ݚ&5w$mӌsjVU2SDRőgDMk(;yзl `RjO6Qz#z0?\6'f. +6jMoׯkjv{|R^sܹ!G fsy_y-/X=mb?H5Sw.Yh QH@Z  # Sg  +ۼF2OR +yn@?W-%%u3v"i1+D?T{.]sK@8=KgC@tI4\;5Iq9t.t^_+r 4o47r?F^/RLtiz>[*NfȕNWm@9}$:A=`8eIc/o p¹@)V +!]lFm5QTZc@{' UbgMߞ^tKF VzVw.BZVDn!u@ z4$1_J@ vg@Dc;d6sP/*Z}Yʧ1'L)o_?]x<R(TlJM7@x PG0b#ehJ3f|Ԇit9'Z1+l`K}'M|ҧc}Ev77+Pt[U{;7sof,|{9fi b&k#?3tz -??yL|{B|w 9Vj@hU Fsw{ +?,Oӂnlbz3ʎ8~ߓ(w/4&q'eC_l|Aׯq>otV>#|au.&myT7+y(Ao y|Ț/f + 3>b'*Ay?.Kuq;Ǖm?m@5yLk +_r2obb:M:}$#37c;.[M[Ϯx: DCT;Dv7X~3eGlˠ<[.f9ӂݝgG8}22>M^:=d'Ny[ǂǛdw?o^#-RZ?/y>=F2^~b*[LV*]8fl5+D]ʣOS`Gc9d8/kl䷗~AvW3ڝ%s^,[]~U FCqgwԊǤ; NA,?|s3O\c3,oUj_i Rn8,̘}6-xz/ߐ`Ytνƻp,Ӿ ~;nO&rq3 m]izU(JDlg ܾ |gjsewԷ]dCӽztΝԨоrOUIxð]e3٦^fa_hj}\ ?l_|W6/_@oo:]d;moB> ?Pc_ȬSvlZma?NRoZp݀׾14"OrNv|h;xY?det~Ԙ[XD;CYOs꼆}G{ǵb4}Wo 7^-τ] ZБ۶q=28;^t|R +jnkqE*635`p),،s=){8aʱkNOX)xss5Vpn梦d9GtK?/x`w]dw]ć9dܜ cP>wȍ^FltK z.O_*c6h59L_uRj6UP WfwSsڳS׊%U]KȻu{f漞}\GT7 /W^D ) 崷Cu!B z5Q0] #Z=tCr!y%9}=W}Y KX)S_E3;^+snMZ꫾NheyJ`Ղt$TKH1*ٖ%J6 \yP2?QeK:'Hf'}&/r_2<^`3܃9U_1s2+uV.ӻb9J~ަwD2EX |^xw1Ǽ1G0":@9YLoznx;#8L,kG#UNm-h#_;<5̥bpi9!'s:$eL7? [+<3>9?*QerF ew -rBO>)Ė, z _Ͽv:gA w-r5Y{g sqf~7yƔLvUЛ,9<h (%&ej*/,ܽ @zN,=ACB\*r(8~w[ ye*靣yq0dRmmNq?ڝxSn=j jju)N|5կsfA &_Dk,QV.)Np^C$hآ[I\px_?6Ϙ=@ \%5!K@pU "s"klm}ԉqO5vuW/ |[lu4Uξy!;X@k %Ti>Ee@A+PPyb4Whס]K}O瑧 jYW!QQ[%]E<ئUy|L4+<+ ^ L4M1U ׉ H6cЋ` UَnSO Le]Vߊ?L t Bž˧ϧdg2_xWRfJ˖޿~9އ, {CN4Ko\Xi-6DQr$&&s}{o=Fw{rhU5'" _ 88|wNJm{odJ;<7t uX +y>UR(UG_'T#:1:Z_9|R|imgRo\{sH#;|R'/np&G^~on.l{b-*WaS)S㗵r.Q?.06s}e3?ƨt⍾7N+F6V[z4M1뻭` +eRεu/K>x-!?.w4Ur6a65 An:o}nzYa}o񆫀bKmezX6y7s;ij۝QNzlo9E^ ^C/<}Fa???wLGeu"~Fs߶];W$ۙuC`ri?gXDl+>u>h*49PCx{>i,6<R.S;cڧU?ڍrX5gS{eXKcQߝuk,N dˑFC3gYFޯ~L]KO48xA<-:%u5I\B[L j$*#h3CxH>P]q'Niz1Ƥߺ/+<owv7B+oµM[vVCӱR u͇n*?̦0:Sz*6 (B`G{A瑏BǾ/y5*E wuW +N;YPmxcΪ9AܲҌ;wQ>e{R4.i!d+}YįՍ4c#Je_Lx|ַx֧ ͝vhmx$ [Q+#lԚMck^IlY4zJ}e>u)Y%Y* kk:kVCWMeMRmaXxxnr&zHv…fQ^N--8ߛs9m፞ 3 C u_åiyQ[nMǺ̪ٚ1y5-`CT#и׏L7phN޾|ߍ4_r=;MZҝ64EZc[mEfF\UT]xrZfkz'jtdqV۾ˆ 4X9 VӲ{ꇧij[&wkU2)t%鈺y$dh܅!;NZx:(`n4q46 t+T/hCkQ~}yto/h]+!4+ʵD&fRYh J?>7㦨$ffkC:(4OjWW HFl!JKULB.NI_&Y~gvY޽2`>Z{%/ϟսR9JS.e"M0dJe殮Q^߽jZLui e i,UŻ'_ڣjc ٳ^Q]}T^/LjjV܇]d=)5Tj>OdICcRul8tM<ܾuZaۛ;Ml:tHP.Cy|=mQ+Q-\E|kLDf.JgÙt+ DX&˕gi6beL>/V:okRtC4^P\V`w\k={hPH:o;e WV?r^Vc]-׎ZK}C6;_nD藢a!vxPlLC 92Qlՠe0 ,7,C5ĭ܍4:}Ҵ GrDm2>Ξ%jR{3C +!S6'qsڃCY7ExRY /ȼ|X4˔3kP4Y:ޝ{XiHJvˏJ\kA-ZvwGba㑌DP,q?ZwvAarzcBx.ReQni2bЧ6Ƅ͸C=O`C[l ;rhG%9"mU]Qfo4Jƴϗ!3bu 0;d7"BQ\@*Ub~O<[rgVYat:m F;d ^61ΩO-о*x!q<?$׵;L_r7btNӇcmEjwOug+yMޤX (PK$ע~8R:f4 dsa. P +23< ͷ_(߁[/-qD@ --rLyI9)ظHP/ZDB-\Vq@ҖcG͢(䳈md(@Qn6&b~=Af_ 4'NWN=A.d8pSkLr+~J1scP  JWsĝOcr|Eh43tXBThS9z|59l1']rv\~+Y0~E ?ekgb15 lr|zL"b.S. [Z휓otKc!`ۣYhϪ 4Ŝ Ig3 y9KfIo?BS@̩*\A"=/ "+ #}޶ @Jݬ՞ϵZD]q jhĞ/gA-UtL5+Y *dtZ5X6Z'y[x]>$2|Depa5xI9 =zlֽvnPR%l%^vߣ W=vL,Ne5 ބBs^D=@HI" +jȖBW1 T~c߲ )_MwdW5 "S\Z~l{S"Yp"TTZ y-)ֵ9 ī3@z HG, +LlQD6@&.@JO8y[xEq)}7iES#Vv=˯)CxnHi",iNeFJ9;5Ƶuw\U2*0%>d޸x%]$g;|lRa|\߭O9W=݈j[Icu2񿽼?:yJg㫚  Ǖpǭ"Ѩm6cu66|>W6FB+KJspGݍɥ57'q^߽C{,^o| 4LŭGߠs Cͼ:Wollh/:¸] ^9,6pzL%('Y2aMVD61;qܧNqwweXӺ=y<85#Ld֞Z$F~a)'ʭ`ѧ>4A³#Kf<.6;ctyLҽtˏn;wo^9~CNve;lvhOID_I%zsOTR`}{x`f`*;,{寫FG|N/POTG.,fl {Y܊Ҳ/tۤ} rDN$ѩw) &&Չ =n;S>\j BaiyYpVXXҊZj˦v ?M4طfed*=Y3[+{zXSR־x#J/BN/|O[DzN\h.r,ynٌ](tX*y=6.CEh'V ô^aa]'ڴ~Ԡg] ҹ +\`:ODrوa+=<c߀ymLőr}x`{rڵmX5vвt097Kظ ~~X9T־"R"Q*[5IS6-M׍{Hxį*Էq==ŜjڡF0Y:Dr&;JlžE. G~urXY~~6E٨j\ wӪ_#:7s=4*N}u5+OZnɟqSRW}(Y3*'~d̽.lj9槛U]|ǖF:JӪ.a\73c6YJ+&}NX>RyUzT M8jfl#/q<Ʌ1 +Fcĝ[b}y$|!2& mTNM]vW58Nȶ7k6rhnŕSYomDq+NOY09u)6PJW9:*.LZR*ns(y.*M ,ww{$wMbXķ2^P줵6tb8tP}W'ě[{UT^'NE1뗒'ol)3(YeR:!T=[RpJr͢gE.™ ~'*e𮗛Su}mvߟc=W~32Zy"oeP=IM~WltZ@S=0us+ %"6Vr+i"-vb}{g hʠQLWM3AvX@Gx[y2[y*24F/;V*R@u[<Zdz}M^BiBex9Q܄n\3P۲.ślʹa蝸xdQX#ϴtن(13Tu\pZ@@#jlduj5+kWs;#oISjJ]te-uFœ w'n2_9^~0Z *2#.3xQ4VP|HjfMv=1*?o'' R&PN1 gE>YO1͜+є:Srߗ'QBY0lCܾM>'oGqXcWvf`윕3\Tԣ(vĽg 1YcRAƴiA}Ġb 4 mBChv(jcRkS诹̛DE&4dyy {E>h(/B䕠sЖp8!u*܏dM?neo%!XvX AΧߝE`,@kc󓦲/gR|,cksY/^eڤۂKl0:pI^7Y£1t>]^A"U$SBcD MjR؎q3 +yx6坋;: +`ƪhOÍ3C^v|^&չ-~巰eu. Vߏ 8|EIzyJ3LMAXP/-tTWUB6?V,t.T: 12^Zv?s=o–`թ8Ye23A,330Arqdr83o1yYKYiQhvOLx䈛u EXHq?|a?iI0ϳj6tM,&?3fՈ(z|\@D Kh7WWm|ro{ڜW4D]I&FVېVmR uLݒ*'ꭌޕL5Mk}Z}BDi"(O> (&vgm.N=Z +"yz[gisS7hQ6},q̘q0Z#ΣlUw-;^J6H;Od2Oخ|OdXL2u-P` HLRf+o``?O|e!ec(xݻڳHFݘ>.=yn1IQ3HnZ^TRv"5runS O<ʛ}Ϩ 7[5"G$ yf??ϭ2a SO.>uӝrJ{/BdAHϻ +a(U@-@ n}@q{ ȬId/%‡8c@, Y HmW@TLjEb7+>?ŀ>0L[^`n>aW|"[lXFQq!}@n3@(ve @ (o&2/@e: v>J/@ LJz]PʖʋZ-8Fy9-_^^|?*}%e@ЋJCo;莒JUy9R\7KB=p諷N9tZGJy|Ӱ8}D0Kp!X+~5ޕɔv~*]7=T~MZ +9R{VP% xRn*s֌[S>;? oV6ɺΪ$ȡ'-D͸o:m{`oo\q㆐m}IIzE+T\BjǖOfə;-V(9*dۀ[QsDÈi:oRl{¼lͺ+u)Z־nVڙ\ ڗjؾ֪&ZTkkM%=Wrո#Ks6dq͡skiw8n\|~V`UZ6[QOA@b2KQ - 9?W*4p;q5u$v\sWETک'}Jul6qH~ϒfz>ڶѷJvRK jh* sՖ^&ru+9WJ7 +nq 3[[FaTbЭ[-ƆH7ܨ}?a,WXbaOyXw)CڂVgC~u\rP*U&R Ԗkj E?qm)ƒ{,˓{m¥ "xҾQAQrFnAXӑدёdĹļw;DUz6-M84Cq=[VUxC1wrlQ:^E"2ѯܽW:AXBo%y\xk1XQ_TfMmGړ+E׺(͸ yvJoܽ WYIsxRrw >Ar[mMYίP#!  O:8yLPDF+Ǒ>g1i{h k7 ]Qw]7 xrgܹ,?ܦdN 44EP>y9X1R ]twqLdH_$\)HKvb;@"z-@ᚋWYN5_ rrφ)2ߛn.M 5fu\225@u쇜_pJC:jHdtPEY%2?T3 (1 (/%_<<@>@V8Y:zʹ)Wm.o cETaF|wbԢ6a_]Ak\> ,xQXVP Q`%$mjDD0M>,vBHׄz%α Wf8[l +KA~rbl)2'ժT)푒Rw>%`Zr66E2.8^q"' HpmR@{;Xg8{\ y" +; ˧~x{g\0SO}3oF~*ُʙeWo"G#l4:n#@ 'P) + ZA?L\Wm/*lQ~k/23JxClqֽR"-`+h oZ> })}e |d]`)om@a$+x[Բ D'(Sz+c|O G- +߈6S3Ce{?w<:H[@Іz(&N h% 4 .n3// !,?O]Tlgƥwl?9?tZNZp6 |äL&TGJgbY,}0̨̾Ɠ|CӲ3/0Z~ E{})in -31 +"4/3҅W͔W6sp}N+ɓߺv].C(K2Dο vVOMrv"贘+[uKYo`e @NMa^HgӴ!\0 H Ԍd).?x1 YsY|7[_?)|g'QTz@l@< R+yS_@z$K-`t9'^xBy}Xc}{J hESg>T'?fZ8*;5tyίK|{OcWhňݞ47Σښ9sчZH8\ a?mÔh!Sp2qafe>Ѧ^c\ h=la|ZJBd9#TOmMdGRRzaɇc\>vQ OƊDB>VuBYGO W +9,3jOx~|үxc_X9k49M K{ٿY9Pa!x\sn{[EȏfJCd<:VCGI c#@w#3U2p7|+B;q0 wz'+,v"iijAKLRp JpFpf܆Ìl4|KʍAT=bcϛ\ݿ1_&e5<w=5kҢܩv KY;LqKֲkgu"\?7'{jU`^wlrϣb{.=:~ue)4usm"YA6ܲjݻҌ{ۇ#΄&>i{ i:ܾyLi@?>=='B!FCTlRɱ0Yu]ҲrS:M^s6k~^u)uZAM[U^kյTl}\)7C:rCZ]jmX5l;om%j:LԤΓ_4} }~֥K%[jk5U۬-[iGk>/F4h [Ɇ-JJ*#w1(uO^ˬzhBn/9]Q E2u͛lYB_ ϑ {*g`Nju(F] 굪9%"Ǣ_R)A8B:mݴNu5!+~UAC[*ވo=+:|q +&w,H,IhZ]~Jq(KyzZ*tPQ>JD@4ǁ]kV%t̍\1S*ŽjO(_,&-PQULiKj*:ezr},:P*h[)]*& WJO-|p$c_KasZ??𸺼;%l^usOrA`lՐjKm۾ɜK{^bF {|Qu+6as>a,B{^Ϛ]z_}׼5#YS;1mtkRU1ؔxfh҆0$zy]{-"Fan8EĻ!hXٿD+ҤMQ.X@\ԝ'9XjAΈWMx-\'$_d1(007S1u:}h#hSjh;hMɵsVWlD{= 2zUD&uP1ċ- cB1kk.0XO&?sU\ڽ}" sپTz^?î"S = P䨪pڰ-^tFZRsfξM8όG)q&?7-<b߀b6'Σœŷ5\+0!uRJy[)SK]u Im|eii˸x(P6|AMr|ߖgϔM[^߇IB.-2n3Bk{f +u~ H>Qwf:o}0%B*$"8Vh<F\KYi6H=[aҝv'#NlTa9;Rxb>K*'T }3Mo,ZW0+˚D-־yQкm +oܶ5uy5Id9A48; 3eNcxOąGK@7$wFV8.Q0n;fS_ӽ"DH*FAuLdL9 \Zdy%Sj%;LNZ8 (gDd|kHr6HL{_M6"ɍ%W$ P>W3@8;dWϒY< \7l-htrY6fpԤbI"zy!4b*TH6i@d1twԓ":с,k@[( +d?[DP[sw+g}4?|z 9\ )UM5%0Jw  {5/t[I,*.<)9w~G7斀kJ= pA8gbH~REWهamksF臞T^usިZh +.,} &یʚ?dH%+ ItoPg'q"ntq ~҉vԣ6_K}ۨLVOz^Qjw@o?JDE2?5CxE}Wt9M HbY@"E # w 3"d "M'6DԼ2^m_Rl()rb'?v:^L@' dS Z7!@; >7ή:1s|Ao1(Yx?;gU B܋Pk*4u;u#w}>P;Ps{{-7FsWȏ\OjOS#_$Wc/Y`106 ۲ Ln, Qpϝ\ş |{"bqY?mooѽz'ɟG6p߻/`Wb ز:?\p5y%r'Z$ͭf[ +*v8Q ӹ= 'kI|}C#Mwt܈Ooę_]+rܪ) 3& C1 P R](*Mr*ER^ux@+BeO4n?[[,vvLƫK7]ji}tyxΦ./y[i.th% 5N.jsbocDy.=Qkc[{7O{d}1f/7UoVޜg:QfjbxI*U)DkؔAG,p#Dw@ +c UyRg72&oP[\xh{S9 !o\*%°BޫO%ɢ+ +.pB-CN(GANz}!kwϩ5׼-F%'4AOtrN{m*_KrId&޻a="A\_@b3R(j/eTZwSi+t9Kf9Y:h`<0Tr?ӛVkگyޮJo_ +i?;I +RHVyT{M]N崰=ɶ:PTx2e;+Ν{0+ [6DSzQk,CMozsQZT5{uT,S?3aBKK5oJLq#yoyɋ<"MZe24~/~}څbD̪>L|IF=uuS,ZV0z`< TDTuUQpș15Vs㇨eBHd~L۳1ưNF׻TJ W):aځ;NeJW +rv%nS]ڢGBM&S "L}Zs6ٛJ'Ny@BM<͂,<֕j:4>,a7!16kw,DR/GfSe!6"АԠ,SGZ:u}/y[|L460XI1AnLr9h4^;ؕ1XsR&pUJ잿l~ 6 %VvXֶ RW4f4)D*b(.1 u$&,If3{l LSEbː,Q* |qn y yj@u\e*ez{A/9yHd뎑WTb˅EJhd?|kzpNseIZ+|O*2.v^oA53Q}!ITbM׏c%&Tk$m*U]o<~b6ZGDOS2 'm+*h ~3)teT!dđJЄRH xcQT1kM |}ILnV +A!!,@Pt k/5KZc]w.V}L=mȫ􄗄H -(=3Q\؎I)u_8AQOSȨU t$2 . n*wbV'DXGyZq*ؒ"ܯ]LJNڰyPS͋MscebIq#l+7@Q%sHl$Qdr ]*  I_dJ2i #B$n>)^U ]$PrٵKaٰJ:y,myTb%4gl$N<Ǻls P8r|e @nJI1&2TPʾ\Td:}@ݥș;տ [ʬPc9ݥ#&M}#'@Bu2>&[ƉC5e@'v$4j)f}h=Gr!u@H!d':UT3Tx;$=zǂ:<[FfFYtOTXYwkڴy8]~2IQM^%z}=]/Ks2.:\^$R]ZAbѥݧ5uy^fI:=LW4kf O򄒖sXRDt64{xF +r1t)X`uؔ؊:yJ|@ d,EYZRsi&54TUƘtU0ܙ\IxoH蟚~a47cr#H{YyVpIpv z\2r3} \7U'v'-FCޚPY%O% ă\tV&Uf+[2&5ax}z#pA?o¿BWo#yl oAD[Ceدt}ԀL7{!FWnwaVc!o^Xɂn~Iﯡwt$F}V^` `_g  J~`^A~lݙWB ^$]~~ӽ PJa}X2n&?O_y=[<$$O_1i}11cďVM?_Z4A ~!pP>NF8#\/^;S̱O' +nɽ\ާTm}ޔ>&%u{͢/Oދ1搣4ᖴ?&pv p ;RZP!Qp0R\ߔ>r}Lu -"{E/bԄ4:]>G= 12x{t +YR^v2֮dZl*chn#څkF_ +а)+_5鍌;uvd~WHJHVa{gZ[M-qn7kR׸K~+MYiQ{KwaM_Kg[el> *{Mz,͢[ l7XzJq/n떅A t9G}ЂW@%ȽF|*w6ngG0Sy]3[TQ '>i]'åv=N2fU7Hv>+n{u4Q蒅`<xaQmɡ!ЇUMsW+*nIÎ_s{}_'hHjD'$l^?IU'ς&zvιl;\Š~LRW{1oMCo;1Me?v8?CM$i{Gy+mJ닣#'P3={Zgcn\L _F-G˦g*[y* _O U@P` 3<)(BPDT&:~ׯ['s DE3lIYn%gNhYN.{F-O v mX|Xܛnf9L )F-"k\ :vNIIWWt{9De[HS/LSbQb(Td[i;vn +0I"v4< U,TzFTOe.Nɯ C\}CZWj=de +z;NrWU\Oم[N.B@cqWPZGҼ*wyU# +5齝M5aJgddo4кY}ѽ:H#A/gǻ.y\cx/;IaE$HR3'HQ.wm3x'uD{Q/mDhc۩p. o+:NٜY " U]4ų.y.57rd|71=)+'Ň̟-⵾˱TOe݂+BQ͎D$f IbsKh博{E'b1Ą %{^CB^grvJ* {q{7b)ͯB \p:ε]} =.Eg3Z J L` >=z4(Lg(M {:f}D^I͸dN׫2n?rΈ:rmy/(Gi7뒻 g'O;bc2OrFMn eH&~cT}%c< 'M{N:.mG,黠Gg`5㟋 39+R`Q)7tt޸e15.oH{D + A*6NWfgk$`7vWta +ms|Bǘ,o\p"a\KM3'/k.Gk;og=,7t&_7>#*e\P籊:2toQF4Yɍ$:=r}|c?'7 x{Nizd2mp*ή8i~a?߹n|f&gbΒ89vȗ/lmL"i?#$|DX"9$0[0zu$7 $ISy|0#9sX!9Sdy̖W*f%!|B}f`?j¤؁*DN + XQNFR"ٚdT$ + c[ q切y@) zs{=I$Z&oӳdw7kA9L;Q'&6'V .h'3Ij5<H1{까z2Nv ܂6AQpj"y7&OlX$@2 [hC|ߤ &җ^ dYoY{}#WyyQ$_eQfq%ݷ9 g% = +(yGׄ! "j0"C@6 ̂T6*6] %1P7:}am;Mq И})/7U3O |nBh@]O@3s t=zqr!\^[@Vo@-Щs +nv +hfZY1N|CbjlӰW^N& HkuRQ-7- jg@shd Z`:<|ON")L<*` +0 0)ZE[ÁGĪ>M+ jPTS2e,a6dfLcE_=3?Fi7dTr`,T +jV}R-L +Z#:cžu{b[K #փ!JIJڈ^@w!l#SՋ@|_IK8`81nsL|6.fn7/+QG6kPr&ta5N,ޢ41|aﰉ/$|ulɛӀܳ@D Sw ( kmC4s\bjVC~Yi:Hg˰ xBqh/:lZ ׿" Y>R1ax +ґԿ4T?߀MLp)'W#ϑy<ڐB"u o ;n5A[i ԠU3@|F@>@)-JG#8҈._/o%Bkol۽ L8C9@u@?@?{cZkW{v-n<%٩C@( ~7-E<1eBlnkw*vkoyn8Fa~[2Z3=32Vk5_Z Jf<@cJ{v{n\xR}ȋҧHι ZM#^S/ڳ][fMq<鯕y{jٲ7֋Ww8h6G_lv\- ⅐ 4-~cF A Zu`"¦7rV&5.;l[ETMMf?+a=i 蘯#etv7{޷;M]bQ1n /nPB!!ua,zڄ8;0vtՑۃ{E_ ~vy/V>.1ᯝ<~2pCƌoS\Kϰdm:ʽ֚lڍ$Г?}*:1Oi;*C+bEsV([ +gbY%k'*OThl|ǯ,r)$~)}§ }S823Vl'PI;g r6XЙ>qI#^E@7x iTiMN>Wf$`uُ7ÆFe~vys:~/|@!re#qR (eQuu}j0,özʹ|LAqܔL$ƧK=i1`2=C9H;Pj}ք:ɀr1~~`PZof.ݠGњ f6z(sNP|=^3nz2R.+J J0pn85TBA Mt J|D +5!mWg~UcJMo Aӕ3#eTǢfјԽ9'-%;5 (G"ez>vPU#oHcϔ8L /î;#)Q'Ċ5pUڳ~fFjNۘ؏\s.ڽ/*a \h]>EiD4DD+UDjE*^ @H}T :&~jMÝeY3eVtv߼)N{qfSmMjkEsyۀH4;Xs@|[@+ ]ED"Y̥lL3@`ã9< Lf;ukcv4H?GJZAsObqcEHg Y| \{ !^]RH|R@ P*:-\$dV;LW/@" ץDΨ Ȍ2,֡Bgj˫!NAbqg9"ol%^JS8(M6Yq%ji7@m*@3 #E]#tҋbh_"::z"WPҊs ȑX-4.37*'9lG;?DzNÅ\D00F8mڀ&ɥ)0Ozл }hr|.}5o2?mQ 2P.ǯ&OJ k?vR.X-6l^.91n=p,v-wӀu>`i4*sLKapJmgm\4O!S) 694̛__AՀ f_ <Dd +o[ C +r% o"U~nR6]*& KK>bTpjwLbrq1Ҽ5 d5;lWk ́Rȋu$u#&šaEBWxD +m_ih@47@ $ʪJ:L(Z%duʮkzc6_^XѯrFn`U``iMX,.>x@O+%Kc`O %:Uο0+5Akot4fE`J-`'1p28=F= ]fN l{NMǔ +?o'7Fjgcʜxx㚽U^~3? tF >fRX/W^hJ8YB3*DѻR}NKE 0|1eoL-׳21\5XvxxU s#f 5v˽x[ @ C~j +n'f>BJ eTu JkeE~xS\agFt4)n2KK?M"\/MֺS;,ڳCoX!!Ќz|跰/o<'/{Y#di\q7ԫĸ2,RyWuvoG#8wvoY('^k5/UtC k+\ƧO@ RƱzyW ͚>jm*G,T=T}&"ԭgW/,nRsFuwU*bZ ˺(|Oez/KP%3c,\FWTX% idԜEhfC Bv!H-hkPץEv*/rO9ǔҺXKܡJA*֌YtKn4};99BFQAd}nʢ{S^b\!^ިNq wL:o}3qE?Q'FbO"sTm˧OE>3_J rNc!gjl@Y2גvW8y)AsZSO!R ;j?oP)zy>.BD^WwJ:T^V2t>^5k[)95w8.plE>Vۋs˖LŞ^ +Ӎ $k5w3lwD'ǽ‰yLa{<-i  +vuk}e5fnT)Yw/ bZ6*} f= jCNc y>%Swn>N ! @f;X&-]r IV1j钡_[oIiZ8,ä>H[Y$.KDx Ee_7lHX3e/7j)z_f}i6;i6>"Qjb,J5 VuOO;e\bmir"=E+_h0g̀(("7?a^|iPX;y"dp)41y4]蓷?bͰ0WAyX>=ީݼ0ʹ8fs}MFZ$KPK-ﴲM[tN{z$5l#k%iDtj# U2[_ζ"^Q6&)L-LgLko;reto&EëZq%8|۞z̰ņNg5l\‚KPLIɤDN:3a\21Dor"=`C$ Nn +a5aE[{j6 l)4]Nyjj:"\HQ$Z'I/mp={R,!}I,ܜ-j܎dE +破0oJLLMoW{asx""lmd"%z=fW[pBi[gdȞ2{%d6ttEmDk RqIzMuUʥN5`d%ęz,DSC4,NMByƯbt)Wr!-:.wY c= D Ͷ\C11uti,掹>N8}ZFIY :%2o0i=>bSLXش yvB>ޚNyM#o" pu \ TWWGzjGCK b7ӭ(:E(uz?,Sb/>K8 ɎFnQlI,sW(4(_Rp0# Djtُ1 $rgPN2J.$)H$/nhu( ktg.=X,*U2[O8<用I f%p7]JPn .E +YGjs8; 4uOG۽-G+]Vc$=hG"61E"bDX'!;?UJqEɶ ;Q8'l(SzK,1XŌhqr= +<iS$HHԦ(-@N1P@.A7GrDOn + p~xX8־6"ܖS oh7\(NIwmK1K,Nq jaTȝ:#h5@n]FޗHC{CA7 A G~PFOAkARADA/' ~[٩II4OaGawK]@_Es+c!C3q2? h݃l(Ad= Qh܅2oZQr#|Z ;1YA $8h{KT? OƩ.q/b%U7u2eVIG[G: ]iEb1 6\FHo BqU$©~̝(Vy,S y,!yCbu@#1F eՎXI2V7 Tk2=&ė"\o`;@0DFD04% H_:&L!ҝGxɦDUD,To6@bLh_؄zWB%mawA/NM,nQ!S!R-ॲ`E#أ#.фBS[[J_f k*:)7"ee[T7qAGDܤv0@ GKb4L޶MW YB U`zw?)oX?, ӆmE2h&BBOBg{jejﻪW}WR2_v)3RD)KH!?t-MS6EX+l5y6m8-,|Ww8Lﷵz$}~/ΛH]"|T飊PEj^ł>RRq]H2?2cхb&SO{fNrH*AS8Y˨ +c' lѢX2Ϊ#kbXQh";jRcix*mz9o799-dxvfԏp^o~Svq}k y?WDŽģZl\zU>k۩J,.g|_kpYL63:]1Fk9B(o os /[߱6l{݆F9Å _1\W``3 IH*EW5smtzbaúiMV^TcZg8J޴ )Őy7]BkƱz4ȌoVXfrX7._ˡiqu[OMUZhN)=4dpgrG73RWؔݤZr ../&ٓ\ٷRUhna|#kGn"n\\?4|ZvT=|Ώ5% ^[O_/Iv5K|:>5a;*-kp"J/ Q<ں`U}-B⡭+^(:>zWm.ݦF*?:7Ts[^&.S0-[TbH)Z) F*(hm w M>pM|+=T=@sT5sI`YtiXzI;‹q76[xb~ O#X!a3߳6dX\[nCfgmH7[jEҎ@ q8yA[fW:|q[.15ax+mtVcOo {,? /Do~Ť_FDoo;g/6agXgW?7;G|Ԥez7P?7;1>eRwNꇜIa3? wJG $UR5߱FN#-`Q_^Dlq!7BFi}5or^Q /z`2U +f};=QUo+lH%SBG3O>oCThR@gq]Tfxs\7 ZTTY5'JTSX A+Nlhj7DVuW}n;q_:]qΜ9plxJRVBZBZ|?ɴ#eXS$\ 'ۯ4Y&w#'ek wWy^-~21T:Ͻ^ݍ0?$q]d i9$G +}A#4DuT ?>|7HouUJyL'E7)Hܤ[]q6BT贽4Vhhmb0gmtznJ%I˥nNZvIlugT |MD@kvsr됳e#1F\x(kMWǕ?KҶ%vrӹ'=TozN +ZsϠ{(,G:%ip&'.jM\ #̄γI`:`]ާܴ<0Zu1wu#-{gWj+EoN/YWw n'(1Ld b)d t,KTmx&OʦDŁ.n\u-u?ǩ_KW^'r8;>c^ºA[:oB?b('D8+Qn^37{d<4K T-~τ5ezsTGs^u u)pאrzR-6: _f.NӹtnI7F l}@\4nd.sJTٹVSH6Wp1*VS9λ 4aO |aLq֗;G,+9?n`' _LRzRRi1,z)tXT,TEmJ3;jV.>VŤZ[\#DCP%i:mOch ->tքʡALiۆF^L + u +H?2)/y58;Kˠppmq2nu3NAWRe*5k7~h_}D86ݟBgLUp_E)pfЛ7gxxY9!GS!& g7oΟ:n>ow2z!+&a7?I!GIIzHo{Q[O@1z.LLkI:J?~ + f +Q-:HM >m:}ڞJ渋w97֫>w=F^V(?֎7;E!+ߓ0 ~(Z QI[ DˍsCX ;䮗 R[i;sEHpz x<΀l{=G±*fPMzԂV@ctNSWŧFFv ぺ7@.ׂ0Xgn̐ʽbj:;{PCzՇ+ r.]z|t*5$>;JfAО;G>9v~8d3XgG1fX?|_ Qz~|T+78fNyܬ8{9oQlv-F&9ٽ˔@;Y(~E`3>@@ ^#hxuⱄ_'us~zxx+k讼BroJŧ12""DkNUnl8 oXxI%HD@Μ +RufЮ%v+GiQ3chf ﷞FiW"Hgo~I௢~m?ݛ?ٛx@!g g!)֛}: iowQ||Et! + @QgP4F05DMzj7e D{w@ zkE>zT{~UiW2 S|U"ݷ+rBV, QrrC>Di6wσ9 yNU5gLnnꕛiͫϞ'BCtəa!nnC`3C*4DBk+&(* *9L<*_׋yZ-5]ij٭\orZ]f}d' jQ/; &txW Db2(.a'Jq;]gK^%*sN/ltw}3W.OM?9}]I]3HJ}K,=?xY{ҹ.=7OxDQzV|՞\gˆ\.'aJGC %+i*6%'XDU+AGsmѧ4(WSRwPN۟0n9-@N7pLf1x1t2ɛ;t۬s[acI|Kb썧Ń>k.;iɞOGInj_Y@v&% Zrw8X5VY*sG;F&v%b?=m' PJ[q j7Qy& h.|T  0wLAgt^?G T< rS2PL\' w ;Ձ+>uQb+ec \6HghcQB0tݿIL괞co;Mlxkx Ӝc4)m^=@Z.&>a0ܧYe}ۥncdžw<>CJbm;67T)G,޼L{|*Fm%ieG\}}0?QI 4&)ÓM:k3J&F7ˡb'y%,[2rȬiSno9"{~򔄨# #Fi u0}7 bnu}b78i-:/i& nkFViw +TuVԡ8_@<,%h[4'zgݏ7{Z9*BjA7,V\vRImZI>xq_lI~) b~hUgRѸ~7]s.ڸO ѫt +Z'j4|<5o FilFm@`ȇz~T$wvjyZg>S+}A4}*'e.Wa(w4Y&qUsAKh;z8i<u#'D[__@Y~1xw#r6~ +˅F2rS\M|%^fʠWۖ跮sۜ^Kz=ihC$_N^ի)տ6:۬tM%9Zұs!b[Q 5Ֆ,in뎵N6n,K#|=xzM:k/\z8(^SNmrM@Y3sVGq[fI䰇x+}wQZ_K2+=h'N/Xݼ;fxZt{usZGy-ո+=YWRwfWVg}yebN۲Bh[?LQKX=o,ܧ^*g~/ +"5Q1x֡JXL[ĠlJ̠qHFzY +6O~6˨|'S~3O~3g99a3owQ"~٤z?s_bFX08ح234B /?(ڻHjosuf[BLiMidM6OՍaJ]y%F;iC k_QD-D! a +]ȞY$YXv\ˣkŰ{bޑ%Ҳ b._*K0*O + vBoFj:b|™tY 7cϫ[9LHX fzsa/6-:?4j/z'%D&t)iaalX>5޷WI[sNO8v]fi7{&(laАٽ.]XW;mxCw;@j c BǗ@(^{WFyr5דu41J90{kZ5H]:l|!X],kkCQu.~RqC`a6 jUvbxQ٧nlkg)R;ܦ3^09N|b%~P7~jDzo?/_Q/ + @rv@< +_<$ %i٨1'[-b>Iy::8&a #lvl{PE!ꆇ&КyUeڭ-k1$&z/Ȕ־/,fFE0‘;xkG'P}&),0\|4S7;N{*Z%vwWݭG,e4gZ̫͘A~ݜ-=߅ +,3VS3-W}PrWtߓ+󡵜xyFykKkLƔACvWy|.q1Tcx3y-ƛN|Q .zy?*v:rnFO|錺Ѿֲ(u\*Ό#?3v6WW"7Wݟg ++aZ3xWO^lDZ v9Uh'MzitNSg*GwvQ[q+(ZC/Rb(*{7ta\8=L|P8y{G_B_pIȿw+ٝSL'LĮpxBjM'ajt-=v1@fj9~*LynHOxpHѻ[;znV+Eew2}e/t6i`_AWhn%\;:4=V}g+j]v妀jZqG<+&ʜ~H5- |;cQ(74\LBu0L^>HpYYye|^)Ud'CBԪ8^WE,UO5T畑"v8ի\CyT .jC]k$u[M eV9g>5O[rPxhn\jm8mp~PXY{%Nm~ j2E'55JJɈ,!{Ok){!%{5ܒ:u~!emۍe2Q=wUǪ,64[Q7p+W~Rn#]n2v?Ebv0{MtM815s4$//[g^1  +0EVJI}z.Zf2n-m޿˗SL>UHWM^./#)HYLMfb`kxF%;0sepb4)劣!yE@ +N\h2'j%&SWp>-lX +ov@g:k/;+M[ˁmb PSꗭ}pR|zߌ {gn93 T|!AT\.C~H, +/Dݛylubrt L:nP-[k6u.q_ Pop}BuNL^WJ +"-4{ktAW&5|Iv(DmtCƎ8Z!N/^Р+Kf}bZ]Jep ήaY FA+|j ruh[12Vt 5,].v;N4sWk2 zsh␅3dzdOS[*5";m&[&ռqN'wm OM*rՏ7c/85xuI@n {o5rne _sae_s-ՆWOTQ\Q2)S&WT Q!Dm 32,@ $=5 JqN$U,`[wG(ad)DmyN3nx]pFުas @`WNbRg ؾKʳYgGSCV%6['gbq,;XC)wvCyA4Pj/&]=W&zjRL5}+,T˸(4K[gLbuZw9yK +Ԇ}`믨nbЀw + jeuO8߰'|LꬢR +ۛT C;mI]djM͛䊶"&&4N0@3z,>*/ruw-3־aj{jiV9:GA~:hz&knEz\ߕ0;w< [e=-i|ٕ+̜Dq~SV^o2>SY u6"?/o/!:hA[1ڷaTsўzպwqT,i ~۰m_KQ&ʮra˿\:'rf<8 vϨs}^T"wrWt4m{>]\ʧ +܅Ia\yE !v,a]Uˎ"f9A;׭z(] +AWNƲC ?+*J =٬@GYDyc7I0Gttd//]_}p[.IyO㣞31;ޗ8f0)l=&#yDKZL^6jnW#[i't;J7].:Uqf)Jr7O3a%Ɯie5TڿM˝{ʨݼ S|I^,  s(1vձkY[2c'uq+Q5@ڣUjgod03DWnɖ6]>0;)vbw S  Q*Dez"pqK=z#O[:\d&n1NB~|[ζ :Щ+7ff vC89x,Q/" 'To٨*ڣĞ)IWzN}=I;0h˃6Z;FkC[aJ5~JqP 3.4>ӨaNODF:9^ӳ8s.dNG l8ިn5H{΅tZy5tkRܗzï ]h̯Neg}uLJuX[Vcr%#bis5ط$dlW YO02ܭb8>/ "F(*JõT8\l%V>.g:{۸ommt4:4rtX,H݋vH(z-a> dobuzU+|=-F)pv(wzvVǪx"׾ĕySF_ϙd׽jW_/SZ@u(W{Feu F{>Uk0xFynIngFwZ=<7gaQȢMf' |b\SpiK` QI>KfMm?|ݵAʾ=r䝁Ep_E7pa9Fsu6i Q#H-|0LAz,x e ʭ:m̠/P&HcޝOdc:=35־'aG/#Ku jԀ- xe8mfFhyt|t\Tv9lƛ e +eM9Y̵6= +1ӧ:4iyȀhL %\Rs 3O*=4F_]&w+/{`D;wY4Ìt>o=7.aPr/\9Ԕh?%_Q(Юgqic[lT e̟MEoi=wbV0E;_ov{֫'=hgm&2RCWVf$ ] o@,kauEۄ+eL{H.c{0ٔ{y(cDRwi!J`r(b ~>%9IgL&G@4O@:&2bx6'%"W_jѯYv7+Wne\u-U܅WMo-1O K #4F([SCڥ{%ϻdTT,9u>T洺guZYޗj KO8؛17sU ƞ=Dũx@nZMgVUvLTMݍ8TcLV ϛIӁ/X,)xkb8C4ٞr2 mFsv~ݤD'JJ+U> nϴh-H=ʺτq$I+]aZB|o?*E?? _ u]e8^#uepv/ Εу%f5ɢOָV-ٹ7l^J.-cDȌcࣥC@@T*FQyªty +aWKpBxYJ7i)ǹ6&Q:w7W_2( ""HwWWӻs?r>{" b23hX]xS3@o;|Y6$9>d@"IZrrWHujsR FŕcEqgZA_j<=8'(OfgMPh&/~hT*u/7H'j)7j_?Q8lU0,|RL*M{Iw4?U:R{9lq\,J/Aa%UVзk&J^AW|&Wi~sFzyjlwW1'zԿBt +P8<5@nФ–ߨaɵskyDc}gPHimBm), 0x:;|Sy2\-+wDRkɯM*TR}i֎*v*./*yv.7HY{#-ZF5Qve=7\np̨ܬ 7Y P`_ei<̿7lBu/qU'wr0u}#밹j*H{T8 \46E^fTY);mFo+X}Ƚ5|@)b̩-߾V$`F_nmwFhI34kGuPn4ݯf.&ep3 6$**4eNԾ^9nإ?J}֡u'nl^~3ms- ]*ˍrJ&zY{$W63g|Abx2%l?%)k.M.H& _xUtw(P8|}M-RoC釽!p}x,䫻楥?JsD-7z h[h\}VGj4xmjUo +08_9ו/Nɬ.(YS*uJ*ȫשܖJfkR2jCEN{cUz'SU'b7ݮm- +K~yJ]({;hZuv<%[McQ"ǭZC/`:gko;~k[}7\mvnBm;@~Q(|^Bh5/ $`" `*T XsLO0;evkw+{};J;J::=$s_K]QщbVS(Q2E$}-q^ br$70~U^Uw#5Q|fӻNjmLP\B̷mDsOmD/XͯEQ tN;"*K2Nj!W]ݣ2Yq%||%mm85*$-!M{UAKIECtxKdu$Ӷ0~3?;=rNATw>J{c^dO~LrQOF]N-c^5e^hܮqKj洺-U@L{^_w]`䣼;#(bh~C瘪"_أÔT ʴ6b?M{4.^wf^NѵzTcaPWm!Y%2UEqTZb+8Ҳ, +[IPeܴ=`/_~l *N'uZص5.糾jFT[z=2.VD-"^ +YN]ŒhUPkc`NdXZ +q}T-줿-[ u@rYChLpP{dיr/茯濕uRU/@ zoZ}(xLAf `  gBd5ZAE5^Hqb3ٖ} d&G4C/=yoc_\W=>Cpkh, ,s?'7_8H'!~-wFG:7wx?wv9soKr~ +Pf MlNpr+Vpai{^ gwGN̫w_/b#bФxJLAk&kP,[ i@|L[VZW"rMl}C[V6O1wSKOYdnbϓai:~ѲeBڀ ΀`Gp%="v@Hh\B/O?V /GR}ޱY^{OU65yOVXF._kUɐ~ ++ Tak i+(:/w(G7!} i? |<= Μst6C8I")Si+Xi:u]RGt'`5+@H9ضWdEY@KUQR 5͓ڸ]L!^{^6vlxrr$O~ziU};x:=^_wx9م$R&ls?r/k$ߖV w玲؞M' tӼ2A!df)@np6aS I7/D.#n ruwZ +v+7I8)VcT3qcR]Ohh<ۧ}?9JJZ:vd4e.8mwsٖÅ3o(J5 +'8I# TAv&BqQO's*D>EʺcԱ%늎)}ߋsgjJgAdU +݉ _&K% +- +6Ϧ GKL(;xFpv GF}^ Yf;R>k6R +nsf<]ګvza-tA9tS!b una{y.t<-&_3} Dt>^99~GC*8{m7.0(h[vݑ^/VQ.> 1 +_ַb4-B\)Pޫ6١OUYYk\{`c(qn%9e]%/V3J][lΙjV=^4  qA8ݟC2_G˾ȿ^( MMx\NN\*DzSRMB[J[4@x:H +{YD\Tysrdru}6=Y V E+)q uB흨/b? ́"&x' ~`2-ݙ O]cT7bR>`Re[=`5"SQQ +<1+[˝Py!K0l#+K6>z^DΞ]{T,Q%?^YeP-͆8SgSrX ȱ]LrqjdV?bј`~KE=2ThJ~`Nc>ӓAI][YoZl,F2~!=WRH8Hى[ܙ'#٨AS'Ez-aItooǞl#i9n{ˊeP1S6mS->芩.a~h: :W{Lϛ' +EYW;hZU4j[׃hIQ,-}bp{wb*QV0+ĺy5&KLڶgc@/31%iw n[I:^Q#(dP$wI8v_9GA~-1:5W %x̷]Ϩ,U4LFLVi~F8>6¥*%axۑݎr?T՟AQlk)Yo3Ztk3٧RY.<)*eBMUͮw{IB+t8αbrv)RQѧhqdkTV ̸i+3IjNj)~lmlfy3GR! %vͪU3_s|5!fM6D8=+ǙYύy}6 +6.@ZNK/%rՕȢK{&U]j?疟M*9V@JyR-Z̴2&x%ݜƼ뮴`xj6Փu58*RUb,Bz%2 0o [/\]"sVʖT1o,U2.G7wmmM{Q'Q/GZVO2U?*iDTPأrYLj2MZUJ`Kl2)WsٵᤌVwZQeQEv]FB؊-{JM.*Uv>*,oUn~.V#ihgqTeÂ-pi"XͯL܂,l =ڨ?j [f ;ze y?;<f~D{ xhE^C|R znx?뵺}Sã Sm_JxJj[?GR3PA" ?AvA^qP&]\uN}@A@Lj"||Jx;``mk]A L4*7hsmfuh[#:\7!ldVhv8^gxEz֕HT7abb%P9 clw*@Z]3n4~YTxIF^gi} _R{=walX9%GQb_KbЪ J] +Y2CO x~8rpz+*EWe3o|Źj{z +DA莬q`JuΞP||Bk)[qOOK ]+'JGݝ),=^Pɉ ތ]~]m{1(`xzkJKo鼳IU|A8A%p{nk r9BDdyʡA<=;Amg/MιV߸ .wyݠdROpClU[ 'ϽT- N} W {@ X)Z:#&x!ls8$6ppqMOu+=10Am;=c}%-`xqtpNNd +endstream endobj 106 0 obj <>stream +196ۂ-}y V5LC +My +rqX|xXw^rmZs5dwg6rrIG6 Ӯ{q<t}lwAnx%#g9JW~v8ߖˉTs1X{JErKcTTJ`T1'Z޶anCkH4Jr~4=^䉲er\m]pEiLOh359{RenpmG< -G{UT } LҰ˛_}kA#u;PMvq!خDBGuB*jK h$&^xMw p6C~Tb5x(f0\~_k?ȕh 6JYU󞣇p-8f4{}'.üJNN:0nE-&qf̡^Qh-z\/G~\~ΝgճTo](pa$A>Ym̠f3pv83!5p&Do[3gI6 8w{ogZuJ։|+? -ϬњQxuj}mӬζRfg֋s~tȾ峷]]ƩNjmWZFA贌:i `hajvsR 53WTPSc(+le8Z4ޔk7Fˌ`++MY7[pkNfи^qJxcޘV ^U&uW/!?"vɵֹA/OʊGT)n;kHFM%7ᾰ=eVMZ6u vסiYIx^ZW-'\{IVOUjMĞ*5Cl;?=jӓN/8Ua{X +p:(plCʭD7 6|#M{1B(_'rהy[e4S;,m yq5x~c3->Cc)1dSm.bT3c#;YkRqY,(cW%:8W}+,+ӄ,isUBһ8nSEy@ x&Ͱu{jGMY 3e~ͶI7ޔ']z!|Oj8`[e@4ɰM{W {T +*%5rla+=cY䊞d%>`}-k 2o|#w(dO] s|9868-L08$uv#S uF6C 3yٕ9,,"Y{?_+:(u"t w䆣Hm` x 2L@l52׈^^c~ S0f{#E35gN?#_Y&@thTln[@{EP>@%@z@8·5y!CA#c +}I g-h?޵Nu#n?d(Uَ AlIC:G L Yl&(A'2&U<J&` Y1)p.S<6im7prj5^y <dd7+aEq6žOeK%EDx1w8ī7U*, ЗэbwDŽ;M@I?4"o^C!`0#v09of/r¹<4_zAo'~?y\yGÎV,Η4nd/7/+AS҃Z4 XJȺ<^>T 0ɪs_U'n 5ӜNX:vAϺn|}?@-{!}$ڭmV쑗 Kzq ө/+Gw^nd==@6*CoJ#<3T\5'+8;|]2S#"5/-ީ۹%nsv6"Vעm6#S*YhR4Qݞ0m 3[Iz4KدpMknm!ZpB^-ϼ#Tw/v^(ۭeޛMqYNy'S'*%{Q jaZSC">}#[6K΋QzλjJjY}^> GdTw剅~[.5ZOdk:Фr+iO" du]Z)g.OxMwL /O_Q,xRt N?~l;%{cURJR? 2+%Jb|v}>hCK[?]IZX>DQ3OEKwR +sU{쇟b73mY< ˿@R rAMջ +} +;>4*^.}'ƌ4M9+)lcf*uʄ\vq ݺ&RV(u Qŧ,Ӆ<]'S9Bq;iÞ`?F+e62ivuv QNbkaBW|4a6(Q_Gstݏ; "ˣ<eV;Tfu ݮ"gzE?'o&\ם>!j61Ά)=VWg7۽E)gS,|OGo;¦s MlBNκS8*u;[tZ봦#wNc SNFuV-I6K_vuj'q5 Y^|]q:;N&j⤭r_mow{4N[ݖv9 Xg.&k{Rr9'> }ғͺMTq,`Us>)X:|>{vZU-}cZoZ~j fg+[Mdn'b_K p6XZn׷ʭ (zkf4h?lT3ĝ5ۙ~֯e?&'Mq5 h\Mm\{cvwӍՇx3>̹b\Sp"i``(/Ҙγvw}5XoHԘjNQ~7Gu(eaݎ/U?T~3]1! +~#s'zD/v۷7XKDL\@UyEteJϷQԴVQztz֯JL +,*H*]t-iX 7L5qO|TREw֓gqװիOS9lGʹKsDo-c27.^vA580Jt'LSt-d *墱aE,[2¤YwO`ǩQRBvPJ3q0Y2qgT NX6baS TMȃѶLJX` Sx|spHWJuN0X6&pd҆ѫМci,9ڴkH.ۈŔ~]b"zrF qK"'"d%uUf9i\3[L+0SXJrMp c + >  BA:6AZ h)wl +Rii8iY :CfJ 1 qmdFdF)X᪷N #9Vß,H (UU'6 7,NGLdk2ż1V@͙6`krԄY +({nJ/.aޓW@B^!ЗК:MdWdtd&%Ȑ#Ȥ?2jh x 7v d$ +2 +S27$͋c?rP&&³֪W4sAM(&9uyȮpDg 3U5̰Ȉ"#ZZSTlҟՄ:Nh-°.xiζޚho(Y:Ws|34|iyxːH:dFd2Oe2Ar4 %Še߉s/mK8Rq|*;?hAZ;V@ -ze'NԏujcΘ@" +h(qVu/INYM.,#G~0 h^jP\c +-&?"GN)@54hQdndL4YSd;4j8Ǥ~<(Rp]m.~_k)Fץ)4]8hu NQn$ɗ࣭l:Yh- +u^Yd+ȂZ(}]C+-a#AŮVΟF.POy>勓  :Pژ)3쳋kG=#O{qb9l9|v;>kT,6jIÛ)+o j/m(L%=Śjv暒g&kbM!JD!`rsy?󭹯[Gm>EڙW4&=!"lr~8(WKfVȩ'%Dy&te Tդ r7Pz@UCxo;鎍k]9i6\WrLSUwɷƻr.^zK,yTй~:|ϻ76Ȝ{9oَ Ǫ+qZ,FED=9̽y8up<v|yW_ud5t+ԚZ疐gu3_vu 9ԟ_* UZT:s/P7?8pPZKq;nj3e<dTrAY$QW|Z['"TwqEEVs)ݹζZr +3rGT":?[ |A֨Au!S@+?:xǫT2*Wl㲙|:MZC`ir$}ZERe7)Q~=oTn\[N'A=,kR8PxʦSL)̹+_LN{<% S;%&5lKIJ{.8l+X`YmoC&Ef-s&QOAUxy zT]d +{`;8ǼqwR}3'b@gHf,|k;dPQq3αjڧ'ΠO cf#{2~jeGF6P̰Nivc͈NV-ᔼ/Mݶ|"wBJQ!"mgJg9NMg5fjqd񞹮gz#=`-,T8'ymnj['=vFmuIh)zTWwĞ.OƗe=-GU0vħUs]=BHVKRPlP*AO5ȯ`[ vڡ o؜.{M=՞F 2/F OLhuF7ɤtҞH "~Xѷ⊍ӉU/rt92pt{*б:z9Vkm{f\"t ӭ亂p[jZ jzgķ2lK,&̱#)ZL<4>T"b! +ߎ9aw+~DI[SErn,ڐi(iWBZH"\UV P7׋Rrm}zxAThOO]ԅ(̍7|xY\_/-ƺUaT-&#cZr~ +Q p|'Z`c[ kPWpfy1')F wS )Ɲ|oz 3x2O#Ea}ikK 癙,R~ȝRն' ٓ&f7eUd BjF30{M& tҳQ#v}#n;Vc\i<]1dImf@c2ڟ;~["X;CBoNZ,2 e⨊ieBu9K+)J}h[V`Λ]LGC]d9;M,>(wǓF-5 <]=\Q%"H&OnyU& _tjc *x|{ LG]usg5N%@[6@cxG@X|غ,#h3x:֊%[O JHS?]^ B @""ݸ$ 6Ι"o~Q9c-BvMmt L`b:O2 _3iv r>qs"*BCF^lL0久#;UQߺ `K|"4XxOp @WDMuILyy MQC:-&Bxa9GŭݳwzE@}5a~B{t.N0O.Oq .\>\Gh4bnEqEN]  jFu^W ):?H}9hc!F<'~.0@:@F(b$Z@Qh#b3q_ݍ\tެ-v_Q85ot2U}y6ۡE=pt@ 'QG #5'%36>$@=mP)ŧ2GM!«%!Kn|{{MwOǑQ4=@ǯ6{uO݇Su^B"yJD4V%\Gmd}znr3@?&՞(& :.i^y&8\wۛ:FT=˧Ss܏^oWtzjJ֥مxܓu7ٞ9jAHC$;VwXNEr2Mhd{eo1z`}XW뢈e׉cQZlBCJzg';O '`4>.N.J~A$븜t~ڃK_UN)umaOφ;\sm`ctaܼ E֬ݮU-']r'}@sĪ^fZt7E>cS07+R]:r9seP4X>zfNCDh/YUG7OE:O%ZaC-_?ƫt#dM855 +ܥS?˞ЫGm&;Ɣ{[91=bA?نζ+Zk./:nCڟ/UYq\VndoaJdv6OP:(XH=-vN,7Y#rˠCQ5> i*{@Udzjw-pRYڕawm=!zu$ey#76]"}< b7$ Dp:=ȽѲh\UjuGm`!լSM>[ +{=VZ(nC,LǼkmt85)upqdE#dr4O#5EBO8;'7oszښg!ϟW.~ԼF*$\C6ťoT'Y8[wJ( sglU}|y넩Җ^<9=',D1O5PWȅHۣ[VeԠ#r@اԲlfa oݿkDI5./x4bէg?4)$fg+Ɠ> +j",AmE2_ЕZ@ŭx߮WKoσ' +§U`й +s4mh~WS>Nyfhg"t[DOqLm꘦@ ߙ}>Ez77a{i[{m)=?ӋB3W:ކX8 ٖ>SNwZɎsh4vRzxAH~Qz{Eχb<;5n=J8l7Dǖ3 }zXy'zϢc=9"|4a_r/U]Q7ǟH<9(&(vT/9-pGώs3'o})5lعN05Sսx.RٝYo4wrأgMCv\(:T%|T|x@ '1?x^^=~pfbsJW6S+u뱍]0W܋>QmPlnuquf֥ G-'zz-qs/~@pg #Yu umCUtׇtՋR^ifyDlv[>*?O0û>? ]q?Oٸâ5~d ́M]h~ݜvEDv'34ǥHSN9.q#?C7MƐ#uO#y$pR\_$=7gu'w57sL8.T^+j/.lubw<рN.]l7]&-$rۀ* F1̇yO +LS|+R#g/l+z#3ql(t{oWV9%$e'0B7N\{O6GeD6aOxr 9e}@c@e{Tfz֗*M̢0~r~fjw`D{Y.c#&۰Dzp5zp4 SjR%XT"[ǣ0"u?y,ohOݎ62ZDbH&Z֜k, + rY{lpn/_p<$\#YcjU­ @B4/chRqO\eϕ7+yGNi*[r/"Q"?^1z[1` Qjq>k1RM9wpqQ]uz5X˺R:$V +ӊ]o+6*nWORʔK/N$n@n6R)C3[Aas&faPw.:蘺FlVumH^5 MoXPQsAU.zVHb"^Ws-<~g ş02'u-zvj hMx9*\Bxo&< vF*7յXOdUOm{eʕ*zƲ]΃._C fyc{Y5g +ﬣ?tk+F*b]Əx$㺛a{yK >WZ~zN+/܊#r؉˰X]P`p*na@'Gִ{Q& R[! I5?%#pZ]}1Yw}Mhw@Q/d8/- HRwl?CGVADOasKN +ygC< +@Wq$ w' uOw\`lɗoV$ [Ds5?r7{ \'<X%XMs Lq܍X+U_fT[<gNqq| /нff!WX0.rsK߭gD ZAB3,lO ^ͯ +H98iA؄˸w̷#|jAޯմnf:%ש_Z YRxO} 0܍hwJ @8̋Ml{GgE#4KEx /0YXVaمe|q je=x7ħmOPcP4 ?:QSz y PP-#܅m'T7opt kԈ9vaCx7{ײ+rOC=X*/XAuxAtOD?2?J+/[m !b Ѱt$oGn!t f>-{E'o83t!x8i|1zjGdj^RROb36O0An]@}@Ts9H=9xvu;P ?ĸc̜ug^)vRѭ/n坸"lmtoBR;&=Ќ&-@vO@N{ ?%չI0.`yezp}fRiy"{rVQxhťgޏsDְ+"N%!jһjj2'G@pyBXzl~W -Qj~?3Wk|>W^Y,V7Ĝ@Nz;n}S}YR8Us[K01.v>ڑ<،-~z:sڞ4[z:=(^ٛ^SiHiwlo5iH-5Gvp!" +TWxw&`A\ЎX3cu&)6UӾ'R;ې6 wa7uO#U G*p?ixz*m:Ή>}iԷTZX-iʤ;vYR{ovu9v_m4nׅc Pwځ޹j~d/IwťWQW 9VhJ>P-;4KR~#|2j*Ȅ4%/%4JU芰>g{ Ѯyi95}ȡg6# C< 'n@#Ht{r;=H~~WT/95c:6=cTPO W'-o'ܐi<%("df"I2xhaS#sḼK瓵뿐@"PMD8DJ"j4B}IIkm׫}܃-+#ְw}Lp26z+ʻ\%BtA"~ לJ ha0YqOpv^5w?s{h)x7TW2E|M/-o.8Wvnڵ_H,<~LjݧO%MPif$Y3_S2,ŵ) WNs{7<$^^ΤV|N_,g'WSs0x]VXoHƤ%q5y" ol/@ ҟiXP 5 +E=t]?O+:lLφQ]\YN@.G)ZhjdzOZ +^FNgꚼ|%.NgFd"7~2s:ofamN;燇9ϣ\??.v!2|p7QQ3<첲F{r ]qC62Dc<{}hk~ܽvUW̛p=B\2K=I$ 6w)%)22G Fu}ը4lb 5ci@ǭoDXvsΩi=Ljq6(˓w7ZWGX8hovO>Z(*~Ļ:2{y!2DNx,ִyH; X<<C|QPg|keAp$moTk*ݴ$wFmux~"N1#9( oL-ohYcC|]{NFmcBɐ9I˞KWV; 4b4W.fg}["9WXM5ԑ>u#U:?`}F^ +7$ QيMw$]VNƛiFomw{2쿬vj_[]p[{{XXonjJo*>vi ԺsdžwYlgly +O]^Wo\g/|6qNCJY]9oL s&@J߆wɴZKr I{L26r:6Ύqf4iqV{#KfW$!pH[w_U> siz!;Nn=l=^9iH:Mf^wZ ;Sp8dܫ@h}vCgZG9?r&*bT*5S@6$6vktY7v]n\$rq?zS(+ jU\3~Nmtq;L{;7غ'8V׮ )̅?tE*IM +#:ݵ[r/hjݥlDlGGiOOߛLS +jNĂPƃܾ;X8nj>JZt2M:4l Ho!8*\U,ޕX )Q~cU"vdXnʝ+'y5 jLW7Gu[kh|SM4FSғMg'ci1[$~s9y+p||X|%y|e;> WGZ.jdGpi7ݽ_#|qʙx̽,nÉ+ĉbGξ p5kDؙk+C0/&*kF@FtRx~gwLR7ȲA볲uY\;hh"LL" ])"|-(Z٪;h%u`~<@[X⬣{4&=zZ%F&ӪY܏S/ Fg :|i]XQq"-N:->xʏ6ϗUjgRTLi4ۙsmβgl4 ͐؇^[/8X?ZuJ +(Bs8Ԫ%;9 3ɆwJ+# `R.l f { +M2<ŧ)&vJؿa_; Y6#eKM{~&dD:QK=j"]W)z3a/(K_@g߾⋆;9y Ro#7m+gHݼ O6k \@w%/LW- $B69/E8"b-ED{f;"vq)Ά+;_P2?+A#j/W[7?< CxXW:| aH:߳SzyI7_˦5j"廷0@% CxYM| 3eg1VQkJᙄV?H ӽj/J BZ[Nםr37cY!L9b[ωv{VuAX>i?tIYk8 2+8wUuHje!YJXlCYЪr5Na/2cYH2xEqΑٙ^^̺Nڎl;&|Dd/Zx)[xm;AC/(LTF\̶ O??2wEmh3knR}&5ڜ-yR~Eb^5Jymܹ 9p\<nc -댍`)P+Nvdp.jL+(r"]*uwWGcFYP9*IET|p.}Wf Hl28_C;Δ5OTTIq!: b)yEI[&{}ɟ 18hv 7%eeBF7\$yAenV:ɉ"Oe/& NV;z{nՑZL ޣKaD_Ȟr\\ϱGSH{]=Ap?GL6zֈ;ˎYh]ɴ׏W|"t^)g5:jN4B{O"-[&Z[Gd0f H2-=Kj=p*AlSc39%iq[ۨ#r\Tm.=Xa7xYۗƋQPQ1O@bo/,W|q^zB,e{?|dfp)40pkvf^V5[+er_ŕzpeK#*s,>3]j]A!u7@!E +YVi-Bߗ[~X<*|V JD~n9˹‘!ݘ,FǂMCcP-J\2DKZ;떨ϴAfS*֢LQA)9,T{-ǬNPm)PwMzQ ҋf>F>N+x9v +[fn63ҵexzKN,\4^gz%lz +c8ؒ!G!Qn|v9|r,̳+r(n$gnI]o|dM|)wPR. HYU9mټw0ɾ,s_~"o+n= ?4V *v>o;B4:*#r8oʩM=+VVobZ`b +?HwTUv*֬o \նW,Η+L#AEn4Y "ؙ"v; me{oAWSk:kkX$ կ7fZ4*#YiT_LΟ|^ /8ת#Bl^n6E,SlX]fwA'a۸{HhN*f53fiի7TlZkPM{UU{/GTn٘2ѬRb>]liM6u+j#CA ?\8 Ww&v\Yx7:d лYz8qImޣ.\])%-> PzjYvPp*>R?L+@s@bi5)טu`_{^'$lkiMmjLCd^ ٨u@|SYM]Red+\8K|+% tRo\Lw˦إU'6?F1Ik^FAݻ/8ordD(8F Rʩ_1A%{x=&,6}4W#V)*xy OW 8sޣj^/-qĒsc`xג~u.b{iRUl<9!9Nqj4K>l3^~A!zέ[烙<{"|f0׃]_ܯX.zx|+:rrqp|Րs!q?;80_A#]. de̶QQsVV3z%Y `R} ^|\n̹u7lsVt+-͡9m:B~Gkm~G%~ךDLB+r` @hŸ@,q`\WDB[~}Xv98cd?}H^|CP}#Tg >X9ueIok9d햭Uc +ub_|$^s/WbNMr b2Uuő {-(o|Y\أ*^{I+䎕ۻ}A !7NgO(56pnz4Yh11KkX +ͳfCPH?Mb"ȸ_w(mypݢjZQ-J97ɍb!8TQK!l|Ȝ[$W]`V2ǗUкe`!$@ n3.82\Oy<` NFAJrMX'og;3IFyϯ9,lMgfǬʋmj 6UzRTE:/7ą+ÛO#|}ٞDۇSPeajn-5Kg5~Y%JWrj Z)P/Fk|z{i2S|V2y4g!}g +EL8ژO>khsʧK`{޲my*>dsv*_\8m]o^3<*r|޻I͠Wo oͦ_s>F|)/Fj8j< >0ܲmciI@xkQl?7!^4˒:-UBG"RQ .!p׃gȚOq>ئmgW^/^6J䭮ϩ^=Y8 _(nƸ2^I^n\% eN7so™:(a^`׽<}qǙђ@_ >y܈g{XDټDټ~5cܸou +;e6>aew6m2b  +[ENr:rl>00?f-ټ7fKLxZ9 ^`Rw*I=aT'eB\,Yc}hxG){Jfhn+jdf bZ㽽,s6Qڜ& +Wª3f"%~޿CPo]F(Y?4kWϱ\ őx ?cZO@K'Fĩޔ A'gnl*doFj \'[mnϩeoNaTBۤo!{5|=I)r+7#[蠓n[-bG=(^eL]SɑdYWF]f.F4D nz*{F>ώ5.F빫OvnPNo.%|2h\*:S1r_f\{0+ux٦;<$e.SnaG9/_=}?hPޓs|}]D@4xѻV +ʢfc6V3nㇼtwv14 +QmxV E(D-/DC*? od*AtNS{h,YK3$j #n\uɈoΖO9e@is3-sG*[WR7m7{b?Z10Ά Y 7 / -> +8R<^d xSaZ^vG#$$V x؝y^MюQx15ꞯ:$ֻ }= rLoѨJ868C,8 kܺ\)w'#pɎL׉ĸ^pERԽh(nM5'K:Q6?(sa[&mL vE9vB >.6|Xai{vpޝޝD/\ w,unܦn{Q_Ζ;wFL5cX?Fоl^Fi)L]p/0Ǎ%ݻw0-C@E @vT9HSpܡjBo㙷&7"p,\lrX- {3(-מZܬȑM|\14Qz62GDeǠ^y N7~#B[;m(0J_p9Β meu½}<yvNpmP($3&Dyc D } +iT6iYrR;̵֦iF53\fLd@~a߹,6峼^BiOx7!@d"OV=LV*i*U0>;t>J柭%-HplLW +(^-ֹq׬'\Qz3[ꢲM:Yj,JfYhܢu:fsdYvҿOfY!ߵ7u[l.TI +Ue֮W4Ea$J=!rq-)]S,sX|0< '_Gpn6,~(`AhmŽ;_oԚd|XPFmSfJz>Se5e2]~.>/\yqd3|VCrF H{D0Vx 0t5MM+uRbJgD>~}^Q2KA20J Әa-j7oD%Kn?/7c4]R8qVS@cS1Ed4.?*=~aqfU=Zz.rqVjRLiH&-h{u1Uk-i}ޱoBT'8:Jtc)}FB&SP&3ct9R{;X.ѷoޢ/<~NHEq~"x"V֣?&R&Q4 ) 3Eta})޹(J +E+EzkTh]|T0& ?}k ZnjD_}D3N%08,FqiDff?,iɊ"z~WRFB,X?9b0} ݎ! SgSCjs%8E)~їoW7#8L((6)7mj oqTo+l +#RH;ѦSS") .qrzY PzqϫrR;PC-Ө4Ę&tt(:I0N>7Q\q] +k#Z us `'YRz] $5sņVF~3ٝX,7%Sn +A|Kq)1Y|Q-mğEKb{WB]#KtW3e7dK<k슳DzI.ض tf3@ԺqIW|rjDIIG "37xK-<Uj5v{>%% bsy`].7\z`[o0 L~NJy-rտ:Vw9 NZB5k橉gl^G{&έEL[C;UoWBz\'WY|TKC젞imZIgK֧U?&t#W&CFSqNkd ]\ MMl1tKq)q?gn=|> .A\0",Z:ie1T@i}\;FYOOsϝO~_nm;~k/[@ޞ &2J_gRZ|5Nw,*U$|{ZwGjG~?w{/ǹ3ߖ;ifmo.~>Ԋx-r&L;13?ePclu87=cycq~wpgWvs{(RƷcw-{t_acGl}HVlZ憹psr=sޱ~ӌ,`}d@7lu=+ _|qio> > S zOX*SObc"o>eZ/>܍5>Y1BQj`𚝓U{]X.5t'+ـڣ9'PKxtՈ ]/<7=,m^7Ѩ;w)ܶlie\g#8z͆~**﫶SǘiqXZY%:e!*uJ,6J=nI]#bQx /ĐzFe,ܡK*|<31)#l^f0FW3'Xw;, 竆{*I0~)QYC{Qr|JӰ XdnXZK h%v $& q2 |='?]|uyt jqry8Ξ}]z"TEG]2wpuOu)W67Xsq].!ފXUx֨^sǶ, =D/yLE\}_u0nvMf%dNk!8 +S`yE2l%<*. +Gx򠰬s9%XY85R8>6Bp,SYr]:86edש[0V$F5T7+E.FX!_wic1Ji* NҹMjWԍ״^%6BFhዣ;VF-{e@!.x&_ϹF>f܏Fx^೰d0sGz^ +Syaldi%xi@z:[]0BNpbW=XWWM =c~@U`͞Ax|"v=_G7yt0:[~ZmP# SW,̩+\T5. +GOkΜPV/>n/Wvj:j L^P. +L[3 +L6 4.܊VqnoeKfB76k5-GeMBW_N]Vǯ5V;t!;^g5QtnؾSY]ժY,{|Py5sׂ&-a}z flyNZQ@4jwɸ3ǤhPΜM wE?#;" E{SIm)G ?'h>WvF]jd4խB/ =5 DQ]wvA$fvIU^wZIĶ=5KX„քƄ=XH`'D`J|CpޮOG9ےh)tu|~fʎ<L1_k 0-[vaV@5*KU12ʏd+kz Z6.S"ⲻvvwChw'v[*ix(;([she!g*aa;bޏ~RcAz;57Qa5.53> Dx_L̳e2#Qnk hmSx&B;)vniqKbGx dRdl XT%пW3*#zB5`Z;Xܒ!!@&z9ȕisXGԌm6^#h:񬵦NϽ| h2z}0;}q`sFU=D>JmS؏w_C 'sH=x=6=)BkhΓm~Zfakڴ;nFzMh}6ƭY?WzY!~kڎ!Y^`N޼LfyX؉:m2FOܑ`6 WBe:lm61nˢqfTzׄkQ;c+߷sRPגAq_؂ƭ9 Z1]Ȫ{3U[}8W3w) ^{NJDGqhsxr4֢NzV9,nT[EQq[l}hĊ.ņTܽ}^٨|= 2x51HEm0TEk>{̇;0̨5z!&vrghbjlr_i¥. ` mh:@J^hS8/dM))u=~'kL2AwOddzsWK۠[!O8&׭@tC?-Bq)6KFܻgfY7"WS{KvM&d V+g$yrgő{aduŬ"nu2͒ikexOQWJisTߞZy-SSLESXBoz[dE_= C:d +z_Nar4%5u{MMTbD7'2$#B9L.Wt nDS<ˑǍ#|jjLMMN}pj)Q|]4x}ãxPѯ6l6BM)v޹*߳-xya6W}z-3!|BލodGA=%%Ӎwt赿O9FIĽ1QR`5F`>{~.ם$'%pE ?=|IxHF^սݤ~*^Nx{y1԰1>.^7sr(DŜLҮ9`}u77zz |$q.\]Ϛ#/߬ޅG"FXh ᑤ5m*vA;x-5`: +ŹB٫QoPƞiSw {LN\Dz+‡ϣ'YZgqVrtz빓6oלCb5] +/òOp]`hrz뇿8=Ơi㜄Ӿa ,CغyR-o gO{ 3dvwv78g!ɾy) m$&8Pi4$_`cȷ$N 1b8_}MFoŇ>r(SȄ#= zy79%S"{'a{k.ێU>]X,[hoLlP6mN0`ggan]~V8ĹZzDb|;ؓEzJ)>s7&w?XX$\oL0 -1my52`5vNc]~hVOJz7X'bm䖯 +BۧR8×r5ۿ q:VwO\|"cnۯ_QԿ,ɛ/:P#Ubn+%]r˗Keps[4*_?J^.dCeWgpP;97^w^\f4^)6郕U7&'\WIj7P_BSLT_A^^:TJM%b8%'Xe3~V L{? p-ubs6v[ =6( {M\uԉj91F;bWgCKJOr^tC-5$}$|,^!Ž  1.E> ٬It{mi/Z6Mt FQj7K(]1sZhQ +d.)I6/1SXm6?U(([샭Z7P,7} BU;M9vt88\M6]+ 56<.Q)(vECm9zuwǃh{ Qn?,\fR>?`wʭ3+f)Mr)k\?YMtஒ[eG l +[6*# +:sͅ9Ъʒܙ4obԭw3NiֺKPUQ 9Ƞ3ns<&B֧A"n+*~:\;#>ݒL(^8R&FRx"Elhv,WUڣT9E>#JT9 +١6,I>,C ji!6);׿ꐿqsv&{]@1ɞ^:KPFƶ:I-T"9)`KC560U>ڻV?12(*b$ҽψ:(8_Ic&eb_8 K)*피GC_Ϝu4Dݭ[_nOh"l]w[WpHx?Lc8:-q]FzxLpjBX_XR.sb=\N\snw-n &ֹ:wٿډ47eu^w Gz.|:W[ߏ:|2U3Rx2\*Q/\'>Ɠ0:b}1)Ww]xԠCZSevFsl6sNm[p}*קZ/ĊUVUEa1VSiƚ8O _?1ZEL ̗K<^PJk ]O*H40ٔ58S +=XnOl,x{ pu +{k(칛]wٽ)ۋyrDA&fN~i>ȟ VB96M2-  ;o) +_wv阽v A0+a<_ o!Y\} '@6P  b 6\֪_Nw Bdmpi~ܙ!-'$jNۭĬ֏nW0W2$ ;(l=/8$d! @m'w v+@^YP PrcPâ._0ق,Z!lqB̈<ծڬ:{dcuK@K +AWTPcNd(SNw(@#^IV@w` +7xu 2뺽0H6"I2fQ?uٳyj<۰lTIAVJ5m0lWSpVq.5< +$,O.\tFF(OgO,(/;;ϴޅ{֍MW fQ;lnNKws|\N>|ve9b$o&}|&W&&+t%eUc>z4Ev/+v9[u4ӹ"wy>Jrϟ8>U<&#ubqgp߭J?ߜOs'llߖ9W*Ȓ3] D%ϓhHzz|>%k}> +؟Hd};KR,M1CHo21r\P?@ߵ ȏmyk5cV R 41e*ϔ+(/) +軦Lwi%oihRg"S>Cbm' + wNw8 +2ڞݎ$} ݰsVmg'd0箫&pXVꋾLR.mBy"Uj\Xov~u+]v= F.{cOvPz/>ۋcW';:72l8g5rᐶa6n@ϕ~J3/HUm] !WJ/!5̎mد  āf GK4f\rso~͎bTKhʵ8D:Wwm#y[ pKY)=3 #HXB\*'vɽoӜ^63ߔ +sƹ! g"1~8\d6m4\Řb"5 )L69Ǯ eˇN9f]-(xSFJ$*mDxt` XeW`5w~v?.͞$? C[WE!ԇۨh#ԘYź +$%Ga +~$lswyl^-cʴ!+ o>][S~wC7`?\#_D^f]y.!@X!BE|س V%S#xEuxxR?dJ5kwlY9íokvRs'sA +}y 8:g|vfxNػC#D_>塑3k#"wJz0Npv/@ I4Y{PYr6P?lCfVgV|lMzVZl Y=ƩA9J!7MwZ%gү N/]=]wƦChշ94yݙ rZe. b}:'d뎧:dO*S-T@jkM ot︃ނ ^9Jʃ ?} ٨F*wm&i%TG +Moybwxd0z AՂyDϿvoc:ʶVB MwǛAENhmjW @t6 ju Aaj:Lwn65 2,_m|a5i@{bcׅ=\,KS/`NZ+lZIq[y6}Uԓ84Bn=nTX_Jn?xru}ܿ\,?je}/ܻy#ђ7< ƒa SGwpmVv &|UD98vewdU5-gR;%j; ĸ*D@FxmزB0V+$塝癘⩏+,Vhہp(`f0z<[T9T<\~ɧ<5e@wK+=[Qbq:?Px3[9߬1Ne6_/n^/ ^1l6>v:#xW2vgyhz,V{Ѹ)*2Eu󇫿oP9ޤ}OAW `E `&iX%r )쨋疆mvn"cԱfH/ۛO1`HI{ OC\Eĭ>BgvP}/N_$K#f)nC]r~ <?5eŕd|6R'sPmܻ/맠&.Q-/p†؅ ϟb>)7@4ehz?Sз6@M`PR- V>Ŭ}E?r!zV i5g1C~_$]l̳!GL:ʠ~0]D ҉  ]o7-2$xi^uhɖ$z&!|.kl$I3'T2}KI~EO -3whOnUL_A&DU_9{'k]]|/$J뤿L b7-aR.VRr}YyB2}'V!_Y~vqʱkVOϢH~Sx8]ųGو+vP}01y p0zrJy$ 7)enR1n]t_:~ .7E̓w)7I)' e\zM3~%g8t^⠍]3GhmzoY5]ٺۤ,]BGBsg蜰j3q +WjOnOP2֏EFW ɧS(t.佬ɻ$3?FasK7[l\ozOs9ؙ8Óۊ蒛ri:z,y7 +!\١݅ZۻI[m88%|Ez|"o'=HvE'}vY~mA͟VZ=:*zj*~ݵ5woEE 4i&/jCJMW/O,,kH˪qWloހ{q=nn{'!ǡ%> +(f5F/Lv=,(>쮈`l/[R_֏޾̿%1YQt9Oj:yNC}7|&R7(KFRl s˕y}jvBIXi`{&n[8 +iaSns ./I.SkO;~U_4Abk cVzYł!K9-WUOي[/^N]Iqr +Йݙ-,AתfELpfw4}<^1ƶ-ܸY^Ws̠o͊/lD6w}zݦ㥦|ں9%t3 1-iw@/RE TŽ> +zN=m8};.Z%V3/1 +d`CYttnIQҥ44Ph0T#, ʻ&sʀ?.er:7~܂{xMx((ٳUTd/{Xvhf8\4t /\^v71T}H[2,tr:thĐ]yi,b`crp7ẋ*iؔ/{7 6 ٧Z 39k3C/򌎨FڣA 2VqfShZU%OyO.5\D*GF[OY {΂[E<'":B)Ҽ1:L'=ڢRDnZtog굦REs?\Pb+'U_V³ X|D)R]f‰}dV] [WC{YEE78C- +2T~{zѭc6DBMnS{_ýXV皎ѭC=Bu}Ą%0cԘ𳙯=6/z̳/[UJv2?<\yP +$ʮş8+M{LmrhKDIzZDZ(HMx(bjC/a23М\#:(4]ݔٮ]xo:W:RngpkQ]N `JU8i{zBWWu`r)n9g߳]0fpS5ꬾ:ӈg7N[ѓNuAޔEB{0A?7l~a+bMUh[^Z dt-VfS\AJI0 UbUA ?\s]xuO:ۥwK56j.{ Ss_-scrul.:5oR@)aBxȕQ| 98c$6s-?F/5vr7LV L֪Srix338l{_S'`]Z8p97`'b](<4Zs֪caC~/(lVMȜMFKN_v#`nZn1;% ^cg6W71'iOLLR߳0^p={`ʃ1VPԲH:"HrUm]eح#Jb:±b=LPa>,֊׆s"v٧闙.Tbil$>JQ| ra,PL?uV Z0D{5S<}D>[]; O%fc% >GjA^I1|ϳ1KVj~eJv"C?D°ހ6 Bu +ߖ.A[e~ q1ɲ=nsiz+R;QEeBr3KTlu4(Or7? +hB9q"4 /.4 zCׅڸ5_~>sÂ;vE-aiw7Z=IR>5Hmކӱ"WH@:!A"1\ppVxf !m +pxI!io5lWROFO_F$uIYDjnuo3*=&kyrB};{7a0aذ0,n%~e-?i-HcK"x}Ҩl& +OI֓#iUe8GAkUkmOIk.E,8?7?7_DcpVb߮j2V4;BF I2̱I2H7*s,z2KnOF{O)L3}OwT՛\S8Kǹ[R(ktO=1ERz14٫QO3kjYJ6Kkkd?ԭh7V^9]ܧ'yv">Ag7 '*Q7Iֽ*oQ/~rSXX]Ki*wLq N>>g)Wkk=Bm'aZuYC $}utrU~=9oF:gu7$9$ein +vs(L?yoӫqE"V-m/ѿzUyOtU"G:[ +n?EcSfBvTsš-5HTq[$R(- ]v2YPZ{}ɃMqsU(~d^ġZD> +[lU +Gbh iӂd ~77p}TB[G0^7Ic:ҡ +st"$*s⹛_:oDUMq:?D-O,-^2 \6W:׎_QjkޔչrqW~|qq1z=N4%ckyUfF|}kr4]"n[\і9@n3٢C ƅrS><=֬]J?YY4Qby2 +,63{6 -k[EV8) +ZV ipXH⢑Ȍ p{.Z"8y>Mֱz/ &hω>>Ic%ogkWcċ+=|8_kyk*fk귿E|ɴ 8Hdva[S?k{]$נkXOH-ig9(Pv$jɉw'in6DZx>n*vVqlEK]u5y 3uҮk7BF'DItłP.Ooyu%^Or;ɮZqQf? ܕcvpG.HMyQs1L@|4niZᥝ:Փ5*F5 +ʻ|XVה'P\* aUuX`m-sЦ RHzM"=OyY#3ZV-s]ah~q'G$/ý_M.Ҵ0yH#+J^ֵv%os12|87Ie`YdxYae!|Iv֬醅?t]siu-⥭_P} 2\7>͕XSH8E`N_B`UY|̧^k:(p^?K,K%'vYcK-ipe}'5J&R?FWj2JnXl/KBDhW!-}|d_$1Ń\z@fj5W0Ӿee/}?YDqzis=g2p{eT{ZBP Թ/ N\Ȣ#lHS:2D#"bu*vkXqLc[/0|7KN?lhY=jMyM[M]!mKBeC +YkP&ǔ*/y}06~kZP. j"\*#)Kc +ۑMf~ SG@}{nX]^YHsʃfi8k76ylxaeT$h +ElqF?^vL + 窦xn *[CNe Bx0n 8><fmƕTs wcs2wmܡea34~D %\ ެ61t]*Ш6>MN>MM +=v:f.Ě) hiyk8ke,cv(Ԣ0⭒.:*$=h`r.(JPɁ)1curO^Kz-ptG} ,~N[/ek3ۙUsy]UMD\un̍e; o$_Odkh1l1/#6(x&Ҭty>=]Z+GuMn_š+l Q@\hFS?sKOeoZo][<5k }j@?)4~h7oHOҠK/Adb<]m!C;6ՊYq0`7`E`؉5ޑ>g Ŷ @o @ w 0RyHiݮ`u1n( 鿒`!O59 +O^8R~ŧ[x ,b@6_|yu%m= @~rƖ`(0x&2-=9"Ekaԣ{~dOW>Cx 6@:!'}?W8?UN@_St]z?YV8i}/LAK]?Rm9@P 6@m fm~GFknO_m1f'¬5;ooOg5f)Snĥ% .;C9c/׫>qwl<0—0`'GqQ sa [̝)>"L EO ;{4z",Koޯ\{%uUBxoY:T~4hw>T+%$^|vckcIiY\H^tm\QQ7ʶjRl[?R/+)LJ̥gZzkf'Wn}aS);{=v}0ڿҲ|a- nPuWC&%Geə8W|Ono,FwuyiO^6+JQMfhf7-榟|ď\o#tUsQSQbbo٪%ӗO&(HL ͒y P_m$V!vg3I|_$"-,_PLճu k{:ֵa +94:oGKEF4`>Hq;losު0yyP +͞fw79[@}oH!>b7궓LUw˨'o2j%Nꍈg~'5e>䑼\hx)uZ[f٪.1Dk`u6W$:q+;9rQŚ-#*b\i7i8p`?ZɇadwqU-jwS>gt`^F{wN[[lAL,i3O"/ƣ~kz::W .@2AQ8KTX\eB,_F:"bA{\V<;?9MXjU^xX !M^u+2LV.BGIUIbEzE+g73`&@#e fF8av>9=M2@$[a|}l\]Ǿ +{p{ ӵgm-[Jm7 pl5bT`~.˲Q>F31;F_XCBՖl^ +>n5d' y8B2Ilds[E^H'(hp}oq5:^ZEoȜ~4z4KM?ɶwӮVH}響ր:tTy9K$*8ƮbTk E!0 `I9]_4<;3pg*TP̙_nQjF#+  +mm<'_j"Xb$;#*HDZ!]]7G%{rf8藋V SQW+MtU ɢq0aQTV7Ly+>#ݾ`k\?'B6,zl'A72G$oߠ6XíCqщ[ײ#[DVcCQS2X/ v.*3|y\:.jKg)u%Z,[[9H%t7) +u \$vgl F0-wJ>K3,xϨc>KmĀ|6/H zzA["6z-a(} (`Vg!i.hf̘QݓhV-M +hVk v3d-da] I)F!Ӻ]^_k6hK7{_{,-'̃uv܈j+ h0i4IS|߾ز)tn6mV~ntZϱH3וtZ[Wmi:[ϟ~5"˝k3VJ.PuYqs߯ȺGC߰s +]&:uE%9qd[ ؎mi%-볳xC\y;c%@0LJpM!_J7E!*-(⸓& 4gۡ7v:`y򴠕z[( ֥o-;u(5 m" ¡u GDRrGC5CH1ŝ)6>MFz [ko^<cw™|GGUPT}{ZYh皦W-+oD닲Դ+z>$m8g< +yWi Ns kbޅ&H Vٷ7NUu~)'&QD\Zf%iGNBïNP9sT} 2IqWj ŀ}̏+yojgf LR0 Fѹs*rZ ? y{AFvy;Z˥3/9qXL՘Z72'g~<D )X! +O=sa|zcs zFdo: Բ9,TqWݑv#ۧ a kq*~kX/ڣO?f8]U Y}t +i:av%ER[O]ӕxd+Ņ$O_U PgBܢΠ١Z܄jݭ={qҹT~ ;C"aK†0V'G(TX773w}ʎl1yIVf:J>I}PnRK>ӺL&bl"7"-E 䜜@zUۤJ#שּ{%'0KgpW)^`L5~ g<{'MFkCw hOImޕB ĨCy2vRLtU$C/|oũq8ntܬEos +Qbuj?,3N#يסik&U9Lu͗ջ_7^ + ٬0Z.>RN3 º;i%QƝ5hP-T܇YyT{z*sS@&>@5F1GZrJQa6*dI|X|~yCy#?io]z1@#9\scQUǯ(]+-U.hJ*VWsFwJa!m!owp^Qs%lz4`in\rg&+^ [r8}B+1=Quڬ)TӋ=e V>^4رN5H+bW9ͮ팲Zd+M'8g56l@ w2Hfq @ܹ- v`?1bzcwlbreJ'5{a`MS:ɕ5 ( O1lj z Q +P*~);Lpf9@5 P (B lgRm#g>NewUk9ֵ(6.^&Ϫ덤m^X6](m=z Q?vg:L{]`*T0Ơ C`VV%Do)K F=`nVW) 5 \m: \sE.X&7J W3 +!`W#`_/0znnz:'Ιgw>.pu~p~t+|̠} O6og6$LG7&6'W`;Os-75 e߀?^zp@@Le –}NpL?8%>;˥Ͱ)OLuaDueO;S}?Ku8~_TA-C@ANnwbl+=?^yI,A*L46tf27 O ?I5O_7i( Ο!'ASeܽD--7<HmYdNbO),sx +meeFopB]焋O/DJ'gVY[;{E{6*{tE1.KpLTCJxr~HΉSt̶6O _@~=],z?UsL$+[6ZQ#4ЇֳbGB]xޖV"6񎭽bZ!?~7=\.=bZdpW6b/qm8 S' ++q/=;oeIi[ mgCZ/FV˯3,b\@h1_5\ˏKsxy&f!륕O1IZýf-D( +=Bw^C/c}s<^inbݷ (U{ócC%0=hdqt7Ъ8C }+^wWA륗R_vY.GE~Ǖ2b6Jlpn\3=4۽)iStC`3،GxɨƄn*H}('omn+]ܴqwVVk|@ƚ~Ey/3w{lx a(țdpٔ="Cp'IQ?XX3Y;fVn' m-~ԂWo}M(ٚsB33'Ί7VY9] 4qX\,Jۑ(U6|n^}ܱX<`qhOH}ҟsއVH#omo;^2êUXaA8c?JB]iUpZ9  5S 'FG­gYyejMNg;Z+4,)]* 4N:۷G`0gz繱j쫿 !MU1ujlDu0뢁7K5={9?Ygr',5ٙo/zkxq +ws8 lp׬-y֗^P:enD⪷w#:Axr44{;>;,Uȗ ?\)'YKuI>SZvV:{*箷na2l,[[a 5RSR3NaKyЍ[Q:3:xŝg;תWJ>-=;6 +lk՘?\+Q:鰾/b?~Sy'YhInxb~g +?J`c1ຌ0d4tvɭ?kLV3*Խy_=|ڛDI%>$e?B猞[',TVP0dt0ޤ{Zryz;dpXjgL}vΊz0-OlZ]PFS9kEX!wM1^ 4ȏGTˣSLQO4_n0s5/o :a:"'}bPȬڰr?ܻ3~GvðZġv©-RWMk)Sfظ^&KQ˂ЂdIIa{Yo޳ިvmeKYcq:8ic=z7*I+]z~Vkj!]Q+F5O'`/z Lc2ޒI;ULQbg]h!o\Pv`ܣf)tplud)WƲH=wy),,Z+4I3jpӔT2;)vxB}\\q^R@XH̋ǼHUw4oڣrlnֱƮU8YjN܃o;1Fjv"!z'RQ.du,X7PIwP ۢJfՀf_UP| J[;gv/mb}+śN Ŏ'3_L/MbRRe *xuKƟ*ƲE}<2:]TFU]UV/T ('cܜJ!dwD]gn?ݭLvo:oZy+сu)*C<f](za1hO{8Q?4ooU9+TfZQ2W*&\- ayue$rU+1*UwC:p^.?Lk˃3TOH㾂s:j2 0vIG pI*h 4+j!!x6+2mYgzc13}fPiI+2[f zoFH-(0n0/F1 +G#cnm]Җ2{77'ډuS-jiTbK5GY! *Z?Y~=lyd7dv*u<~pCmZK]C,YOQvy8̗+~zYœ5|Zʙ9Ij6*ZpB3xnE ( K$tOrl v> VU+:bm@pQ59A4!8c5.[oXx=mSo>44)g#ŭ 3C IW*sdoe@n  @e,*Y +P9;mo}@>'@" ײH)?rbke}fq[XKc S_Z[% ++$iJzJ*/TbI@ChZ/m*g@ %8N5U&v>ɛ7OB}m;f+~FA qX!'0p\lB2755 г.8CH032̷5,<Vg;.+p\E+_WCUt=@_?bܖkQ`h]kPȮ#+s6`&'AǀN 3 R5 x{MPdP„-@*k,ßFN)zfD.޲ ~漣Mߙt{|S{A~2<F@ez4S8OrR}}jidCBQfۯ ߖf=C7"<kP"Pﭘa*ݶ,$^bߺ@~/OSʑjSj?HM!/gB\gXl>CS| ֹյ6_pxEijELLK]ܾR?%]ޜS<d[%U" O5KSZǼt> #sVu\xŸ%腞_+™۠)@P$NbR?:r,*MԺ.v NuБ޿"'KfSyNzT|Cl͸|vq2[yǓsHqN#aŘ<OG&luk[`=jƸNwXx/_ϣ +@Qxʧ+ |=P%?߽H:V#d}u}?Zl Uvy^ڳRϾ9̵lmU1C֓†iv?8Ԭ;l㳗_<VnLozHeTtÌ*˱P fFi33ؙKZu[OUw^Ã-vLg?8)M';}[k /=~ZLrtO3  +AP _6Cf4̅:9[vvݝʆNg1IF{/(=|gnF#2?\#iExsv[sr Gbi`edtǣ) 2-;*.jWȈkH:1z1s9 49lwu TdImeyW{\rmϗn>Y:ƻ>4ܖ Go߄p?g)˥ڟⱝir͒2^OOeQyeS=Sт8ױ?rMxf~ 93E6bҵ;EۓqFPkyfcwҕ^5{tv4tOPߑgй_@&2,=vc{WF@:նl4%_ڭonQS7,_Tt)Qi5ri{tVx(lY|NxvVb +6sZjah6lG.oOmnZ!ͷnf9T t +I"ٛ@zg7Tk6+N:;u;m2@;Nyrvh?uQ/?~-Z)qSOw>ʙ/rj 3%xFAr]W^[Vk j7k~x隀bD:]sBP%AIG ;{Su4?msF5Bzy54nsRSB-FW@,tX-P +o6`7,›ÞRy a(OzQbjebe<5Q=8M+>As?= ;~)ԗK1>j[+?>3y>z܅.ݹ7Xv4X1&,;,YK]S~>N7͛-cgDǞ5G84X)M5Umʢl-'Ijo~}󑫤I)s&9ljmV$8s5]0ɵП@KY% h<&x^ź.4]Z8T6~6noQN %9O9Zڪۈz2qh ɰbxz>-:mϨy*PrjU䒭ykOª}!a8-=眪B +3cSu$yʯ;:iv1)׎*rE(o+Nմυsf™ o2, tR2T!i$0!KHlBGTxՕ?tf:ip2'J9/ƶú^993-^ANV"ZkmwO` /,ݰ*D eb:w|dHPnDWxD[cfq@cda^W;8W8,%52n͊)t;%cZB]ցbު&/۝as{#愑 vA+j3S|> #{xxH8~qk~v0حћ/IKeY}*ܼ:zUUACw kn[jHgY3_vfKI>9=SNsv7e +'˱81Y1ut*{>'$, tVvczycGz͢٬EU{%\+)lَ J^0-_\i =,KPz7Q4M3pM~LtaȑkQVZص3DU}jmKMRE>JnHLk%{T,tU>~IR]0c;gǕ(W7ui#eR㳾.kFp{#0v㨰Ռu2*3*T.j1[|ހ(3(•-&RQ5 F+@kk!`v~?R1ԜpM [gcφAmlE00&5EVV$[Ue^6HȜVz€>@ ٲ ȆMpviȫ+ {r!i'm3@ K#oMa]#iNo?.!B\cEѳSÖtp/K' P'e5?yԇms&^e lg"@{j;>=j17/0"xCpxZ)q,|QEp1,$@6 hcBN_&KC80K X`@ `;9+aX'L^*"` ઒?vߟڛW ;565g|>-?"u4ڻc~hҜMӛ%/ {<z˷}^;l]ۖ|r7dx#_W>7oS2c ԝKhziTQHbt~d'PT܅&YI/s^JByItP?8~qdZųW*WAOA9 1Q(k#4 [se;pwhʱCck8bCGd&[&ܻpo28W}7cb\0/❂Nۛn^f١KvK6)mJ{wƬY_,/x2_5 +ٶ.yc/(.?zCQ~sr q슍ݡFhSm2Xu]sG(Xx6\~VTRUhg9<3²ӃV+VFNS&28%7^\?l9u=.)(,CiyXIiWǩQvӀXJPڼm6f[ٚ!Fg+6zSB i'qOs3{T{ؗTh6VionY>NRDdR`@`%Fbd's!g^I=&tt=D/s('V٤Юفoz";l w,&߱Qsd;tiӟ觴=ӵ}:p(ŖwKevpj`eNѥNbݙ=]|]GA33i [4auxa:MwqNZc'ς=Zzm w|w SSF?S^/64_ΕxNdJ+ +<X|Pr˅#.#Gd-Z|IYk.s,k镧Y0@̪-on_tr~|ZW>|WkExMn8`U%Ȩ3*rņ[QsY6|+7Dsv9,=}eϛ6[jvuƯ+]]ܮy8h}IOƲ4[ SUki.Mh7xoݫP8;$M拗2ë~A6Nּ*%kA]/wՒЇMZtf;#IƳv=S8kSkuEl5+U8'PNruCrA{Mbym߹)4h0[|34*7fYvN~ۿ;O7?vGZ#TֲZeIGujϨYk~|dx>I-xH5 ߇דQV~bMέZE Vv b!jAuoEbXuh:vbf5vg۹QnY9cr6kWf &A9Աp헨X23Pԛ|6x3Je*f*KГX닙AO5#ddsA.f{4wq.gO{+=;*2RVkULW0<o(DwĆtNCTrV&a2"6@Og 77nV5ώQE*hQˌA;WW+ZぞJ/5aJ& :\,aꖏxiunPkԜ0[}lzk^NIna0;FVFFƢ_u R Ӄ>A +Kܖ2Hi7/n!sW8mo.<0)\ئ03B|rSrgh]7Brٜ  +Ƥ?ttǺǁzg/&FZB/RbC [᨜b*bVvٛY2KPzhEH͝: }Tۗn,7(LgiŨ޴vx VV-NYz$VK;˕J3l +^@ݳ;}Xc{R R 0't\\5e]>Y0 z<:qm q]P{RX謍.m2vNgY^B>_->AT#[+Oo];`'8n{༽8^jvL@[h?Xcg1}{K!2 jt#H((EQL(b?kϹΟUE"F{{|Ox#p:p4}#Dd )8o@P"j̢Z:lcb*)^r#[Pgg3*L(} R=d|؇.n` /T^Y{ȳր7 pzp7zy=$w4Tp\X4=ѻyVZ<'_uͧ$li_ ?WA_ ,QUjZdxPNCP][P]bt~j(3+ښs[Pzz1C;4̡WAOOH?/[w^p ӛ^@/+@|qsu o,+],~%*+?!Ov9k!O/gSOk\ mG{Qz &[\!+<*GbjݓX*" s35?Dk17_g }?ԧqo7oF/4e~!ػޛ֎kH~E }?BLfBzb|s"o3O.tcduNr̎ޥC!-#69Q׏=xݺy ػD\aHә -¯)ebq_>H +vڏbeqXrЄ}:IǾ.Y[@XI/_!~{iP֩UaK?JNZq[^Ծ5Euf#L2&*lr۸8\?+ZL*;nZjK [u +āAx  oNqNn??.Y CjiշT6|uI鯤kX<7Z657Xҩ/<9GoguESNY]kVR} _h5׋Ώ;N}g͟ԩKOed4x"}q ?qa$ +wsN]YG熃ʎL`qdF^v_=[jeWue&a]̉5Ԏ8#q <M|M7 + AvJ›脝jA5 Ebl,,FVyO=;t0f Y+gg}p[#laYI?l#Wa-'qLoy<1f|G 牭cMv$Y:#="#& ,6|erqn9 iGǾƥ\$;jM׾Tal.ͬY< &Iw\SvЭԱiXԩ錬z6հvMfM' r3[k0oJzz_;.}̄ܞ~l{ӆKԘ5^NިkLmD|("jmodqUͤKG,zu4_ge4zkʶG{{uiCQ1HWE\GfAwUK&5לw[۵ hj+wʮϕa +̓M2ڍbm0E*#۝ W@G-݃6SZS18il긗flΩ&}L*Z`yJ{?-;s;Uen/y"JWc$ &lw^i*5Ϫ>gzlk=d"c:8bVfx/#Z2p1(AZ/cpNfѡ+'ҩzyFʵƨ5p#=" M͵>>ٌDtȵEl +0vWY0VxA (,i)|&f/ė=Gyxt]l=v\0C?NdB+߸UJdy{ٚ꒷Գ֜SZ5Qiޓ@Uѫ>͌r婀+r*A-r`CZyK%ηkzw?;mkT?grn4tRiKu[{D[BhSMe5Gj97i%*I8w"|\U'g sChc$8?Lz{ʼn^c:khC]w-V9LyKW[d!nOCЖAz^ ǕK9ZTgY̍`_ST9ںLn:KJjNK_]`~I4@]+G~>D`=[ږu9L9idҭT')"ԍ~?mfJ(qG^uBΡ}{ ѭP1ۢ:E!h~OGkFdO ɝQPRG!2&3-k㠼Z|,ZMo׫{Ex|XD56X}fQC&'tkԶ"GT)r4/%M7Ge~2yCĹۑt1mch>X#(_kfXcnYN?ˮ Z,祎!={UG8g&wыzutiʝr(.HBwG+SI$Z"F1oy^uy]aS:H1hfMJK\\t-ݼ`[ͷ:">3WO^\XL0 *\d +T[fkgiKþ\v?epwxUEK Fdfu,ߘq"WQ?㨹|q[r7iiAY*nQ=|Lf'k,T!laąi%6m1k9m`:.Ң )if 풺wK%΅*)Lǂ9׀)6@ =gҁy뭌z{׫0mĻt+3Gp.D͇gV~w,ҧ:XjO6r>slWȋ좑IcV4Kno0%-(:z(- Ukrk"K|{K> w٠)mn|tfN~vY6lq_etr3 XW {,}su2ERĆꤘ؀82%@;M8'J`̟esEs-yLzg]wsL#^J1aI<ܤ+(_kn)ōdVXh|\@! '(? S)ˠlP,<u~BW;8٘ʮ3-r㿶clǭ˻  +f;*?xn9ɵ '{TQLRo)?I +(6b%4@ j] + z/`B8'6d/-d?癃bI.7j!xtQ|󄔀^mxCꀞ97u|K9}u}sw@ h'coqXk~^'쇗6 dl#hp跙Ā࿽?x_w MP1`D?W$+ G#뇟@<FE IkDp[T_m9[#WzW ?T8s[. L ,H/1U\ TqDjo=|RDO`XK7Eb-_˽1D*JL1wOw_7_vߟ.B|-ajg-# VI"q?K~Y/t6O71rdiUn;h( u[[ѯ!4q.avIЧZLgX}kء_»V'ѡK9$Q~uC!ϫ{s8%PPX

    h ` h}4c6QjXezT&ʖSZYWaC]c"ܫMiF:y[,;>j op[skξҽI__/_^Pԡ@z f],ց^,0:jH}ٷU>Eɍ67/qb(N9`Qmr|=v '=/naeUUB%<:QHK4PLK+w9L!vQݧ {IZ3HPޓv#\N +?Qji:7.sy7?/!xyJ7ǟ>:!("!dR>štcsDnnܼ='WɗOS|Կ/} &:볠,kaF8]~,0 +Z)5.YC =ͬ|t{=ِ#ը= #|/2ao|\ +z-T=_ r%d=^ndLЋڏK1?DksRۀ^dnZ!nK;6mc0guf!㞋 k!>' l*U +Z]:\=I3xŽ(5QI46uՎ-F5v?5F/nB(`WOtB0{8ۭIaBFKgcX贡ksG$ @Χ$Ag}48v=gN[6Dg࿧[jM T2ӑa ˇ=ßۧTs  0u"H!,p:L=X>?w(VkSF=i˺44s29πyBFC]ŴMjD:6ְǢ?0vgNQfr#wO>@^ -KQqLn`y~Dm}!uѷg舄)q`q=iG- '.IDDr$c \᭥?m C=W^=Ӝcx0^4"՝|uFZ]n0x3d!"5 qߋn%˾w.{˾pZ([џw'YF e**E f6eR- +yIL3,VuZJ]r8z!XpF=#yxWS#j'o/oWVDUv;xDB2V8KӨ\•A&" AF\Qѡ\Lh.@nU0MꧤiЬ~|nw +Bl@AMչWNaJ1ujt46^hCt71Q\BG $6BKO\/7mdtT9ij_'?0ƾjgNngp/{4-3fΟDg1ÏϥM?J0ŨK7x-f2RwE=|OI4rS?ʑLV0׭r0Ub13o/:A~|rH>&@Ed^7_wⶉ;c0 +G;\l_Oڂ58a6MoP#J(㩂{eO4TWC,I#ƚ ngH^$ZY7Xy&R']0M1Ok*#@诔2GPHkg*lj"pW@s9+lžճV%j*^kI4*>QJUV @h?70#[K8Jj^>5M: +dNp&53 D"\J/ +AѮty +X1 RhI[(mjR0 +E7' +=B%Lk4sZJx?`LG7Q= B$Q_/8DK1׷^Yq8zi0Ҟ*rAq<8}()8X}uˑ54x(8tM?o.# NOH nI;S!l(Xau$ȋКAb\U9BkWrԔ1FW1vΧޗnmxcj'{t;ZfS-<\MўTi6'n˃_V'I%Fn"gߪ=8ZL+ôrԩL`ʃ|pm{ #jip$W_dx ø^UQNMWIDyZv +R:(e<Ԃ@Q-, (O~rD? _GR \p S>6oc<* Pe w M$p/MxA'`s~ ƺ(o#ert37&of(Z"}93R$;FI=JZ YbQf:|] +ǡ\Ԣa2m ^DK7Av+iNQ~_ʄ6W:b\'eadQzf<_Im)6< J.?) !'.>bkܫqT]DHc%;uUg(+iDbGZE Oq訧Pӱ>:[!}O)3ETf$U +S0W"HRo/נ^΍+&'BcD2U#os|x$ +agMlwAx~Jڍ+Sh~0}5BOcZnV(95[s +)ђDu@x'UJ}TӏYm"wzv0(E1Kw24m%ɿ+US'fiܯ2 L5\ytnJix*How{/:t Mء1'/66qMȒH~F尯T_fx'A'7׹jqvҝG֘ LRs0ZPjqƨ;V8CW8j`pۖ۬wDyDiA$I|lt rHwUwنo3:Tȅh~ D wN~LX>Q@buw}LhŃ'ag.|amo d-~ +NU"WNuӀ|S4 w8GПKSHeNdR;c[3baqA,FS0}pa+{}s [8q +R%s{$@38٠gI[EŒi)$"X䴌yI|Fa[vr>!bԄv Uͮx^Yot4);CsVj9)/Es?o gc%gt!2ϑY"?[vC;ooeJ84;@xVh$ UpXRR tTqiwلuyMup"M"MD!Jx_;-ڋ(79p+8{@hVrs~$B/I[쉶:,V|6Fc&۽J5g"6SO'_kYީ+ϧَwT~Qq'w1:ir72{/M7N1HR#ZW9놓*R6(ޅKg} @!hv n ٻswq}%|* 9q9B@.}y ޛ=HkCuP&4Cp1x%1/38͖?w7. JlTT4&Sf+iíNM1sn&b!'o"DSē}ɑ,rWϓN-*~RzȪO]*M+zٛمt=PZŏ'$ %%1ffCqAW~yՊ`XFxΒCմP/_ / {| GFkoC 1ID_܁2sȉR϶ziz=`YvWqw R+ְаL?#F4֩i"?̖=P*8>2~]ܠUov]Q!ݎ*N܈vʫR2ߟS: giZJkQo5bUcő_a{"JJӝS zU*\}(Su޶eaobV>H|(r։?nOi* |=^q(BYwlT_5Thg7NOhL +fם>䗻{]dWR{[SyKu3n2E +|D S/~u%Los +'2v =#ѾDizj,ˆ@ ?L#oҥ_7J6i54(k7<ŮSˌ&N:_ᤞ9Z`+u8OonCGnFP)V $v渏ZP2uh@7D!f\m|h\%"skVwu8Rz%0a-rdd.(aeg<%QݹR@0c޳G`eq#fI$~8y6)QԗF,صuв6_?yLy.F mw3(ʹ˞"Yc>,<>2 +KA?MHMPa :idƘ!??Rg'䳜Ć_`=3찧z88,PFr2G`_e Tnl>?ڍdW#,\N\Zf9$U1s4Ѣ q0~k A S|v`5iʯakڞ_+`/B?v-Wh&]97>t4{%{.Zg|nHoatbx͹ʵïjh_'Bn1fAWI^2Оiodٲˁ';g)D!um>Ol.-*wNֶ .=􆖤Sgf:sس2trjp ֈ{}wA^NEO%N?f>O5҉T+ If@o4?`+L*BՎ#$iD)! jӘzy6Oȓ@3mx;UuG:AF%oƄ` ^ +)s,LCRb_^¥eKok>]Rg"rsJe[<˞ȵWt;GAǒS jE7a12s{D+:RflPRޛ[*I3ׯVpzVq7\7etm+X A0U ʱcBwܼasJKJGֿrtG^Eg?9oW־Re@h]Mas1Fqq-dgA$X<4?tRCl ?q4cVPA+"xu\5Di{j_Qs t>y}aQO޻qs0ΡغF83ɷ_ s>"L\qTxB廸-)//gv?fC{=MPtؗ#/D3дA[2MV$:†Ս$EYG:Nl.ݗfQn)'z7@c4~J/BA+@_#m ~rͷ! ܎!*' G^nYIc gA9nyolNNWn5m197fM7DǵEo7u.zX7O!'#vFE9Lv~N=7Ō&*lm*Ӣeȝ ſg^GĮs~vmdڐgEFLgot^LV+cp_\e +챒ĘϽ;P|ލy0*~Q-7o;:Q8|m뚉t8v\4揨_Au uU[$Wm[R꘴a춹K#FCCC3?:Q<4t ]S%^i mcbꏜ1R{%è61dnK\E t4vgAl_ ,"{.6t>t8 kt'D{hM`V$brDc,8f', +{ +#ۃ[8>kOqn~$Dɔ0;v:XcӔC|ݕnZ$h򏍆×%v})4!w5Ȱ*ΨdžΉ cu 󆊥$:rO)>Hאa,L>v:5JX^ڔYpa%uY{~DD: ̎eX zΤ?)>'*U;=R)}bpW~Q{KR#/m>/7YUz8MA }/i{W)̊UKїĎݑwq}/2<"aonb'wG1aSg(KCaSm/o]E1B~ѳJ5j7?Ws*(xb@:ކ۲pFo{ Iq5?y`ϖ\ +|pA;;BR@rN39%Xm{3F@*/g{l^ڸA =Dˆ-wf`6sl0}U4᡹)R t{%m/Ԕqw mBEPPi7~EKu<ٲŮ=?{WN |-y]Z6Ȧ>ZNװp۹G_n8c +b_^q|W l8[.-{}.}x*3)찄5b??=ApBND I:WMz3!on2:9L2&6MbՏW;ERnӅ-͗Ot?]m%AO>o]~#[ϰDi6smxRqΡ)r/*A`_A=oφ畻΍Zf5Fr9׍2˫z57})֫+]nt#YGؖϱͻo;0Z1 40y;ʚFYG9aŊxm~ ώdP'܅qW 2&"brRSX߫'q-osǡ_w%iVIN|_Pmﹶ*sH0, D̞g5|nUEU:ک`*Aұ!iQ1B W~8OS`bbp"Ky7PK(oLGt{OJC I|'OzӾ| 0zg#\䂟 l򳿦0lXv*03ݨa{r窵dWdɣHFTGWTKOz%z3WHG/QDS8Kd~';2St290P (#iz$;?*c!'˦ kTPlfʣ~c)sLnVnRɬW~ō.:q:8/;$ ͱ}]";) ͖Dm0bKR N,4 +Q@V[ WRdX|- 3Zᴍy{Ê. )jj EhR*6+{ISE葧a| +ج,t]8ސ5i/Ep-UրtHs@w)^Uf-pp\g&Y3RҸ{_]{=x$Oθ>E\:*3^ BZE ,~^R, l:ӌ!nqMZIf߲܀ Hu`FX3NR3 𸟌΍1I k#&gwLM(q)\JMnݢSo_Qo|{\˩So1BHו"cWR8g9Ow*ZΣ)k߿ݷTG7[w]Xg6nI>0gRb2 㭔MvOL=jsԊS[3)OS@y}RkmƐG2l\ԅBL|UŔ@\Z .nuϘ; hx KΎ icqOp'K:Q-%6cRUciR^Rc9JWJ@Zc8ͦ +^ן쮖n!s"?pJS*H fsF^P~ ]ᶵkϝd);ӳ; h~ȴ5xuS}l,: +O$1P+|WnW\p@PDE- ܻjHOk[ [lY]h'V8ɀOmԃ{BD\1>RYb/K׻YDxOQ3Bc.&;YMTWp*_ˈJm67Gh(A7o9Y9۵걇m5[gvzXIi`%*I^GyJgH[O;څf!&;#sjYcfk9ۨB6,HaG5Z]B7yЉ?S*?,lTrZlzlsI Jy +sdPv(QbU +Pȑ,`wȢ|̴[݃#L 9w]cƷW2MLqCnl%_}U|]G|v. K,9!)BthdX$[܅F-n-j^GNdžcWyxc> rlD`Dby+Pw&SpC@2O93)^ +u^{s2/e-VḵN/\L-Nx@KDO/yթx{ +Q eNxk?&p5n#=غ_L&'[W-BnIUKSs-1ϝñViYwWڑFe,=u@+% +R6!컕eL씒s)rdBeomRHy'LۓQR >ׯ޽C"]LW'8e֐+M!-n5dZ6MYD:!)D).p͸V)L]nvde.w4WeIoKƻCd*Sz#`h16un4FGlM8F$8k[rV CΑ:gN|4iua4OUz0L cVÑ$yTdvD]b7j`TI(KުWBg9͎id)]Q.?^ߎd:9&pQFAa5nK.$v*5~}nQ [mOPXP&M18BYw{!ܙsm9.zgM6Ca/Km<= r5[jNl="n$ -O(O*膿gZo}r[Tj2n xoֲTĿ6LR>dzrwVyW2M3.^ѡSk 69tr Yz7$4-W=Ke?fk火d68ޫo ]y[JA遨(DYi + Ŋ -\s*!ɃZ*WRQZۆCoB{2hĿP9A<36uZ'_ݣÝZLڳ|KFܺwDKK>],9x54|NCOgmBf,o8u%M J8Jg9,;g@`oWA7NƏe4Fٞ!A]N>WIUR1sYK3x>\dǵ1^zKgVsF@50d˴'͒U+GEܟ%6Zl`|hbw`nElBЂ%d;d];.y2 +J5(`V#f4{#:3J&Yu熌0dk؍9mLŻ wDM9fyipvl:Mm<]P<[pBUǔٙ=y\uNcۋ}6VmQ>zԡ{ŁJkѨrd;; ijn[8cy. d7r^9;`s*3h59h 3z$6ϋk i.T#&Pܗ5iwJ\H„8JPQ/Oj΅~f2Z{uT4h\*#Z&:U~IM9)P[dmRN!N'3R#Mb5n]Ybl7= rh W, eUE ʍs+F} Ni?{ۥ2}w{^d[r;g):"wzmb3>(%֙EYnaAt7Nk_r^dh efJMr&sfUR:ZeSq_C+84;^d9l<'!VNy_`Om[?_;'p0 HT|ts^Z]ˬ⮏4}*jqJ5z&ivfZĻm ڰGJc a=fl۲Km3 1h;~MG2YQ=AWM*_)/D_* h 3M%غ #b百of[{μ~%$) &.]XnSs檋}i=۴J b\JT OAP.KNџڐɕ[jC|ɖڐq3_ y3ӪTI󫜳.<{Rvv6iotP@]xm=B|?#tOu0HI^ٛ_ߠ3n7{zkL7љo5io7LBz'CL7O/{$~<ߤq3_oθsz:өsKBXO8Y"'$? N+xl_$[)UC'~c{$dl{TMq+""ڠvеV+XgKNn+G}7?gCzφލuB9[-H +{9.[aَ\?DN+Zs +NMl`9"n[ԶKeXՏJfƩ 2kc; FZi'`LӲؘa%Z륻?P:qk BӤ#^x6&q-vH=m+O> LPg~BR7Kk䀐 lFx7 +]>UOk +]e0m{Qt F=u/Ǘ&5s'h#AszF2tJ#ʇD W)'3 +[i wvЅ:\qe|c󑒄K\oB3@Cicra;bkh[?cpwǹ*O5U̖Cj3m(4MxA2} ~?f !wc|#k묦UO9z7׵Cu}5J~:&`Q 8!}JAeAOӀXc{ /eoCE)VΖL>י=*͊\hIjsΨ:}R~| Ó} K;XuSuXW=-'|-p6o+Sl$~Kz\j2~rQ$O5YԦ?4W_^gB^/B=~zfV].g +1΀Ssr˞ߍ[@c!?- َuoUJg~],.UQ[uKx +OdnV +]+H%JWstF2~i@Qu?ٝ1ɣRRnIE IoWHM:laR*B+v0,<^L)S}yީ !` +֗ԩZT{`q"ʱ",zJ +P +<m]F #xgRwҮ\VλuV"z6VT=X_}/5 B^WV άSQGVJU7 _ +͙}kVaiZ dӿ?3])6-RwP^i{>汛sRm߶0Z _g{qk\jK(<+1Q|_Uy?gȏY˺%@jYjRS#rey`joI\yvniǪK%~h1?#)tT1۔j5]5s4״yۇezHVυ< +endstream endobj 114 0 obj <>stream +'l/g}=/ KO#u&Ph`(M +S6 Ԗwb/UO&e%gC15"})6$hTxMڍHJO# ψ}> ]^FWs#e ^y2 *ٚJwXeYa<-.yJzhfDf7 +Pr]1}}&%s[«ef.5Hqju)=YLe/7=o_+ +ݨU[X{Y㙩`z?3#c+<q3pUiO`;+"k$>DZRt9'APۚk13P +7/~hzvp/DPN#*"("F`GD܇e慂}%Ů /}Ȩk0d +k ]kL_fXękD +L'~dJI&Rԛ_$~?:?![!]&Bt+~?:~&қ_$~;`;#5`*-{7%/d,.L6F|nSl[|## 0N/L +d(_şRkH %k.{qwI&mNk!NJc!*w!c(m8[ɝ).#n?"!?>Oؚ,Pw̃:d:ՙX7\ 5lz3|H}}ܕ87iΩcrvv ?tb;9{!D~&5T"jYqFftj֧ xvKh.>B8(F<{)~ Ik +(\O󨮞]zɸ┇ݦ(Bij*n,JM߈kQu[ʶ?-zrmnH%BܠT+&?PcfXjX'qAl-գ\2}R'gql\NUP}#8:W⳴ג $i/WnujnJ͈c]B+n je˫Yg|÷-IK'd-HR@WZI{ +tRZtJ<~RUpBp#/E^.J9#eO3t~.?W0-M=UѾ)} g*[x[ZrpV^2"K4Q\R?k:1w\\gi'<#m*[ֲ} B_ݮW-\ "DEy8ª]Q.UKQn6-YFR/R֟Rj|ُ+Q,2:gn a+wn+GQ?,-}ڤ) .Igb?{cT>g @%޽ K;7\N LerqiYiǴXz#/dĺ:Q*CwB52/k{«(Os_T!Y?@D4בCxVg Y-e!l}kEٱu,Ϟ#x.~z v&HOT{U_mySbjrt^M'3BOz3Uc^ır"Sz$kTlZ%`QppRM!p~g+k+ k$~%X?ncz3ۍ͏^AR߅e7ӏqJݶ)ci]+j:yd<{R>d6B겘7;}:" +1VK,W˷OmJMVut_Hst}v#|}Z4j9m''΅YLG#R?)B21zdiUG1Z "B2[,?K܅.mw5/x>䶝0/ 5+՟{ϕMn`ոSN kH5u}ah2c5xјTkKx]kj1v5[Se9<ʆwiҫ RDϥ9%PR_Z5~>:̳A`ZU丹S={gy,zP͋6be#٪tw(co%`em I1.ϟA# k=wK>$F;M~sF͛$}ªx 3JBW{Ŕu4x=ݕs`AŶZcz<{AcuCƣ>f W`ۙ0gShUxќj iNa`oVm}\Tm@XFN':,.d EpT$,8KQ&^ʪv 2ZЀ痮\G);px2DTR7L ""t̴ӷقSEKUo4k[h;K\h:%zʳR"O!PR={ KƧʨ ح7N."縬ogKH[0kqC|ooޝK3?_HZFM|lM hg*BϙmKWx>:W5YIe}o5Kp6μ9}ZJ@*" ͇5E9!)tif[k +BYۺv>;W(۶c[y0B?X=7SaW)\GP(ĎorYFu1߄̶We^}Yy)Z50] Gѧain 4o;Qj 1ĿF rϝމa]ʯ>-Hq[X1 (§+\:0,Sn&.rژjd JlW?Rp0OY <)\74hU^d͏4 ۵^)+K!%"0 1]"x>U"HNck(uԮMuuJwOn}ze"]zmvE> GɍÇ'rpO,K10fD(b<kZjg޷6z0Q+e$}p'zE`#鎝Tb|df}dV3;ƱD_sOÝQUB[Hv>I^c%uPd7HJ <3x +aFW?{6ùg>dgE6y8.q:rk&;'*3Xy!Qc@@O3"3*!3)67l`?p8p er?zzQ4\;#m&sӡ!YQ_L:#t.Q tig_\~ڽ.6z&GPN$wt.Uij~\VU5ڥ!IqpoC}[1ck8Q |Ǔ%Ȼӄקݳ\yٷ:HJ&7; ޷,CT/kc>.[PY +aRe,R'XQ$K{IYZ : C!mEy,ؒIq{3[;rQTq7<,FrXI?SSE`4/Z!OwY{a2Nݦϼisֹjq%BD;մ7B/"B~_]S[ܽd>/CCj>/Ia%&IX⃕?8~xtZd~o򬏔vtC~n)1BpGlN={v,s P \\X+sw,76[fC&oܮx$gVqJH(pXU/Xux%Iؗk,{"a?>R#ؤW 7ݞLxQ%zS>idy>OIySe_zқ$%). ,fF;Wdk_?alm.J9Oӗ"bM1/Wz19CAwZoq\ټ\\3>ݦegP\g|-ux{묧GY[1 +ARㆿ["}xx64͌NT]k/΢)r{Wj!?i '\D4w5y@+)R1Yh5i^o骋㵺啓6.ΈLq33KL1愠.*wk?#)B~tx /b,7mג>m M{blDz%<΋5W&|7]^>psmL2ia 3(O(5?]}ٜĞNC'lz6)8au7z ~2^,8D9D^ȩˏX;0:d6& ,X4mL%9~"Ƒʚ_{$& +G\ߚmBnTo|iADHJ!>gJƴFAR߀\ %/u kC<_ xjk)(lf5nSgQ1 bPLAd gΞ<( llGBv^IPIthKd<3)_@ZRFv*AškV>vT{z?U//c߁;:>sero GJ뛄(ᤚ ʹ269JGJil Ji9F'3>̃yGChU[/L/h/]cD2ɣ!wA)Nn  ^ jƵPj = hI@~>BDf/8L7|DJ(m4X;tzߊ2V'4>x+|8_dXgWKLܡ$WyӨ㖛z`[JץqbVx'P-I!U'b.tYU?!?SiE ]=l(}kKFQy/!oyfYLݨrfTk5-AK(k% +;."Z?O{ep]bx BUu3J328htvr[gW<,֒:{=ɿh}EwBs{7%[< >7zt,6y92x46[(U=z3j*YeTnuc= !=QVmJ\tߊW5(?mhMP;aPTF힚Ұw[{R[skMYݙl7mk\ 9{}puUыĉ}Mv!x#(䪎FU Byb:368-2[U[y9B<6|B{rT[St^-;?ur/T]P72 t{5b{•^wzrm5^ݙ;o}{ͥ8VngZǚSx=c4Y"?6=(&/PpP겠hgX!]hv rh\~\NfLL*sP61}(_SeYfWLٽ{mrPnBgx\lk.aOknXzAiSa/UN$%u:vx/>2O[᧤ +_Wj~*>g +lrؿll;OChRY )j.:bн1Kqh5|Nl[S]įWͲq|VnyLi8ȍUxwѾLCfVmlۮLtt1\ůf;ryvfmKs>BXS[N9HOB#X6[{q\H[JV˫K)B~fdbKzߦ˔at_Pgﳠ>p/=.np4 N绿v4L%VD찉_Tdo(SX$:BoKjVEqHq77ӵZ7 += (Oԫ@ z0.v:h3ngt%(FmeUtI11ꗢ( QRlb(s]%T9es2JMh>ބXOCK钝KrLrC:d/1>L4J'Y GF&A=:5(j:~({ G+@)vŗD%I@=]1ѹX摕ݪ%L+EV)d_Ku+yi#S'tڥf݇ phoK/Q+{<~4B4ѓt cv9qZw`3b~];4ۉ*)m=BJ SoQ6&!=H|K\5~GB|UO&eyopo5Řךy{9GcN݆tp}ĢܷF }kN4($M-˿ ]x/[v]5r7bbNι*߹vZ;DVT& ?ӄ3|f`o3Hҋv1 6{{y9K\řƯJ0OZv2:-g-ifK +]|3qLnHc:ʕcF]#=yK5e}N&O7Cʅ#4܆/ +>$L99;qeBn@ZPeQubRF|CrƔ枉toFuR;W'z iLd #ψ:}O(g`v(2UmxNm >rZ̬a/rS#?&^PȳH}R壇 e[~ ͎t:~3!_6%]̹ys^|7$V+͌ %.i˪cqӆ7.={w">BSҧC*?mu2IڸϷ1OnGXt3~4NC7ei00x8#gԪh,'I2y@a<յsnrm{k*ԴXAnn;ҬAП?<>zU{/hk2 ~| "e^̼X| alKGOOD)~TlJIڽ3&5CX\vWo=yP0m%kNu{[|lq]p`{\ R$3W ii7ݴcY|g/`Մ`4 pz5ك9+&hο4`(nPPƖ^h砥1KAh AFO Į}e0`0+i;EҾWd{{C'Uu{(~qlZ9JM*b{1 , g%MK.0G2W\NȝwBٛq#Zw.+6Q ⢛R2z?W?w X-q,Frی븰g٨? nWft{G\} ;w`,'PC􀖇^Z(\֣)Ok/Jfjdj-(0Չ˅LF)ylrF㞩cxlxj8B-^t\焴%_xBV*vBp p适zK[D%~/ǫ氙1EGj[OKz=4; ͐8*>1-w;הeq|vk qv/vd_|UKy UEECfOx6f hŬ~":\#{Z$i?dl3TNLl}}å\>8Y֯ +?Tt t7F ! -sPګ[d^rLc=WhT*<7dsḧ }]w>hkfk:l6Xˏ2\: Yںu7e^+r͛wRDwR˧z}g=x -F 1UтU .պ㍲2bߕ~ke4BJ\zqgM4Jh+[&OF8pן7O<.,'fJ'qK.hdgɄ6Gqߛvȧpū}OvV8$}HN \¤9N I` V%.l"=?v}ikL]Ƨ ۘ.RYvfP:2.uGlש7Kzbȓӹ˧2_M5ut:OSjF`/o(m 7,( AxJh+o^3>0.^oJ_-DߴIA_LM4yF %e25jl/HF*qJ5|a|ø\ ?͆<_ugdb׷6Gðo"8zc41%nA\}l~ +E'@]`ҹ9(tZ(tn?yIyD"SR1iyu\)^`Tstzr]R(D6Hհ<*FtKao~EH{/zm0蹈*7i?8; :~O +IS <ڌ]N^9j3:^a;j>o5ko |ʂpfW5>53$nT Yz^]6B@*qdޖrn3{LfpJfgҝ\1y#;&!/$;8KvNvB")@aʁR17 ;m(ۑ:V9Ә^%z&9K^QG$7{.5+X^D"Dd"=d :}ϟ + +#Zk`pX m/-IK5/J{o-VEGn{WTaT<*q˟DwBR>'e7:RmqHv&I)^ [))5fAZOpm?8ݣŠϢ]:Kww0Zl@O>"=B|2^k.s(ОW8}̩[t۹=UpSU0LOͤAO!퉟Ѯm<+PS~OO-;|ix8y]qT;ta_b+u!(7Ң̈́5EYʚ"IkkFQ.90lC8tL9=)'AN -HO4h4ihRѤ3!h2N#~D3tCZvy6X 0TW?3^?9 Y9~qK6@+t)!g^$'h#4sE( #4KL!λh.r;=BO?؝7{% ZX+y"TePs.@i_,ZoQ} }Wk:DtiXD0x)v{q~ݡ +W!+f}gZnge + +P.pu5yýZ+)z]:Eii1\%u1,E3/ /qu7s4{v.] V +. 0+q 4Ź"J+ET黛-3w7k&CJ-gsjw >8E\*j0}. oВf uBߦghe_~o3.HmLDt68G{ߚgED/5x-My+?) vAO| ׯ-#O흹OTAjwz 5- fY쉖ᴍH;/2iuB|zޫl +}0,@rx.l\C6r׽^t{6Zs!5݉h~n?[\΅C}K !;G2i^6ч>?ڀ _AH8;B*NKOV/;W:e%z\WtKS\i'K[#tZY$$RҚ1|HR|{fWY0Sl TG$?s@~,bX]:n ֿ,nٞͬ⑭r!zLT-iZ*5Żzvuv,^l(Dq׭|- 5OhU9j*BÈ7h(h<갽8X!Xe!l5zמpH -s'^J j)$-[Pp|q=Ob;t*'T_~p5}iΝ.!3IYOD]AOcSԪY6+ +Akm?v)ޛ*l]O\ +{ 4 M}O#\_7@9SڙI,cU8W^WW 1h,>yt&Q"ztg>`8cmp=3" 0|+@UTM6ǰC +[.rVqVwf{=U\l;Ziu 6u6WmMC-Gzp/V龹)@_7Kb +D Dr\zڞZ6* 햪\i rdalP!V-[N\;\ƣSSzS~*s8gō}d4Τ3K-<'T(~f/5NUHjgusiGrj{tzlY^sbѲ ,tVŭ/oZ+:|_?=`'쨟b)( %3¹QT ]EDu$+Bn3EQ `oC9%s %BKa-VJ/ +q$=x -AqZ1J=Jֵ S6VrJF*Uy1OwBX9Vg|{THBy}`̯ڪpma J" C,.QT %NIYTϟ/K6XU]gUooצ깉6IɌ<l˽HDvW2ADO\vMDsND*o1(O9ڶ*#wV*Zު'eV_qqpu1q~'p옏ݡSV :I\*@垷k"aG]0t{ܽѹwg,hؠ{ܝ`;[Ⳋ,*:hY1xA2ܦP3oXP@vI]=o:|arO!Xb2 ]aqv2~n3AA[;U?@\6ZOK%=H) +rsT+媍t2]-6_{ϷDb9;>}iI"(&kϸYέQ,[xߴM^?{ X!p!sW粻y[ɤtoj *QВH2 Hﴎ[muڸ[VFU +;ĚpoB~^_3a[mvXEd=^r; x( 6ZhOf6?y\:;7۸#UvmZoV>oLo4ɣQC=E]4D  ux[_i!M^n}}:dgLOR3N c; `>QdlpZտ/6kVaJt'j 52fncq*\v,fDH$zY-Y~rɣWzim>'⒁Jy[@ k<؀Q^V a:HUKj1b@_ tiT@F BsxQ0f`4HZ HWb4~5 MBQKs8>ٮZ3@Ns.KlY/ AzN=ZvDKWq%u Ek ࿴#f};:}2<{q,ifm?Y@<@f +Nv cWmY׬*[>p*뛰^UӔUz]Ҽ 3\8>S7͂AF=jZ| 8# 4m|@pA~ z8ȧ]i:GI1;!^pE \.ˆ8$P{Ciњ;~P_#gl`.(<EI7 +M\;:{:uOun;:EaQGKkg;({~2T,zb $]KN?vQNQ h: _Uˑeu'A>[xgcd/w'ZtQ7O72 ZSŸt, +t^iJ%IESmy8u&Se +Z%gPwf%cyfOvc'kOaY? +P8Dѧ^c҇T>$ԅ'tx!@G3Ep+6l'Nせ6q> ^YG=+gV=X 2ƔR,y/R^@a +qt)F7}.\et喇=[o#sWFkqߩX"'ߴ\qJUl÷p\b9^(ybG>:2xQlƛG]. .7"¡Џ7p?ekcX+O?)c5 Kg(6ޅ?:ERwvYo}uby7wj܍>aUzGK͡ +\)-dax/_֨j!2x! Oj96WF gMr˜tP9cg;\.^i@Tb +G5 2iwR,)*ϖynjOSkvgm֙KA"lsPG99\>Y@r.3x^a'}m'NLvR^^/=&v +ROXsnh=&OuP  __Ng/wa]^_W*1.JƹmzU`uCJ菓7'B3xP&"x$Aw|W]Um8WxތLz:0T:l3L}uy̯WX9b?#^[og T?kPD15x=ەk|s.v#Ǎ.,/)jϡŃ]3-ٰ! Ru.dE5Wc|* lڌZ݀"QԌ Z7O9uBO܃ө ެsWɱ{,6UW/!3Ikm1Uf 9.$tV+Y' -`/P*jua?my/dr=U"ѼTw8mz>}?g ]1̭ 6oS<އdÍJ? 7F!yhYIf$,&6+ O@ vnp>Ԟspx$zHZېesxƜ(qK:^^]In!p[@.Ւ CW$9.Ա4Jti0~͓=3%.魝ѫܔO5=AklR5tІUgW-.'+ +='rb98^}%ג\зKzZkr#nmDlK2vF6+3j&lP*ùHD溄`55%Se@rym)UfԭzIM{"}z*Mt8. oswQ%ѽvߩU:(WUY˕.wOPy= < lϥ̝YhyA+p ~֌쪉o]Pmt<}gd9tg^gM͑q|]LkӜմq'ȭ3x&Sm3]6'rT4Ϙ$;I-˳w&[nGʽ_mBؽ*QۦBss9!ҬUo|EO]Z3wϜ4K$fs-~|\Ԍ0I "e^0%orGƷحR .iFLMJuvtnע3]fmO\,#z~~4ZjgɳrFdz]OR\qIҪ:+b٭miSýNz b+HX52 HqۮW2q(5K-[򴽴ƯqZ `^ԑH.<7 |iRnF $aG:9.V,wy¬3}nM[×9VS6];nw A*wH%DZ%USUFG2]6ҼJ8N^y>^\O}R|sB{&OU^N>{1T̢4 +s/VW6Uo0zEP,qJPX@.5O)^_Y\X]ƕjHgXVq>+3o>o!>@dU*X3{YVFNJ[4,C>S9_<qO+gfb9DPaqL QaُϛÁ'hV䵼FH쪨wVE7[7[ʄ6 )b\f'òo0^׷{Cؙn+,YT[PzEQQQ@0/̽^Q&afdrDvY#e\#:+FQa+R6WF;(^((mu\&n+`TQI{v ;D[jd5%W'6Zd|4ufovwIX`DTH<e9f<0`c,qȽUk1ǜeE968#`kfYk쓤cSs>&>^ssPAn iD}ZߤJU'}|P2vz_hF)_ȤϢs +_RdAl(tsj!wYS8tO~|v6c`'ddn@^& +!7Ig_R +@*(01Pɫ[,/T/>ޣ&^73on\ziFj~#GA+^Y2s涭ZU6Yṕjؿ rQ/H \.hz6^e2B\e׻pNrک#: ;[͵^ -CWe{^Fڊ=.ZQ ElJjӈ4HT-Fj௃>!nase02[|(Aa6̼++hxA.R^5VM +GX*&Q[+PodAz6}7IHG>mՖfU۞U}׾?!]Ȍ52q5fpЫG]\]Boj +L1jME. I`;C;j +Us?țY1kjNz~kwUqB|+וJj|gJ2eViQKy{WgTǩMFѾ>o .g]GPL5m._`Nߠa$_4`t{>".=7,;݁ڱ'3H9g: +l;оLQ9煨({yzYU9?gà +Ģwo>S}ggOH iP\P>7]B0cuMw`n2;NvNkJo<[eee$-!^Ƴ_vWt7ePv,s,_/Xd81v3vFr.6ϫPhj=tJS~^fjÉf';-E5{sĄ3Vfdzodи ib7fbm" 'iPhR?4>:S>˕g:׆ 0:Z~o~?#`L;ߢdѵZ1\Dn=n!V bKZM8G7DzgzhܜRzAu + %?ӓ_v+RjzlM^~-_L}u)lzWs_:~?zo3@Ծ`]4q|_#y{"\0YX)io3ޅ:>rq<30.3[8N)%2}ke>I*AYقܧ}{ܖI>wJuOݢڛ,>~<9!H"ՊusZkK2^Ы)0PTñ/k' o1m[o w@qJD Deo;_n\U@!pp 5P /^o$d>-"aD_N(T@ +;"B\O?1_3*>YmO'zQtYU ¥豜4xa +'k#( @O/?'|>B.z=n}_x]}2`Uqx1G7/ sQ?=s3wXL=$4Aq"!_\$ٟ,=R @O>W+Ao"#z*wb-xxdeaftôIf!5Wz/ +5(v†TZny +g1, +l[TϧLL[|Y0arTz}^GR,Q @QAZ'7!n +i5AW| uIŜAq5|'" +((KW&c t׏ :7\ա;Tc$â)mF J9=fqXơzς=-.FG-ehOH + +p[L.bi6(A(.dFf5|z3uBj7;"9[_-S)-`;mdOJG7k(.mϕRx>)A/` x3;B+@g4w\nIP}VUbűj.";~"0*?y@ڽ% OgAw·wwXW;S۲okpn{ +q6 0 /~2_H_ \2zß +V_S9+礤/A+P0:ѹ4k3iԩQsEp=^(N(e`uSON>l&g%K;gRC9vX궄eꞹiF3{F/f&RYvXc׎4/P+ҡ^LLҼ0=j:U]:qaي= y?!eCy.R~Eӳ%6gGs`6p7fQ+ڧQa+njj)0^O Ku^f /&s|6|8p +!'=U6Y}=4A=]y=4F'fֽa^ڟb=OT:Ӊ7Hm}G*KCbn\=|.l+Bz4T(݋;k.9K{j:G=]Uyӵr (Hy-޸5]`V#n)8җӗR׈Rm$QJ"OmŹSas7GZxʢY7Pϸ0Rxi驈SRwI5w]ӳk2F׋ukj.|Լ=_ҵ^w’0_9}uխάs:Y` [U&r E +k#)Ii{:$ 1'J*/6AN'RݫKRu'wxTjU̪e=Fgq:_/j:B/K{ۄ`CݗđgW\ըgq#EvIJ5MiD8L*8x u;D.ؓzhb3á. ~7 mس l}摾jwՙǎ"2昭|R{`'LfEPE76 YTpQ8х^vzBŻk4:3NfPh>cvf6\>Zc GX t65^fr56;Z#vyqJ-}o=sf~z抩FܓtI:ܭ6+2HMa-oe<0*bYӼ`0Ao"x/*\6c#>ճ1g`@8C0߫أ"&*e^4֦2A &㼥6dృ(ƹB|-T9;V=veSb!ˎĴP5uVG&_rKV4A:|bvrM,zm +-Ma}rI o46Q^Uy0UT{'$3I=fMQIOkәMp֠E4eQy؛6<&qeB".QR3=F18-> rWچuO^e^egJ[)W(~F+r0<$ s̉YO?ʥJ߸[^+ +~ 6UXZK7kէݮVQ~Q[lE q|\ ֦Ldev &_ڌP 42W"'lh\LKʞ f+)}*F, + $x T%߯-KwhHWDyL|DG^"Uŏۮ;OD%{'g]лcwty@@ @'Tm4 ~q$6?o Mxzjwh$2%%2lp_d97 3n#=t@:H*h=lb"Tk-h`o7y~S&- +ՐC#WX%CDTWC9q_oSsQ@O>})UF;M:i\jRh?qG'W'JWv|fSskuk[)تi_׺s!!D%,D_(0u*b=fx  ɺ}8+{Sxٖ5lN ^\;[ρ˜?xAdPw {xX5ѧބf5UaV'tz'8/2$' +MaK{Rx{vu|ǨX2םj_ش꓅A߮u؇C'7e{2Y/vxC@t:B7o19(0p +ypk_(CisMe}T >߸vQT8즡VOJ˝-j d?oN\v}Ӕ/nv +ƖgǥN;_< +G?]޹ ?!5kK#PQu(_Y4~E_شw塶ݷf* > +rONz_<# ~^ +mT᰸3D@簷r˺+y=L%UPFV|'V'C"|7oDsn*zeK6.NUwU#z9{uҸwg}}Y/e?ѳONeZuPUV!WJsi=vIQCw Çg{]҇癨HV:F6ݐoPxVJGY 31dp{˱VCܰ}dL~c2U9bCInqsT?[jJL.Y-Ϳ_7I=A;fYm˫|W[DfsePΉUQFEI>d9bJJp~ZEsuC +%:K]FW+rGvjASC> ƚ[.yQzH8#;Uo(#{xܨ5W*1ty z +{޷ݱ`~cm薹- '|V2]{|JOF+i +[}ZŻz6|;*jg1Vah 4H ~SRt'qVLŮ +)7=5(%{ *kG߿9cf4 vm Uk>L45[ +=>Ym/1eq`>cEYkNQ, NT;BDZ0V#Ai11%Tujx %H y53OUΓXDבN2#ȹ[¤/6W4*ߖQ-hך6YmuguHQ}JHv1SSи%CIܤծ|GtW,gvL攕z]@X٦Vd5SP纣z= j"~j#U<(4+EQ$*_nk+#V[AN\|5.vhb>k1̿=x(@AZ} @ *ldUf  +Ⱦ@0{N\}@v\$㑳PGs}]GRz [ѦҬ$Bڭ $yD3^RSi {!y# +8=(.JaF4hTIJ/(J/qم~T~Jcn}i!+<ڻz'xI grCr(X07ћOJzlyU%QfB.\ <Ajqv8~7>\tv>6[x?w#y@o'iV7 ב gWT#rrs䘈ǴEU#*l;@]?b^mj']$ό/zb>;tP\{~%F;y-Pd:(f P`GKP3Rd?y3\6ͽLiѲ0㤫Ɗ2gKDNt]$rWATe*Adž?wGF8W]~Q'r774-yε{+Its+OMe'IAUS$w +rf,yRlgo:tJwmIs$byh!~78@QXKw + Szp/ߛhwO;buhkUY L +sa(0ff=ק?Z&r@R.՟뿐%%u[Zw1&C<%Cfh;6W]fCrQXr4s oHq|BM'llUx1U՜g= (?YHZR^ j_ Brs녗j$AjN`%m fgO NzVφwW躹Q#l~8n \_=y,+v:CzD"fK="AE +ZHk{)W,\M>_ OMĔ=-|z~q6t%R(W<z㣮 !y:N.6jGՌb^ljᑹ.A:IBPR ͑T蜗` + K3 I8+rrn9yihu5̭#DhkrVw4)7c[{702ۄ =N:T+[!I4_57׉D٧I7<u wwn Ԯ;Gw԰}q$5i0SިQ3&24PbhkjFMKQ+u:tdx!}0 ?gu^rspjxUB=fVZ=lsA>r/I +3>5t;JpZr)5,Wuav1 BW7q0cs7uu :mX HFwG",Vs!C[~Va^t4L<^pbuG> -]lieKRu'h.:赽e֚5" @W(_i ^C'wQmVQFyIw9":]wPOՑ;t&+byK3_6~ +/%|Q&OjA?v6]?j,b4PV86n+WVBzW$`xz Q뤳gB(] |Iv(y՚H6Uψvv*ΔWӠRkxvUJ[ ƙsmj*-6*P +֟]"vI% +~< M<9 ͡ [Ͱ_gг_opx=$fX\IƂW3~thHڅj;`*۰$V{5: 3&ml_k#=͆a^K?:t3ϰ%Mr9! !يkۋ>Ew+r&V?Lp _ZK=t/x <}~_H'4u%97L#$`g%9Ir[Kr2>+:7pQ +k7m md;Z9)߿Ìtxc9S'TtѦ4i3J2z~j7z~6ݻyP!ނ>]O^~?jx,=7oܛp8a +/YjٖƱR6%Dݬ>Vfb@O !TȬqqa}|5S?˪lnnr {ÿg=O-[.RKnuX[v18/_濋d }dWW-B6T#@ȆcjMkNjN{vy(-7D.F T'),)֏(ѶVAP\>Zv[}֬7UszW)K-c.)m0] qݡ<_ޤށyP=&ް$ +lcx7?''^xXmUH[Yv̫RDͦN1lؤ zLnWIPh(&vǎ0ՌG |5^MmU8o5 3mi-owͤXw['%{DMz:>o`5eysxb[\B߅G#ASw/"W='n~Xzi*Oa&=;@AL bFn7[& +jp{ sۓj8[:Sz}0ՎiKU^u׾ѐ=u .~ю կG i~ЀQEAZ<-˩iQzq=WF#)$)]Wds#l}.FhpSU{ɺ? >}W20?b)3CY ,3+YfDrU@n9.}Yז׌aoG*q}a6) K2'ƲLжOnO$ōJʍ7jשMɸVy۟Ų= u@/qщvnW9P*/g\_ I6;niieLօMoJDi-馦u&D~:^yef,l)Wu`S0tHvU)#'?\q2HL֜NXwݾjJ5F]gWWnrB/d7{W2\_-t?bp+A2snLm(Sf#)ܡN#)6skD´<+VP&VmhQn);>py9|q稶G^u5_(ta삢dAdr!("7ص97tv3m85sq0}. +"{KfFsMJSP+?(Z5ȯko҈hpo'2wNQC?/vX'.i_[KėnzOzy"zU5NdD}%vx&ZXﮒ 5V_+qqsEGGGCڄh_f(Zh<2>2R53ỉ\cZKȏF$eM(]f +5 sj9z# cM+)Sd8Oa}y+mT rt87aaa366YJY)qZ +erOs ? >t'ZU뽳ŗQG`: B ǂ C~̮!Br=,T$v¥A%l> q,)]zF},~fk<ݟvMCݚ5fCjit} ,FdC)jd663q뛰2뛋ũOR^?&캄:KuT7RMV`^yt <MwWq( 04PtfUM۵v*+x3}Krv?t{Ƞ{=w&k={a ;:iFe罨I~}s7 n I柳GDߛrqRa}[S'-s;׵ +:OPZm6mNmLk(mpZȟCVͿguࠍsjln)^beHNK4hU~lFK] ĺ//\=^*lXdԭg&g j~kPv}ʑ歘e̗+f GX=˱w. +\ݛDqF *R6 +Ze}J+I]͛BCG\A˄W!4Npds;j[T[qګ|Okp%ˍz;A֕=ʰgottW?yg೯= Nt.ٴ>>v)훘4wODf03uT3YNqXĩbTgr|ahĩ M4qȭ1oiz !?G0Z(Z#\)֑,&U:wx?ՠ\ M&K2d>5l2b>v F%F/8<:!ZD -TVG_I5Vsuݑ` }71Ϗ̵77e.`16&ƒ1''7ZDC?\$\5*AF'\?کz:Y|70^u-Gע8T=X`+0r.דejPv Ip/2IN" oMwmM'hO-﹧_sG7gpRP~֝-˴Ad&=Hg潪ʓ6nO+N(39) +tS.5>b#sm>N|FDa'7 I-"Dvlud.+źFqA^㺤]@ظs϶l>)/`_[(V+=[A/_Zg_|;ላ"Ss`ߨX~e}I&T%B:C#|js~.̓GnY}N_{aXot_4YxIyWb&/d.$uX[lQh9#N`w/@'yMR +:qGAy9=CI]~>lfYHKzL,ꊼ)1߾'iߗ_`_Z(d|'w\ +Et/w@P|rR8D08±Y"fmU2T|.~u&=xҲl1ZabY&nIO.Cj<[2ζytc0dDt<rp\iXa[s}2-#OkpBAPD^ޡzhR?dH +'Jdz4 厺R-ƏVqROݶBjIFCXO:Ur;eo6!ͦߧ6qps qx.jSqxCd^>,-_"H < Y&!Kbl^c}BUdor`ykԝ.BTK _QvO+^ +{R.%`sBVƏ&_j#tuyرeMԩF̃gs_ JvM >?_+/_ҶBòXwll&/_g6'%m'O;22#( Cu>wkW,mS%?11Kj2X>gl>gv@rƦB׷LS_qhz>kʻ*?TpZ2G)vZS~Vӆgs%~籝p)UD`r5wv.a0]l&l2Ɋ I3TK%q+kjbRUդxc\p.M;:po nՔ2" +ÛZ͋c3ܒQ^-{OØ} C?Y ElhacK%~;92abū_dSN-V3zWE_?v2S pDspGfKI7 cz NyIQ~Q6 /M"0|HaGOId`K?~ɓѬtS}Mq%Z2NC@ۍ4U@§!e@Fm'É;pIsUnŠ 1_ej5m@i͏diE)+wMxv-mɶ`DMG'['lim82:_2@ .71 " =ڧ 6@^mM% +l.`*v|*,V\fkҹ6|ZﻇΌ<{EE 3Gl~3Bi>}aqJ]}蠲=z-'f iǿR$9N+]glxG$޿apkCpXK/o>oL=/ p;rXݠ*LXrB7 {/Z-^r;|)ov2 I*g.|D5ǢQDmsgPP 1=wɧJkttsOEjvP<9uܮ?. /:ᬒv­\NN6be6JN; 25+sդS&nVh}`lJ]\y Z(v_T<KV A/_K]x;n`Ʈ=l{*@=t3q*DS^RsUc> %U3_xpHė|9_XGw:v@[s=a @pO!}轁{yϵi͛ -峩 ]ogͅnd)Z[WW *7s) +^K)$[4D 6)Ġ)x\o.bpb7kwO+껒֬ΠWsxX +D)NX,rA{X@7V_[&R2~|Lݍ:2G?gtjBk4_y7Uzk׏Xr +ڨYe}ۥČC^QnFX0eP3 g.a,j;8 4~`BOl8V4yd߾Vd9#XY,ֈzwշtTnDwSжdq (Ȗz4Hsi7 @W}v8f?ĉv񇶂Sq2(pE>Nd'g~'I?%?mAmğqvQssP1x +hf4oQ(*`Xwj^i2'L ;VЃ2ܳ<*s&o&#,(-\rُ$^ +1wu gMb|ZFMFs:°ˇr5ZYV 44wy5_l8BA/IZ*8]88q8侩Y{(EZ W`e+f.e};.0ۙy}견sā8Q`v(.dZOXE @& 7 eAӢb冕/jMl:A~DO`w<1yb t&d mӆl.G=YʱB+^4 cH1QGR%$7lsx7}%h7/t^S5{ymwt, +Ϝ~^ۏzgqfoWķi߻+GmPc/F4kH ߌod$+x2ϫw?Ew -؞cu% /rX91M͝X F:H׏- + lDRC|}o[֖Nf|xaVol@윔N\o/ IQlY&C>c/!^mrijo]kHCT(( +mTlF +ԈmRejā=>IAA7$  |/K=n=(LR`񪬷IFm^ ;=EM2T:U *bQ%)k UMf|Zl98MT`Άr _Z!N]a+4)yD2H1zB-{ExWy9z>:no\==C=z+0Z ms_^0H&6$vV¢U^2g *T$4jIi Br7xXkP1޶:qI϶F=sb}~. nX%.wOyj7 o-[OM/}\iiή9Em h$nջSvNFC +jJFbltwԛ_7|WS@vimogc}1!k3R>54f)i/$@L]+L b VѺ+K>{v 0s#[V [챞޾n-Ǽ)iYj@5?\))r +==pE v)jV}Uu_-GtD[˵n#˙672}Faأ80NO3d=>VA7􁘹ޔgɞ\}eXbm&ja>xz xӀkuMwYVӽ?Xx׹B>ga~wN)^7$Y@u\]l->T-0l[tJ؜nίnNӶNB_-Vt5R8Qy0,Y`Uoڲۿlfz>_>:T~< WutʋV6in`4*pn2%c#+,ӟ~1$o H/(_Y]se2O1@*]vPYMB9˶ɫ0!oW?ɮ Y/JO֪wJjexNBRhԐr֋l}lv EEUj㒣DNv}at yF5BC 7; psy䑤N +n _f+%p#ƢWn#wp PgkkIV.^Pll|gсF^Lb1&i`vF9)8%v^4x>.wNDϾ۸w|2W;G9՛ewEwælrVήb8t>{ ;CiM+)[s3>/{_roS⥬{Pq@}ޔsCX~I>kIӰ71K<:f^"w'e?A{$1xcS.22SC\zeO͕-7Z=ϩ.> ^8 |Uyc"[. 'A# F? 9Ow9[sLz8n=3FbedG]r ?$ۤS bKRCGy]ΕyskaȦV0n,Wq0!BȎǽ0:w}c Y>dr.ȐS_p4C5tzyJu'k7SgtҔRV,H-惵J%{/|:U \q3Q琳|F<~X/-V|N[iNcjrZNB& +f=WuUzjLes6~nϤmlgr5ڬE?r8ɠbEc35o5>o0,.L+|ϡ2>n> lc( Pzֆ\ge8]{̦C+~Ke[FB ~\Zځ&n=NOr[9cNKXa91i$<,z_WnQto㗽v3wd~һ'nv{Sak[L9zgXvfK΃M8|^Π9֜j$F~~\5VfS2OgW})ҹc|;YEoоMONP+/_i5Mcn;^_߳pܛt `oID飩l[.yf PNߌ:]-k7==\ ?;hOvyҚRSVS8%tn FilڭQuf}z# >jt9ЅUĔV0?0]OŠw|n-2Yp7^amyA۵ +~|n7ߨcNnW͎:Q(QCU׫:V\/Kä2˗v+]ξ'e +p;Iggߘ5IةHz+ S +.{^X [aw%V~'#+w:׵DϴCHcvV?384 <c(rpȟt7>6|hgkq{fWw))<)B*Ϣ[ T/L*“^GGٗ:+ +G+?%$9T04bvXܪYߪ.ߗ^ʹdyGG0!`WRW,/]e\V&j:]oDO3'yƕQhv?? [qZc4WC In}S >I~|6ѭG45$ +fCIb3Q/-ϖH\6|/|AKTz˖] roMֵe;-. I.T;F}9p2jA#K;"n#pkW/i}]}8MUTZ/FcYl_svcٝyڹwkXpPWR"5+y1^b/WtvwkUS +z z)ŏ'qxa7P׷:\7퀽T]Ⱦ.o<4TˍsL S*rR<=(~ׄ/dcfXXٝOxANy .ʽzup>479! 3N̻Z0g/q髍[Txs +j%q}*0z黹=Bp¡6fZ"Wyd*:khIW[]57,]b\%6cXhERS(^]OxQ=uЦZx ܛV}OHx yw#F]>~!L ?FmW`wҤfAօu&4>1X6JQ8ɝٛmO7K'(3}Boˮ+K"6 q#Zo$ٶ}KG^P{O~yJ75YOѼLsM5ar'}nE}9d +(( *_9wV锵Z-030IMA>siu 6w!#`h{~AN&ty'\rUD>sUlm}/Fvqq~[ Q]bC)N0 `i~}OZFZH#'fQxx`? N ۵}@Y=2aݵ*V)2[gW2 $$!Η1W)!mnztz42M*l'8Ͼ(缫P1l 3 0Ml m q~zq= zQ$K6te5V׿2TJ6_'%}׻"v:yRHÐ9*A_ +W;L!pY}+]+8z] c, +QPз߯su>BղˊyXFQleReϳ#*eL=Am;A<*C2Z%yIg4D~#kYE֞"c+#[_~“WNrٳqg-蝉րRVr Lk[rfNI;՗5[eX70IC%"nRo^1<ӅW-Ķyo?~ſ|gMNw; "ЖI>,<16"LGh~Pm$Wp6P'p0R.c]8hk ?.'R+] g": .TG._㈕y-YCb脒XE&m)]HhgNr"\hxPr1!#_^?J~p +}L5}~DWo n}|5T}J%[' `XgoChk˾A>"U) gnS 66 yY_5!bmy/3ؾF-OG|?eKcyqR%TdKLprn}˰-.8O,rj[l5φDIx}^ڽ1H!1znwi|wH}֩z;6?<@={R>Aսff^ͷ#hdFl Db1d L—Qo~ϻ Uޱc8mKݺ5'^OVS ][a +ϴK;||bn #cXqw!N]F@/wu]lEʸ|V MNhU) TJd u8nĩ6]*Khu +c*gZ~Խ*ι>,^7Ws+˃l5;|@*]9 ;cl6=pe鍮ߋnwVhϷLvDpmBd3}QPKw.r_j꽀l"`Ն )(7]O(W%&ێ;aym.ezqf8G8>@yvz7hw%xBmZiկwC]*)5 /.[4Y@o +ցQ,TK{ >+q L xO|L~ZhT{B*dgC?'x Jbu5N&ByVҼ_t?h,ιVxU *h3nF$C3b A1#kz!k&;&JIz]52r|̘2$^OIZ6&/;I躗pzl3>YCmg~{=G1YHqˊVqhr~r$a%iU'vv c.ΈKY+NVfL.+7S >E7%_A?8f->@W̿Í5gwSM4dOeC';c'ioH)k9_NA%6" bxP +. {f|bN,)/n,S6ѳm5}Z?,tiEƛ+`JaNCTJe?ů|[#IϞ|6+%_IXIiJo9xdNsέǽpT)5qm[ɽ#jÿ}mzK W2x7CI.ટa}{$|XG!pL}\r!F-m nsُ%}`N8HZ +tM +x( VK/>|K~R/4Xs$ũg~ZZAX:~ WSUw'0p9}?]׈"{l;ٻEgQ~4<? +SۯuiO&2F*z"qڅ +w^BrgQS$Fe?݀z?;Ʀj*n/lkYly܇|]6/ޠAqJgM <{Z={+»U?iqHJޥs<:nL;9)Ž}ኻvk70QwE+ŵ}\)둸38ҨznaW'`Ӵl[ ϥl^3nd(8 @޿Q# UKߗ;'7O/Zuji5 gR{7vr^Vk㪰Ze׼o1%`#@.\ͫgєFBlFzM!<= Dgq=ݐ\j ˙?dW&8|qSQ?/>zv)?=[2U{3B-ftV --i`)J +;JQ=FY?g9'E8NJ}ٛuzy[+9jGyAOc>Z`I^8~'-,?=z/&%3=r;Kؔ->K)viQ+3U(.xI^KřwF(/<,g#4ؖ r4 a>dO!^X?K{']a j7Ƽm.%-?uTWh.ԛwH^[ J4 +m㖤~?s~/="qYu@y:2Yt(5%gLGaf3rO!Ft'Ith[':n@[㚱VyP㧫ARE(v[n.Bg[ہ&-v-\#nG;of2N4Id*&9OuBYK֞/mݣZ; XAŴQ4`ڐ'\Xgxf5;AVW7{dᇨ{w:\̗SZk,Sx%nyvQ?wf[%2p@*~WCزE4yj>Ić$tF̳-)q nE+]rFx^ID W߰2j|Փ)7 rF_6ZD;OPüw +džGƝ zU7;l2/pAyjSBwTGг~RV?W\ͶJoU*Uբ#qXs15hlOlS1@S%ǣKe>Ǻtm<ǹʥ:qq/ Op:5* _*(.pՂP V;"Ď E%˗^Î^oeo1̧THMH"NhC6_3T&\`jiD[+mN.ۥW1`Z30x008 L*fx=3gkÅ kJ=O[ E$Y,TOgeNc0 >.:M45xZ{%,/%W+73bƭ 3زnlunFU8@2@ 0ʘ0JJN՞װw0hڛ$'K^?JMHf<~UX|/xnvRu A vλ͟A@_v1060B|r>#GyFE( +43I?Ce|$f;+NJ>%}x޶291bqTŧ@>bW{\'C)})F drc=Hˌ8!IqIVk+0!v;3wg^A^uՑb>^-gT;gVu3.ۇU}ˊSCiCAx7 W[#Iݬav2Lf'S^AT?7}^<#kc whɷ7^0o衹&\<~]u[ thبʥb$h)I{ݬieUNp$wLbX~Oi@=Ծ?6}Id@]:*Vh30UmQq/[/ӡ>?X~:g~Ka7U~ un;煐[N/$geL݋$ n8-]Il[\ЍʅgR37:SO=b":hK9Fgg>OWx1k=c +ngMGìuJ~ QM~QN~W,s<>:w/Qzø=^xm GP>@L&Þmj+GlP`KO) v+4@ٶ5ɻҽբƚr)`eS6oJl:&٧s'~=6Iq J737ۯjǍ̮Y [qڛo;Uan;R卾?@չ8s=HuHl7gǼ| +o%-pKG9$_!&U^vg`+ U/2j-oA 5&>2(ͥew[ +byp=UiE"ˬhZym[ǔ"a9-S,r᮷|l/[ZJ;cGemg\Ha5ؤՠM2!@gKKh$SiMx%Z!uJ}|yVZ4}1Fo9hܿ|I˒4|hVD޷`ÒnuY_OO~vW'\^kjS9~ݼN7Ԭҵxed޺/^wrJ]!spwGsAYzhBuTnK 0׎m3*Ua bgNd*ݱ ˨h{Gσ~Ւ Ц7UCg!suKWJV,CW :K:P"Apb!W#6(,[9H)LW밐E̙{/~z}=tz{%N9DHu&KXR8/63}Fd1Ne²%0;0b} +s=?/ISN%d{OLN1ˈ 7wq/]~&]ߕhNU_6/ րTk;[t }!J|CO О/3Yb7SňixX&_IA% QHnK?#:ڃoQCTcJBQCԵ&6U=#5}S+LZהCsRkD +"a4zEwy,éVwkqv՗]qX&&18!:Em_5>g ޯ';>pXGԺ9оVɧĻ̝zLCYW|8 h띙P\}?gr>`ٟ$cqF|QU@L5lGŌL \vϠ),(дm8wEv^\i 6\5|$g^/+Y?4Q ,ql#;0=8]6ްEx[p9׭APZZr@S6v?J>RڸVbZQ)_ƛMH7]R{3D  p~ ⨫Uq, Ww +Op>{Oz[8;9eFz7gp 7ݒM5Z[qg[ +% "(H"Tŵ.PqPUT7`t#B P +?y5`((l =a5sTր  i؝(2}QS%csZTSL۴͗ e{q&4!э7 EtqfVj_eءcskP2pHע~\|zWW{<zVC֠o7_]G]ԝ`_T~7^pIURj\wg5Q|*DTp1K^Qw("tklOMY#,o4[w|tݢTnv]T#ݞmv[p[<"gj.^h/ >?='+jF,3YkZPE+`/8!@?"Zׅ^g?Nu\g[Gfaa=]rԜ_w*r4AXm_{LFx2 +UavRʘï_t荔ô(ǵKi9]7hi۸5w=>:96j:Y6FT0@MVKy[RRk0%ejXbp Ŀ,g- gHūo1:pSH? Qy z2XŞy]8 +,_SXXelN2!GW^#Ϥ26=3;gbb0m8-bE-YU.SH/ uB$26NZ@O@@t +;P<h 4yO/ifbte=bu+pц!Ak,2+5kqk^/Oa{NU$MAS 0 vp~0G U B@vh?`z!c +kS/ml!IK8'xQX#9zL3yY2ː)&ɜ'/WϦ6y+| +z+5⫮axb>qx;3xO)mJ|P^+y)=mw_VeۋED&(}$JYC._إBSW aqK]+W"fߗnY?&M,;5v7:-ڝ~%-ai4~EQ62 ?nJҞ$vQM$ZWHU|,1~տb^:~CyMx,ɨmzo  Gs5_9B8< NyH!IZ7% CIT$ >/]7(m xJB餕HԹx*t}@4F'OO4zKr2uG>.ۇ5O{G{QAl$I&=j'鴂'`3j2R勅=+;trpt.[_*̶IqtnՃ2m6j.UdCߛG1;iH|<}5mWl91f$5$IE(I}t򲴨 +*߳:^r:yc{cVv*9/{Ƨq{h r>Y ;'ۏzͣ+H`9]O|!K~;,$ +dׁ\sWMDބ#N}| +R:M3I/(Ã_9"L^GnVڶ' V+v^1hզX@k™kl~ r6K1kWoVuYC)G ף0Fܥm֩{Y:n?i[1k9[xC(~ntiWay8g29l1'1I[)[q{i~*nn^ CG^'@4GK4>smPg] kcܲDs*xrnmW*ϚBê7/*U灙]"W+YzCl|5tf]FCA5bٕޝ~Cxso{׉Wӻ2?lcSgs{;Z,,LٯZbma\P ?֗6-M/Fj6?eQk'F~U?*[ZQ*o]+k` i9PV0\u@YH/-[ɬn9tXX-SAܺwz;,i)~jvZJ4,[WUʮ2(@,{>?v{\g2ld9ޢ MIե?{V2ϓjnq-L*+9gԷl0&7_ԉZuN-;GJ5i^j<\^9rH*aI\$"u %X;#eSS_Yuf G븜_t:Z$m mEk="(Rg東v[.E,+ sEOI[D.,nXUCOK ڞsoNM4|=ހQoYGn_nzv$pkj?$U+au܏u_gw|Q zyʖ>qؚ[_̟5H2tdFg?n[A>`B %Ud튋%VGH9ae?$ZJC0ҳ"dR ZP]msA .=NlXcܤ!eO E%LYqo*[q ;eRǵƕdJXOg97eMֵ74ո2vM lӘCS-%xM^BtA`K>rKD!,]Fϼ:%lH9yь C(k=@1oEGK")]egl㞊̼*.OYg@ٻ;ބp\*:9Y|7#{tù,qD9qYhV2.(.0]Eǥ;ai w}7GY\ V2УU/? mwšK@lp`&t1de}4lkq)6y#h޳GM6Z049-AT2PƍF`OaھVϷPy*io_QBC}vI(euzw@Af]b0jQD]!}7FܹyĆyGvE&dg;/-" 1olƓh¼ fK%:uLskx,*l0WNl=rs+%U"h4OM>kZã$YahoPc#_7V2>^HxV2~U0(pQto_[髧tW=?Gmuխtr YZܣmmGhr&*½a5o9; +;W_:d5a!۵S,^U^Cc>_JM_xDr˲" O ߵԟt>z"ðWC熔fz + [a;zUAqF;u[ s! +Iwջt wJ픔ͧz{j4[gEk"J#(+(kSRZ2bLYƪ3?zXdiF`V,r;k.b>J|T30pXٜ:T5 m >qbRĢREe'+!VUlبluQqwMх(-w _X elM=Y㟶Zw>yR:l*͑\)ޫKq˦]F|{8N#)˨[ ,K^/1ɨ8ل^ kA[ټ|"Y]/rFJPX]"~\wc%I L()b $-C9A /&QXuV|}~L6q=s [ni%HjL/F2'@um$P|_ +]E?lVێWiAi&:YkFSϿd1{B dwfZ1@K% Qt2:i-~7NdwIYл%XD?LM0,Enh㼍:UK6yVN˛kq`n6`> $r,9VFm$`&`o `9]uLUi-ηOckcq{b2A]jgdU_e\cl/J^a.ـKҔ]7x;WC ~ 2*%8@Jn9AxaK>םuilcGN^F$dK&(E %,ۅPΕ +endstream endobj 115 0 obj <>stream +_+ BoYEa:?kz\qx\UL@ ^tCKrjk>OSMRK]i*&۸vZOg,HVdScO ȸ|K8Χ-O7z]#^']Ö.LYrcpyŤ$ʧbInF|~z{Tʟ~˕L@!jPoyGRuA;V V㋋mٞ4~;*Vuz/K6&l3:,=NMGE^O nLG7z-~r˭J MZELYC-V[{M?M +A+-Xga&BcUI"VN^,(Fi#ξ Txĭ5Z` y'= +:68T逮qdoزnX}4 :ҷ\ps}?7Iq&thNSc?HA$>%K2?9?=N9˗r.^InC(un\*l'ZMzG׾֭lq.yS?WȞ/M^Y.lC:V9~xjA^Z"t'#pbuSZ|}P2(y:oƚ8yw]c {ytUe<)-{=Ķڔrf׵LUe Zd;Kk*CF'TcRQ6 +'?C5g^̹{{]>U U7~:,rKsy +p8I-bxRYKο6tgIlhZ_d.ғxp@~}ݏFLw>\ZCN ^^Z]獾b1 w}+6*sﬠel 990ڤEYgcl;*zu8"fȄ/R\z`Հ +-r2z=]^]R@8UZ,ksE{tmhd=x*Wz<vv?= Zgo(k?Z EGZy;;yY㦻]]Ou}|zcjFG/5\h>vI |NP-b<5KpWoQM<}kA[6#I^T%ȬHyrnesj7 ki`k{c0k`r$׉6(H2ǗSf[rޫa*cڝ vh:]9eUrA^+ykYt%1 (ՙRۏT98sR#{ܼZ\1ڿWwug묽K^ ZEDzimHVƶhqxћUۉKj_ܿfl3>zz +'-|&Lf0<}.l}\>fxbDy\=CT;>l 2jGEfPoM/ AN*|g~ +0C'Ԛi)vFٮ`BI4ٹqpFVgɕɲenay>SIPY3?Jlr? ͪ2~ߴ\5.&N`VKcOz6iP\>P%T{GZp^fuzpw<`oNۙL-:6j/zI^*?dxxo9ZQIe(gk*l}y#Ê֚υ֝wMBH+Ej%$peޒArf8ɴKz]?gtg sQH Fb: +13Вe:d =w|MOOK2Ѵ1ګz'{FzZX +Aק!XW*[V!CI:PPf%Gܗ&osBx9fB~󡹌 3;3~43_s}5S5S: N1=6mU:(x,@Ne4~anLu8(XYcC4q00u:+*|x^pK{epaW+?x}s,ЧSdzN2Mo[\Tջ`oiƏ3/'6So!LNdjKdn 0g=\":, 2v6,l Id e?gdE^Ɠ]kGZ˯{`:.xMŋCx 9X'p_js)E0sfqZ*6*}k 4F=:;i*6(2UǹUYYU36[YTe۳^^h*dm +wн3@^M_&9}B,40#"4V4 {acϲ a|wTBC1Uʘ+54U@Έ.P_rG +H|s.ףhi ֜l8.g#*%:oEws3nkWsTGX)}*W;K3Zr + _(J{ e0wijü[RkOݓiW:yqӬ,VAֿ?w 3?c 6uE[%z՟#u9x(LXwU3/}4>-[H̩zg[83(_gÔV0/;Y@i6HItŃ:nذFF12\aS2gg+l-l@Jq! 3&뀼T[6s!}@*a,e3|LLI j-c}Xĝ$֨ab{[YTE:I$od$.t(#@S x\4Hk'X.M<t1lVze?Nuᾮor(">)1p$wjh4Vr"-m)/8@w 0 F)~^fsЀLۀEEʼ*'&@,_~sc[Ah{\,]z9 R֪4C06fj3l__q c 8\zDI_oBwT5Um.3PW&s0w9;=p2t ӯ 싰CwG~Zy }B$A0ԁ# bs[B&NCKnIZTIX{T>M+#R4~3nH vhTC߸2zIQߛv ~KfeFo2kWn3*q osFduɚԿrd'ŞMTY/ϟyc]:{O{GQ.b[v}kԈ+ `^˷/.(9 dgXx=㈬dpR/,T[ RMʧiTlTSIۏ=>1'z= ~˕G0ŭ .'YAD{a<%s0WuOlߝ]3}nznA$s\\JSFc۵kTy_tz^#樿éV?U-[y!~- a!l#Cm^^pFm\f,@[SU˻vיxʍƫ&+e *i;7q5&'!+jVݰYۨos~r@ƥ 4ޚ/]ץ]dvFNeisfk% m .HG9cVf3}AXIa@3ZFKS_$"uV{P3㖘W]g5>`;Hnus6B}6Zpg2۬n,bIp.(ԱahmkCm'k۝i^_+ ouZ8tk z[lϨ8.O7 KN13[4wݵfpf2"7U!sag}aiKޮ2Ѫ=~zz[ dT6-pJ +Sʭ+GyyAbO3Z}M\>;7jDQ~8 + yb` T{Sn{;z:ԯG: +ݢ5Wyƭ,2~ֹ37g9gGt=Ίj7Act3ԅ3mg=OwEP-7ǵ׺' NnEBA B7&ʧn.Zg!,t:3:-dtqR/V-.} +UWeugJiU'S^[m闒\gYJ&zֻQiuK0>oCDβ3Q{=NW}^ne%ne+ot֪:ڜpfy7]=;ԠtZ3ecg Pԇ0i{-]R1HcγثuxonRcd;ڊ}^to5&濥Z,CN6;F* 5΅h0w[LSRRM'wf.CG.}fzo>_DlC^ȊTIv^ߋWFChgKո_ږZ+X̓T4 j .H?9D>:`ii+ [RNgJ;v%n5 (~Fci b{G6y6}љU@-vvV1CȦ!|Gj^Y`~;ul3*@rMr&;Ֆ3`-p! pN5e$ɕqHO/Cјyv.r'}$Mפ;;FK{6>0ZhV5R^yQc%8/TJZ(\w~HŶ&;$;m9q=?FWS왥K2gMzݻ362"śtaCSXԻdJӭtsS?7*˹;][iOۿƨtˡ[0'?[Ocs YlJ@?ݼ 3E3C= -kT6e&2(Ͳ"inwO"t+=M v'GU:,;i ;Ov<@ vαUBZx{j¥ȢkDQ]\L-ֵ:*+o\Ye]yXvIL0Ei|0(2\8H}5Y>?Zz06HE^]hM>Vz[oe(;3G22)Ѫ@)=nH]H-.9]Ս !eg0+MqܫFM?o9L[l;si^=9;"{-i13k~J1x>H6?r<3bM(聐E&Nx0MbS-.6N"FG!5>{4;:?"g`Qrtbԁ q=rLB37ZTmva,8+&{Ntᄅz!׫īG +1>gt:5k=d nVJϦ +h<֔ϼHȘ포M{3eIl`:%aM3b&IZoSzrAV{1r7t |-wLzdzm-̼j) ږA fZlٕloG$֣rިiԻu[#iAh 9`@нԢZt\$~<,/c6[K9Rvv3~M݂_Bz:d=7WOwEZ pKJu՚U^q9]Qws">?)V#bPL4D$:S갗2 +jKUlSn$9%߀[X @9EHq2ꇁ j}B$)-Hy?Wcy;0(2ve= +6vt m|W0J +2J:tU+ 6 #t $x?xd'7;5 9T՜ W[{/~^};z츌2§\jdB& "H@smRYhzIRu)=S劀xt&`Lf ~9FC>`$|C2k[Ӻ5Ū6SZ,e}E ]~El~Gf2O 0F(H?9K n96~ǏxM}]-^1\<f 1nޘZuZIҥ'+k;G]QjU9#~hp&`n_QS'fkvax20w o |Xw8!UTH}>5s(|}%ozl~"{RH˄/G+PQ0nUQsкr~'Y?#k>VrT2{p؏5g@ܽ d>Z'G= &p&{+7^Bm KQ n'ǽşNfFf 4!/p kbWX!hi" ic>?] ^_B{V7_fe( +y~;AxşcڼIf昁/1u Hj_+d˓^I'iJ`8O3ķE4x 4s!ůOg_Np›4.Qzn'e sCWO_q +Y_YHKtR]\8Ce:5c/"8^p|\̕Lx_ +Ӽ'2V?wVFzѷQ;?Ѩ U|[ӻͺX'y<8QRZp5:5{g-nN.bL +mM<~\_9I{f}s&odn=gܟSK^_N!agu/'% l WŠO2t㫢>Ru]l7;mhXmVk h5woIm)ڱqtݳqVHBVv0w= .kYt0SDoƮKDՠ3tQ.ZjZiC#5],n>5WMͽQ4 jD@[jE%^RJҤ>$O:ȇ G ^~T zͶ0[ߊzU^E{+.̌aDyuP\ۊv|W=7EnYANd4P]TWIYևSg4T&K~#< ŜZ{\Oޠc`m;ݮyuMCw ]T +u:K_Pm|VʵR;jNՙ yӫ*5E.?^t M R+jßǣ !aW/ƃpw fӡKBcN]wTeSQn71:+3qk8՝SW*^<*bY.3}:߻nG<>(8jr.Q(̛죴[$hG?l`<hG^wtҿMiHt/n7!d!ㇿJ{h[U9'dn2wG}2-v,_Y}arП$kƼC;MyI3I`Zgʹ9>ΩL֮iv'nZ{r֩B# Uup:_ o`:2tP6b^tayb,m!v|(;VY+: ~-h=Q#[tOZXz [دMs;"ɘg6ma'1vc|aåcĦ~Me"Wj𙓳wS).`$otLmQ>B05χj,2 {y84)C5&*b#W{hKűy4e@VwZH#t}Xvcsm7#USj,_k^ J<*mI:,ch8\ͧWdHm܍'q@O(x70yw)K*!fEwdCt(ˉyvګUmY9?o{ݰ_|&TmFW^Y2q\XqˬQjFduE+-qg/!?=XC9lp=hDTtd(7]8՘(\p'spkW>TM 1Ǐܴoq`f!V9ԒW׆"闃*J\7I_.q/͒cf1NLupUt*{+e`*YGg4 +俾W?o'9HGi?Z[|}]eᅆ&#^}^;P \ q8nO" QMNvɳjZ,|J}<"^y!7}hH"*Xge؄ ;gUJx])aYQ EN C)22s|h.N#CEQPCM.R1.F^Tv,fL8KMveP\.ڃ%۪ZۍzkÝvڽ]gFrLWZOJv>CDեxIKEɶS㨕~ \lc\C~$e:; +NqE :8wF2Mo%:,\x5U:pxhs,T9$Ӡ}V :VWJ6. @dzp`tIąwʃeO{r.1-{쳫,l =ϖ {W+YG'u7r ..PWǬR7{5l*.Pia Dx1*hf$* 4 zd$fAsb'- jb?RD"oN=gtcV//`_Рަ:DiVzN|!nSTdJ@*(Nӡ;G +j ^ #!1 XK26 䢑dav8䠴 +f@Ke^_ #)'"6-$љLxG:cĴZ*>y\ؔA2 nK_9 P~[Tph{@7?k/=@o2WzgvF jjY@~\4I]/կtY#^lӾi6p>mͯrs UhjtwlUiL>1: uYnMG$GEI8S97It'h`PgWW3W-IJ +PY@gO#~:YGׂӡ_vhE_)`+@ ) [--t &TLSZ:Sdz[X\/u9w|=OM@ +!ᄩU6k3Wn A`DJgA O>?j|;J~dAíGHMʧU4,U,4{ifWcG;tu*s؏._c_qˮmg3[=q/ʢ뭴 +.l;̶yl'YhNmTsrjp#tp|ڨwv7'n{9" V@VY [4чw;ɘ MEO[k_ag}syY!b&o;Gg5RW*]{./NR^[ٸ6#{˝?k_n\(X}I>U.Vvj+u9C?l皂J>2(b„9o9zR(xaTkB_h|@U {^m}n_mBI2&]z1oy"LCbꋖ=M[mwn|دwG,H'6^v޶HxmX̕FrΤFyε(jo:*u9K!9w:X%~DHm4]gZ<2 1yz7fE OB}Gow$}Yw8P+0iyZOv{OwG/(8ep?8M9%.K믿EY}QOgl!9FF0>?4VD=!Qz!`cA}WiGNzA#(---#2ɀd{8vgUqQĞisBC=zu(K\FyX l<0=}Ε/FOֻ +.cb>[=e>%jB[zzOׇŬyc1( wc1oУ)HJI5c:y_vu{5ow$][^a<W~+^3\].±Sǝ*x[|6yaҾ/rlO,\_{kT!ԭPai"v@/~Ӯ0W~zx&زBq܇8Z.Ki>LMR}&6[Q^V[fCZ PӜgOe8\Q{o~5 q7i{ +8wsd봱Ʃ6~g5^@͵UUf=#ϒ>E2EZ]2hk2uJIh\W$煃cX[:-R]qͱq~o^G}kLvv6p[|{3Žl|P.u(¯ 譇|ݲ*YƸցwkG,brn; ֹi҄!\҇}tpywvoxl$lZPX7T= i\e96N>TȨQV d+4PhsUf$*azܓFLlr- 2ڰguv|@hh'{oqWM4틒vH\ZH;I#]Njus-eR\mQ|7 +^;$<= ԓMMt3MJ2lWkP?צ>ʺo?SxQ':'l+:٘}S7Bk0$ J`?V}Heץ0 G"ʼuj50 ׈*~7Hg^'a)[5Yh73< {aG~#4=)L,:zZQJlSEvRE;X"*_(tlj*ߐkY^YWXK_JyOM6bD.GCEg]?RxR3G.P*#TeDI]ZH/bq',AD E݄U]Q/h\zvzvR=U_eZv+L kHg P},KqY}uN5U~|Hh䈍 ;wdP3|lmj ^ta0#}Mk_3jpnk#Oh-B(GcA{$(4 ÃK'e(&Y5r論x>T3 Uݴ_Ƞ['6/(L +Jk. n;ά~ɲ?GU*n]CmayZ_jbxʴpa?BYnuۂUܜBR9 W!R_fҗ]VqѩvC*ӡU +7a>VL֧oy䬣Fh+FM<~6xBV@x2TP]urrXJOlJ'- +WFm4&Nb^FsR0bg=IlVɽr΋AjK1/y>BzvXXzwD_)b}pe6ޝ sZNwXmeCR% 6 _hڗs7@1hgX+~]>;DOR[h*h)MjAefy';ٝ ϋr5u+|?6n%`ru،ep `i`2|Ii +ɰ@0MnWMf7502. _fެ"R n$.poS2"⁠6Y@>Fޥc!g& -~}1 @u!"a6*B~3l GpPė] +Jéh1ːwфM(7 +b5 *7_+Κ|ythp'K/{c@f8,-@"}laHOt8 [H BwH)d7Yk~S4y\&LeuM[P\vt?sPWB0 ȋWw +:c")Pa P݂=ԧ4D&:ah;:w*nJ/&l`Fvg?Km2 +T"aBZ'Ss&#VB`bc_ 9*`>߷lCfOWhX 4o/4Ac`^y MOoWhڭqn*y }l:?tYo|h=G]; s֋4 x O9?imd?`Pzuw8BLpAL), +cT󱣰 C!; ZL:;-NqX_{ + I/=K?iiGɖ7W4܋ gOOaHQUb;s΢e& *k} +m*~r*닉SB%/-*^hgxqϩ>{`J⽊),>78xo[t:meoXe}oUV8*),67rwbVOԩ}?[T\?Y>-qMLO>C?RmS6G֗XuDXUYTA_{1gyҬex8(i)7cenWWxmD%  <+;sthf9na\QaFFTSt5h~O.O@}5ɁN5F~6/<Ij5+P+*0gPhT)>`іRL/Po\~]՝'\)ٹdꔠ$Ҧ +HxQWQ~ùԤZ^DQ6Z`cA} |ʺ[]‰ xף?oa%{.;,vz !/ܘY2 v{p;E#u] 5pڄr=zl 7O?m}ٻ + ܽF<"Ywyyn^~* u2H!O/ϷdP* abkY%z|7&P|0ve;YG4w7=vH E%7sh΄'T'ժKgK1Xq@cw,nnI=s=WϤˌcPj{)4Z?=߽^.Y݇IPԑ?fFo]Gny)KNZ;6e.h§gwM)uC]j4Ng}||z`6/F9;d& wlV;iw:X! ~8/ob|K3/乜m(`OGN/y[kӪj˩4.lnI_uTP ֏6sE4^3h$,-fQ5c/M.`fΤa 'wr`6cgj(9g/T{ZeGEbOwOXfG?V!!~Dq~[ǿpp%n[|;L#~o]6#x0jw-qnh '̱+P/+c2HA*>Y ++9fkrc%W%"wODA(-7gMKhs9~F\.d.Ԁ=L_}ܚnɓBa~!};OH TSj1:Zb^TZ +mՓr^}#nMeJgM%_%Suhl-,_o/<-}˰, qq>8nAWct![B|ՓRjY + +_!u[rk_(EoMu{[7KQЏS|okO?<ɡxMxU%b!e5o5ꯘH?oz݂15]W!hRԍ꣥IQ%s\2ʶBG=:@o_OU{4ܸ\F#v:s& W G_ + fcr`z-j2x۾9^"iN󧚾!?' +@~XjUc&N)\hS06"fͻ&Lat *=.i4X.5V.ؾ%BТ(xX.CƽՐپ5|h|U aD._<ܬyRAii4eeܧ;cB8?P +y|RV" [K&+e'VÌ00e Lf"LdX!ΞOtw":oc]4X#ZUyKl]C XŤweZ]>Av1+Z6iYYbX\s5P$Ua[F \4rŜ|"k? oL % +:zcm [,ODHn>+:ME ^זuZkBuX\Rf}hW$)3 m#\M^z'gjja+?&dW]* erA/석ʛ[naYQ/-_FMW&UkPZjNWOProUʴK{~Z8W]28 eP\L ";z=꫿xYNfs _{?V ʮʹmGG(^*g #+_,]".(d[8xh}M x> +e.E4_.r.^΀M.ir|OzBnyvhsd֔O˂TT4p;hBWު׾xuU(bTR˭ժsl@P40Yß)jP{ )'vBYOc5pشtz5зJ=Ftj}{E)r)(6SB@Ih5j":F@mXȁdy$@LUp5{wlPo@@/}H,xJi|kkmzcIpM@yfvgxXX + u `X]s'_,0F qL `4MMX^Sq;z7Fi7"crGQ*W]Qj2˟/[ m`y~xV2QhROt.&yw`*=Gw!m˥ưB>r*.ziFwe2kc.:tB)'|v+ ʣ/ / Y bƀH# N#yg -{c3bW3l@ .ӈ; +8֏ڠ6nFhyT*ʳge<ހ?]2[Y9ͱ%O%? +$P_JeN0!m5 +=j/s&YԐ醏z^*wU~#A2VK:OI_ ]j1fO*CtzSQy=`\0 V~ͤ݇G>n7[\w 2;Un`y2%?CG󽸠*W:82\plPN;hW=Q{'H=Caڟ||k]ǨQ +y@_RLmX,t +!pbYcIDo?P׏KvL]J8ӓ{|!~RoLn]}=n\EgmsT uPVv<:=v~&<+(lshi2[KS_`'rvZSX +E "RVvVnP76e[ݭobWtdӅ&sĿFm0mԩ•n1q*&+֒ uV? mI +~:(7۽š]T-cRꓢBNЇ4{qm^_̉(b6L$<;r= +h7LZ(4*`cMzz{S7\}430?_8j-S$̡L"iM.v8D^I;P5L·PbíjԷcL=,~MpNJewchrziB~gL'HsPo 3M2 j7$zf7vp.a"Z|*]_G>N{~tѣ{|}e5~$H;[s\^~;V3Gniw: :z77Ge!#=,MbY۬6YR1 iv޿]4 [#Sh +zujpw[8;7wlH /FnlG7U2 C)ö/8sg&ϢM1 hyЮK~ͯ^@L]1F|3i%xVq5jOu sy8lRnҁ1fNOtgI]UÛ-1L.$B#zOvFl ;٠AT9gc_ Ns TNr;B{`Wz_"TOF-vR[mjqѵc`S" LZM'ܛ/_ ?WYaʱl-qcmN?NvV;W *lJ-w4by}Y% ^)vv^9ꜥP2Rt/<$lpk/0 1 4ow_6u$Ξo&զ>U Cfά>D]V!#SPMbbTN^!M?SfzT]z}V“.@>،2 y\M9xv'qhئ$m=zml@塅uSNOR]l+Vĕ.k7\kމQd_"VfՅ*ec˯p)C8 3=3E*a*Z#6mj(sW盶}r!s(.IO$K_j=!}s:h*K},Dh]vG/xVrFs;q C^iQd7CgT=%F|ug:qz/z%1ES2o}9.->I5 oIT۰&~*JC5g7{_to sd|=qV~Ρ:e%wmJ6zÞ>.3km {aQX lΝhxojn=6`F=}M˝TـU_d#u,嚠/8oYx{p>$X.,Ŝ%k=cnLX.a$*tι%Jژ$%ūЙ[@M#yR#63Y(%z +}wky߈2~| +6-sYbT*φ~-[^Ʉ/ &d:ղ&# \2 H>_nPj?V3.Fth}+Aw(OUIy;ܞWքGSDw!g8TAn /d~Zՠ|#͐ؾO1V|`n`NZ0gN)#üד]:>oJ ZQKv(;G=SzcaNj liQQ +Ȗtc&G÷k#Fsy扞>c=} B>o7z!5ڱu\dz;}yrSa0Z \QiKT (hXRvB\EpZ F(]5?K<;:Ԗ㊫ +-Ӵʐ/SUwe{3Mwg!nǒJqbtMMs,h]8G7 +q2/V!v- !r +23%s1)&ift޴4-∰2{=Dɼ';5ƕ8䵅[s;Ma]ʭW28VW`}x>l`K `z?0\l|榶Yک5`ĖPCywB) n`/1qRوdXVw3GTo +Rܨ"PVr@A-p:45d ff);~Wz| *;i\aV6" $eXHz*MG*,}qm"3&qx4HmsV*n +:4Q.dgW_+ })DV . gy yCU,2 (3\I@dE<Mdyh+FZ*Z}lXr(~{??Sg@ҿW`zm5@c͟e觎0zL0/>[`)~#/o(.qz/l4ZgʯQonڿ:ū?*-W` },]ph)R컦dJoz~|P'phܬstx9r|ƻO9A^҄)RhXx'?lS N>WOe+XQ:1{IDUg`^ƜW=_-4!/pE8|@:Ey|~|'MMo ްnKU${׻.czZJMn1玕Ei\yZCYgfWdZԁEuɢ}dwKAuoݦU6GZ_&fwWv8|ZϽ(=ys(~gv0՗4ճ0i|kL➪5!8cYG(΀ufUIg<|W:a@j)]*ۅC0*4uMJ&7'Ye.՟[v^gnHx3M|;O[VZrSZ8Ϭ!kCm[tf[9u!9Og.~Ll$dB/1{N/QDpQ= +#*\Υ*:X̙?VYh%+ VZI]л?FYw<ͥt +FX1nPpڣ|$hDiBh ă k߁>_}{P]zZ/`rX>,]SDl|ؓ+dגEwF V.mh/]:;Y3P\j!4O]~q*)}4EwA/ˮ@KH]Qyߩ~b<8xz;Ie ɝ7s:6YtťLpA^E{wo 8 m2{ ;95X_ӝ[Z}'cɑ\8ڲccζvCۡ}$_d]a[[5 [*K +g6!k^Uܘ=ayq޻{W+xlsKrlgc#_v}& +Z"}goOܳ.Yf_~dcsfpcѬ^wvI=ʭ'-̧,C.5-D$vGI`å+͵ 'vێ Otx~=oLj K\MK[굌g@Iz{Z lCmz LzeH5n?G.e IczHNbp<ܗgF}m/6Cvh]pK5;vA[VoJjgQ*Y;lD.ܻ\L.]),]S1"[QoX!v#~g8 3 E)w7r!OԥI+1ҥij:ޕc(dPZ0`(cdN$5unʽ k,h vͯ*֙7i,ُ^}:V>c`nfWpwjپIͯ9(Vֻ*$ZU1U$ +]|@lާ攛V{ + qK ܕWyLn;nz\[ZUYp#D9+IEQ ŜFǜkwr&>Xwu:vxr f3Qbtrmpr^\ + +_adIb\kF _k(=b⥼4[nB)珬8.٭>ٲg= + iPtߺQ{P\uRVEUHGN剬{Q}!O0đ5LmDcS~y"Y0cVVF vrkAk[<3K9 C]w1kzYX]~S~P(-y+}Iȝ†C>jIU)H|Pg? *8ᵄ2G-"ņAQ'KO/w;ӗ47VߍOf]o^zrU+E򍋬Ňy!yX4%B$L_#]+2:u }n.9 9 ּln{,Ӈ*4 %%-P~iNP#` $fnukS[^:U3L["}DgsIGG"NW!5 +7Ǯ*rm2A%cœ ;3vE>XZ1;YuٷQwQ^'ߟmxݫpguDySηF=Ih-Kތy<>8NIPo}dF`}z>ݦ'h=pP=S[!_xHQLcT[:Ն[>Uqn"?vQ;5D-~mKe;[y-PB3oBrse}E_>~n$vY $oxOhMNvN >Hs0 +5LLˉt7ZV~ +DĘ'_>s~B5u=^-WzԂJ/'Bd͐7TL?&Sn `e[)9 0 +ϘŴA{OSDi㐷!mgWaK_Zbw+&l3P6"f|á_ͼ~fi3O 1 3u +b$#STw; + D2-bXUվ:.ֳNc.}*C31;=nc|H͚`Ӓ ȃG)NC@$Eebqͦ2). ?ȋza -{L\ +Z ƈGtf*ȻT#O5\O3r:F8uH$YfaTᥘ7 H K@ݎǟ})p#GH:w:Z<$@-{>| 6锊p݁i.Z 叓3$S;[crsKr\ؠI3S)j>+Uz4z>/)7`; +b 0b9 i1tE}) @'  /c!o +vP3,؇/•deO +)(eTܫ-@&`rML}L/ݘ>f?e,Jl^S[-I N+l;%-`!,b$'zFUʷSFVgDX"3Ǚt^lG.{9Z#;D9+p#IiSh8.׿agpv p% ӗ_c\(VY9 Q gq6COK _t|q|pR bcZ'B +N %i)jqjnDN,i\Uy$Ζ,!]>yA o~0[dggN7);㮋Nםw|Eu}koNa;_( ٭8Q4 e;ªo]/6S/M׮ S.E:>ALA۪A¶}SB]:GjKzPΛ| 1J=6:᧝3 ?#d04o>TWhJ7<n\jl4CF!;Ӥ%]歳5T{8P`gqNPcw_ Z},u\ԭ5ؕB;cJ̰˘پy<.][ؒ.*VDV8tز +G h6 27HL6=j:' +n[QenԆϿs<AyR:!ׅލ]o].5ۄWnuux@pZghFD j "̥RiӪ;S/vm|?VE> 8b9ukX'`+ދgwգc;.~:j@V;q_tyng9 ?aʋ +0IoRF|}RئWsƕZӱQזqf3ѤlWllzKn\ 0Z"?BX:0)ѡ4{~f *k\i?4|ystwpsIY³qWY YNjcQZEnDރjqۋbPLJXy57MY-{Ɲ?$37J~m3խ:Og޵ +"hUz0t_ uA8Y_ֳ檙!ŝ#u灲Zf^Fْ6߸oD>[}82a=tX7\iyh9AЩktP =תw}t3=m'7lҴŠf%~m%8Z'2{.6v!gv2n$ԔyR}kJwSLuâXî~[q2pDalLqlҥz,f6!"%AG>}ExDv8|Vkxx8M[`ze@Bֺ}#ˣ|5uʵE4S:lߗ?z]ʅڷ!|K "[7s:n-7V^'3B3"&ɀ))>W8ąFT m/ +Jr R_.EGb>0V_y5r;S '٤:yK,汀T +[J56ɑӈ)ᎂr}oza$&vК Q*ϐAxnrOduz +S =#]YoQzB&; +Јb{|ej=3}?\@zP^uӒAIJIکWݎK~#5Fs:f $ d6 +ip@Z9i6gg)wt` gW/Xq(54J'3EiC)ӚwxO,ߙ4_"V "g bfo j`@p?Eib xXBRE7 ;1γ]k\q<31acTdxQ?FP6Fm1 +#au95eHXȱ<ҳS< ,[O1G ʷSx#@G Ji}B(5 xL}_+Sx%;^_u0_vJgpOt Jп4mT) D\,=V?RS@Ne !23op: kBQSK 2OYӡC'$N~kkP΀<~߁| +@!vPx JRԾUz=s#~ي +qn3? c77U=ܛjgW:D*78ݐCnjL*O鹌p屸Ua< *eigJsQ@>qRBͽyibzCηݕ+l7nu u*2Bu+ uUfМŰ=:AoVzt*E\&j81w1:d8DbCdSU:A[xŢOvɳ%& 2+x䔌_G$@jPnYGk6olyc0P6x[rC}*jF^^Bnaٍǂhۏ5+]'q^~I;!v"jvd?x@%?r w^ ֳ3/YNƿ뭥3uVosf.:4PJK+~-OFW %̝xCP}*b=W@iwbaI +*/̮x% +unU߭Hd[m5%T_9|8+Z/zqYX\@R<uRz?/ +!;~t,сv~z|Dٸ|2a>cut9Ԛ]{C2L? m?Ƹ' hC6&]4V 8_yǭ}m@-R - ?~8Q{k#iӂPJ1J;%z{CK0$ (.خnImi9V^3zhT$pwʿ^knd*"C|(^T(<확ۨaWf>NӦY,{θ:)nh['Ik$ۮRHUaq}zlm?'˳^࢟V'Ǎj~(G޹=yVTgRa<|;[snr.mVaˇviE^ɗWGm\Oj +d%1$z|9uGgK ߃V5yʗ~6h +ʭs8mqy=tqV˪ v9pr/W+"EahVhT$<׊01e4nEy"\u2RzW6;C k՘PԈ/Q "7Nvxq*۸>(¦UܶO"8C=zbe'qsw(|@E꺿kV_hY.c~enoA:+IW8xc`:.*9.Gr7 JuJtcwi٪XB_m̋_]mꝹGt0﵉wjYlyx~ yktY=:):' +K!.?I%<=ڶ8z]kUnsU s7XrhƃY<у%Z% sɌ h\vc'iΟ*}]e} +|:z. KƼ"ݠ\$Odq5 rF3!|rd;ck|򮆝ޱ$mKŚ'M\ +t}w.`b\{$T7UGePiS>r di_%uāӢH +قwBf&<|@\,˜KfV\f\Hڞ ~쨶֥W)kviܼ}mG6@vrTC6:R rQ1@KdWB38X-g|ΐ/=K[",)CV]a_^O/*-J~uhmY-vlHE9:T.,UX;>U+xxp 5*{fVwٍp{DpJ7LΚroxJ>.eT[6M9juzU±L(61|U[<7Y֛0Py(^{Y&bņţH*r +uf*i`0\9 {N=G]MuV#Bm,l%Ob,1A#df;>Zm;ybObt5u_ʊ8u )b^f؍Xz.2 ЧhB4k[CSjyoC+ tx_*$1T, .6>] d48 46.xnW8(b~F9xILMƇ|tߨ*ŭ,=yC܇ +x̒a\_%gjR!'޾A|\0WĄ /ݏhu s>S67U1ޓ`K"[+GwQo9Y W~,z(Jd^X>ѦژĶ8;gKJN-VT$wf+r`3p.3GWt[MsH0#x--%V8'K6>~Yx߆CWWq2_,y~JJ4P1.f Ó@sCc$Ñb6е(u}d4C:_1>v:׹~: ާrkа2B!~ P%Hߓ5UY +>O/WSSz! *,T "{=)4RO\> <,fNGv~0N+ԷABGd590%7!;Ԑ@uJEϢLvF6k̳˦4aUa2.,Fx)y,%d4`oIqQhy |R`~%X`L1L)8&sSJB 0,-=EZ +W&_KRY?\]/["4 Iʈ+&׹ dwiVi&R7[NR?3,&)J. /9)]mXosNSpIqcbBU7\uV#}ܜ;㑊2KψƳ;mx0*gy+E|m|R8=|K7^=/O%G *TxqZ\*z]/ɼ.%c4m +`yk]&6Ѡr&B%π !r7! ,K?."U1ae)qL3tj,M πpV8 t!^yKlՓ r_1FLseiCy|-39'pg_!xiK?:z? O\УB0~|3d*g*`<FCCn @?;Ö*>evY q\F'[|Z3EGj?^+9<,m[\]7{)8h `bB`NmYFk.ui#hvDWͷ*'?޾_*xNb;כ7oUyS"UKJy쟈輅 9OhS[etnG:'lⷛYwm Djq(D)+ ώ +$ @\@܄ Y&?x9c$da>CѿC/?-?a_Oe~-[ȫJRJ`} +@~!||.[.g~PTOgW:IV?ܮt|6޳Q[iE'Zs0VKq򬛋dyq14g*NL)c:])r"a_ىܠYi^|o8ڏ 󸋙cV +XJ#P7k!y>&9ңmʼnִciӕծN1ٖ$@׃;c~Z"۷tU}췶: ~%xʍ Es5Jr2y)9&E+z_V s{zlb⫋BgU=:>nRk˻拵Kq؀!:F o(Փ m|!^ld=;I t]w}wVʊ_lth('FVQ8ị5053f*L}J_jԟK>^\x=x ]l;ZL]zz6tzl XekkּufW~mpM<}y upz5԰`Ǯ*memhM/.gCٍp"ovӢw?[f[᳟B퓵c+[nXYܫeVO /(H<> +;4Peq'ϿZS'̋u#8(˹:hz돸: qg^6NӨ +mcT~UCy_νwՋ44hҵsWG;\cf窄EkHYXՠsWײ`+?t$.̷ܾ_: rwy%mp.r̕m|Kh X2ϣcСjtC#϶>zN|Fg 8`ŪBTblyNɻjYP}7U*Ӫ)cvyRXtWX,SƿF1譫 ?uߛnȋTru +=ӼR +jq]9#y}^TEKqw ͼ[yևTJ W[?<²з钞R<^b>|Rxa͞#{u:zm}jwJ[Xòl<鴿F]E,R^t~TBy|3*SR ¾-iH!cRboł ;F)x|q9ZsZn|ͳlN.wY Xrr<3|P&P)**%÷(@ކΓ욤@veEFGݏ\z7lF\D/Xn !>-XlԬ%<۸rIՔ8lo~o}>ov앙TڴЂFϩ(=ȻA(/mR"oV*vm:e8zLwmyܾӥmDVXD,di p T}3G&QVӤ%wϬ=% )&w0@ 70>r<&6XYKU伺KcFurfG*/뽱vb)Һj0[9^-MV,rDUt,VUt'.L)MaL @#'| g3%Vf.Z9hetd#Uz{MmpIv6RZ`*`vڔvik^j>|UeM|[=I(v-3RJ ?㊠xwh<WVC/.ۃ +)J$aN>o19*Z͌^lve;v^g*l_4KDc8ֿ OWٖJ3;+p[UCK9E]a%;@F>M/حljWLɪg= +)f> (c9EAfI{G]_alk%hk+UroC҆am :M||/]/tH`U2pXhTwgvS^ĀRόr/c1 T?Hx9|[]M~$~.?ZDH>xEL:90c2 V3'/ zř,)l jZR }A;r3)9;%@&%@:bF)$}wZ^KH. 2H!( Q|HPTD ,sX4_`V9@;tZtʅ]z)uӡ,@dicZhh-@f.)iź?K^#`wzʓ +pah?jN+;^ 70Qaf4U<0 +SCY*E"LSQ/l0V?uzJ~؁:,~,H[mLU3hi@YPdJ{ 8#;)"Sn9x!ضcm GNT=HOa4.FIe$<^5%@UlYC +DӒg%ndt88fn+i:nY*n @(jtkqS)bhx5?':;9,9G tCW߷nx:\]HDw)yzೃLe $W^ H15@7[#@ +bTIھtQQ$ C&OwṶQ%e$AUz`s0~9lR|9ݔD}%ov( w¾[MXPz[K2-@:I_b^[< ; ؓZ_kʭp.!PWWg#R؃y7Uh'34n:unh6Dˢe=ҡCr(~s!2kdYJC.Fo׀w H񰵶-g_`<r٘&䴵W'R16Sq6*(hf&n4c>@2[ Ɯ{F2 8ͳ +D^bx}buRŅ#FK9C":qOӡC:!o0q"T}^ HOry@n.@6'Oȩd52rtoff[Ujf]lΰQ. +џd 7wZWo6Qe]Te +,^vσhf\jupAFn ye#H8uP8"V)!ɷ gķHJw$ߏH-ݩGv# -Kw # z߹d>aWP͝L=G0g},ء^rNYC2Zu+Xv/qwOqnFpo]uiAr:u!ON SF^wyAjˋCldbƦX %Uc,./,rFtRS,> ]DR:NNWqbdukȎ&CWr>x$j{ >)t}dC^aSۆ.nqɺg7.GPCe T{`ouȰ?6Ҧ]WݮpzScXi VclyH5LgK|ke`+^ :.d 5.(U*z +Bv!tNJ$+;ʾV^e׾\;lvlʕWJNl4huluk|V6ս]MOBWXuk?[$5z43# $DQ1gN%&Jթ]!WsjojQ4C¤CsF> +oG8j1W7)WpԸoj]J¥yyUNmȞK]g˝[(/\)iD+%*is+/x8)"ki +ya6ϯ>oF~-sۦ?s-MKle.u80X~F ͛҈S )z-TVxŌNY;#֨]9pJ]A .qͪQ(^ Vށ&rRy2*g&T/Tƪ(06358↞ 5ိtӢg蝧vK?z #xqe +XL89+dbR.i$LX˗ +/(&/uvlA`1%Ap AmJ>;衙QW$x +~QjYCzTƃUiT(Q ZdyAQ?IZFr-s=T1>doޚvgUM~=t|SEƣfKNlr|Qn_X4 zVJOp)T6x{UGJyTDc_ZiqX9aws߿q.}9zٞFP߬!=kӓ8;3xD͋G`-@ềNbFRpp&L6&sӦLf!iC=.fQMD$J [Fٰv6qgeTn'Dg285_ o&*![rvg$+D1x};bag|b֨DZq2 #DBvb1eF S22!e/"VE_Kޕ +k_3uhTZp|8Ķ< fhM;H#\|~!\Ac\@qǤk]-th/UTCϿՏ^i'tcu"?kϹȀuGі"щdĩGT[~mP1{1؅X%[EQɓ!ɵu3aq$5g"jeS "ZKx;sד d(a)U{'W&UUNޑ}aClT. w4/-ט8#C\ M-Ǟ.q I/My=`eDzj9&hjQPlƊ-w\w<ѹهx&7ŞK[=}9F&A +]<N@H PF U{CؿnE׽, uAW0&? oAH/*x=% Ξ KC1mo@3Ŝ=-RsZJNH(`49})V:OoRz9_'G \| J1r˟Ah"O:"Spd7r6 +4|@RRRhF)A!x@M=:':RO.4*y +PTNM4c[LG6jK2oluq?jm̔ui8!32S +( 6\@S"BZrNn:fB^,O@[Э,<􄟦#3y +t86Wt&TZUAL')AY31vԬ4xB5 +}Ghd@ h" %ޔ r1T `&n0[ko i.ylf +`x@BxfI=0=棗"|^vj*qb":̢td!h hw+u<`W y!TORW[iv/;]텹K68'!3jY.fW^`8"r})B\@r +Ȧ>U=/Vn$ODC@>'tl/$com4TbGpuNF@)Œy!6Ex4Ejůĉ?Q}. - +Z{s' rdwd0O]\uw"K>4?TJ~yС2 uo_Lr&4~e)xG o .b d2 2 H7}/LKYĝb !VMBnsi~*ԡ,?&$eATfAA4@` Ib#(+]쌳xWb͖ݏQ=s|CU&眉o;}3ojB6 +TO SF:Z-\f@;5,| J|iyeIqo]٧<)t,Tqi'u|?m-߭NgYbF |ؙM0tflx֩?9UQj"qGIF^Oð*;@GȁC0С!> ݲ;>­6|=zY \ Y EX%wܜoYY*}~N >9fDލY`ƅzUGrإ?x <;C۫R}w]ú{r50K6d4z'竸9F&L|mZTX'So7?s$S[i{ + 甈d݉n]:R; Tv'^m˷;*wC'N6:tOoU2Е\o{٫ls +n܂w +kyԈ;mn;rhwTV=޲ѷ5L5{6֚kKQ:`Uc_itj0K6-,С 7w?jjڹ6~#y*-$[񇒶wԜdJ3&Mdi7Ser%p M^לPWݷHU\J%[wr'a7@!d`0jAcP_IGb/MTt=6j]IUmF+N/^^LkW)y[@^=_HS SNqRn]T4TeV+7l84@du2P+Ml;]VʴdW(R|h81EevĺGo7pڍOcb(i:zJvߩ v(0_/;AjNm5TE| i*A)oaY]$9oTX9Fz*qgJwN#[K( +塃CP';:S)oS CݕBTT>SGN\L-+Nb3F aONf^uzRTng3ݕ/N#хaY;@ʛ55!eF37$~)vMN>$^84YNsYh]uі)eƴWtQ [3.=%<<FHd +KO$H|q.~tca;XrDоjlQݥV5 +|W&G<2߮P'*#b[# j¸iyƹ+8+(89'8`C{5_J2hFʆ J2#| {ijGK>`'{;O~OO9=];inZ''F,)%.83-%TrSMwȋNY5xn1N2^FN/NQ2JAz&x/*9̦0СK k:IjQcCyQO{902:Jll%"Nŕ/^DZohO$p^l,$zo +l7fh{I zT4:q&1Zi"*+bi5s63d\Z9~n +x9˹ {d/#J +;Vm.6qE镽n>S/~LdGQNM@J֚\JGAlFlx$JY@aDZ>Ov~oUӷk:J=5u!B〜s^ys}ttε3Cc36vѯzD@GF5+- *} I1/dIJ\Ԉrn;O\ |uqUmXiZٹ%[I15\% -vjB:A Y_鞰/O+[>޽4clDˈFYdq݉IGj2EzKake=Ў" = vFL:2/;!ʼq˞Xx78Us5R5\&+:r޲ dW4XrЙmD᫚MtyHTb47H U_Q0NPM8Nb,'EAl:Lw6$@qPBy^{?ɲ\T|CUdN/>7=;,-LRnD8״׏\jj d $6BE1;sLt.ԉ 6r>@S3%޾zs ЂWNNT}_?cʽ^9m92OГWiI-fF?d>rqd4rfsZ8# /K_ + d΀LO[v 0#*V`;&,M=])tk: +82aЙKuEQ5-.4VAN6۹#0oGs*e`ck0ǝF.lûx-mi#^x2;ahoОEޚ0 YnpwA΀w@l2 a)tֵ Tb KAP_^iFZIZWdάjD]/6Zyt.f|>gvqyOsOA$ + #   MAf kLh}@s2a3b= v vddoo+kFsq$f0STQ|}cz 9q\nڱUv!0wJ?kI4\"Lg3O>ٷ +Ը#GCCo"sjV\ AcEF]IM[uZ`1Z*YFԸL*H9n*T)y!rfd+kC1&e:@ұ/ۈMm^EH,>|/C 5$-!*̔SyZGɩpOd݊ҁP9/z8Tmg8Qڟ`>>˿l/ =,=v[bj_ +Ec튭?*h+iC]76 5\ ?\Ay}Bg`Tĵ>>.s68C r=v]a/W:(7nK'ݖcN-/Q5楙&ܣ<峡YUuk*UcJݨFbbrx:C( 5([^nŽҟ[={̓ +B"1bW 7%홧.Hi\Em0ٽH +s*cJڝAXZ#E.9?Z|S !ZO l/$BIDsy.ek'=[*^GVxig,8*N۴^)o"6|'WdaO;1+s^h : ^,p."ؿʧv$l55e!a0UzF0[:rs$0uL6A=sR~OFļfn7 SkS?4J9ijJR' D}RN9td^ޓȧT/BH5ADGSqgbqe4O5+_hW;T՘kK4!e_O}ĶZ+Fl)޵ mqfA#+I]&꺐 vWsl?lc"ӴjVl 7$Uwj棞KQ*)&noɓZ3Zђf_'s q5iFD3BTpVsoqe%K/O0 ~Vr-(<"-+c;FMg˕+eXlCl5mq)&k +6N3JMdʬusE_p]Y5ȧV#ZNGN 祑u*(I30<fy*j 5)_%Q^ԻI*AŅ7^$>w\"U~II4]p6Ь5Qe}9ѥӣZ,y{dv\lSZng7eG+q}΅187ǝg6A0۩-&;ݎLBL"?iaF|Q+CeJF#r> g5]}w|=H1^,'&6$]EM>79xGq26BqR'5KoWLbvic+iPԔ/:T@pJC(& Ш(6@sQ1<=E*0@Q U\'O\.Q3jߙ<̹?2 =PZZAyvry?,zǀH +/w9Ǯހ)@HB6a9\P'@l ?. @>-.ƛTbh'M[e.-34SHVo+DwkX?z@ro(>!Td +P3WYP(<䓆m @Ņ|Zɂ*M/ga_&I' +4 f`0fZ{Ч1_9:wuD@c'Ȝ*'!ۋ7LD-B:ˎ+I^H_bj)СqբAp>ܫ ׀uomGp*`Q|A KVSY@W 1QX$ֿ4j2f/n/7T?ܻVWGs5nX321XoUU%+9kӥB/GH'DTxr K=v7LCIpx@EN`Eh!  iL__E'$î~LzM{qzϨ ?ʬV-RH7MRíċ)q$O ^]|b)fڞ -P?xH+,7o/a_#$|zC]5L} jcwʾN/\$uyYDC7kQCD[ƀ2 VTmaPl#A,Kmn-XM/ +oyuZzu!ꞡ%{_#! +,KN6'Jު`PDэRTTf«fBRL(e%gݸ/H'کĦ?y43\QvI~ +uJY{9B`5&' 4{-ÕƋmkv R/3uL`/Llލ\+ aa!i } Fn`@W3\ʧ:z` XbFW*3h&K.|s8xV6 #&V~ L2ia_SQ2ѫka5Ou!S}k6.3ibޟ&R X򱌧>wJj.[A\ZGާ?_rqp\Gz!KyweQsCD㋪@Y^,鿇5M@#K=ÿ/uMmk8'w!pH T!KZs-6kzQ(8cS_-H J*!EOLw +\vs."{ γ5=Vݭpm@5v*dy;?akCTG˾AgR:+w1oϐ) dGWV8is/|ָKcvI;n{+_<9piquz>TOeٲKQ5(a4wenJIm7+%z=!JaA?W9"xXy2 زe8UfX7gz=Kn쒛_46clهuFfu3ҽœ==TvU;4d]>ޟZ߮*ΈvJ$eBpjn?ŗ;J}Xӭ b. +<\͈Z?2]YUad!ybc8Z22[w\i~ KaY鹛\uR%6ٞu:mn&D0񞮪 fjba؆ؿi?w<+}y+HrgxU4j^qO.:y5FbuRU|!ҹK-@ obpRZ2{r`~+ֺerpY;Y ˀSm]Zpb:ޏp/6Rc?W~' aKiLk%r֪Ob{렝S`5}_N$`/e>:Ifȃo*[2iaeq>MښZgӔIݎ<+/㴣JR J:lDB4ěyDlLL8@B5nXl Aiu\=S1Z@7L7*Z͉['NoY&G%'_|oRrdzT j{{/lٻYgU)0{xpZJ%! D*?OHe󈺰5NW7/;L|O6͛M̼U[1q۴8IW1>? .&)beXxPMX} :YMF9W>arEv#{ulkdm/He,i2VPj%jJ12~U/xF_Ug49WU؛?Q1lT4 Si%Uܷ&_(dyNID3S02[֩z A*7L.5y ْyݨo?ojaHI~PWdrْN!3AG%w%ncۃ7O1oUb~T?N{>MIcPUTTwe8Do$RK[2T$$rn=zNAל -;PoMi+-|~T+ ~.7TdC>cq+z;Dܗš ?H~O|SM\EQ:hb`@+ UF=MB<{  id? +ÙLKqkC4:uGzBժ-A=e_ӟtLGj)jsP'R\Nz#kxgoOL=Pu4JWFju9n7tF7Mߦ#4Pߩvy~9B<]UKd铰~;Z)y|˧~K}̾6@gQuK<|]&k4Q}y͜RP1M8mw|Y]mmey2nJ4OL'߲ v"9( `VM+='ҴlI$_A@ߙmcޫ'}ZnFox>Fg܍|17a 縵hͧTذ:T}CEw.M/?|齪晁V{M{> "T햕z+c]}cOV +O>LYiEN\I1vNb!"9S XgxgvŹm׋1:c^y gG/ą?>;(~$v݀rgx( $ Wwy;nn:MI*% R"rQT٩MМJ3P8"jגm{]6qۼuOpWƧnѲez.l: M)<;vGL."II9>mONBj]gjvt%%B4h("uFeAjʱ,JImCeFM0{B;m#-WXfp%2Za9> +8#\W`/u2o)1^n<^[W:3-}`uԮל5^:xKiY' tOQ!׀X:]7^ f}{p5{ iIƒ{8.^ : +s'e^GOnL*z58Gw.#ӹm"T+x  MOYLK- +1/ẃ\ݼyfWQ3f:ܼU f;`JsFяb|K.#e`.D^c^duO݃Rtg) C}߲kzch>^u'lw:iV&tϧWYF?EᆰϨG_)Ͽ +WU%S&?gx~ihf'{W[?mЌidw%cI47i?]UOn؜@N}O:|NaIt=9]%xCJ%겆(Wϻ/4cyesuX;Su ZGÌsK}R$ZE Ѹnk#RAmb7Oav^i-Т{y[jL@Un`-dPnb~Tn8K,ZK)b~[/}WLTJƧODMݫ 6@::fvɾW2)dB4"#(#8{l7z1jN;T``>g졲>,7AbOELހ@NK'2]#쯲zޤ-Qr_^ Q?)KuT? ;}=57Uz.ΣGWnE=i-NPjW8%+Ę| qhixxI/ΡuciiS&Z[~tJ׿eo;PF& k-qJPQΟ+ɞp/0/kFT+X;gǙ +<30Xz`׹FhnK4Q6U7+F#+x%IPd h+BnGrf#D]?7^u]A!={ܻ /n9i'weIMaHf\5:1nayvbQ(S7SUЪOм {О1e軭gp_! ]BC̹ +nzG=+ٹ[s_<daH %%XzoĆ>Gy HzݷN#mk3k'ʰ2WD[o$]мwnx` blàrƠGw^}Sd5$( VzF9}|y|qX ȽG%K"Nmpe䅾/g{){yPB0ju'ɵ(hT@ 7}thK!V4_>3 +ArucBtl/P2&$ xFlz:?J88JW^|O HRPha}ꏽ8۽hxlKb{dȞtБD9*t7x0EPwl8hcFu礯ܮ$ZBrR/ qZ7k<< yp 9f}kZ]3kD{ xm-hv@N7hMςL% '6{|oIs̘K_E.uIJP;hFIB> +Q/Mx2|(<~Vg|TvjZVci4ܫLx$+Wh V_s`Ӥ>s%y{Զ˽(oj (;"62fCiIclDݫx-ak⊵CҹL#<6*l ږxIF9Q\Kd}$c`lJ]P~1cYYf(ݽ(Lmn͹&՝[xn&˿ 7yY&Z\~t? }+2>;b IT]e,e[L^[6 NjӁ3xM_W|)rb^ ?lkWqёY)y5{?'b,AOE_>ԧ2?Co&642Εi?O_+jً{Vws)^_3z쬥cEΈwJ,ӌn!_ڎ%x쌖zǷ"ME [h+o⑀'bm:rRfM&ٺ0i:NJw:?]+rq`SFmg`= A}67Bq/w⹀x{U j??36W+Ӓw,zduk8F֓°`)"}]xJw86 ߼ ag,E tzܷZyɠ% +XFnɬ38f85K6lT,NKx1QxhгV+л㗅^+NZW͙3U/x~ĿOX/~4?QO5MH`8u1h/uE5"Yǃݦ]P 07U"-k!:RO/c,ҡ@+Ft] *6QJU0`XI]F@3@gzΡiQO֭GO"g(S v/UI:Y}0}_6cZ.'wʸE0Hѭ}Uwi[a +®'bi&nkbh\|[˹4Y)mu*tzNwz8ܩ/Y+1d|*P,@K7@X@rW%*{bq)nZey>"ԋz>-=\d|qJVy!J9:n-׿yw*R\Ӝ0| `18hegv$Y3k3K\pLa/g?Z^]nwI\{؈kT?EfPU."U`X poAh TowW)eZBZ7S<7tG5iiۙZvkK&|(I굹a)'Z7UjFt:SŻT4Fsk`=bY]ͤmӺRIW{i|m aڹNVr]oX~h`!a/Mh=y[U̝Q?.uf Xj'49ߛQ#.wbUQn_#NnHFis8`z1$&oP/1[brlUMuc.i1Re-7`ǪuB;;-fIuj*]6=7lwjiycD{3+&fuEtnKc璣҈/8A뗍]1BtN,߫o .&ىɐW oGXηJz>Fm.BjWԸLF7-+HsHD :^F^ƖJ׉O.Qv. 8>!MXS!_QЎ~#K&62sMm" qL'CfW0IDOTx鑍gg*l^Dz.MՍwqU4fґ{Vg 0_WlC!$;fV;ho3ӣ|rQbrws‹s+YԗqQ]˺R[/m!vYWd'Vعs/(eVb~} (-J)PY^C<ք5(%zAXE߷F]GouT%Lˇl f͸ +6)09aZI+h6:unmkWtW'ݡxy#)FoAiݏѪ_R}3}(uor}P{qI1"RڝkY{e~l1, VLn[ !eN/[N.$r-SAֲ"oJ]7qlV_5^t mܶx:Jm; 2Nmޛ]1z vO!JB>x0r +c_@?/NZs3zbRHrI rH!ح34h 3;jo"[ߗz q$,XOjR!rߥBP/t%T Ӡ>v=zZSiOpȳN2O,:d'Jvx5[6?_*:o|Yy5*?ėK)GDl ^xj0{PYM nPRsuqVʞPQ;\&i4Æc*mFͭ>Ӛ`n-K~Qbe5Q)FCX兜EP|^UxBj9TZ;m~)55Zaq[jvj7Ss8}NKV42 JWB9JBBɠi Ox*3!\5 -:UKBG[b؞W b1c:ք2dIJw1ZBlɸi tO癀l;N(H,WƅYr1*k)]9~ﭯ UYCLNW2}vxu>| Tw\D-S[^926%; +endstream endobj 116 0 obj <>stream +,@;a.:Iؗ-ous7xXN2fl6R[|yU{-յ;xE;ɝo:^)OJc- ;&b_zni^W/!>/ R5a4{Pރ"{&D [4~͙\NIGc0uddIX:tQ5 +=d37իvGkkNKIr]e_@TDŘbN +8@U @Kl؋ZuB'sE:ɦ?lIɆa&pANYRW5K6ЫFOkNG;uyKЯY["-ϞbL-4(r^1ܳ1:;I-!=3b~մr rrbjP'v{2Z|U-r;|Mʟj(KB_%[kCųX%znG'qC2%کXȜL1`*^\g#;e4uBGtst/T4;>A_3 mChC<λ{mk);|fTC\;qy -GF2T|;/Eg9K= }AAY/ VSJ|U)DQh?1'Ŏ̺kT,f;IqըNΜ$öV9cI]SwR^h~Wx vQMFsmkFW>n_~xе,r6 =OVorAwZhV.Fʳ"5O3wM0~*U|%I؍GKXߒ.8&4kl=j(sйTX]o\a+ql뫀p@0P|<ъ&qO>&S!1QaXzNPJZ֛1*}@x6ʕE67.|>9~}Puw8{ۿW}#TV4WfV'!8Z]3y1㈧jJIDM@Rw:s: Fibt}STT{389_4*Y_MƢj?h0߉o7g԰gjdȘue!z n@QqɁE!2q=:gZ^;+ϟay G-mQѾe2+~ɿ]lm )>3;4]Dkbc,'!w9`1?0A`X 9%~:GG_ W̎2rֹD"[g564A5ZUk3K)]h`>M_0gа1Dͨ961hLTWTqg2mZy 4wV7Q\Ҿf$ZY< eNo?F16TWv4YN)>xlhcL6 +vy{+jx:=V8;pm9Wq*EXEUfYUu"^Y~FN)h3;l>[Lts;|bNF}=R0߄(m7߈@*`CiPyg~)h`|2OAE$u`/gqJoBe~⾴oϚrFn~"_#{=,5K0禑m8tkfWF{W;ϧ3ɷ$U# -&FS@\%J E+;'X9_D U&~ޤo XZyJ$%fX7 &r5=o'B^lY~~ ϩXmͨ^| z?+_")Tk Ojy|߬ߣPiXzWf- 8=G1ZFk37RrruTB}ҍ| `K -)a ,eQ8_KY$Ye#7q]D-LF!@1/u]d@pvU_`jd?Vq:HZZ9AVGsR +V6d,]NG!9X԰וּqĞ?=:ZrܽG6D3_+ +ݛA ش+u +-19Mc=V$2u.%_+|}ߢ'Vcګ[uLezn|% qG_6Q:A":n΋(6aD~=.q +cT-/lC#G^=AM\;w'%zHqssTX*īBr=3sv~+{6 .Nfݠ<2QG (H{YԖVK;gĹֲZ3Kkb7LʵD6N!lT^8D7`1H"|5a.-Nk Vo ԫ~bceda cޥ?-wn[oϊgA +@uFAAԯO^>qݎoY2X1Sf &R_G <_Ug#Uտ +N{~oE U@V*XoMju=[.b8P]ơA&[5}?z@dP(I oC?ը$,Dgn2ب_ƈ^]I:::W:T 6P0q"gGXo7ѩAAPm/øڠPH@-* ?N{Sz֥áK9E 5hbKY%7KRאW%M]u{KsG,2gW/P]P`'P:(PMwu]׭.'nmA 䡫6 _(T\v (ToP}d( +4+NGym{wq݄P$`CK׸C[H9F\BE U/m~_)IW?,[@Pz*T P 4H +=S Zdz_{2swNmdOGջ.+Uy؇bIUWdLK'+o 3pIB?%GXl; >ȖhZT|z9s~Ëؔj1$ pR+REV{cd⦕x*h5A]2)@yAx@ALA1Jvv\6iÙfW甊nvb2eYΊ9q:;pkb~[Jc憃ќek +oh %PtA`_c[sw (K*&&gcɸY +c/v\nQqUY,Ula 'jqP9aT2ẃ=w{nfcOq*! V]I(VVŮIhlh3e]>z6 jƒr( {.X P^,lUAl:ΨV:{y*gF6q.;e>SX9ݑ|<_P(-2J]u!2+j&{_yA8y"Dci<Lo=gvBh~&)N7\^wMr7F5-Q9k9@uv,>S{׾~§&{=^'p<1[} <}vі) ǩ[U]$2n#Ѽ)jUO.@ќo /'NF-] Fԛ%>3uŜ3 +qܡGXwx1ww3z|Q܏*? G~(hrېz󎛴e[iJW\WS*ޣ!jWꆪM;F+Sۃ96v1M W2BJmzz +0G(FwHלrꥇWۨ,R5jfsN&WY =ǩ"Wy~RML;ʏn@PեX: 443<[t{-IZhc~1lMk03`n\[.~ZȎXN*q?m Du`&8c8}d$i4hx-һsIIR)1FBҠ[_]݊|q*Ň51A1uZ~q.;FHMK5;8GGŌ99N/I| sx~XB~ x +0zfɷ펤S>>-^ rJ*Czfws5o3lEiⒼ<\?p2õ/4f5|p!$/(͠8S4Pdfq4DExe-OZfn.uJ49vKݢo8qHF1E 6* *`k lb@LʀFlTW:&FFdsR'7iJjχc\vȬ*5 *n2mCӏfs>WJ:v(;|-E Eo8ޓ|oA6~z|q; 2Dd5H_r+.$TF:T.us !~N>l_6h5'W:VF- _CUyf>WBwLWVd<0̥Ϣtw(nXv[7^ h +k[+& @DA׌#irYRg'ġ2J$0\;smv:q6M@)g2p|roeZ`| =8ljeȀ邜D +)̆}~sa.PZ(`!WVcxƅ@ж_J@EV lbhnOԎ~~#< A>ao p|5 w@. p&_k2.BH;ue]>d AAE1aw1׷Wﵮn^èQi0(iaWsMMV^@_C ǯbmwMpKw@ s# A Ԣ iE{KwrzwƙP_ +i/¦c29F!RWI_'ysX/sϝ`U:r&mWΰ +uvP[Wb@:` +UۀBwp@s+R,.D4{V +5q_@^dOY=}lT)J7|? ,濮@9 -&"Sk@]$,B*ysK^=g=`PP IVhVRuLMsVG yދkKg厛؍vs&'tO&:UB[G'o"=)^jq +ydMs~'2A{lžR$Úē¹&6Ju@JPE&-P@\vmX9͒KG7Xd9l{ݩ\xz#sC4^R녭wiMh^ݮvXʳ/$ cBN[7 % rUyxWKl^ô=nKQ{uz<&ҽػ #ۍ|J~phNJ@Mf~8 h؂M'ޣ)|g3Tӭ?.>Ol6v nK*7)l5;+>X3޲b-x6њ?BB_%@ۆ:-\|iU;:9 3شݰ9x<ݴx[ji%YrRϬ1z=_8e|Xm&1d@VЪ@͉ V P$X۝nCTP%<~u/?i_j]i㓩FKu'\虎k|gBBhjЮ^[]?.njR{".Γ?h kV':vr.hu7֡k>wSQxgћ@gZ~~C J J+nPg&ߵ=:6zKU&fǍI^+\x\203dX暈8Vsy̕y>J#+s$@Ih7ۃ.0 +*bsϺurR۩Bnayp]zQ'i$\^; 3_p9aS F2vܛ>~C u5'v8ťz/WMbtK??P6+gKP#˝ti:yLf_Yžv}{$ҍd:8H^[2n+Rf۵5VȭvӞ;&dQDMVfbi>>PW7vq7+4Í$k,d\I$5q-gᨰrl>]SrBxĚ]! c +X/LJjUS0Jb'^2r2(Ybձ]Q7*QWLFꦬl=ljHɓDui-X"ܯ:kEf' +d]pܺ;*)aR}HBu Zf:f!>hP8/h6ؾ +QOv7w7_p98AMayd d1bm JzĦ| .]r} W2@wkXΉ21{LQl5Pα;x96A6gB-M 6B3Ȍ~vX@[!<@4u  |m߃,OÝń3y=۟+ ;$>xA<v\(u2lko4 0j,eN^ Vi&<4.<z`Z<Ȕғivy.wSl}358w>x-^$ PF/@VxЮ}QD63omZd > saŰh.YKמ佟 9%9hpGAP;Inqw]'Ba pY;r5a  <˻p`Y7}:o_c^RyK{C67;z\l|9xX}]x# ehm Z&߀po< ^f_LK< z{_3\_Vrã/FwiXYБ%eو/Gֱ簺oрsC:i@[Ck?b-NP^j l!@PTJxSUԢޭckw4y_3~!erxs=Ptީ][tǫ,wa2b)sgb̍JDa +],]Vުƫ9zs'ⲷTo\ގ=24F Q>>j1?n[6`mG^KXm`g@o[ q +P9ą^z>w_˽%[Ydž2!gc'vU[A cimpƬ|EvE&2\A6TS@kfP"}Lx+SXvgxupqlF}6y;3mviwcYמ+u#̗L㬘O?_?R9zijH ^|}>Ѥ(d:_G~_5Us;OlxYc2"n>g ;pE]ltN5>^Of*#c @?J$V-jPj{f6}x9+ƉQg*;^~XlZdڵ;/*2z`F9@)}S2yM[y%S3PPq'el{ Gv;0EFsYq=l4}Csx\7ֽQk\u(:{;%gRlMw3Vkޔ|$-0]-PP"/$kaPrg䉌|h7X*OԷguߘ^MѭpYM>f΃҅šRvQZdjKt  ӚL b$@ФoiT3y' !y|)x;bt׾&sF֡&FIbYuqu^s1C棏nF#vF PhbFiEdGkdƎ>ecܜqs3 %z<9 zYm̿*0$-!g{mhyךIMwIV9?QtݘDt]АNjԶwFSaWTW,n/l`:T4jVsv$[yk:Ʀ˚SR}:UIϱzKrqO/lUJO{P`|zzI;fW's]S`-vޱ'4 +Ҥ\aKv 9ikM\@ e'rEvb  twCed;$".}f=]eJ1 o yF_֩fy~[gKYيƸ#~K-h b)fǮ-\[/$wW9=Cm|$x5q6t8!L˫{k{_2bVfαJm_'c'np yִ[gFu>zwvϺY?}kxO$6}(D[f\NgZ624v;)3I3j+sRyB&|}>ZV>y:w,]oA3ɶkM:\;?\d)8xlڏ=+U;?S멋)^d'3)#=54gfV[5_eVQO sͪsh7kyFgwHB>Tzr ~D.m…or3V%g_qjz"T6:\ɇlK;W+21gM.hӎh}&4Q1X/ +XO^4*қ-4Wb2:~1eY:L4S␲Z˔V[U/`vL Ǿq^Ý7|Oc_q8Uq֭mi^?Ű/^%f\['4daغgZq 3q/}]7沍|ҹmǴZfL}Kq;7ryf4eV,2ZZUskUxz=J|lj|+`1?Kq*LHiW;u=![b+:f`78m@nN>o& uZ=],VKb6 Dz=?9̔B5bq;/~ES8҆:*;R<,o{]bG`Hiv|-Rz\v;RkEf- +3}+7wq$q,w +?c=9Im|Ӧ2?UOm.#j=M~u2 qP޻ >8߹ܡ v*C +t,PyԦd<{ rsoS!t@j]R-7kĩ0#v"w-5g)֩ܰSXF^q "qoOTJ.%+: ];B2i)^ž:䟱]3/=H&Aȶ^8㓍k H)wtDt~ ‡+vFGn;X +ID=[eeKzXxu:-\P*@V @Z @RHd-Si<_vgW֘{^;?.`z,A +Ĩ NT2_Ү"PM~e}AٿRhS7 ]DHKTn3_| +yUhfdW`Eo3KkhU2CB,:Pl𓢈W 7KmeZ$ga0x7L xNXBshz_nbyon旞. [G'2SϠJ菙}D+W$K=bmN)WXQ+~ -:y\LAW@r7ϒ_\n86,W(cOu0I@63!ȆAf }]@Uj6#T+C[;,{K#tؕ, ;SUܯgzGFֳ9AZ_Yz(ZkV\#/Gu$ng,&>&8l*5a:X{gk7;l_N5{y.,j})=f"5bbXWNGQ~uX;`(⥹O pNN}xm0SƃS¦ƣOF~Ea)_fTTSiP=^G9 V\wRnwGgy41$f__$kМv+` & i P2쿨|}=F7{\t*I ypVoQ%;ūWwz$Σ18lټ[N_Xe}.e]EY| IOΙlX+6 +8T +KŚ?b=E3 +/a<"n>;:qL=  7ҰcU'}ƈtae LKSw@ uZ5 +(nSDS|P/r} +Y;G||ϪFz԰Ak-:iEbBX,hhY3U~]#4:!ͳt[8ƯR-X ƃ5 gP~@9O=T"2/91[ vnrq[uvY?>\E9l#+מ_^(UŷYwqma6d6WOP;b~W]XNRPμX{Y??usÇ{jP+ob xUͿlOfS<-w-'(Dn,jZ- g|yH: T|Z'v}NvjJ g;M!Cm\7ֵ;3&0ѤK(n#E/B]dŸp?R˗i6(Rx1n5!Eل<;kJ\`ym~~ζi]=mFa%ġЫhoQ{x +25JߑA\Vru>AR'7(L-_ ++f. Y! } tTQQu"KI&dXҌf{*Z!W}?-1zRwB/'IaJ_gA}=RT#zoH>dZb8zM*t ,ZĬ-r&|7=޳CjHO +ns|e@`ePf h#|z* zբ2w}4r.YK.(6i1;'4Վ7:bEBqq)g@r<9$g gaHٚ;{fq9Lkj-T. ,*lW 큰IHM]=NXǽЎ"L7Lnw}'\m5Ȓ ۅ?$"\dY~٩йыrOkwGV}bVQD:=˭PnB8թv)4ϟ2o ¾]d@ }g5MwYZuZ5bP=v>nP璻,ښ\'?K Y1Gܜ-\s9irWڅ"i=iR@8\$yu9euP|FKmIc޴>:xX#E4S[ҦgZJڴjsM>%sfjq]`7|г_ZR1jණ4K&Mk[ \ofJj/NE3zff/-n9(vpiξVfoZfb1C{6TENv/Ttd*l5Wu*Cxz=oO#O G{]p̯u՞(~ި8ޛ')l7-䪱e`FZ#zɶ'q]`hFѻtnB&{j^[3Q4!/ZP-rnїoϊͭJDNVm̷'vVR[jŢ'^+ܩhW7.eh̤|mZ{xi՛dE7; 5WVE2;=2 {=K␢^gs-,qW³${l&JOaaz9sR>*̲\$bF.,` +$ǕgZ~]tW[C0QcMo>AʷҨ"~#-.%p[&9?äz_GK;+SdQK6*'顚` NuzB_dե蘛lc0+Ӫ̡ĆR*zԮ=,sQ=dOwˋARik(RKaX{\szu8ƛ:ٝdwkl+<:#*SĦEBN>2!v_2dʏ9]kf˟ȭoAzCy/yzkzGZe a u@SG@j ]o >gG_4HO:HM38.gh1_2Pf( eg O{"Hg<,0q@|3?..* @Z: @G@Z mo uP]?*-o"&"# (Un G(Uj +tȱe +tWHH{&/1~RZ$2[] [3G^tLqs&@|G( ']aT SfA7H?|-Cto}0 8}迓4wь]=:~ȧfn`- +YA@a2HhH"+hkpdM228?FYx#݅* D t +PO]3_|˻> +9VʝS[V勴ӑ_c@7lX~? k#$,ݹT=%L4Umv-0d9v]>{F[>Ӌ7yF| `vcͮvː[ á/xbPtdZy_Oϲl<>E_lo9k?͇T(LOu_/.^ZlR:ŇzPCv1TG\ؽw# \V8u'6 N`:mv!x;@QܻI1?}h],Wo99Nkg>ć.zܣ +jeKΒ{5߶#\y=IN9f"In [&Zͧ{m d](5^xNzWӹI:1:ꧩ{ʃ(O?)mnK!Kf1?hse$hLusb}i-nY.98c3+ +tvR!<@;s^ËȌ'T;j4׻]JZ6ͫi8^ [C{ϑՏkEmrLQy~3gsc\3F8>Oj^BSj2Bk`n oIJW>kŘ;ZhՕb[~D/y1OY' 0횵מ7:X]J:R3fۥ>`_ |uք]Nl T^jz;Z̽{}FqlF&2olߤa}g8f.1fؘO{)qJaEkJ +֋ZIhK简#]Xû*.QvL uޚ!ߌS}yMt;]uc%ԪECCfQ7G#>c?'.OOBvldqo/m1_$ߓ`Bq6ݛ:_J+ L;K!x>JqWxJ:A7:eRKyfMUo⏣ߛ^l.]T)me5(@V~<[wlo&T#~;m@R(1傌$v0^__^֪N29mZVee&i:5~`_=m|򰘿7Fi/ /o:3.-|*<ǯܽgmǔ44~'BgqnY2ױz3R UNZk$m~]wP;~Vo}K–3JWA +Bo,)I4+W;BSo6 ܖ73űp93N RvS5㉵<c_2g;Q {r#5Ŭ\\/ʰvJcc̶!táTmQ5]7չ|V>x=Ǧ 3nk5[ #酶G[^LT4;wPCJZ7%Ϭ)M`Dv#d=˫|&.f!Mk }.wftfǃOL2(3XsO5Wj+d漣M?iTpGSWVUE_*,)\X}fufX &SoוCwU/4r5咔Z~ŹJΌ欳z:؅U0z@eurP_Q;|17͢6▨zvXZU}ƽ02Wyʲ+\vѩp%."+]^ Hܯ6tʭj9/{yfÒMK^ 7)\w}r?_. zTXe.uRFC-IXqE>*q^8Ia öqo 3nڻ 0v)kKK?{累X]D͔!ϧ&vh4j:_*];E%(UV88Z4_L9M4i]VtBV3 ,e*$rs;x0f7̻SE3kr"t]% +ŀhI렟J㠴ku94fo5Je.wIʇ wq ;{ܤXqHK {z]Y1 U~#"x`1&+H=.k^xv_ d\\P8t\%u,8hVջvM֞a-3JC/;}DGw O/mNWoڬ8A3D-B.M !6OvE\1p(um0;˪u!P,an~ f#DpN-/1qΥz/dAz@[g@>< Q*~rv qO AҒMNAUr ն 5,_. 2-1n ORĂpęLsA䙟-bw .O &e?0j Ž Uij {kjw)ZЌkƒ/]g,oD EqqQTTd{":FXȶ3RK04z< lfhLh.p8 $'NRv"x8 }ݟJk3]9!!_L"D*atTV|Ŀ xD粬CcV8C4 Q Hv!@"I''_Ƀ,kӯ#FJ0%`]¯/`k0p$;Fg >/?]+3V +_ץ" 2Jʏ Mǥ\Q{aD6-4G,@ +h@ £sF~Dwe7; ^V)otsC#lM-%yp;`2^ 5HY&'di _ՓW2b2q @MTϕˇ"T LJ+q> u;5loJd^87N]kǁ(?9?$ Ȍ@Q3Y"OIot( ? B +Vwn' y{<ި䆪rGyoC(wbyX t?/yəP ng/9X+m=5y'5A* jQvMnptQo8Ѯ9@i켄v-cg[ڼ|F}6Tkz9r]pV+/?2ꦖ&MtZYU@A[ O>,yMrɢ2XlIXJZ6i܌sъV=h>8xtF~ 6jOY[}V/V;"Dȋ* iezH=w@6$j4!}}mAzG2xL|ӚRF@ynUrDR b@k%5B-)uL[qB:@dEX.tS0' UĹ\ɪ-w~⛛fOd/s8ԫ msVk5,rVI-^{Td\Dǥ5K5F#Ae2=)1me&Dw{[/b*O&AZͻ-trƪk\KPč~+2R=T׹H= Ok밠?YiEN=BgOlEPF6) r}:ܬGpnR곳8%bh\|ӵ|XPA2O λ/oBRBZ] +k.;G)Hp @B2 7 i)wi6Ό&fpGgnVivaMxUsIT{u)C{Ihы_\9u3{Ȱ3G6q/0GQvhfYw.Cb&~Rrϸ1텣j3Ht@9gJ7ROQѵ">[yA/u=j)x#P$:4d}JfW'dwќ=srg [m7ѡ138zZړ]oZ[_y28>/8-A~TFVM\.fk;U?{3(Jh3ܲ"\R \F'wGm<=s V-2XՇ 57j=l?.SWd/Q :Zaa4ļ #v0Ume-V^5L"cg3p p Oq'j$:8U՟ysz]٪yk5MGs{_ S)ڈR_w8WߣN[s58],W^L9+@QUCAU AJ 23 oFj_} l嶠*Uݗai= +=dcH}~ա/טݛ.}135Ty8+ۑRuە^ g]_^#T5:c u v<mS.UVoK(xT:q^܏YTߴ[{.s! T$Cw~К1n Wk$A%"}' )=,=p8zuRd+?y}yX@ʐzqbg"d͵;tƱBr|3ɮ+NXm2.|hj~2eqH:z={wuO6'Q'is=R;Պ$meP%IvҲr`=!ԕJ)l𚵯:%%d#mZxSRR*»\οH}6,\}dYZc5gu&9e ݨީ^cpXvorh[~Z5zT/SIU1^jT)RR|*b$f٬V~#)zy덳 ŔoJg~ӟC_9-!@E2WjlәWwQQ̈l}XjLT1/+O+*w)**5UXY'_Qi'_rryսK(Y_Tcr1w=|.ٻt%yE t]Mn46_3iSبL-Suf@5jLy| {Ϣ{^Zqy`Zg+ޏ{fhO p`MP{Ex3j:ҭ0i +x]޺6=0io[}\^H5牍h9״zxtH2GSV:4ܤsj*eK> D8@#!y Z Qͥjc?> z:P=թ&ǫ}!\| RX \(N)S;-x;ϑOoqIF_r[cka}vL! -ˢۍaGHMDEeA"!/zѷ9%0-g/ɗ6K|GO1&.{U- +g=A]lDߐ9%R!߼v\lHB[@/uU7zn8ຎ)n>YRw'zm&~ (T Vw5m@7h^ßgRu|ܽ} H|Iq qy T/<95`dXK}Ut', ж9HH_(@Ho-༜OkO~©[L[_;ӯMO%]=g !VN;J%]=lx1N=)h1Bi>D;" g _ X蕿9ЂK}{3#_Hf+a?X)dh\Ev1ݙ럋]BkNb{^*8{=<U7R=Wܚ|.pR7lMxvx֤kz`~BR}\"L{6nUǁg$+sSf ɋ@?iv +^KW,ZoܷM8*e9sveojC*|EY'8e~hOImXyp4J~~51Ȇ6 @V"<u/+;祈ZFZ}-'?`0=olf5w;V꘣+;Sक़z>چ^V)Q(l[6;Eʟj"b9@ZQ]pƀm=;{Q!1PNr~x;-+/\C͌MywQbxQ靾c%8j-ʓL-cv )ߛdm@[@7@A~RE3|vxzj-t;-]yWmXi~UOw%"O;IN 1WTyB~RU_^Fo N. _.-Uo3k>c-ekS9{KP64+HE]KHxx(PZ5իLbR=%x >]HdErs 0q_segھkU^ۄKEn'®WTN:iUMi-GAFQ!6' xzy/q|[ܩǥCˍiYI1ހ̢%A%:Of%]wOii~Xʗg4bձOQJszJݾ<-\>?wj;k\~reu{oMy>cphvEh_"mBAO[Azs)q')FVEw#9Okv⊻{v/'}7kY[Sb^ݮ?.Z5\=O!$DԃH: c{ģ7vlVf6]$') ~X:Pܵbc;TPfc˪Ur)~wU^x|uG- jKzHwH5˯f&R޳wf[7s*P;aE},Ra:wy+w1K!8g47e>cvD8%l19jrZ>_HW=^N]~ވ}sg̥LgYX\:&ý;.ZeĺwL·;>'E Ufqe#%۬r۝/$c @6kj}J`IZqe0:#q)&Ȧs/'zuk,?99kjc'3^+䔽d .f<;d$'~VU!,JSTID6m3@t}Ђ˟n7׃/~W3w7YmW(Vٚ +]ng5{2 Tf>[~"}0lAP7o"'ƈ$ 33]O?%-OwnL=e362fkȱޓO.цU~KjZ,&Dڝrn }cSbQOuzT u$jp.@=)/r[9p ix2z5u +Չ><(_dJ_MS˸Lu&qȄ 2F:Ԫnjُm55gSm>zS{OnALjd$5|8#'cSJ\p}pslvz\ds`Zvx,H ؠP͖ZYn=54-hNǮ6Hi7I)Qh)I!u֫5=5{5yN \VERF^,|/~Vlc"W]~iv Kէ\7@J3)vɨ!ȃ wwLRoE[5-ͥ9j/3دӽ;dYk@T=szfn0rbq>UzTΔ\2pH:*>wԆ? Ѧ9ѴO[+voqDGN _y{ FAMt]Vyk60劇廨 xW-U?1gfU}o|3:]˭"780:t/f'bK_i~GbW\뭧4-)7;Vj[u/~{cgP-MK$~ǟM;kd'#u0#/9E'+r|WbZfi`֪t1]f=ޣ?P'y~: \ᮡv[|҆]vsum[[җ96>|NF})!@ +Abu;1 :Sni [dr䉺,Ufuk{kVW~/Bl[RG2='su F/YH+FL鷜waoX{ѣ*-nZ IzF nY?7dј;Jrr M*._o2IX’7yβx4l%8)?"RCK;3!64ו±TnG}nWX*m[e!0B,.sN$USxs ۻ 6ʝ${ ltEaXd q[$#%ɔsf0^n֘é_ĵO=itw!HQ\^\sذ. !rX,v%Jg웻F\~J`9Q g=6xþdב~ox٩1|*QU w-^5zPOdʼuQ$H*cdk;JMHٲB6$>DaKCT{'t_Lu1Ql\çqz.w&"[ yvZ#t7 ]vs| *a~h5ffgQ(AND޾ + %¶ T263?O7wQ<|QB 7K>!A-G[t@C><Gt#д xfzK'O^(y/!ub 0Ǣ4uԩp\dԯܻ⻭&U@}7AN(gڈ #p% kO.?HjQO(5 (f_\o(53oŎ[wft!|2/D_$@Rd"v G8޺/)L%}u-n~r|7mL8q\rؗ>} Bm70+-}bg%s.٣SnT)d p'*ڷ`a `R(xP2 k' + ;B绷r8C[ yʹ`YG0+ԞЂn~*|_!ӈLbL@&[#i86KtY24!ATڤ7nkghMFbrټO֒d~]ij1b4%jM4S,Ʉ%Q6DΘIUtz\DcAJyxk:5saH|u烧3Oo}=Sv46z6ji=e1]ںu.jSR Y`pok2\^[SqKtM^͗qM<NKr3pyDԏ=_GejU_f 7,kܗT!g<<(R\NB(ltRdJ/@)!f[pzn{LH+2.f/ƬR/PKV~ZU.v@9OHAi9ɗ(c hY . Gcy+y:ȅ8ci;Y!&_Jml> +QWޤ}R24xٻVGڭ'#طI.9E{m> nHq)dbmb%> >5nv~PӫrfۙoEp#S(WnA8, sZeVSHmŭ7 3ͽxvNB{X5 CfqU[SN.oM-fRgk!VёW\7< +I3i7Syo8~ՊW>\.ްz'fblEq(Dw(=))-fqM/k*ޙH[q]z{nO6[H1BZt'Mڌ@XXK:Cmn}hώJҮx^v# +ȶAV˫`\ _ +tݢkVJswF&OF/z,%$B AA9!ɂs(Z.LoÐ)/r{ʽemGCk`[L6@VƳ:/yP]NvWɘL.j]2AO' 5{^oE+:y1E+Ż>᪣K%*>[ᄮUyzrxwՉAfe7|t6+cvL›.EZ56nϬV|䧃Ŏy$ 1+ٿX[kS9q +-RbGAV{Ȝ5@^~qr a3("7GQ(*lp"MXNkwv[zR7/]̪\wуp\Nw<2OiJ''b{ g~ %mݢe\wg*N%x713GnsAnvloDiҟ0zQZ@(}Y?Hg8Fwh9y+̠f+Jr +ۮ#\iۮxz\FmkZn G@3`~a(P S'gzVW^@Yn?;[[̽]tw?I wQ#Mq2av^j͝;kblY<οYg^}5zuiEwnKCT N$;&Osҧʳ_@/Z%ltŏFRPѻ'?+&ͮ5V>M48WȬ3ӭ;Pf ޽m||]B"]Bd֤URoQЋv>'-:2FRM4LFތEpϩ0Cl~nX&\uaAMr6 떲kpfQ6sTە{n^LdvXć +9NΉ;8w`!lG7bJ~fN$l .;*VƇL$[^{J6FBq}ƥnPBrܢ?1$RްZG:@ah3̴s3"0 mݧQ( 30ᵵm`g;GUt*bW +p˰ֶ^}/;9>`Y>WR3VVO5ϒpYXuw'nR%IvzbgĻ,[eA3ZսЬ5^ú'χ=5BRJ ! +;2rb5^jg# ƍtRf9DXDGK!jqg֊M[2涗O-fps/`j:DMz/v^"dPnfE\̤Ap Ap\_D8ZQ[Zx"b6Jl9 3h:Q$\>?O#DPi,LEx"2F~av&gj&dG4d$=x%"%+do'JR ;G~ES| +NhONPN>6OqC~Ԍ +uW A3((^(揉͊u}WgX=VXf(\7?OS-Qnt5Aԏg6Nyg>| ӐnjP3_YԬ ^U%<6q\EDJ6Lf(+J!{;wF°' f8 //}J6(oޖi1~)t<~9$zbs=R? zI6.eg㶕 ѷZ`i%੸?-2\Kcҋr!.k6x}Tڿ} pH~A +a;-7thpķYFxJLΩ,v !V# +iX{i"sDgD9[weYNjxлfZrE䅽LAь ਖ਼-)ga >^W÷X[˩ ëxu.O{~l.|2tv.\:PaoCn6)_-Ls )˽(/NCeȔ.@qMJ))Q\ܻz˹5#\ك4>g2m罫>(ל^A/R/BUҙ㎆x`*P~g1:r{pQ/sQA"WA"gdؽgh8.a|ŀibfEG-Zљ0Mpgݗ9<dv-+Fv1s|֌BjxפB1dP D1v T۝TaMaGZH' "['3.79^8sISn(7T֫lO Ljp:{U6֏ýx=*KAܖn=6_ov𽯇jÜp_Ji gImRdzMޞKt' v=Vg1TaZ\, ^L|7b +{]>,9~KNqvs7T}RN͛DW~"~*EaCOlryZrrmZ>,"i#rR|؇N$kNf([(n'P2s,?2d s^)>剥:Ι+z/@ޥsoft9/43lTأtғ9OMŃaPw_ΌzYO];\>жoEq_=uh;9m +rXo^y_sxfO7?1jڏvGEpGvky"ngh\[_HwM kb[xйٓO㡛B{RsF0["d Q#E/_fO e7ta2YdZ8ON>"7gc?ܨW}v~ +ұI%Ӿ# .ɷQE>ּ$E>g/UƂwU=Pkmr7.}Ao!Iiv88ݪ-ɲ0޿p}D +zZ[7ұ;yfy]6rӌ;8wrbؙSCgQ.W>dy(7s5t?L:T^%snBu<ԜT՘Uz+Ɉ{ͦ[.=xyv"dvƞCmc?>T6.9Ap{. +5lSw,bչR3,XkӔ= Uw{ Ƞ[SNԫ|Vx]Ǥ&]n=vwDzʬFSOm̅/(*xѽEƾ$|GO%+ )k1k +&#L!,Rf|<[Eg>C~7MkDnG糘L=}IŚʝl$q1XZ a>5~tA xcaW]Pk4vg.x;516^r.!峌. + T/9)paK*A݀^UP5~:RD͗Nl"P}|J}ycn*n$H2p +0YU+~ݖcLh +HBpC i&\info5ɡWI?`UV^0N0_)̛+MjbVU"u+?[Dܣ:TtQ*ۯY̘w<=x)v*>/u>aNN)ڇnAT7J$+v^+Q^G-}MGjL*q0\͵4ƧnE +fw:Ru)<dz%lvRZ}ixi۪Ǘ&}aӠ@K/k: +leaxܬ^tp *ѽj: cqny ߼ iٶFǨX^'i}\NMr +ݨj2UgS49T֑PR('k'3Y1#<"~w5G2˰?+7h5ŐEcjQѫĭLoҽyҺ[=ȂoRŽ9垑~ω;/EOWI7xI,h°*ޓ]e6/[+Pa#=Eǡnvz]0n 3:j|eTȒ *^ו^x-`5@FrFȢ +aež*CzQpO:_\|̼. +r(wxOP{9 RlTT*/[Aﺅ;sH~"Kݥ][? CUba߃P"2͛6fn:4M:>NZcSd?Na{!'x'ӧhO>eR\LfvSP̢O鯟?]0n{-.VM+ƼD1HCT84:v9">O7c7F9h042bic{F~YfgJ(ϴt=sFHo8Z4OQ~; +߳ϭw#$ljiJ%H_Vb+H!4gH2}>RBvqѹ[Wϟ軌\jDyjͶx +\ǫSxYAH#8?k1Ö&th/''yzI哎͗hDZg@qlPO:A鶷&=?wYX^ˈjZ|,?Izj˰=Ǡֵ* ++ 39/(I3CUd5Je;f#IOr޻[:xwB._Mӫ7L_߃|Vsru:uyF9?X]y\5H  +^2QܚIסyGvȭ߀`[ +5!y9oٯ6;N\yZĞu#V62o>m{-w}*X7472 +AfT^ˣ+! +WvKlu8JZsgq5 }3ɱ1KG MSIf Jy{ L.mw6Qcl.P5洝0in<0Ւ!.-MĐ.W1BK6A~u~Ӆ+J+'$2L +CV3%Nk{]C.ۗn1wЎ9ZLn|2\2;H1BP鲄ZL.~O?\]TaC%2V,FY'˳甯ԝ"5q0bjBN*_NnAJBW +zcx( +{YBa? .cMkCEWT""Lȓwq"uR Ļ.LEXq4cY-;~_$yk ~]kK1^L[tV_}/(jٗ0S7V_kQ!;]|,BWg_%Wn*>=5E=-̈́YHS퐭{6wkAI!,(<~|ʴyx Xg~lk: BTi)!H̺=A p +{ ,N/\^(YNؒ,3/fm`,lc67Lu)>t>b7 )S&cxpKFNv*cIFUFҵz(QZiP@An~$|Ҕ%Q3Zv$^{KFS7 +0lV԰Ii=IYn+ڨvdFկ[ Yݝ#>K grrc`G9o鮻!Z}>G,uLt]s/ Ri& * ?Y!n]$k5fZօw[ee5Vf%ߔ =n Z9g.ݭ݄IUIٔRPKڂ2u6`=-E˫.Wxq"L -Zx&H\rNƢiN[^.sWe](U5F(R`f_ TM]+RU{X9K, jPwS|r3\5g6fš"M}~39? G#WwR.1~/$q kڭ2 +p#xaRYLTH?*>:U{p0N5F#FW~q&=H k屎}ﮂW ҥA6ekZv墭5"텉 + q1 +x5'tGn:F9.6YmUbS?x+?iG>6{]f>]$5QbT8OCpe3dR ld(ϖ$4+g- 6'i5M7\,^Uh l2]]}v@рyQfpTeD{V| +]L'ӯԤ¦d뺤LDv%[Tiט̡W4G0ԯc5"80f2G軤CH_Ьԯ8@s)p7,ǝbۓ1rSţ<*?W]Иn^ft OLv>v cEw@Ď!eIdȡ39 $7?_aY)n)z_AgG:ћ8s~V5\eO|wKGFvp]?\Xу\4k/Lg5v? }ZK2tY=5Ê}C#S줝^3]s>hirl{yi7{yXv u]}C'H_]_LœPđNwԸ_]ڳ76A3>>~9(+E'`)U/7}~@cBmЩ~pN ~={|Xo][>wUtfA#CRv܅k*fʯB"x؆9ۅDS[# ֍+_Jݕ{d:Qi}(;m#|jHk',.hz{juy+ >s2̲s&RNXu4o=l6{~}zg; 5"{bʓ>o:y$K:rs?uon޼qy͟OP3d_[a=Џʮ)'dG~݌So3AB Pz3qI.>\MV@ H5EU*V$J8uˇ(ZZgq}z;k0 +{n!=|m; 2ٱ7S"JŘ] \kgWE{幩KN.pW'yvȹqsP- &6 jK/m/o-9Xyeϝ'BOi"Yplкa,QU +Q0p]hn L[V +`v b8T %#"`Z`wsټrMʄt: +rp|j1_JAg4)Hy`N^e! ]H|[>f7 +t1njes;^k=.CZf}iI퍲^ӛRHod^WHf2R2LqcĘS*F؈:`h|yuHD.GI~d}ng{P0`nooRDfEk]`~F -(DY_V$[g_ŧݵX9du_ܛrT{4 +Gۤ=o[] +dغTMPe%3׺ =-؏`|?$~Joh/o {{s^8S`7(.'QﮃsѠZq +5R#r8iaޡZ]]cP- WYq I.ԓwn>$]*ں._Kܨ?j:rv4~~%¶7&e/grXkdg4nl='6EaeI> ?H` pIfhˊa1Y^yill<3յаt::0{#K+БT${}_( @5F9AM,_nf7URAi9~f!x~݌хGLֆ >r{Z}2yNh®U-Բg"U'^(]7l_,UA^|=>xyO<_q/~\g{r-+}xo荲jY HD&Rn!Vw]Ȟ϶l??OA^|4xO5pRYUN3d[cO}+zxmݱ8 Y톐Mƫ.+vBO=_J&$Z6;AH LW0Kޙ=,![co^mϦu)oZlܶlI|9Тںd6Be\}hn: Jhx*Gl#eojqOq:egmŖYp5aޮ- _k;ISOO}!\DI(0d_ $y./e]>qx_6t#K;]@}7fY@;wR񀢃 ըT>R|,H4o [~yj^g3C?+=X^8:Mo.O-ED򳚐{uA><cݩjmiif'԰lHi{`7k+uY~]Ox29*sreqS\S k_H +MιOMx_z`jr."\U~\N=߻so{ +.ݲ4{N BW}^\%mYAwFWX :FΨͼּ?.): DNw|&vzp*N0qk`|a|T(^%Wf<;diBʏYg++K6.bsE\Cxe6{n,kh6tߩܜ'{aɾƆdNl'uSF~!^Ssp63&{+`I~ΙT;݌]wbRMk98;0%ΧLEC<r ')^TU@7*!n)i!$,^vFV.Vrii8lm#} t:3g +4_IUɭh?hx<n;"Tԑ Q]qyUq>XKZ_1b.1[T)H9ri_I{lmf;*%#swc@}]ްeD?љou_2j F 4E M@OlF%/ ^p#`}=Xg2G +ilcZأA)yfn1ׄKr;}dS ;\< l/Ϲ2-TN?tRJpaqχ~K~b< ac_;U& ?{ǀ-"D<|spZ*Kj"873Ͻ'^4jA̪,Fa0 +F;(y W_=> +Z}Z׫5Y/z٠ ]s 1EhC^OE8wھ{5^8 ,[kB8y /`7lf2eXhp)A]{: 8 x,!*^gF X)K?QWî.nq:G{VedP0v:Jv+>:!/X_h|lB֔Hqcbdjx'6Բa}}:{ٿȔj}Yd7&톖xSU[`yL@aM9uÅah=z)\}d/E2ͮ _cZv +tҕ;?۶ԕ6K ֽʞ ]ȡ?vxHݽSTfvɂ .` MΒBۿ'4ܩӝH? x{nO3R-kܙʦ*44WU85Dcc}Fk3]"~UMݛWsvSbX\z,ӴAw :Z Z [tW٨6gvsGw]=b_^XI~臊r*H5۔/z p{)M0AM_@)+<3)o^%<⮢>jcUob,v.Ϳw٬UN;+r|w[7w f@yuULƀ@HHVHݙVSkW(BГB.FGΟh{uSޖߵ#W`~0]85)W)B@*A <HDe_#0+Ok:Fq4Po^b=ٻljGnȊWHĕGʜ=݋yp@&@K@ @_ /nGFns)i(TY>tﭐ6r;+{+U%p7@X=nH:*%ÚDV,7 :LT${moׁQ'9V*,^"*TQZ?NBbFk +*܇D^j@e r!_hHGOUlüFQ-+‧H\bYr7_6H|f$K̡ڙdNʜd+sB$ߙD>kY+VKc5.GC Hhian,>@ǣ8n 22}IIiIʏ̝~>̈́lfu&Lb#l7f#&gW~GukȿRL*6TR̳?Jh rK')Q2K(s" :^1kۋ.ZVWe!lx-)>lo+l;G3rYsuv/{{{ qdZ gF:?d&QL]׍GNaDV:Hwwݻě1l Q?}?;P#Vf N:ec?`gXռ`e˾?h3: $W#^WgϤ?MT/jPO~o{=y3Pu񛮈NMr\ ' ^='{CK.T-XJǜE%ݚ;# yv[O=5:q +6 7kegv㶣}~E^uGyw~N2:6]LX({\z#Xfql:h71 FxOj! p4sn{/[Tp2>9>e Au+hÇmIb9lh՞y:Lf׀ݾ074?MME}bUW@@+v1Be>J?`*Lxh7~8]èv[9KmUW:CH&X]t:<94%?jeGڣH)ToSg2WU^%{mMn +cnu +JS8 + ~ОJgyܡU`6qAĆXqrO ֆ:TP\{󷠠r4e9oC1yfrs#%ZT81mUd9GsP(7˹e璜F~cCeuf+>!";t|7u'aG]@tոl5RW@LS>ex܊"a>Nrr[?9)j6waEcu ՌVJH-m}-&Fʦ:"g0<:e6or8Փc4rI#H[g4<[Ưnì.vT w/Z-#7j]ﭿ3Z ;e^Uvylcڲp:|&ĥ=nmYM~52x2M *7;bssxvsϐr2 v,A̎uFv]7wur| W>ZFM|uhf9^??/O% ǦҨS[^r]kn-u҈87.B 9a 0evU +l Z+Gϲa ĵֈs]g7SrzL PGn u ei:.Tb +AO_x'3P@n`nR7"$07dcmnZgִ3OcCH*IyEۏFDf^#9C\x8/p 1xYA9~ + .);!1јa;3Ne=Lb(=?7/ !7ۢ|K V+bA$m^X-kgq_W |eXd=8HZXduD%S'uvP]`ݑ m9jP\1Ï}:sp/ 7ƌ["! ʳOkXʜoP>hl6hE>YmvˤêY]DMư5V}9C jGzK0fb~66 +o:;v֏6 +{N2&(5,#vpʠy~8zYٻQbǶ?^FJwy1A6u;)3V` ֋㳲M,LI*Yj09@g,O "҃srˇ. C\wGne/'t"`ts`9v>ǥ=9CuXWmڕҬIYR70h볃{TiDvnM[ЈDEN;'!=2/u"U0Co~7v<*xųٳ'm]0RoZzjOGI.] E=SC(kE/ǒ]:R|TDzN(ɻ\v1o`Nr>pB7I.Wo2=ګ03eȴܡ:± ⵓCf*(U%3&t!rL$zqvx%gw F + +~0G|V)s~*)މaGF !x]m#0 ssmV jyB8+-r-/bt ^pE:Rh0p_8T7U +PKX2'3 :H Uݰsm( +#O"0D @r@n:i!3 2~uvtom@-6?qi+#9~ MkS⏌SDkbF?la^O؏_w +mpepV*?!~No'Lih`* `Tdyu,w)/0*S_r%F#UNn.N7ܝc䙱'S9zG +E`Tdy;:+,lxhdb#h})d4s!ӧfOoq<w +Y@kc}c +=vlIwi_ +pWU),7A?]oH4sh՛ L)YrEѧ_BL:\&@g2.mBz}d2@lH#kWSg'AEe_c 3)xL?՟O3z^Q~%|%J)zz#׉밝. 1|ni`O&ImXP_槨vz?C^EEuRd3ӟ~[ul)S~ؖw,|ȿA&a^H6Tީ<۠gvy; 29]DQv/XUֿc>HQ(֟S`Z\& >.;-}_oO?c!/ǡP,CsCOt~yȞXz3wkvPq^yv8:uDq"!qT`^ҍN#=UDFLj̴]yuoV:Ob]٦-̘I$AEb+εs4,b}tZ&K4klӼn="ߋZ b欵!Kpsb,ho˼*l3h_x> =_bz|';טJ63҂ݘJO[l4f^m ,X|E7 +uջ֚m>3*k6:9-ugJ{h2>L 9d+~zt;b?ρ?^FϯpSXnX{ҟy6 ɬmo>Cw)֪MuRHL^8)d | P.o!/( +~s˥$8VꠣNp7.=?Ö_;1bnuliF~#o_d\=-C@!V j|vlH>5FY:K]`0!ԭ#SF[r[ٯ^j#PYմ81ќ`k5OLhBVl wuKCŗN?)ΈgW-pVyȀktkݲ]{F^ _\Ӣz0D^6 fy}Wxx>MxÎyIT~MQ}* +GPN\oPRװtX n8j"pMFp Y[pfow{u,ڋ#o(#2Rxz۫oieyLw#-̙*~" {9,k*E)mCu vXL\^C '1?;CUuf}!ũ3o0Ve1TuŢW6F앒=@OcbQ!E} ߸C Y7W7-G79*| O:];[J6IG)v D1_LNL>8 +l4fETdmNP׳y8Tsu硟7]4́ c #qXb +{3Z9~0{]\d[ $.޽o(ݤTzLte˴{D$Xֲhm``F^-8'k79cͼ'V.wqS:QW$$9'Ѓ-~:cjxJnJ@!WO^RћO*N xXPI/`͟0km^ICqtȐapd)5HayhZW &]Xo뢷8Ɏ~991fgK/Og +/4*b$1bWBZpΧu& tJ` ھؾ&Ve\X^v+"z?Ѹ֔wHB,dA}.‡7s<]q%]=Ϗ: ?ڀ +isF߷,m ӭ/Rbfh#)p 4x5﹬]x3{~dY 4j8 tuQS|i91V!D?S t- 4ڴFaNrm[iFǝiv}4*4SN[ΰ9PYSZYUkdӮQ/f:r |/aϠbv[yqƬX[K p3hMX>X!/'q_6{m$ +5ЫWSxU;_x<ƟM12zg_)3.Pj=ζe` +n%FYWMhmuZ/99#5j6܆"*bbyRLRE!jxQ7')gUNb)2 M7_bVft}Ք`QPXX݁mrTx~2 +^ sFDNƄE5< + < 5Vo,,w@\?2zWx㛏GF{OF3S"v8u׊ɬv-?YEysw1oM>ZVmܢyi L/{2E'oU:dllr Cf#BcX5@WOy)P`P`m00q6UF?2Z_M}zykWFՎIʯ +NrpuO}> kg p3(YG!,>YbhɧpMpS#]ɿjnruJf!%4Hi$#5o8>AYz|DWAG^軸{uoq;nqe ggx8΢R9X=4?ufjc3{;CO3d[_CM3'd}=avW߻{hnrJ~7 "Vܸ ә7Єj*|$׃^Ԧvc>f r +8Kr6b!̝K_H;)eƒ &|N.Q~u G\OOm|[!oW{Rv1QvMom!.ovnmZp2)$ȭ&1V&/f,̥83 OP8Q/ 6VZc6F͎uN̺&vz&pk:(ͬ~8ߝgTܣ7\v!(E<(CiB>۴-GmL{_r0r=`-ƒ ~Z(fN2eg 1}J.1hv*Dt&me]ʼnO2 W1w[Tf?ov;,n.Qg1'VҢ +gITgzF2̱GLCx@6;V=YLT6/j==l\@oV5'Wl۟y.fqePRQ]<f0%P {*f̬歬5ޛjw68HXw)4ݟ7:%].ZGc5T/.GqwIF\wkpD; +uW5,l' w7mXzn6iG.Y[M'FHT^uf`f\^n(s{{ \hxz}W@twf+yuԴg81Q#?mWe$ X]ڨ^?|v:y5Rd3ɝS >BVw͹V8Jn.6578+<nj9.w8l(Ŕ&$2RiW$~<xP^?^pJl-Sg&"z.4BH( +rXJionOT^saryʻ$)v|%ì_Kso`z(v2?TaCt.w5SAw_HæߐioBr*jeˍH*J .Z\g1tF62sM%ǿ+bTՖI {|O"}Ty7;!ݖ5e'4pquO)w&hv̖(|=^2bPკeκX`{P +cU vúQ\̔,tֹ] 0͎bPC!/B@Y!N2Ecc 6s p臟_W|$O6}N^y:WCF1M3ߋ7\W5MK jEj$ ,߇[O^0f}cŽKƣ2yhe=#c "BjԘC0sE>?`F^ o-nUIx2|靸 +vѶ^u;sxtlNm4-И㗨S\>Hҵx'Xտ({laϛO+{n9[:yf'΅Hvc)+[}_7fxeJ)G]f)H:~<:^U#@BҘ+! lUw+БSlۗj1v#ƛZ붿OGϱmw7~RnkS{v au.L +f:I uBWeWմݵ}Fq,%Qmpa;A~lۿ3v-( *ZKBuZiD\( !t5#J:V¶N&piVRv͏N!U4wos!B=I]uQ,X[l-g,?th:bekA| 1E Z.t \ RkH [$iթ7W2ڬWmH8a}rSLE J+\{rx-wqA_%uퟲxR^L$|mA֤fK2Ճv0UYT9x#2-?̔Z;W]EioZvSȖtgB^ hya*+X{k BSEN[ +՟ u?xE0z<*d#Y#Ȫe-ڣ/& ݯڝ*_vt]Z!fS~%Wi_eէQQ8{zdWG^⮉c&hֽwّb<{a26Jeς'Xo|$7t1 :eR* Γ/ǙY #{7*oa^v7it͓/#ڡS=)Mx +]RQŤnxsdi*FQ-pGze_vSJrOgVoAUf'asdGt~Ҷ>j367yMEݚůnD[ˏ[S%bJ_[SڛA]vh|:wr߭xCדkoӊfAbY^k,%=?-fN22* oe6ZkY"=^N/6|?Y~x^;7ƇjvnO.+b.$,Jg/g%D&(Vb_C|gkNs\W$c0c Yo92H[~͎nn~4Zspn%5UkV;8u[ok+G+ yE UzTFRKJ+;XmW^~3Đ}N5Ϊvj1 ԁz+K15nx^sir' X +NO [wXQ-ՅsCC{jB3(:iyDL/ߣ2gרnk0_-g.<۔%gS";Zgx6N+7FIX<6<\:CCQ[}g<U\П/ϟ$~f}(Dxt3^Ej7 +nfVS4Kj%BLGUfx z :pt/.ȺDot9,wZ}ڽX~>վ)Z5(͞~yqM?\cvVn6x~f5euh:ɱ9ogKʀ)-gJ[Y{vq2Z+vfLX?C)p]ozXam=uqZ^KƔ~^?\lمr '62BJ܂fE}{z>uI)9uמ=/Yяk@eҞmƪ4^3[nvt 0\8YgDpV?S`̑A) 99nJ<9IUlP{.0l$m;uo;9I{ڿYzʙfߪy>z({8Qt}1uJ/dִ`_h^Iک̾**ޔfHڍRILwϢυS<&S&GWJ&- bLưqw!nBkP#έɪՔҕJyj8J"JNjlCtkb4lㄿ܇R"_p$kZ+5d:h;kpZ&wv/y@ cPď="l7ΚuT*djtؔPk(:J|@pF^{$]\P`7+ r+:e@HW;,%-08F=Rnt~".*Ԩw=rĉ& ^ f:po[r`g-J{dpf][j:bm<m3~,.tfnkJ<>IܡA)Z-+gw2BlQ檀85.$(gKĸ/T9v([^G&Syo6Jt1G~ ] Mk3|ߛW +?Nc`#\Kk#G\=X_-\c ԏ43_K<7 RFbG* GVlgrp:CO$ #YKY\8{"2 _\푹n𲎶K=w?-Ƚ:ﶟ^ wa=|g7rzDJq[9>AՑLw [x\bCFˏs*.;?-[-9~c-a3"ZZO*7c/)9D^r@g>h:بpզ:6$ZM; _=V/tt3YdEcٞ(-šυx +Xo{m Uu14گ+S\9'b+S{v})t17$[0aj$h7^LъZNuͼ#3pop;@V{*iݮǮԸV!8Y7&{-r%un :z[1p\z7~#os>JhFeB#'$"%<ؘpK4OPZPbqv{oh]g몾`j{+lfVck{l\ip gQ'O.c~Su=Sk"G<<)wLQUJѽb6n#P !DdrvsYhs>:ʷJ.+ktk2ʽxMüpicTK畢/n~{^Q92.H [_Stu}&=~ė:>Vo=5Zm4ע|ߜDʺ E-9_9+=4eO}k/#"G^vD|/M>7pP]][guQҚϳjge? +ͦO QfTOۥcY9k2fOc[<>²sk^&ZPV|0?ղ)3fό?78'ornx$4eBw+/.~N޾4nۄ[z~dmsᏄI- 'G^~jRto ڙ[S?/#@Tb(1i{آZ.P"nmvlZ4Jl$lȮ3gֹ#+7yi_vh朒GE^G_94$ɷh^ö>7fntKRqLOTVk]ȑ'E)jd=g-/UK3<>GryɗM+|J xkl}-qˏ,C<[tdQ +nysp>gzvJ6.‡Tes̡Þpf qWܳ{҇N\eKQ!ٶv"|KkP-_M:iDၼTPj'W+#6 gl'b}9kmB/8ڒ͢k_&Yg˩NyZNf|C:;R%;(޺igO],՟3*8͢ty\P; +]V|+2!YVp=K'|!7c_ *F[={vK :*n=긂C9& JNx1*MyROsM~@`G=_|˨J,T}2{#o6@1j{^z5=HY9N@5k%:N=qG@}dw5EϮAo@v-/.F{L{ap((_w{P1j~r솤I2[I7ʲ,5ztyl%m}$x6pdF{"yiVw亞zH#q\r{TڳCx3vb##źޙ^+t6.kOc\טt +{eQÇbO;5G#bMa&Wɬ6Kb*8Lj-@ ج]l97|o]zXrUxb}I!s8aR#Y`ɛלr-тqԀRO^OE)7-FMiЯfBA ' F_r2k,&meĵ^"JyA~ݛqA8y\hH PFgT +UͲ`Ц,S),ο(5(*9 +N9bdW^k=wf:SZ]E4WZ(\"I ɍ/cNBփlPb zFݻ]\@WC +ts ĕ6 b0;Dv$y}ETkп'N XuxU?BwǴl.ѭ+}ܧ3n3hBoWum +.]r$Vcg SN災Qڼqx=ƾO'=mS`mvg|)4S=kyDOn3]oW68ٰ`:}wCDAu4 >#=וE-k)s@r QEQŜs~ݫ{9^}dJ,* FҨqrB~]H.s ceeq ʨ}{]Li|A $ *yƍîYYCAڢߖsp(\Xʎ铹#v~Uo ` +LI=dў~Wzwzm$|yÐ 'Gk p(ҶCoc7rvƭӭ1YW$qGyx'I%.=Nܒ{?^hI^ Y.@bSQ +endstream endobj 5 0 obj <> endobj 34 0 obj <> endobj 62 0 obj <> endobj 71 0 obj [/View/Design] endobj 72 0 obj <>>> endobj 43 0 obj [/View/Design] endobj 44 0 obj <>>> endobj 15 0 obj [/View/Design] endobj 16 0 obj <>>> endobj 91 0 obj [90 0 R] endobj 117 0 obj <> endobj xref +0 118 +0000000004 65535 f +0000000016 00000 n +0000000187 00000 n +0000051607 00000 n +0000000006 00000 f +0001280969 00000 n +0000000008 00000 f +0000051658 00000 n +0000000009 00000 f +0000000010 00000 f +0000000011 00000 f +0000000012 00000 f +0000000013 00000 f +0000000014 00000 f +0000000017 00000 f +0001281413 00000 n +0001281444 00000 n +0000000018 00000 f +0000000019 00000 f +0000000020 00000 f +0000000021 00000 f +0000000022 00000 f +0000000023 00000 f +0000000024 00000 f +0000000025 00000 f +0000000026 00000 f +0000000027 00000 f +0000000028 00000 f +0000000029 00000 f +0000000030 00000 f +0000000031 00000 f +0000000032 00000 f +0000000033 00000 f +0000000035 00000 f +0001281039 00000 n +0000000036 00000 f +0000000037 00000 f +0000000038 00000 f +0000000039 00000 f +0000000040 00000 f +0000000041 00000 f +0000000042 00000 f +0000000045 00000 f +0001281297 00000 n +0001281328 00000 n +0000000046 00000 f +0000000047 00000 f +0000000048 00000 f +0000000049 00000 f +0000000050 00000 f +0000000051 00000 f +0000000052 00000 f +0000000053 00000 f +0000000054 00000 f +0000000055 00000 f +0000000056 00000 f +0000000057 00000 f +0000000058 00000 f +0000000059 00000 f +0000000060 00000 f +0000000061 00000 f +0000000000 00000 f +0001281110 00000 n +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0001281181 00000 n +0001281212 00000 n +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000000000 00000 f +0000054378 00000 n +0001281529 00000 n +0000052021 00000 n +0000547387 00000 n +0000054680 00000 n +0000054567 00000 n +0000053242 00000 n +0000053816 00000 n +0000053864 00000 n +0000054450 00000 n +0000054481 00000 n +0000054716 00000 n +0000547462 00000 n +0000547898 00000 n +0000548986 00000 n +0000556278 00000 n +0000621868 00000 n +0000687458 00000 n +0000753048 00000 n +0000756249 00000 n +0000821839 00000 n +0000887429 00000 n +0000953019 00000 n +0001018609 00000 n +0001084199 00000 n +0001149789 00000 n +0001215379 00000 n +0001281554 00000 n +trailer +<]>> +startxref +1281724 +%%EOF diff --git a/docs/images/rayBixelConcept.png b/docs/images/rayBixelConcept.png new file mode 100644 index 000000000..c756b88b0 Binary files /dev/null and b/docs/images/rayBixelConcept.png differ diff --git a/docs/images/sequencingScreenshot.png b/docs/images/sequencingScreenshot.png new file mode 100644 index 000000000..6b4479240 Binary files /dev/null and b/docs/images/sequencingScreenshot.png differ diff --git a/docs/images/sequencingVisScreenshot.png b/docs/images/sequencingVisScreenshot.png new file mode 100644 index 000000000..bbe4959b6 Binary files /dev/null and b/docs/images/sequencingVisScreenshot.png differ diff --git a/docs/images/sequencingWorkspaceScreenshot.png b/docs/images/sequencingWorkspaceScreenshot.png new file mode 100644 index 000000000..a1a8c385d Binary files /dev/null and b/docs/images/sequencingWorkspaceScreenshot.png differ diff --git a/docs/images/setupIcon(klein).png b/docs/images/setupIcon(klein).png new file mode 100644 index 000000000..ab364c990 Binary files /dev/null and b/docs/images/setupIcon(klein).png differ diff --git a/docs/images/setupIcon.png b/docs/images/setupIcon.png new file mode 100644 index 000000000..ae3a0c21b Binary files /dev/null and b/docs/images/setupIcon.png differ diff --git a/docs/images/setupIcon.svg b/docs/images/setupIcon.svg new file mode 100644 index 000000000..6d9b9dce0 --- /dev/null +++ b/docs/images/setupIcon.svg @@ -0,0 +1,85 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/docs/images/source_penumbra.png b/docs/images/source_penumbra.png new file mode 100644 index 000000000..8df02b67f Binary files /dev/null and b/docs/images/source_penumbra.png differ diff --git a/docs/images/stfStructNumOfBixelsScreenshot.png b/docs/images/stfStructNumOfBixelsScreenshot.png new file mode 100644 index 000000000..0350e97b8 Binary files /dev/null and b/docs/images/stfStructNumOfBixelsScreenshot.png differ diff --git a/docs/images/stfStructRayEnergyScreenshot.png b/docs/images/stfStructRayEnergyScreenshot.png new file mode 100644 index 000000000..982272e32 Binary files /dev/null and b/docs/images/stfStructRayEnergyScreenshot.png differ diff --git a/docs/images/stfStructRayScreenshot.png b/docs/images/stfStructRayScreenshot.png new file mode 100644 index 000000000..a11715295 Binary files /dev/null and b/docs/images/stfStructRayScreenshot.png differ diff --git a/docs/images/stfStructScreenshot.png b/docs/images/stfStructScreenshot.png new file mode 100644 index 000000000..4d6830778 Binary files /dev/null and b/docs/images/stfStructScreenshot.png differ diff --git a/docs/images/technicalDocumentationIcon(klein).png b/docs/images/technicalDocumentationIcon(klein).png new file mode 100644 index 000000000..162922299 Binary files /dev/null and b/docs/images/technicalDocumentationIcon(klein).png differ diff --git a/docs/images/technicalDocumentationIcon.png b/docs/images/technicalDocumentationIcon.png new file mode 100644 index 000000000..00222e5ff Binary files /dev/null and b/docs/images/technicalDocumentationIcon.png differ diff --git a/docs/images/technicalDocumentationIcon.svg b/docs/images/technicalDocumentationIcon.svg new file mode 100644 index 000000000..d831246a1 --- /dev/null +++ b/docs/images/technicalDocumentationIcon.svg @@ -0,0 +1,120 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/underConstruction.png b/docs/images/underConstruction.png new file mode 100644 index 000000000..4dbc75a82 Binary files /dev/null and b/docs/images/underConstruction.png differ diff --git a/docs/images/underConstruction.svg b/docs/images/underConstruction.svg new file mode 100644 index 000000000..c591add9e --- /dev/null +++ b/docs/images/underConstruction.svg @@ -0,0 +1,71 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/docs/images/visGUIScreenshot.png b/docs/images/visGUIScreenshot.png new file mode 100644 index 000000000..650b0ef6d Binary files /dev/null and b/docs/images/visGUIScreenshot.png differ diff --git a/docs/images/visPatScreenshot.png b/docs/images/visPatScreenshot.png new file mode 100644 index 000000000..fa513b383 Binary files /dev/null and b/docs/images/visPatScreenshot.png differ diff --git a/docs/includes/constrtable.rst b/docs/includes/constrtable.rst new file mode 100644 index 000000000..b6d2b20e3 --- /dev/null +++ b/docs/includes/constrtable.rst @@ -0,0 +1,18 @@ +.. list-table:: + :header-rows: 1 + + * - Constraint + - Class + - Description + * - **Min/Max Dose** + - :class:`DoseConstraints.matRad_MinMaxDose` + - Keeps dose above and below the set minimum and maximum dose. Can use a LogSumExp Approximation or a voxel-wise constraint. + * - **Min/Max Mean Dose** + - :class:`DoseConstraints.matRad_MinMaxMeanDose` + - Keeps the mean dose above and below the set minimum and maximum mean dose. + * - **Min/Max EUD** + - :class:`DoseConstraints.matRad_MinMaxEUD` + - Keeps the EUD above and below the set minimum and maximum EUD. The EUD is calculated using the same method as for the EUD objective. + * - **min/max DVH** + - :class:`DoseConstraints.matRad_MinMaxDVH` + - Keeps the dose volume histogram above and below the set minimum and maximum *volunme* for a given dose level. \ No newline at end of file diff --git a/docs/includes/logo.rst b/docs/includes/logo.rst new file mode 100644 index 000000000..1ca9f6f2f --- /dev/null +++ b/docs/includes/logo.rst @@ -0,0 +1,23 @@ +.. |matRad_logo_header| image:: /../matRad/gfx/matRad_logo.png + :width: 110 px + :alt: matRad + :target: https://www.matRad.org + :class: matrad-header + +.. |matRad_logo_header2| image:: /../matRad/gfx/matRad_logo.png + :width: 93 px + :alt: matRad + :target: https://www.matRad.org + :class: matrad-header + +.. |matRad_logo_header3| image:: /../matRad/gfx/matRad_logo.png + :width: 75 px + :alt: matRad + :target: https://www.matRad.org + :class: matrad-header + +.. |matRad_logo| image:: /../matRad/gfx/matRad_logo.png + :width: 60 px + :alt: matRad + :target: https://www.matRad.org + :class: matrad-text-logo \ No newline at end of file diff --git a/docs/includes/objtable.rst b/docs/includes/objtable.rst new file mode 100644 index 000000000..37d5f3e0f --- /dev/null +++ b/docs/includes/objtable.rst @@ -0,0 +1,25 @@ +.. list-table:: + :header-rows: 1 + + * - Objective + - Class + - Effect on the objective function + * - **square underdosing** + - :class:`DoseObjectives.matRad_SquaredUnderdosing` + - Only dose values lower than the threshold dose for this VOI are considered for the objective function. The deviations are squared, multiplied with the penalty factor and added to the objective function value. The penalty is normalized to the number of voxels per VOI. + * - **square overdosing** + - :class:`DoseObjectives.matRad_SquaredOverdosing` + - Only dose values larger than the threshold dose for this VOI are considered for the objective function. The deviations are squared, multiplied with the penalty factor and added to the objective function value. The penalty is normalized to the number of voxels per VOI. + * - **square deviation** + - :class:`DoseObjectives.matRad_SquaredDeviation` + - All deviation from a reference dose for this VOI are considered for the objective function. All deviations are squared, multiplied with the penalty factor and added to the objective function value. The penalty is normalized to the number of voxels per VOI. + * - **mean** + - :class:`DoseObjectives.matRad_MeanDose` + - All dose values inside this VOI are weighted with the specified, and normalized (see above), penalty factor and added to the objective function value. + * - **EUD** + - :class:`DoseObjectives.matRad_EUD` + - EUD is the abbreviation for equivalent uniform dose. For this method a weighting factor and an exponent *a* have to be defined. For the calculation of the objective function value the dose in each voxel is taken to the power of *a*. Then the sum of all these values is taken (Σ D\ :sub:`i`\ :sup:`a`) and divided by the number of voxels. The *a*-th root of this value is then multiplied with the weighting factor and added to the objective function value. + * - **Min/max DVH objective** + - :class:`DoseObjectives.matRad_MaxDVHObjective` + :class:`DoseObjectives.matRad_MinDVHObjective` + - Only deviations from the reference dose over/under a reference volume are considered according to `Wu & Mohan (2000 Medical Physics) `_. \ No newline at end of file diff --git a/docs/includes/planapi.rst b/docs/includes/planapi.rst new file mode 100644 index 000000000..ec7c21204 --- /dev/null +++ b/docs/includes/planapi.rst @@ -0,0 +1,30 @@ +.. list-table:: Properties and their corresponding API functions + :header-rows: 1 + :widths: 20 20 20 20 20 + + * - Plan property + - API function + - Description + - ID + - Folder + * - ``propStf`` + - :func:`matRad_generateStf` + - Create beam Geometry + - generator + - :mod:`matRad.steering` + * - ``propDoseCalc`` + - :func:`matRad_calcDoseInfluence` + :func:`matRad_calcDoseForward` + - Calculate dose matrix / distribution + - engine + - :mod:`matRad.doseCalc` + * - ``propOpt`` + - :func:`matRad_fluenceOptimization` + - Optimization of beam fluences + - problem + - :mod:`matRad.optimization` + * - ``propSeq`` + - :func:`matRad_sequencing` + - Sequencing of beams + - sequencer + - :mod:`matRad.sequencing` \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 000000000..dd8557efb --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,109 @@ +.. include:: includes/logo.rst + +.. |icon_about| image:: images/aboutIconThick2(klein).png + :target: `about` + +.. |icon_setup| image:: images/setupIcon(klein).png + +.. |icon_techdoc| image:: images/technicalDocumentationIcon(klein).png + :target: `techdoc` + +.. _Home: + +==== +Home +==== + +.. image:: https://img.shields.io/github/v/release/e0404/matRad + :target: https://github.com/e0404/matRad/releases + :alt: Current Release + +.. image:: https://img.shields.io/github/downloads/e0404/matRad/total + :target: https://github.com/e0404/matRad/releases + :alt: Downloads + +.. image:: https://img.shields.io/github/contributors/e0404/matRad + :target: https://github.com/e0404/matRad/graphs/contributors + :alt: Contributors + +.. image:: https://img.shields.io/endpoint?url=https%3A%2F%2Fapi.juleskreuer.eu%2Fcitation-badge.php%3Fshield%26doi%3D10.1002%2Fmp.12251&style=flat&color=blue + :alt: Citations + +.. image:: https://github.com/e0404/matRad/actions/workflows/tests.yml/badge.svg + :target: https://github.com/e0404/matRad/actions/workflows/tests.yml + :alt: GitHub Build Status + +.. image:: https://codecov.io/gh/e0404/matRad/graph/badge.svg?token=xQhUQLu4FK + :target: https://codecov.io/gh/e0404/matRad + :alt: codecov + + +Welcome to the |matRad_logo_header2| documentation +-------------------------------------------------- + +**Date**: |today| **Version**: |version| + +|matRad_logo| is an open source software for radiation treatment planning of intensity-modulated photon, proton, helium and carbon ion therapy, with experimental modules for brachytherapy and very-high energy electrons (VHEE). +|matRad_logo| is developed for educational and research purposes; it is entirely written in MATLAB. + +This guide is the main source of information for users working with, and developers contributing to matRad. +If you do not already have a local copy of |matRad_logo| or you want to know how to get started, have a look at the Quick Setup Guide. +For detailed technical information about |matRad_logo| and its functions please check out the Technical Documentation. + +Please send us an email if you want to receive the |matRad_logo| newsletter that informs about the latest developments on an irregular basis. + +Quick Navigation +^^^^^^^^^^^^^^^^ + ++----------------+--------------------------------+-----------------+ +| :ref:`about` | :ref:`Quick Setup ` | :ref:`techdoc` | ++================+================================+=================+ +| |icon_about| | |icon_setup| | |icon_techdoc| | ++----------------+--------------------------------+-----------------+ + +|matRad_logo_header3| Webinars +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you like video lectures and tutorials, you can also have a look at the recording of the |matRad_logo| webinar at the brown bag medical physics seminar at Massachusetts General hospital on `Youtube `_. + +.. youtube:: 40_n7BIqLdw + +Centers using |matRad_logo_header3| +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +On `Google Maps `_ we are maintaining a `list of matRad user groups `_. +Please let us know if we can include your center, or if you have discontinued the usage of |matRad_logo| so we can remove you from the list. + +.. toctree:: + :maxdepth: 4 + :caption: Overview + :glob: + :includehidden: + + overview/about + overview/cite + overview/faq + Changelog + +.. toctree:: + :hidden: + + genindex + +.. toctree:: + :maxdepth: 4 + :caption: Setting Up + :includehidden: + + setup/requirements + setup/download + quickstart/index + + +.. toctree:: + :maxdepth: 6 + :caption: Technical Documentation + + guide/index + api/index + mat-modindex diff --git a/docs/overview/about.rst b/docs/overview/about.rst new file mode 100644 index 000000000..3ccfb003e --- /dev/null +++ b/docs/overview/about.rst @@ -0,0 +1,46 @@ +.. include:: ../includes/logo.rst + +.. _about: + +========================== +About |matRad_logo_header| +========================== + +matRad is an open source software for radiation treatment planning of intensity-modulated photon, proton, and carbon ion therapy. matRad is developed for educational and research purposes. It is entirely written in `MATLAB `_. + +This wiki is the main source of information for users working with, and developers contributing to matRad. If you do not already have a local copy of matRad or you want to know how to get started, have a look at the `Quick Start Guide `_. For detailed technical information about matRad and its functions please check out the `Technical Documentation `_. + +Please send us an `email `_ if you want to receive the matRad newsletter that informs about the latest developments on an irregular basis. + +Features +-------- + +matRad comprises: + +- MATLAB functions to model the entire treatment planning workflow +- Example patient data +- Physical and biological base data for all required computations + +In particular, we provide functionalities for: + +- Ray tracing +- Photon dose calculation +- Proton dose calculation (constant + variable RBE modeling) +- Carbon/helium ion dose calculation (including 3D RBE modeling) +- Inverse planning (based on physical dose and biological effect) +- Multileaf collimator sequencing +- Basic treatment plan visualization and evaluation + +matRad is constantly evolving. If you are looking for a special feature, do not hesitate and `get in touch `_. + +Contact +------- + +If you have any questions or wish to contribute to the development of matRad, you can either write an email to `contact@matrad.org `_, use the GitHub functionalities to file a new issue, or directly fork the matRad repository and send pull requests with your own custom developments. What are you waiting for? + +Development team +---------------- + +matRad development is driven by the research group `Radiotherapy Optimization `_ within the `Division of Medical Physics in Radiation Oncology `_ at the `German Cancer Research Center DKFZ `_ in `Heidelberg `_. + +We are always looking for beta testers that can provide external feedback on our code and developers that would like to take an active role in the future. Do not hesitate and `get in touch `__. diff --git a/docs/overview/cite.rst b/docs/overview/cite.rst new file mode 100644 index 000000000..123396dc9 --- /dev/null +++ b/docs/overview/cite.rst @@ -0,0 +1,137 @@ +.. _cite: + +=================== +How to cite matRad? +=================== + +Whenever you use matRad, we kindly ask yuo to cite our publications and/or the software (with appropriate version) itself. +This helps us to keep the project alive and to continue the development of matRad. + +.. _citepubs: + +Cite our publications +--------------------- + +.. image:: https://img.shields.io/endpoint?url=https%3A%2F%2Fapi.juleskreuer.eu%2Fcitation-badge.php%3Fshield%26doi%3D10.1002%2Fmp.12251&style=flat&color=blue + :alt: Citation Badge + + +The main citation for matRad is `Development of the open-source dose calculation and optimization toolkit matRad `_, which details the development process of matRad and provides a detailed evaluation of the dose calculation and optimization engine. + +.. collapse:: matRad main publication - BibTeX + + .. code-block:: bibtex + + @article{matRad2017, + title = {Development of the open-source dose calculation and optimization toolkit {{matRad}}}, + author = {Wieser, Hans-Peter and Cisternas, Eduardo and Wahl, Niklas and Ulrich, Silke and Stadler, Alexander and Mescher, Henning and M{\"u}ller, Lucas-Raphael and Klinge, Thomas and Gabrys, Hubert and Burigo, Lucas and Mairani, Andrea and Ecker, Swantje and Ackermann, Benjamin and Ellerbrock, Malte and Parodi, Katia and J{\"a}kel, Oliver and Bangert, Mark}, + year = {2017}, + journal = {Medical Physics}, + volume = {44}, + number = {6}, + pages = {2556--2568}, + issn = {2473-4209}, + doi = {10.1002/mp.12251}, + } + +---- + +The first paper about matRad was submitted to the `2015 World Congress on Medical Physics & Biomedical Engineering `_ by Eduardo Cisternas Jimenéz: +`matRad – a multi-modality open source 3D treatment planning toolkit `_. + +.. collapse:: matRad conference paper - BibTeX + + .. code-block:: bibtex + + @inproceedings{Cisternas2015a, + title = {{{matRad}} - a multi-modality open source {{3D}} treatment planning toolkit}, + booktitle = {World {{Congress}} on {{Medical Physics}} and {{Biomedical Engineering}}, {{June}} 7-12, 2015, {{Toronto}}, {{Canada}}}, + author = {Cisternas, Eduardo and Mairani, Andrea and Ziegenhein, Peter and J{\"a}kel, Oliver and Bangert, Mark}, + editor = {Jaffray, David A.}, + year = {2015}, + series = {{{IFMBE Proceedings}}}, + pages = {1608--1611}, + publisher = {Springer International Publishing}, + abstract = {We present matRad, an open source software for three-dimensional radiation treatment planning of intensitymodulated photon, proton, and carbon ion therapy. matRad is developed for educational and research purposes; it is entirely written in MATLAB. A first beta release is available for download. The toolkit features a highly modular design with a set of individual functions modeling the entire treatment planning workflow based on a segmented patient CT. All algorithms, e.g. for ray tracing, photon/proton/carbon dose calculation, fluence optimization, and multileaf collimator sequencing, follow well-established approaches and operate on clinically adequate voxel and bixel resolution. Patient data as well as base data for all required computations is included in matRad. We achieve computation times of 60-100s (60-400s) for realistic patient cases including photon (particle) dose calculation and fluence optimization. Memory consumption ranges between 0.2GB and 2.2GB. Dose distributions of a treatment planning study for a phantom and prostate patient case considering multiple radiation modalities are shown. Both the computational and dosimetric results encourage a future use of matRad in an educational and scientific setting.}, + isbn = {978-3-319-19387-8}, + langid = {english}, + keywords = {matRadGrantOther,open source software,particle therapy,Radiation therapy,treatment planning} + } + + +---- + +In addition, a joint open-source paper about matRad focusing on educational purposes can be found at the following link: +`matRad - An open-source treatment planning toolkit for educational purposes `_. + +.. collapse:: matRad educational publication - BibTeX + + .. code-block:: bibtex + + @article{Wieser2018b, + title = {{{matRad}} - an open-source treatment planning toolkit for educational purposes}, + author = {Wieser, Hans-Peter and Wahl, Niklas and Gabry{\'s}, Hubert S. and M{\"u}ller, Lucas-Raphael and Pezzano, Giuseppe and Winter, Johanna and Ulrich, Silke and Burigo, Lucas Noberto and J{\"a}kel, Oliver and Bangert, Mark}, + year = {2018}, + journal = {Medical Physics International Journal}, + volume = {6}, + number = {1}, + pages = {119--127}, + issn = {2306-4609}, + langid = {english} + } + +---- + +.. _citesoftware: + +Direct Software Citation (zenodo) +--------------------------------- + +You can also directly cite the software through its Publication on `Zenodo with a citable DOI `_. + +You can either cite the generic DOI which will always resolve to the latest release of matRad: `10.5281/zenodo.3879615 `_: + +.. image:: https://zenodo.org/badge/doi/10.5281/zenodo.3879615.svg + :target: https://doi.org/10.5281/zenodo.3879615 + :alt: DOI + +Alternatively, you can cite the specific version you are using, e.g. 3.1.0 with DOI: `10.5281/zenodo.14181851 `_: + +.. image:: https://zenodo.org/badge/29671667.svg + :target: https://zenodo.org/badge/latestdoi/29671667 + :alt: DOI + +The respective Zenodo entry gives you an overview over all available versions to cite. Below you find example BibTeX and BibLaTex (with a few more configuration options than vanilla BibTeX) entries for the latest release 3.1.0. + +.. collapse:: matRad software publication (Release 3.1.0) - BibTeX + + .. code-block:: bibtex + + @misc{matRad310, + title = {{matRad (v3.1.0)}}, + author = {Abbani, Nelly and {Al-Hasnawi}, Nabe and Ackermann, Benjamin and Bangert, Mark and Becher, Tobias and Bennan, Amit Ben Antony and Burigo, Lucas and Cabal, Gonzalo and Cisternas, Eduardo and Charton, Louis and Christiansen, Eric and Cristoforetti, Remo and Dallas, Marios and Doerner, Edgardo and Ecker, Swantje and Ellerbrock, Malte and Facchiano, Simona and Gabry{\'s}, Hubert and Handrack, Josefine and Hardt, Jennifer and Heath, Emily and Hermann, Cindy and Homolka, Noa and Ibragim, Raed and J{\"a}ger, Fabian and J{\"a}kel, Oliver and {Hueso-Gonz{\'a}lez}, Fernando and Khaledi, Navid and Klinge, Thomas and Kunz, Jeremias and Mairani, Andrea and Meder, Paul Anton and Mescher, Henning and M{\"u}ller, Lucas-Raphael and Neishabouri, Ahmad and Palkowitsch, Martina and Parodi, Katia and Pezzano, Giuseppe and Ramirez, Daniel and Sarnighausen, Claus and Scholz, Carsten and Sevilla, Camilo and Stadler, Alexander and Ulrich, Silke and Titt, Uwe and Wahl, Niklas and Welsch, Jona and Wieser, Hans-Peter and Winter, Johanna and Xu, Tong}, + year = {2024}, + month = nov, + address = {Heidelberg}, + doi = {10.5281/zenodo.14181851}, + howpublished = {Deutsches Krebsforschungszentrum} + } + +| + +.. collapse:: matRad software publication (Release 3.1.0) - BibLaTeX + + .. code-block:: bibtex + + @software{matRad310, + title = {{matRad}}, + author = {Abbani, Nelly and Al-Hasnawi, Nabe and Ackermann, Benjamin and Bangert, Mark and Becher, Tobias and Bennan, Amit Ben Antony and Burigo, Lucas and Cabal, Gonzalo and Cisternas, Eduardo and Charton, Louis and Christiansen, Eric and Cristoforetti, Remo and Dallas, Marios and Doerner, Edgardo and Ecker, Swantje and Ellerbrock, Malte and Facchiano, Simona and Gabryś, Hubert and Handrack, Josefine and Hardt, Jennifer and Heath, Emily and Hermann, Cindy and Homolka, Noa and Ibragim, Raed and Jäger, Fabian and Jäkel, Oliver and Hueso-González, Fernando and Khaledi, Navid and Klinge, Thomas and Kunz, Jeremias and Mairani, Andrea and Meder, Paul Anton and Mescher, Henning and Müller, Lucas-Raphael and Neishabouri, Ahmad and Palkowitsch, Martina and Parodi, Katia and Pezzano, Giuseppe and Ramirez, Daniel and Sarnighausen, Claus and Scholz, Carsten and Sevilla, Camilo and Stadler, Alexander and Ulrich, Silke and Titt, Uwe and Wahl, Niklas and Welsch, Jona and Wieser, Hans-Peter and Winter, Johanna and Xu, Tong}, + date = {2024-11}, + location = {Heidelberg}, + doi = {10.5281/zenodo.14181851}, + url = {https://doi.org/10.5281/zenodo.14181851}, + organization = {Deutsches Krebsforschungszentrum}, + version = {3.1.0} + } + +| diff --git a/docs/overview/faq.rst b/docs/overview/faq.rst new file mode 100644 index 000000000..2e58206e2 --- /dev/null +++ b/docs/overview/faq.rst @@ -0,0 +1,55 @@ +.. _faq: + +.. toctree:: + :maxdepth: 2 + +========================== +Frequently Asked Questions +========================== + +While this site attempts to cover a set of frequently asked questions, it is not exhaustive. +We suggest to visit our `GitHub Discussion Forum `_ for Questions and Answers from the Community. + +.. admonition:: How to cite matRad? + :class: dropdown + + If you use matRad, please consider :ref:`citing our publications ` as well as :ref:`the software version ` you are using directly. + +.. admonition:: Can I run matRad without a MATLAB installation? + :class: dropdown + + Yes, if you only need the graphical user interface you can also use the compiled standalone. + If you need to work with the code, check our :ref:`guide ` on running matRad with `GNU Octave `_ + +.. admonition:: How can I model custom particle machines in matRad? + :class: dropdown + + You need to provide your own base data file according to our examples (*proton_Generic.mat*, *carbon_Generic.mat*). More information about the format is summarized on :ref:`this page `. + + +.. admonition:: How can I use a custom HLUT table in matRad? + :class: dropdown + + matRad has the following procedure for reading HU lookup tables via `matRad_loadHLUT.m `_ during the dose calculation: + * matRad first checks if there is any `.hlut` file provided by the user in the `hlutlibrary `_ directory, with the following naming convention: MANUFACTURER-MODEL-ConvolutionKernel_CONVOLUTIONKERNEL_RADIATIONMODALITY.hlut (e.g. Philips-AcQSimCT-ConvolutionKernel-000000_protons.hlut). + * If there is no file with this name available, matRad would use its own default lookup table with the following naming convention: matRad_default_RADIATIONMODALITY.hlut (e.g. `matRad_default.hlut `_). This file comprises two columns, first is the HU units and second is the corresponding electron density/stopping power (comments indicated by starting a line with "#" will be omitted). + + Generating your own custom HLUT table can therefore be done in two ways, either making a custom file with the mentioned naming convention or changing the default tables provided by matRad (not recommended). + +.. admonition:: Why is the matRad GUI slow (macOS)? + :class: dropdown + + This issue may be caused by conflicts between MATLAB and window-snapping apps on macOS (Rectangle, BetterTouchTool, Magnet etc.). + May be remedied by quitting the app or disabling the "Window Snapping " feature then restarting MATLAB. + For further information refer to : + * `matRad Issue #550 _` + * `MATLAB Answers thread _` + +.. admonition:: Why are my constraints not being met and coverage is always bad? + :class: dropdown + + The most common reason for this is a stark mismatch between the dose calculation grid and the resolution of the CT. + When dose calculation and optimization are running on a dose grid, matRad will still resample the dose to the original CT grid for evaluation. + While optimization itself will fulfil all constraints on the dose grid (where downsampled structures cover different volumes at the boundaries), + the result will be compromised when resampling to the CT grid, and the optimizer will never be able to meet the constraints on the CT grid. + To solve this, make sure to use a dose grid with a resolution that is close to the CT resolution, and to use the same grid for evaluation as well. diff --git a/docs/quickstart/guiintro.rst b/docs/quickstart/guiintro.rst new file mode 100644 index 000000000..d6b0382a4 --- /dev/null +++ b/docs/quickstart/guiintro.rst @@ -0,0 +1,135 @@ +.. _run_gui: + +#################################### +Running the graphical user interface +#################################### + +To execute the matRad GUI using MATLAB you need to: + +.. contents:: + :local: + :depth: 2 + +If you prefer to use the :file:`matRad.m` script to execute matRad, check out the :ref:`matRad script `. +For more detailed information about the different features of the GUI you can take a look at :ref:`matRad GUI Overview `. + +Step 1: Open matRad folder in MATLAB +------------------------------------ + +To use matRad you need to open the matRad folder in MATLAB. +Open MATLAB and navigate to the location of the files, if you have cloned the repository it is most likely located in your local Github folder. + +Inside the root folder of the matRad repository there are only a few files, of which the most important ones are:: + +* :func:`matRad_rc.m ` - the matRad configuration script setting path and environment +* :scpt:`matRad.m ` - the main introductory script to run matRad workflow +* :func:`matRadGUI.m ` - the main script to run the matRad GUI + +The function to run the matRad GUI is called :func:`matRadGUI`, which instantiates the :class:`matRad_MainGUI`. + +Step 2: Start the matRad GUI +---------------------------- + +To start the GUI select :file:`matRadGUI.m` from your current folder and run it (right-click → run or F9) or simply type ``matRadGUI`` in your command window. +Now the empty GUI should be opened: + +.. image:: /images/GUI-Guide_emptyGUIScreenshot.png + :width: 650px + +If the GUI is not empty, then there is a patient already loaded in your workspace. To get an empty GUI you can clear your workspace and restart the GUI. However, this is not necessary as you can simply load a new patient. + +Step 3: Execute treatment planning +---------------------------------- + +**Load patient data** + +First, you need to load the patient data. Therefore, the matRad release contains the :doc:`../datastructures/cort`. To import additional patient data have a look at the :doc:`../guide/dicomimport`. +To load a patient click the **Load \*.mat data** button in the **Workflow** section. +A window should open. In the folder ``phantoms``, you can find different patient files. + +.. image:: /images/GUI-Guide_loadDataGUIScreenshot.png + +Here you can select which patient file (``*.mat``) you want to load. Upon opening the ``*.mat`` file the patient data is loaded into the GUI: +On the right side of the GUI you should see the patient-CT with the defined VOIs. On the left side, the optimization parameter table should now be filled. + +.. image:: /images/GUI-Guide_loadedGUIScreenshot.png + :width: 650px + +**Set plan parameters** + +Now you can start to adjust the plan parameters: + ++-----------------------------------------------------------------------------------------------+ +| The bixel width, as well as the isocenter, can be adjusted but should already be set to | +| reasonable values. | ++-----------------------------------------------------------------------------------------------+ +| To set the beam directions you have to select the according gantry and couch angles. Every | +| gantry angle defines a beam and needs a couch angle. | ++-----------------------------------------------------------------------------------------------+ +| For the radiation mode, you can choose photons, protons or carbon. | ++-----------------------------------------------------------------------------------------------+ +| If you set carbon as radiation mode, you can activate the biological optimization. You can | +| choose between an effect based optimization (*effect*) or the optimization of the RBE-weighted| +| dose (*RBExD*). | ++-----------------------------------------------------------------------------------------------+ +| For the radiation mode "photons", you have the option to run a MLC sequencing, where you can | +| set the number of stratification levels and additionally you can run a direct aperture | +| optimization. | ++-----------------------------------------------------------------------------------------------+ + +.. image:: /images/GUI-Guide_planParametersGUIScreenshot.png + +**Set optimization parameters** + +The optimization parameters are used to influence the outcome of the fluence optimization. Here you can set the parameters of the VOIs (e.g. min/max dose, penalty, overlap priority, etc.). For more information, take a look at the :doc:`../datastructures/cst`. Using the '**+**' and '**-**' buttons you can add and remove VOIs. + +The column ``p`` (*penalty*) determines the relative weighting of the objective within the overall weighted sum objective function. The column ``Parameters`` lets you specify additional parameters for given objectives. For squared over- and underdosage as well as squared deviation, this simply corresponds to the reference dose level, for EUD it is the exponent. A mean dose objective does not require an additional parameter. + +The column ``OP`` (*overlap priority*) is very important as it determines the assignment of voxels to structures. Consider a voxel that belongs to two structures, e.g. to the rectum and to the prostate. For the optimizer it is necessary to distinguish to which structure the voxel should belong to during optimization. If you assign priority 1 to the prostate and priority 2 to the rectum in our example, every voxel within the overlap of the two structures will be considered as prostate during optimization. If you assign priority 2 to the prostate and priority 1 to the rectum in our example, every voxel within the overlap of the two structures will be considered as rectum during optimization. Assigning the same priority to overlapping structures will result in the overlapping volume being considered for all structures. Be aware that the skin contour usually encompasses the entire patient volume. If you want to make sure that target voxels are not also considered skin voxels you need to assign a lower priority (i.e. a higher number) to the skin volume. Please check with the provided example patient data to understand this in full detail. + +*Note: Changing the VOI Type from OAR to target will lead to additional beamlets or spots that need to be considered for the dose-influence-matrix calculation. As a result, these changes have to be done before the Dij-calculation.* + +.. image:: /images/GUI-Guide_optimizationParametersGUIScreenshot.png + +**Calculate Dose influence matrix** + +To start the calculation of the dose-influence-matrix you simply need to click the **Calc. Dose Influence** button in the workflow: + +.. image:: /images/GUI-Guide_workflowGUIScreenshot.png + +You should see a window pop up, showing a progress bar of the calculation: + +.. image:: /images/GUI-Guide_dijProgressBarScreenshot.png + +In addition, the progress is displayed in the Command Window: + +.. image:: /images/GUI-Guide_dijOutputScreenshot.png + +**Execute fluence optimization** + +Once the dose calculation is completed, you can start the fluence optimization by clicking the **Optimize** button in the workflow section. The iterations of the optimization are displayed in the Command Window: + +.. image:: /images/GUI-Guide_fluenceOptOutputScreenshot.png + +To adjust the convergence criteria you can specify the *maximum number of iterations* and the *convergence* precision in the *Optimization Parameter* section. Default values are: 1000 iterations and a precision of :math:`10^{-3}`: +(Precision ≡ \|(FuncValue_old − FuncValue_new) / FuncValue_old\|) + +.. image:: /images/GUI-Guide_optimizationParameters2.png + +Step 4: Visualize resulting treatment plan +------------------------------------------ + +Once the fluence optimization has converged the resulting dose distribution will be displayed in the GUI. Here you can adjust the visualization parameters to display different slices/planes, use different plot types, etc. + +.. image:: /images/GUI-Guide_optimizedGUIScreenshot.png + :width: 650px + +To calculate a DVH of all VOIs and to see the quality indicators (which contain the mean/max/min dose for each VOI) you can use the **Show DVH/QI** button in the *Visualization* section. + +.. image:: /images/DVHVisScreenshot.png + + +Step 5: Import additional patient data +-------------------------------------- + +matRad supports the import of patient data stored in the DICOM format. A set of functions designed for this purpose can be found in the subfolder :file:`dicom `. For more information about the usage of the import functions please check out :doc:`../guide/dicomimport`. diff --git a/docs/quickstart/index.rst b/docs/quickstart/index.rst new file mode 100644 index 000000000..100af51fe --- /dev/null +++ b/docs/quickstart/index.rst @@ -0,0 +1,71 @@ +.. |matRad_logo| image:: ../../matRad/gfx/matRad_logo.png + :width: 80 px + :alt: matRad + :target: https://www.matRad.org + +.. _quickstart: + +Quick Start +=========== + +It's the first time you want to use matRad? + +First, get a local copy of matRad by download or git cloning. Having done that, we recommend you navigate into the folder in Matlab and execute + +.. code-block:: matlab + + matRad_rc + +which will setup the path & configuration and tell you the current version. + +Then there're three options for a pleasant start with matRad. Choose one or try out each of them. + + +.. rubric:: Option 1: Using the GUI + :heading-level: 2 + + +For an intuitive workflow with the graphical user interface, type + +.. code-block:: matlab + + matRadGUI + +in your command window. An empty GUI should be opened. Click the *Load.mat* data-Button in the Workflow-section to load a patient. Set the plan and optimization parameters, calculate the dose influence matrix and execute the fluence optimization in the GUI. + +.. rubric:: Option 2: Using the main script + :heading-level: 2 + +If you prefer scripting, open the default script *matRad.m* from the main matRad folder + +.. code-block:: matlab + + edit matRad.m + +Use it to learn something about the code structure and execute it section by section. + +You can also run the full script for an example photon plan by just typing + +.. code-block:: matlab + + matRad + +in your command window. + +.. rubric:: Option 3: Using the examples + :heading-level: 2 + +The most time consuming but also most educational approach to matRad. + +When in the main matRad folder, navigate to the folder *examples*. Open one of the examples given there. Execute it section by section. Move on to the next example afterwards. + +**See also:** + +.. toctree:: + :maxdepth: 1 + + guiintro + matradscript + +.. + diff --git a/docs/quickstart/matradscript.rst b/docs/quickstart/matradscript.rst new file mode 100644 index 000000000..0df553978 --- /dev/null +++ b/docs/quickstart/matradscript.rst @@ -0,0 +1,215 @@ +.. _run_script: + +######################### +Using the matRad.m script +######################### + +To execute the matRad script using MATLAB you need to: + +1. `Open matRad folder in MATLAB`_ +2. `Set patient-specific parameters`_ +3. `Execute inverse planning`_ +4. `Import additional patient data`_ + +If you prefer to only use the GUI to execute matRad, check out the :doc:`guiintro`. + +.. _Open matRad folder in MATLAB: + +Step 1: Open matRad folder in MATLAB +==================================== + +To use matRad you need to open the matRad folder in MATLAB. +Open MATLAB and navigate to the location of the files; if you have cloned the repository, it is most likely located in your local Github folder (e.g. ``C:\Users\username\Documents\GitHub\matRad``). + +Inside the matRad folder there are several MATLAB functions used to run matRad, named ``matRad*.m``, and ``*.mat`` files containing base data and exemplary patient data sets. +The main script to run matRad is called `matRad.m `_. It can be executed section by section. + +.. tip:: + + Editing the matrad.m file is a good starting point, but if you do not want to mess up the original file and git status, you can copy and paste it into userdata/scripts. The userdata folder is ignored by git, so you can place your scripts and data in there without affecting the versioned source code. + +.. _Set patient-specific parameters: + +Step 2: Set patient-specific parameters +======================================= + +In the first section, the patient specific parameters have to be set (see :ref:`the parameters screenshot `): + +1. `Which patient (data) should be loaded`_ +2. `Which beam angles should be used`_ +3. `Which radiation mode should be used`_ + +.. _Which patient (data) should be loaded: + +1. Selecting a patient +---------------------- + +Lines 20-24 in the :ref:`the parameters screenshot ` show the patient data sets available by default. Un-comment the data set you wish to use. The dose parameters for the different volumes (min. dose, max. dose, penalties) are set within the patient data set :ref:`cst cell array `. If you wish, you can adjust these parameters before executing matRad. + +.. _Which beam angles should be used: + +2. Selecting beam angles +------------------------ + +Lines 35-36 in the :ref:`the parameters screenshot ` are used to set the gantry and couch angles. Here you can set any angles from 0-359°. Make sure that you always create pairs of gantry and couch angles; otherwise, you won't be able to execute the inverse planning! + +.. _Which radiation mode should be used: + +3. Selecting radiation mode +--------------------------- + +The radiation mode can be set in line 28 in the :ref:`the parameters screenshot `. You can choose between photons, protons and carbon. + +If you decide to use protons or carbon, it is possible to set the lateral spot spacing (line 34). When using carbon, you can also choose between a physical optimization (``'none'``), an optimization of the biological effect (``'effect'``) or an optimization of the RBE-weighted dose (``'RBExD'``) by adjusting the parameter ``pln.propOpt.bioOptimization`` in line 47. + +In case you choose photons, it is possible to run an additional MLC sequencing by setting ``pln.propOpt.runSequencing`` (line 50) and direct aperture optimization is accessible through ``pln.propOpt.runDAO`` (line 49). + +The desired number of fractions can be set in line 31 in the :ref:`the parameters screenshot `. + +The other parameters set in this section are generated automatically and should not be changed. + +.. _parametersScreenshot: + +Screenshot of the parameters section: + +.. image:: /images/parametersScreenshot.png + +.. _Execute inverse planning: + +Step 3: Execute inverse planning +================================= + +The `matRad.m `_ script can now be executed step by step: + +1. `Load settings`_ +2. `Initial visualization`_ +3. `Generate steering file`_ +4. `Dose calculation`_ +5. `Inverse planning for IMRT`_ +6. :ref:`Sequencing ` +7. `Direct aperture optimization`_ +8. `Visualization of the resulting treatment plan`_ +9. `Show DVH and quality indicators`_ + +To evaluate a single section, you have to "activate" it (*Left-click* inside section) and then use *ctrl + enter* or use *Right-click* → *Evaluate Current Section*. + +.. _Load settings: + +1. Load settings +---------------- + +Now you can execute the first section. You should see, among others, the variables ``cst``, ``ct`` and ``pln`` in your Workspace. + +.. image:: /images/parametersLoadedScreenshot.png + :width: 300px + +.. _Initial visualization: + +2. Initial visualization +------------------------ + +After the patient data is loaded, you can execute the second section to open the GUI: + +.. image:: /images/executeGUIScreenshot.png + +In the GUI you can view the patient CT, change the plan parameters and adjust the optimization parameters. + +.. image:: /images/GUI-Guide_loadedGUIScreenshot.png + :width: 650px + +The usage of the GUI is explained in more detail in the :doc:`guiintro`. Here we will focus on the "manual" execution of the matRad script. To "manually" change the optimization parameters, you can adjust the ``cst``-cell (see :ref:`cst cell array documentation ` for more information). + +.. _Generate steering file: + +3. Generate steering file +------------------------- + +In this section, the steering file ``stf`` is created and the matRad steering information is stored as a struct (see :ref:`stf struct ` for more information). + +.. image:: /images/STFScreenshot.png + +The Command Window should show the progress. + +.. image:: /images/calcSTFScreenshot.png + +.. _Dose calculation: + +4. Dose calculation +------------------- + +In this section, the dose influence matrix ``dij`` for the defined beam angles is calculated (see :ref:`dij struct ` for more information). + +.. image:: /images/doseCalcScreenshot.png + +Again, the progress should be shown in the Command Window. + +.. image:: /images/doseCalcProgScreenshot.png + +.. _Inverse planning for IMRT: + +5. Fluence optimization +----------------------- + +In this section, the fluence is optimized to find the bixel (*photons*) or spot (*protons/carbon*) weights minimizing the objective function. + +.. image:: /images/invPlanningScreenshot.png + +During this process, the current objective function value is displayed: + +.. image:: /images/invPlanningProgScreenshot.png + +.. _sequencing_step: + +6. Sequencing +------------- + +For photon IMRT the application of a multileaf collimator is necessary. By sequencing, the applicable dose distribution can be simulated. The fourth input of ``matRad_engelLeafSequencing(resultGUI,stf,dij,7)`` is the number of stratification levels. You can adjust this number to use the number of levels you want. + +.. image:: /images/sequencingScreenshot.png + +When the sequencing is finished, the `result struct `_ is updated. + +.. _Direct aperture optimization: + +7. Direct aperture optimization +------------------------------- + +For photon therapy, the multileaf collimator sequencing can be further refined by using an experimental gradient-based direct aperture optimization algorithm, where leaf settings and aperture intensities are optimized simultaneously. Further information including references about the direct aperture optimization algorithm can be found directly in the source code or in the technical documentation about the :ref:`fluence optimization `. + +.. image:: /images/daoScreenshot.png + +.. _Visualization of the resulting treatment plan: + +8. Visualization of the resulting treatment plan +------------------------------------------------ + +Now you can visualize the resulting treatment plan using the GUI. + +.. image:: /images/doseVisGUIScreenshot.png + +In the GUI you can see the resulting dose distribution for the calculated treatment plan. You can choose which plane and slice should be displayed. You can also display a dose profile plot by changing *Type of plot* from *intensity* to *profile*. +If you have chosen a biological optimization, then you have several parameters to be displayed (e.g. RBE-weighted dose, biological effect, α or β values). + +.. image:: /images/GUI-Guide_optimizedGUIScreenshot.png + :width: 650px + +.. _Show DVH and quality indicators: + +9. Show DVH and quality indicators +---------------------------------- + +In this section, the dose-volume histograms (DVH) are calculated and visualized. + +.. image:: /images/DVHScreenshot.png + +.. image:: /images/DVHVisScreenshot.png + :width: 650px + +The diagram shows the DVH and in the table, you see the mean, maximum and minimum dose for every VOI. Additionally, the dose and dose-volume coefficient for several confidence levels are displayed. + +.. _Import additional patient data: + +Step 4: Import additional patient data +====================================== + +matRad supports the import of patient data stored in the DICOM format. A set of functions designed for this purpose can be found in the subfolder `dicom `_. For more information about the usage of the import functions please check out the :ref:`dicom import page `. diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 000000000..cfd3c1de4 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,7 @@ +# Sphinx documentation requirements +sphinx>=8.0.0,<=8.2.3 +sphinx-rtd-theme>=3.1.0 +sphinx-toolbox +sphinx-togglebutton +sphinxcontrib-youtube +sphinxcontrib-matlabdomain>=0.22.1 diff --git a/docs/setup/download.rst b/docs/setup/download.rst new file mode 100644 index 000000000..30f52a666 --- /dev/null +++ b/docs/setup/download.rst @@ -0,0 +1,56 @@ +.. toctree:: + :maxdepth: 4 + :hidden: + + +.. include:: ../includes/logo.rst + +.. _settingup: + +Download and Install |matRad_logo_header| +========================================= + +To get |matRad_logo| have two options: + +1. Source Code with Matlab or Octave installation +------------------------------------------------- + +If you have MATLAB or Octave you can just :ref:`get a local copy of the source code `. Then you have to choose whether you want to :ref:`use the GUI ` or :ref:`execute the main script `. + +2. Standalone installation +-------------------------- + +If you do not have MATLAB installed on your PC you are restricted to use |matRad_logo|'s standalone application. For this option a MATLAB installation is not required. The installer is available for Windows, Mac and Linux `with the latest release `_. + +Steps for installation: + +1. Download the installer |matRad_logo| installer of the latest `Release `_ to your system (`Win64 `_, `Mac64 `_, `Linux64 `_) + +2. Run the respective installer for your system + + * **Windows**: Run the downloaded executable installer + * **Linux**: Run the executable install script. Make sure that the ``*.install`` file has executable permissions. + * **Mac**: Here we provide a dmg containing the installer (Since the installer is not Apple-certified, you might explicitly launch it from the terminal or by right-click and then open). + + After that, you should be guided through the installation process. + Note that the installers will want to download the "Matlab Runtime" from Mathworks in the process. The runtime is quite large (~2GB) and is required to run compiled deployed applications written in Matlab. + +3. Run |matRad_logo| + + * **Windows**: Just like with every other program, you should have a desktop icon. + * **Mac**: Per default |matRad_logo| will be installed to ``/Applications/matRad``. To run |matRad_logo| navigate to ``/Applications/matRad/applications`` and double click or right click -> open on the ``matRad.app`` application. The first startup might take a few seconds. + + .. note:: + An installation warning appears that |matRad_logo| is from an unverified developer. You can solve this issue by opening the installer from the context menu (depending on the configuration either Ctrl + click or right-click on the icon, and then click "Open" in the menu). You will then get the option to open the application in spite of the missing verification and thus to install |matRad_logo|. + + * **Linux & Mac**: To start |matRad_logo|, you can alternatively use the provided ``run_matRad.sh`` script from the terminal. It requires one argument which gives the path to the installed Matlab-Runtime. Refer to the ``readme_linux.txt`` and ``readme_mac.txt`` in your installation directory for more information. + +Patient/Phantom files +--------------------- + +The patient files should be included with the installer and will be installed into the desired location. For Windows, for example, they can be found within the "application" folder of the chosen installation directory. Moreover, we also provide extra links to the open source patient files stored in |matRad_logo|'s native ``*.mat`` format for a `head and neck case `_, a `liver case `_, a `prostate case `_, a `box phantom `_, and AAPM's `TG119 phantom `_. Otherwise you need to start with a `DICOM import of your own patient data `_. + + +An overview of the functions of the |matRad_logo| GUI can be found `here `_. + +If you want to import patient data, check out `the dicom import functionality `_. \ No newline at end of file diff --git a/docs/setup/requirements.rst b/docs/setup/requirements.rst new file mode 100644 index 000000000..441b205fd --- /dev/null +++ b/docs/setup/requirements.rst @@ -0,0 +1,58 @@ +.. toctree:: + :maxdepth: 4 + :hidden: + +.. include:: ../includes/logo.rst + +.. _requirements: + +=========================== +Minimum System Requirements +=========================== + +Software Environment +-------------------- + +You can either run |matRad_logo| with a local installation of the source code, which requires a MATLAB or Octave installation. +Or you can use the standalone application, which does not require a MATLAB installation but only exposes the graphical user interface. + +MATLAB +^^^^^^ + +We develop and test matRad mainly with MATLAB version **2022b and later**. Our automatic testing framework via GitHub Actions currently uses R2022b as well as the latest MATLAB version. If you run into a problem with these or any other version, please let us know, but do not expect us to try to stay compatible with all other versions. The main reason for limiting us to these MATLAB versions are incompatibilities in the mex interface to IPOPT (especially on Windows). Other incompatibilities often reveal themselves as missing functions (like ``isstring``, for example). + +MATLAB Toolboxes +^^^^^^^^^^^^^^^^ + +To run all dose calculation and optimization functionalities you will only need a basic MATLAB installation. For DICOM import and export, you also need the Image Processing Toolbox. Certain additional functionalities are available with the Optimization Toolbox (``fmincon``), the Parallel Computing Toolbox, and the Statistics & Machine Learning Toolbox. + +Octave +^^^^^^ + +Note that compatibility with Octave is not our primary goal, but it is also part of the automatic testing framework on GitHub Actions for Octave 6.4. + +Standalone +^^^^^^^^^^ + +The standalone is built for Windows, Linux, and Mac. Only the Windows standalone is currently regularly tested. +Linux and Mac users should be able to run the standalone, but third party tools like IPOPT, ompMC or MCsquare might not run reliably. +If you find bugs on your operating system, report them to us as `GitHub issue `_. + +Operating System +---------------- + +Since we work in the programming environment MATLAB, operating system incompatibilities are not that common. They may arise, in particular, when using mex interfaces. Our precompiled mex interfaces should work on Windows 10 & 11, Ubuntu 18.04 (and later), and MacOS High Sierra. Please let us know if you run into issues, but the first step should always be trying to compile the mex interfaces yourself. + +Especially on Mac, there might be substantial issues due to their annoying quarantine system, which prevents the execution of downloaded files. If you run into this problem, you need to remove the respective quarantine flags, especially from mex files you'd like to use. + +Hardware Requirements +--------------------- + +There are no hard minimum requirements to do dose calculation and optimization with matRad. We do treatment planning tutorials also with systems with 2GB RAM, but that means that the cases you are looking at are somewhat small (low spatial resolution, few beams, rather no particles). If you want to do treatment planning at realistic resolutions, we recommend 16GB RAM or more. + +If you run into memory problems, you have basically three options: + +* Buy more RAM ;) +* Import your data at low spatial resolution, which is possible during DICOM import. Remember that reducing the resolution by a factor of 2 will reduce memory consumption by a factor of 8! Alternatively or additionally, reduce the resolution of the dose calculation grid using the option ``pln.propDoseCalc.doseGrid.resolution``. +* Reduce the number of pencil-beams by choosing larger ``bixelWidth`` or, for particles only, the longitudinal spot spacing. +* Restrict the dose calculation area by specifying tighter lateral cut-off values in `matRad_calcParticleDose (line 211) `_ and `matRad_calcPhotonDose (line 61) `_, respectively. While this induces inaccuracy in the planning process, this might be a viable option for educational purposes. \ No newline at end of file diff --git a/examples/matRad_example10_4DphotonRobust.m b/examples/matRad_example10_4DphotonRobust.m index 479622758..aa088f4b5 100644 --- a/examples/matRad_example10_4DphotonRobust.m +++ b/examples/matRad_example10_4DphotonRobust.m @@ -2,7 +2,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2018 the matRad development team. +% Copyright 2018-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -175,8 +175,7 @@ pln.propStf.gantryAngles = [0 90]; pln.propStf.couchAngles = [0 0]; pln.propStf.bixelWidth = 5; -pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); -pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0); +pln.propStf.isoCenter = matRad_getIsoCenter(cst,ct,0); pln.propOpt.runDAO = 0; pln.propOpt.runSequencing = 0; diff --git a/examples/matRad_example11_helium.m b/examples/matRad_example11_helium.m index c02b2541b..98c17011d 100644 --- a/examples/matRad_example11_helium.m +++ b/examples/matRad_example11_helium.m @@ -2,7 +2,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2017 the matRad development team. +% Copyright 2017-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -53,8 +53,7 @@ pln.propStf.gantryAngles = [0]; pln.propStf.couchAngles = [0]; pln.propStf.bixelWidth = 5; -pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); -pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0); +pln.propStf.isoCenter = matRad_getIsoCenter(cst,ct,0); pln.propOpt.runDAO = 0; pln.propOpt.runSequencing = 0; diff --git a/examples/matRad_example12_simpleParticleMonteCarlo.m b/examples/matRad_example12_simpleParticleMonteCarlo.m index a85c35789..99f4dadb4 100644 --- a/examples/matRad_example12_simpleParticleMonteCarlo.m +++ b/examples/matRad_example12_simpleParticleMonteCarlo.m @@ -2,7 +2,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2020 the matRad development team. +% Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -37,8 +37,7 @@ pln.propStf.longitudinalSpotSpacing = 3; pln.propStf.gantryAngles = 0; % [?] pln.propStf.couchAngles = 0; % [?] -pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); -pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0); +pln.propStf.isoCenter = matRad_getIsoCenter(cst,ct,0); %pln.propStf.isoCenter = [51 0 51]; % dose calculation settings diff --git a/examples/matRad_example13_fitAnalyticalParticleBaseData.m b/examples/matRad_example13_fitAnalyticalParticleBaseData.m index ba2d165e0..ad4ef6fec 100644 --- a/examples/matRad_example13_fitAnalyticalParticleBaseData.m +++ b/examples/matRad_example13_fitAnalyticalParticleBaseData.m @@ -2,7 +2,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2022 the matRad development team. +% Copyright 2022-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/examples/matRad_example14_spotRemoval.m b/examples/matRad_example14_spotRemoval.m index bf8db495e..08e835617 100644 --- a/examples/matRad_example14_spotRemoval.m +++ b/examples/matRad_example14_spotRemoval.m @@ -2,7 +2,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2021 the matRad development team. +% Copyright 2021-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -52,8 +52,7 @@ pln.propStf.gantryAngles = [90 270]; pln.propStf.couchAngles = [0 0]; pln.propStf.bixelWidth = 3; -pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); -pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0); +pln.propStf.isoCenter = matRad_getIsoCenter(cst,ct,0); pln.propOpt.runDAO = 0; pln.propOpt.runSequencing = 0; diff --git a/examples/matRad_example15_brachy.m b/examples/matRad_example15_brachy.m index 1b718698c..26e83c515 100644 --- a/examples/matRad_example15_brachy.m +++ b/examples/matRad_example15_brachy.m @@ -2,22 +2,21 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2021 the matRad development team. -% -% This file is part of the matRad project. It is subject to the license -% terms in the LICENSE file found in the top-level directory of this -% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part -% of the matRad project, including this file, may be copied, modified, -% propagated, or distributed except according to the terms contained in the +% Copyright 2021-2026 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the % LICENSE file. % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - %% List of contents -% In this example we will show +% In this example we will show % (i) how to load patient data into matRad -% (ii) how to setup an HDR brachy dose calculation and +% (ii) how to setup an HDR brachy dose calculation and % (iii) how to inversely optimize holding position intensties % (iv) how to visually and quantitatively evaluate the result % (v) how to verify that functions do the right thing @@ -25,8 +24,8 @@ %% I Patient Data Import % Let's begin with a clear Matlab environment. Then, import the TG119 % phantom into your workspace. The phantom is comprised of a 'ct' and 'cst' -% structure defining the CT images and the structure set. Make sure the -% matRad root directory with all its subdirectories is added to the Matlab +% structure defining the CT images and the structure set. Make sure the +% matRad root directory with all its subdirectories is added to the Matlab % search path. matRad_rc; @@ -39,130 +38,128 @@ % towards higher or lower doses (SquaredOverdose, SquaredUnderdose) or % doses that are particularly aimed for (SquaredUnderDose). -disp(cst{6,6}{1}); +disp(cst{6, 6}{1}); % Following frequently prescribed planning doses of 15 Gy % (https://pubmed.ncbi.nlm.nih.gov/22559663/) objectives can be updated to: % the planning target was changed to the clinical segmentation of the % prostate bed. -cst{5,3} = 'TARGET'; -cst{5,6}{1} = struct(DoseObjectives.matRad_SquaredUnderdosing(100,15)); -cst{5,6}{2} = struct(DoseObjectives.matRad_SquaredOverdosing(100,17.5)); -cst{6,5}.Priority = 1; +cst{5, 3} = 'TARGET'; +cst{5, 6}{1} = struct(DoseObjectives.matRad_SquaredUnderdosing(100, 15)); +cst{5, 6}{2} = struct(DoseObjectives.matRad_SquaredOverdosing(100, 17.5)); +cst{6, 5}.Priority = 1; % In this example, the lymph nodes will not be part of the treatment: -cst{7,6} = []; -cst{7,3} = 'OAR'; +cst{7, 6} = []; +cst{7, 3} = 'OAR'; -%A PTV is not needed, but we will use it to control the dose fall off -cst{6,3} = 'OAR'; -cst{6,6}{1} = struct(DoseObjectives.matRad_SquaredOverdosing(100,12)); -cst{6,5}.Priority = 2; +% A PTV is not needed, but we will use it to control the dose fall off +cst{6, 3} = 'OAR'; +cst{6, 6}{1} = struct(DoseObjectives.matRad_SquaredOverdosing(100, 12)); +cst{6, 5}.Priority = 2; % Rectum Objective -cst{1,6}{1} = struct(DoseObjectives.matRad_SquaredOverdosing(10,7.5)); +cst{1, 6}{1} = struct(DoseObjectives.matRad_SquaredOverdosing(10, 7.5)); % Bladder Objective -cst{8,6}{1} = struct(DoseObjectives.matRad_SquaredOverdosing(10,7.5)); +cst{8, 6}{1} = struct(DoseObjectives.matRad_SquaredOverdosing(10, 7.5)); % Body NT objective -cst{9,6}{1} = struct(DoseObjectives.matRad_MeanDose(1)); - +cst{9, 6}{1} = struct(DoseObjectives.matRad_MeanDose(1)); %% II.1 Treatment Plan -% The next step is to define your treatment plan labeled as 'pln'. This -% matlab structure requires input from the treatment planner and defines +% The next step is to define your treatment plan labeled as 'pln'. This +% matlab structure requires input from the treatment planner and defines % the most important cornerstones of your treatment plan. % First of all, we need to define what kind of radiation modality we would -% like to use. Possible values are photons, protons or carbon +% like to use. Possible values are photons, protons or carbon % (external beam) or brachy as an invasive tratment option. -% In this case we want to use brachytherapy. Then, we need to define a +% In this case we want to use brachytherapy. Then, we need to define a % treatment machine to correctly load the corresponding base data. % matRad includes example base data for HDR and LDR brachytherapy. % Here we will use HDR. By this means matRad will look for 'brachy_HDR.mat' -% in our root directory and will use the data provided in there for +% in our root directory and will use the data provided in there for % dose calculation. -pln.radiationMode = 'brachy'; +pln.radiationMode = 'brachy'; pln.machine = 'HDR'; % 'LDR' or 'HDR' for brachy pln.bioModel = 'none'; pln.multScen = 'nomScen'; %% II.1 - needle and template geometry -% Now we have to set some parameters for the template and the needles. +% Now we have to set some parameters for the template and the needles. % Let's start with the needles: Seed distance is the distance between % two neighbouring seeds or holding points on one needle or catheter. The % seeds No denotes how many seeds/holding points there are per needle. -pln.propStf.needle.seedDistance = 10; % [mm] -pln.propStf.needle.seedsNo = 6; +pln.propStf.needle.seedDistance = 10; % [mm] +pln.propStf.needle.seedsNo = 6; %% II.1 - template position -% The implantation is normally done through a 13 x 13 template from the +% The implantation is normally done through a 13 x 13 template from the % patients inferior, which is the negative z axis here. % The direction of the needles is defined by template normal. % Neighbour distances are called by bixelWidth, because this field is also % used for external beam therapy. % The needles will be positioned right under the target volume pointing up. - -pln.propStf.visMode = 1; %Enable visualization for stf generation +pln.propStf.visMode = 1; % Enable visualization for stf generation pln.propStf.bixelWidth = 5; % [mm] template grid distance -%Template Type -pln.propStf.template.type = 'checkerboard'; % 'checkerboard' if template is created automatically - % 'manual' if template is needed as preset manually (see below) +% Template Type +pln.propStf.template.type = 'checkerboard'; % 'checkerboard' if template is created automatically +% 'manual' if template is needed as preset manually (see below) -%Template Root - mass center of target in x and y and bottom in z -pln.propStf.template.root = matRad_getTemplateRoot(ct,cst); +% Template Root - mass center of target in x and y and bottom in z +pln.propStf.template.root = matRad_getTemplateRoot(ct, cst); % Dose Calculation engine pln.propDoseCalc.engine = 'TG43'; % Here, we define active needles as 1 and inactive needles -% as 0. This is the x-y plane and needles point in z direction. -% A checkerboard pattern is frequantly used. The whole geometry will become +% as 0. This is the x-y plane and needles point in z direction. +% A checkerboard pattern is frequently used. The whole geometry will become % clearer when it is displayed in 3D view in the next section. -if strcmp(pln.propStf.template.type,'manual') - pln.propStf.template.activeNeedles = [0 0 0 1 0 1 0 1 0 1 0 0 0;... % 7.0 - 0 0 1 0 1 0 0 0 1 0 1 0 0;... % 6.5 - 0 1 0 1 0 1 0 1 0 1 0 1 0;... % 6.0 - 1 0 1 0 1 0 0 0 1 0 1 0 1;... % 5.5 - 0 1 0 1 0 1 0 1 0 1 0 1 0;... % 5.0 - 1 0 1 0 1 0 0 0 1 0 1 0 1;... % 4.5 - 0 1 0 1 0 1 0 1 0 1 0 1 0;... % 4.0 - 1 0 1 0 1 0 0 0 1 0 1 0 1;... % 4.5 - 0 1 0 1 0 1 0 1 0 1 0 1 0;... % 3.0 - 1 0 1 0 1 0 1 0 1 0 1 0 1;... % 2.5 - 0 1 0 1 0 1 0 1 0 1 0 1 0;... % 2.0 - 1 0 1 0 1 0 0 0 0 0 1 0 1;... % 1.5 - 0 0 0 0 0 0 0 0 0 0 0 0 0]; % 1.0 - %A a B b C c D d E e F f G +if strcmp(pln.propStf.template.type, 'manual') + pln.propStf.template.activeNeedles = [0 0 0 1 0 1 0 1 0 1 0 0 0; ... % 7.0 + 0 0 1 0 1 0 0 0 1 0 1 0 0; ... % 6.5 + 0 1 0 1 0 1 0 1 0 1 0 1 0; ... % 6.0 + 1 0 1 0 1 0 0 0 1 0 1 0 1; ... % 5.5 + 0 1 0 1 0 1 0 1 0 1 0 1 0; ... % 5.0 + 1 0 1 0 1 0 0 0 1 0 1 0 1; ... % 4.5 + 0 1 0 1 0 1 0 1 0 1 0 1 0; ... % 4.0 + 1 0 1 0 1 0 0 0 1 0 1 0 1; ... % 4.5 + 0 1 0 1 0 1 0 1 0 1 0 1 0; ... % 3.0 + 1 0 1 0 1 0 1 0 1 0 1 0 1; ... % 2.5 + 0 1 0 1 0 1 0 1 0 1 0 1 0; ... % 2.0 + 1 0 1 0 1 0 0 0 0 0 1 0 1; ... % 1.5 + 0 0 0 0 0 0 0 0 0 0 0 0 0]; % 1.0 + % A a B b C c D d E e F f G end %% II.1 - dose calculation options -% for dose calculation we use eather the 2D or the 1D formalism proposed by +% for dose calculation we use either the 2D or the 1D formalism proposed by % TG 43. Also, set resolution of dose calculation and optimization. % If your system gets stuck with the resolution, you can lower it to 10 or % 20, just to get an initial result. Otherwise, reduce the number of % needles. % Calculation time will be reduced by one tenth when we define a dose % cutoff distance. -pln.propDoseCalc.TG43approximation = '2D'; %'1D' or '2D' +pln.propDoseCalc.TG43approximation = '2D'; % '1D' or '2D' pln.propDoseCalc.doseGrid.resolution.x = 5; % [mm] pln.propDoseCalc.doseGrid.resolution.y = 5; % [mm] pln.propDoseCalc.doseGrid.resolution.z = 5; % [mm] -% We can also use other solver for optimization than IPOPT. matRad +% We can also use other solver for optimization than IPOPT. matRad % currently supports simulannealbnd from the MATLAB Global Optimization Toolbox. First we % check if the simulannealbnd-Solver is available, and if it is, we set in in the % pln.propOpt.optimizer varirable. Otherwise we set to the default % optimizer 'IPOPT' -if matRad_OptimizerSimulannealbnd.IsAvailable() - pln.propOpt.optimizer = 'simulannealbnd'; +if matRad_OptimizerSimulannealbnd.isAvailable() + pln.propOpt.optimizer = 'simulannealbnd'; else pln.propOpt.optimizer = 'IPOPT'; end @@ -170,7 +167,7 @@ %% II.1 - book keeping % Some field names have to be kept although they don't have a direct % relevance for brachy therapy. -pln.numOfFractions = 1; +pln.numOfFractions = 1; %% II.1 - view plan % Et voila! Our treatment plan structure is ready. Lets have a look: @@ -180,7 +177,7 @@ % The steering file struct contains all needls/catheter geometry with the % target volume, number of needles, seeds and the positions of all needles % The one in the end enables visualization. -stf = matRad_generateStf(ct,cst,pln); +stf = matRad_generateStf(ct, cst, pln); %% II.2 - view stf % The 3D view is interesting, but we also want to know how the stf struct @@ -188,29 +185,31 @@ disp(stf); %% II.3 - Dose Calculation -% Let's generate dosimetric information by pre-computing a dose influence +% Let's generate dosimetric information by pre-computing a dose influence % matrix for seed/holding point intensities. Having dose influences % available allows subsequent inverse optimization. % Don't get inpatient, this can take a few seconds... -dij = matRad_calcDoseInfluence(ct,cst,stf,pln); +dij = matRad_calcDoseInfluence(ct, cst, stf, pln); %% III Inverse Optimization for brachy therapy % The goal of the fluence optimization is to find a set of holding point % times which yield the best possible dose distribution according to -% the clinical objectives and constraints underlying the radiation -% treatment. Once the optimization has finished, trigger to +% the clinical objectives and constraints underlying the radiation +% treatment. Once the optimization has finished, trigger to % visualize the optimized dose cubes. -resultGUI = matRad_fluenceOptimization(dij,cst,pln); +resultGUI = matRad_fluenceOptimization(dij, cst, pln); %% IV.1 Plot the Resulting Dose Slice % Let's plot the transversal iso-center dose slice -slice = matRad_world2cubeIndex(matRad_getIsoCenter(cst,ct),ct); +slice = matRad_world2cubeIndex(matRad_getIsoCenter(cst, ct), ct); slice = slice(3); -figure -imagesc(resultGUI.physicalDose(:,:,slice)),colorbar, colormap(jet); +figure; +imagesc(resultGUI.physicalDose(:, :, slice)); +colorbar; +colormap(jet); %% IV.2 Obtain dose statistics % Two more columns will be added to the cst structure depicting the DVH and % standard dose statistics such as D95,D98, mean dose, max dose etc. -resultGUI = matRad_planAnalysis(resultGUI,ct,cst,stf,pln); +resultGUI = matRad_planAnalysis(resultGUI, ct, cst, stf, pln); diff --git a/examples/matRad_example16_photonMC_MLC.m b/examples/matRad_example16_photonMC_MLC.m index a746c8543..6def7eec4 100644 --- a/examples/matRad_example16_photonMC_MLC.m +++ b/examples/matRad_example16_photonMC_MLC.m @@ -2,7 +2,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2017 the matRad development team. +% Copyright 2017-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -40,8 +40,7 @@ %pln.propStf.gantryAngles = [0]; %pln.propStf.couchAngles = [0]; pln.propStf.bixelWidth = 10; -pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); -pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0); +pln.propStf.isoCenter = matRad_getIsoCenter(cst,ct,0); % Enable sequencing and direct aperture optimization (DAO). pln.propOpt.runSequencing = 1; pln.propOpt.runDAO = 1; diff --git a/examples/matRad_example17_biologicalModels.m b/examples/matRad_example17_biologicalModels.m index 712c5e4cf..f87b4375a 100644 --- a/examples/matRad_example17_biologicalModels.m +++ b/examples/matRad_example17_biologicalModels.m @@ -12,9 +12,7 @@ pln.propStf.gantryAngles = 0; pln.propStf.couchAngles = 0; pln.propStf.bixelWidth = 5; -pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); - -pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0); +pln.propStf.isoCenter = matRad_getIsoCenter(cst,ct,0); pln.propOpt.runDAO = 0; pln.propSeq.runSequencing = 0; diff --git a/examples/matRad_example18_FREDMC.m b/examples/matRad_example18_FREDMC.m index 3ed101830..30d95d3c7 100644 --- a/examples/matRad_example18_FREDMC.m +++ b/examples/matRad_example18_FREDMC.m @@ -2,7 +2,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2023 the matRad development team. +% Copyright 2023-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -32,8 +32,7 @@ pln.propStf.gantryAngles = [30,330]; pln.propStf.couchAngles = zeros(size(pln.propStf.gantryAngles)); pln.propStf.bixelWidth = 5; -pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); -pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0); +pln.propStf.isoCenter = matRad_getIsoCenter(cst,ct,0); pln.propOpt.runDAO = 0; pln.propSeq.runSequencing = 0; @@ -89,4 +88,4 @@ resultGUI_recalc = matRad_calcDoseForward(ct,cst,stf,pln, wOptimized); %% Compare physical dose and RBExD distributions -matRad_compareDose(resultGUI_recalc.physicalDose, resultGUI_recalc.RBExDose,ct,cst,[],'on'); \ No newline at end of file +matRad_compareDose(resultGUI_recalc.physicalDose, resultGUI_recalc.RBExDose,ct,cst,[],'on'); diff --git a/examples/matRad_example19_CT_sCT_DVH_difference_photons.m b/examples/matRad_example19_CT_sCT_DVH_difference_photons.m index 0502f88c2..4082459af 100644 --- a/examples/matRad_example19_CT_sCT_DVH_difference_photons.m +++ b/examples/matRad_example19_CT_sCT_DVH_difference_photons.m @@ -2,7 +2,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2017 the matRad development team. +% Copyright 2017-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -206,7 +206,6 @@ % Obtain the number of beams and voxels from the existing variables and % calculate the iso-center which is per default the center of gravity of % all target voxels. -pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); pln.propStf.isoCenter = matRad_getIsoCenter(cst,ct,0); %% Dose calculation settings @@ -316,4 +315,4 @@ %%% Save the results to CSV file %file_path_dvh_diff = "YOUR PATH" %writetable(dvh_table_diff, file_path_dvh_diff); -end \ No newline at end of file +end diff --git a/examples/matRad_example1_phantom.m b/examples/matRad_example1_phantom.m index 2f16d265c..0f90fccef 100644 --- a/examples/matRad_example1_phantom.m +++ b/examples/matRad_example1_phantom.m @@ -2,7 +2,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2023 the matRad development team. +% Copyright 2023-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -107,8 +107,7 @@ pln.propStf.gantryAngles = [0:70:355]; pln.propStf.couchAngles = zeros(size(pln.propStf.gantryAngles)); pln.propStf.bixelWidth = 5; -pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); -pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0); +pln.propStf.isoCenter = matRad_getIsoCenter(cst,ct,0); % Settings for Optimization pln.propOpt.runDAO = 0; diff --git a/examples/matRad_example20_VHEE.m b/examples/matRad_example20_VHEE.m index 2601513f8..f94aef487 100644 --- a/examples/matRad_example20_VHEE.m +++ b/examples/matRad_example20_VHEE.m @@ -1,7 +1,7 @@ %% Example: VHEE Treatment Plan % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2017 the matRad development team. +% Copyright 2017-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -53,8 +53,7 @@ pln.propStf.bixelWidth = 5; % [mm] / also corresponds to lateral spot spacing for particles pln.propStf.gantryAngles = [35, 110, 180, 250, 325]; % [°] ; pln.propStf.couchAngles = [0 0 0 0 0]; % [°] ; -pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); -pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0); +pln.propStf.isoCenter = matRad_getIsoCenter(cst,ct,0); % dose calculation settings pln.propDoseCalc.doseGrid.resolution.x = 3; % [mm] diff --git a/examples/matRad_example2_photons.m b/examples/matRad_example2_photons.m index 8523795c5..d2b2e27cb 100644 --- a/examples/matRad_example2_photons.m +++ b/examples/matRad_example2_photons.m @@ -2,7 +2,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2017 the matRad development team. +% Copyright 2017-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -123,7 +123,6 @@ % Obtain the number of beams and voxels from the existing variables and % calculate the iso-center which is per default the center of gravity of % all target voxels. -pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); pln.propStf.isoCenter = matRad_getIsoCenter(cst,ct,0); %% dose calculation settings @@ -180,7 +179,6 @@ % Instead of 40 degree spacing use a 50 degree geantry beam spacing pln.propStf.gantryAngles = [0:50:359]; pln.propStf.couchAngles = zeros(1,numel(pln.propStf.gantryAngles)); -pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); %Let's rerun the dose calculation and optimization stf = matRad_generateStf(ct,cst,pln); diff --git a/examples/matRad_example3_photonsDAO.m b/examples/matRad_example3_photonsDAO.m index 15c350e48..9ac82ff1a 100644 --- a/examples/matRad_example3_photonsDAO.m +++ b/examples/matRad_example3_photonsDAO.m @@ -2,48 +2,47 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2017 the matRad development team. -% -% This file is part of the matRad project. It is subject to the license -% terms in the LICENSE file found in the top-level directory of this -% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part -% of the matRad project, including this file, may be copied, modified, -% propagated, or distributed except according to the terms contained in the +% Copyright 2017-2026 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the % LICENSE file. % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% In this example we will show +%% In this example we will show % (i) how to load patient data into matRad -% (ii) how to setup a photon dose calculation and +% (ii) how to setup a photon dose calculation and % (iii) how to inversely optimize directly from command window in MatLab. % (iv) how to apply a sequencing algorithm % (v) how to run a direct aperture optimization % (iv) how to visually and quantitatively evaluate the result %% set matRad runtime configuration -matRad_rc; %If this throws an error, run it from the parent directory first to set the paths +matRad_rc; % If this throws an error, run it from the parent directory first to set the paths %% Patient Data Import % import the head & neck patient into your workspace. load('HEAD_AND_NECK.mat'); %% Treatment Plan -% The next step is to define your treatment plan labeled as 'pln'. This -% structure requires input from the treatment planner and defines +% The next step is to define your treatment plan labeled as 'pln'. This +% structure requires input from the treatment planner and defines % the most important cornerstones of your treatment plan. pln.radiationMode = 'photons'; % either photons / protons / carbon pln.machine = 'Generic'; pln.numOfFractions = 30; - + pln.propStf.gantryAngles = [0:72:359]; pln.propStf.couchAngles = [0 0 0 0 0]; pln.propStf.bixelWidth = 5; -pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); -pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0); +pln.propStf.isoCenter = matRad_getIsoCenter(cst, ct, 0); -pln.bioModel = 'none'; +pln.bioModel = 'none'; pln.multScen = 'nomScen'; % dose calculation settings @@ -51,17 +50,17 @@ pln.propDoseCalc.doseGrid.resolution.y = 3; % [mm] pln.propDoseCalc.doseGrid.resolution.z = 3; % [mm] -% We can also use other solver for optimization than IPOPT. matRad +% We can also use other solver for optimization than IPOPT. matRad % currently supports fmincon from the MATLAB Optimization Toolbox. First we % check if the fmincon-Solver is available, and if it es, we set in in the -% pln.propOpt.optimizer vairable. Otherwise wie set to the default +% pln.propOpt.optimizer variable. Otherwise wie set to the default % optimizer 'IPOPT' -if matRad_OptimizerFmincon.IsAvailable() - pln.propOpt.optimizer = 'fmincon'; +if matRad_OptimizerFmincon.isAvailable() + pln.propOpt.optimizer = 'fmincon'; else pln.propOpt.optimizer = 'IPOPT'; end -pln.propOpt.quantityOpt = 'physicalDose'; +pln.propOpt.quantityOpt = 'physicalDose'; %% % Enable sequencing and direct aperture optimization (DAO). @@ -69,38 +68,38 @@ pln.propOpt.runDAO = true; %% Generate Beam Geometry STF -stf = matRad_generateStf(ct,cst,pln); +stf = matRad_generateStf(ct, cst, pln); %% Dose Calculation -% Lets generate dosimetric information by pre-computing dose influence -% matrices for unit beamlet intensities. Having dose influences available +% Lets generate dosimetric information by pre-computing dose influence +% matrices for unit beamlet intensities. Having dose influences available % allows for subsequent inverse optimization. -dij = matRad_calcDoseInfluence(ct,cst,stf,pln); +dij = matRad_calcDoseInfluence(ct, cst, stf, pln); %% Inverse Planning for IMRT -% The goal of the fluence optimization is to find a set of beamlet weights -% which yield the best possible dose distribution according to the -% predefined clinical objectives and constraints underlying the radiation +% The goal of the fluence optimization is to find a set of beamlet weights +% which yield the best possible dose distribution according to the +% predefined clinical objectives and constraints underlying the radiation % treatment. Once the optimization has finished, trigger once the GUI to % visualize the optimized dose cubes. -resultGUI = matRad_fluenceOptimization(dij,cst,pln); +resultGUI = matRad_fluenceOptimization(dij, cst, pln); matRadGUI; %% Sequencing -% This is a multileaf collimator leaf sequencing algorithm that is used in -% order to modulate the intensity of the beams with multiple static -% segments, so that translates each intensity map into a set of deliverable +% This is a multileaf collimator leaf sequencing algorithm that is used in +% order to modulate the intensity of the beams with multiple static +% segments, so that translates each intensity map into a set of deliverable % aperture shapes. -resultGUI = matRad_sequencing(resultGUI,stf,dij,pln); +resultGUI = matRad_sequencing(resultGUI, stf, dij, pln); %% DAO - Direct Aperture Optimization -% The Direct Aperture Optimization is an optimization approach where we +% The Direct Aperture Optimization is an optimization approach where we % directly optimize aperture shapes and weights. -resultGUI = matRad_directApertureOptimization(dij,cst,resultGUI.apertureInfo,resultGUI,pln); +resultGUI = matRad_directApertureOptimization(dij, cst, resultGUI.apertureInfo, resultGUI, pln); %% Aperture visualization % Use a matrad function to visualize the resulting aperture shapes matRad_visApertureInfo(resultGUI.apertureInfo); %% Indicator Calculation and display of DVH and QI -resultGUI = matRad_planAnalysis(resultGUI,ct,cst,stf,pln); +resultGUI = matRad_planAnalysis(resultGUI, ct, cst, stf, pln); diff --git a/examples/matRad_example4_photonsMC.m b/examples/matRad_example4_photonsMC.m index 4e292b422..6246e086b 100644 --- a/examples/matRad_example4_photonsMC.m +++ b/examples/matRad_example4_photonsMC.m @@ -2,7 +2,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2017 the matRad development team. +% Copyright 2017-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -41,8 +41,7 @@ pln.propStf.gantryAngles = [0]; pln.propStf.couchAngles = [0]; pln.propStf.bixelWidth = 10; -pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); -pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0); +pln.propStf.isoCenter = matRad_getIsoCenter(cst,ct,0); pln.propSeq.runSequencing = 0; pln.propOpt.runDAO = 0; diff --git a/examples/matRad_example5_protons.m b/examples/matRad_example5_protons.m index b1be3bbcb..9efccaa17 100644 --- a/examples/matRad_example5_protons.m +++ b/examples/matRad_example5_protons.m @@ -2,7 +2,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2017 the matRad development team. +% Copyright 2017-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -61,8 +61,7 @@ pln.propStf.gantryAngles = [90 270]; pln.propStf.couchAngles = [0 0]; pln.propStf.bixelWidth = 5; -pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); -pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0); +pln.propStf.isoCenter = matRad_getIsoCenter(cst,ct,0); pln.propOpt.runDAO = 0; pln.propSeq.runSequencing = 0; @@ -112,7 +111,7 @@ % Now let's simulate a patient shift in y direction for both beams stf(1).isoCenter(2) = stf(1).isoCenter(2) - 4; stf(2).isoCenter(2) = stf(2).isoCenter(2) - 4; -pln.propStf.isoCenter = reshape([stf.isoCenter],[3 pln.propStf.numOfBeams])'; +pln.propStf.isoCenter = reshape([stf.isoCenter],[3 numel(stf)])'; %% Recalculate Plan % Let's use the existing optimized pencil beam weights and recalculate the RBE weighted dose diff --git a/examples/matRad_example6_protonsNoise.m b/examples/matRad_example6_protonsNoise.m index 0ed137802..3cfa9f170 100644 --- a/examples/matRad_example6_protonsNoise.m +++ b/examples/matRad_example6_protonsNoise.m @@ -3,7 +3,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2017 the matRad development team. +% Copyright 2017-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -46,8 +46,7 @@ pln.propStf.couchAngles = [0 0]; pln.propStf.bixelWidth = 5; pln.propStf.longitudinalSpotSpacing = 5; -pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); -pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0); +pln.propStf.isoCenter = matRad_getIsoCenter(cst,ct,0); % dose calculation settings pln.propDoseCalc.doseGrid.resolution.x = 3; % [mm] diff --git a/examples/matRad_example7_carbon.m b/examples/matRad_example7_carbon.m index d57cf9a93..4a3ce3e5e 100644 --- a/examples/matRad_example7_carbon.m +++ b/examples/matRad_example7_carbon.m @@ -2,7 +2,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2017 the matRad development team. +% Copyright 2017-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -64,8 +64,7 @@ pln.propStf.gantryAngles = 315; pln.propStf.couchAngles = 0; pln.propStf.bixelWidth = 6; -pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); -pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0); +pln.propStf.isoCenter = matRad_getIsoCenter(cst,ct,0); % dose calculation settings pln.propDoseCalc.calcLET = true; %Let's also calculate the LET diff --git a/examples/matRad_example8_protonsRobust.m b/examples/matRad_example8_protonsRobust.m index 9e7dee90b..9fe6fd434 100644 --- a/examples/matRad_example8_protonsRobust.m +++ b/examples/matRad_example8_protonsRobust.m @@ -2,7 +2,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2018 the matRad development team. +% Copyright 2018-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -78,8 +78,7 @@ pln.propStf.gantryAngles = [0 90]; pln.propStf.couchAngles = [0 0]; pln.propStf.bixelWidth = 5; -pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); -pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0); +pln.propStf.isoCenter = matRad_getIsoCenter(cst,ct,0); pln.propDoseCalc.doseGrid.resolution.x = 3; % [mm] pln.propDoseCalc.doseGrid.resolution.y = 3; % [mm] diff --git a/examples/matRad_example9_4DDoseCalcMinimal.m b/examples/matRad_example9_4DDoseCalcMinimal.m index 2ba1f209c..712a3fd6c 100644 --- a/examples/matRad_example9_4DDoseCalcMinimal.m +++ b/examples/matRad_example9_4DDoseCalcMinimal.m @@ -1,7 +1,7 @@ %% 4D dose calculation workflow % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2018 the matRad development team. +% Copyright 2018-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -46,8 +46,7 @@ pln.propStf.longitudinalSpotSpacing = 5; % only relevant for HIT machine, not generic pln.propStf.gantryAngles = [90]; pln.propStf.couchAngles = [0]; -pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); -pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0); +pln.propStf.isoCenter = matRad_getIsoCenter(cst,ct,0); %% diff --git a/examples/matRad_publishExamples.m b/examples/matRad_publishExamples.m index d937edf19..05a73ea75 100644 --- a/examples/matRad_publishExamples.m +++ b/examples/matRad_publishExamples.m @@ -2,7 +2,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2017 the matRad development team. +% Copyright 2017-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad.m b/matRad.m index 737dab26f..a369d6215 100644 --- a/matRad.m +++ b/matRad.m @@ -2,7 +2,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -38,8 +38,7 @@ pln.propStf.bixelWidth = 5; % [mm] / also corresponds to lateral spot spacing for particles pln.propStf.gantryAngles = [0:72:359]; % [°] ; pln.propStf.couchAngles = [0 0 0 0 0]; % [°] ; -pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); -pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0); +pln.propStf.isoCenter = matRad_getIsoCenter(cst,ct,0); % dose calculation settings pln.propDoseCalc.doseGrid.resolution.x = 5; % [mm] diff --git a/matRad/4D/matRad_addMovement.m b/matRad/4D/matRad_addMovement.m index 606f5f1df..1138a5ae1 100644 --- a/matRad/4D/matRad_addMovement.m +++ b/matRad/4D/matRad_addMovement.m @@ -2,10 +2,10 @@ % adds artificial sinosodal patient motion by creating a deformation vector % field and applying it to the ct.cube by geometric transformation % -% call +% call: % ct = matRad_addMovement(ct, ct.motionPeriod, ct.numOfCtScen, amp) % -% input +% input: % ct: matRad ct struct % cst: matRad cst struct % motionPeriod: the length of a whole breathing cycle (in seconds) @@ -20,7 +20,7 @@ % a positive amplitude moves the phantom to the right, % anterior, inferior % -% output +% output: % ct: modified matRad ct struct including dvf and cubes for % all phases % cst: modified matRad cst struct @@ -30,7 +30,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2018 the matRad development team. +% Copyright 2018-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/4D/matRad_calc4dDose.m b/matRad/4D/matRad_calc4dDose.m index 5aea56558..a074778e7 100644 --- a/matRad/4D/matRad_calc4dDose.m +++ b/matRad/4D/matRad_calc4dDose.m @@ -2,10 +2,10 @@ % wrapper for the whole 4D dose calculation pipeline and calculated dose % accumulation % -% call +% call: % ct = matRad_calc4dDose(ct, pln, dij, stf, cst, resultGUI) % -% input +% input: % ct : ct cube % pln: matRad plan meta information struct % dij: matRad dij struct @@ -14,7 +14,7 @@ % resultGUI: struct containing optimized fluence vector % totalPhaseMatrix optional intput for totalPhaseMatrix % accType: witch algorithim for dose accumulation -% output +% output: % resultGUI: structure containing phase dose, RBE weighted dose, etc % timeSequence: timing information about the irradiation % @@ -23,7 +23,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2018 the matRad development team. +% Copyright 2018-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/4D/matRad_doseAcc.m b/matRad/4D/matRad_doseAcc.m index c6d96c2fa..112d7e122 100644 --- a/matRad/4D/matRad_doseAcc.m +++ b/matRad/4D/matRad_doseAcc.m @@ -1,11 +1,10 @@ function dAcc = matRad_doseAcc(ct, phaseCubes, cst, accMethod) -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % matRad dose accumulation function % -% call +% call: % dAcc = matRad_doseAcc(d,dvf) % -% input +% input: % ct: matRad ct struct inclduing 4d ct, deformation vector % fields, and meta information % phaseCubes: cell array of cubes to be accumulated @@ -16,7 +15,7 @@ % % +++ Attention +++ the deformation vector fields are in [mm] % -% output +% output: % dAcc: accumulated dose cube % % References @@ -26,7 +25,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2018 the matRad development team. +% Copyright 2018-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/4D/matRad_makeBixelTimeSeq.m b/matRad/4D/matRad_makeBixelTimeSeq.m index 70036cf20..3b3a52f11 100644 --- a/matRad/4D/matRad_makeBixelTimeSeq.m +++ b/matRad/4D/matRad_makeBixelTimeSeq.m @@ -1,16 +1,15 @@ function timeSequence = matRad_makeBixelTimeSeq(stf, resultGUI) -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % using the steering information of matRad, makes a time sequenced order % according to the irradiation scheme in spot scanning % -% call +% call: % timeSequence = matRad_makeBixelTimeSeq(stf, resultGUI) % -% input +% input: % stf: matRad steering information struct % resultGUI: struct containing optimized fluence vector % -% output +% output: % timeSequence: struct containing bixel ordering information and the % time sequence of the spot scanning % @@ -22,7 +21,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2018 the matRad development team. +% Copyright 2018-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -205,4 +204,4 @@ end -end \ No newline at end of file +end diff --git a/matRad/4D/matRad_makePhaseMatrix.m b/matRad/4D/matRad_makePhaseMatrix.m index 861717ff9..8830bcca5 100644 --- a/matRad/4D/matRad_makePhaseMatrix.m +++ b/matRad/4D/matRad_makePhaseMatrix.m @@ -1,21 +1,20 @@ function timeSequence = matRad_makePhaseMatrix(timeSequence, numOfPhases, motionPeriod, motion) -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % using the time sequence and the ordering of the bixel iradiation, and % number of scenarios, makes a phase matrix of size number of bixels * % number of scenarios % % -% call +% call: % timeSequence = matRad_makePhaseMatrix(timeSequence, numOfPhases, motionPeriod, motion) % -% input +% input: % timeSequence: struct containing bixel ordering information and the % time sequence of the spot scanning % numOfCtScen: number of the desired phases % motionPeriod: the extent of a whole breathing cycle (in seconds) % motion: motion scenario: 'linear'(default), 'sampled_period' % -% output +% output: % timeSequence: phase matrix field added % % References @@ -23,7 +22,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2018 the matRad development team. +% Copyright 2018-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/4D/matRad_postprocessing.m b/matRad/4D/matRad_postprocessing.m index 64210ffa3..303ac679f 100644 --- a/matRad/4D/matRad_postprocessing.m +++ b/matRad/4D/matRad_postprocessing.m @@ -3,23 +3,23 @@ % minimum number of particles per spot % minimum number of particles per iso-energy slice % -% call +% call: % resultGUI = matRad_postprocessing(resultGUI, dij, pln, cst, stf) -% input +% input: % resultGUI struct containing optimized fluence vector % dij: matRad dij struct % pln: matRad pln struct % cst: matRad cst struct % stf: matRad stf struct % -% output +% output: % resultGUI: new w and doses in resultGUI % % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2018 the matRad development team. +% Copyright 2018-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -93,7 +93,7 @@ if(minNrParticlesIES ~= 0) % Find IES values - for i = 1:pln.propStf.numOfBeams + for i = 1:numel(stf) iesArray = []; for j = 1:stf(i).numOfRays iesArray = unique([iesArray stf(i).ray(j).energy]); diff --git a/matRad/IO/matRad_exportDij.m b/matRad/IO/matRad_exportDij.m index 38e578556..0f8ec215b 100644 --- a/matRad/IO/matRad_exportDij.m +++ b/matRad/IO/matRad_exportDij.m @@ -1,11 +1,11 @@ function matRad_exportDij(filename,dij,stf,metadata) % matRad physical dose writer % -% call +% call: % matRad_exportDij(filename,dij,stf,... % additionalFields,additionalKeyValuePairs) % -% input +% input: % filename: full output path, including the file extension % dij: matRad dij struct % stf: matRad stf struct @@ -13,7 +13,7 @@ function matRad_exportDij(filename,dij,stf,metadata) % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/IO/matRad_importPatient.m b/matRad/IO/matRad_importPatient.m index 7404885fe..f39f29e0c 100644 --- a/matRad/IO/matRad_importPatient.m +++ b/matRad/IO/matRad_importPatient.m @@ -1,11 +1,11 @@ function [ct,cst] = matRad_importPatient(ctFile,maskFiles,hlutFilename) % matRad patient import from binary files (CT and masks) % -% call +% call: % [ct,cst] = matRad_importPatient(cubeFile,maskFiles) % [ct,cst] = matRad_importPatient(cubeFile,maskFiles, hlutFilename) % -% input +% input: % ctFile: path to CT file. If HLUT is not set, values are interpreted % as HU and interpolated to ED. % maskFiles: cell array with filenames to the masks @@ -13,14 +13,14 @@ % recognized data files are treated as masks % hlutFilname:(optional) HLUT, (n,2) array. if set to 'default', we will % use a default HLUT -% output +% output: % ct ct struct for use with matlab % cst cst struct for use with matlab % % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/IO/matRad_readCube.m b/matRad/IO/matRad_readCube.m index 24173192e..bc1b4afe1 100644 --- a/matRad/IO/matRad_readCube.m +++ b/matRad/IO/matRad_readCube.m @@ -2,16 +2,16 @@ % matRad Cube read wrapper % determines the extension and assigns the appropriate reader to it % -% call +% call: % matRad_readCube(filename) % -% input +% input: % filename: full path of the file % % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/IO/matRad_readHLUT.m b/matRad/IO/matRad_readHLUT.m index bffb3d8d9..91f69004a 100644 --- a/matRad/IO/matRad_readHLUT.m +++ b/matRad/IO/matRad_readHLUT.m @@ -1,13 +1,13 @@ function hlut = matRad_readHLUT(filename) % matRad function to read HLUT from filename % -% call +% call: % hlut = matRad_readHLUT(filename) % -% input +% input: % filename: hlut filename % -% output +% output: % hlut: lookup table % % References @@ -15,7 +15,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2018 the matRad development team. +% Copyright 2018-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/IO/matRad_readMHD.m b/matRad/IO/matRad_readMHD.m index 6fe944334..fac9c4e75 100644 --- a/matRad/IO/matRad_readMHD.m +++ b/matRad/IO/matRad_readMHD.m @@ -1,13 +1,13 @@ function [cube, metadata] = matRad_readMHD(filename) % matRad NRRD reader % -% call +% call: % [cube, metadata] = matRad_readMHD(filename) % -% input +% input: % filename: full path to mhd or mha file % -% output +% output: % cube: the read cube % metadata: metadata from header information % @@ -16,7 +16,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/IO/matRad_readNRRD.m b/matRad/IO/matRad_readNRRD.m index 85c55c7df..a09780314 100644 --- a/matRad/IO/matRad_readNRRD.m +++ b/matRad/IO/matRad_readNRRD.m @@ -1,13 +1,13 @@ function [cube, metadata] = matRad_readNRRD(filename) % matRad NRRD reader % -% call +% call: % [cube, metadata] = matRad_readNRRD(filename) % -% input +% input: % filename: full path to nrrd file % -% output +% output: % cube: the read cube % metadata: metadata from header information % @@ -16,7 +16,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/IO/matRad_readNifTI.m b/matRad/IO/matRad_readNifTI.m index dfeb3ed9e..8fc95fe92 100644 --- a/matRad/IO/matRad_readNifTI.m +++ b/matRad/IO/matRad_readNifTI.m @@ -1,13 +1,13 @@ function [cube, metadata] = matRad_readNifTI(filename) % matRad NifTI reader % -% call +% call: % [cube, metadata] = matRad_readNifTI(filename) % -% input +% input: % filename: full path to .nii(.gz) file % -% output +% output: % cube: the read cube % metadata: metadata from header information % @@ -16,7 +16,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2024 the matRad development team. +% Copyright 2024-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/IO/matRad_supportedBinaryFormats.m b/matRad/IO/matRad_supportedBinaryFormats.m index ec5354088..8587365ef 100644 --- a/matRad/IO/matRad_supportedBinaryFormats.m +++ b/matRad/IO/matRad_supportedBinaryFormats.m @@ -1,12 +1,12 @@ function [readers,writers] = matRad_supportedBinaryFormats() % matRad function to obtain supported binary formats % -% call +% call: % [read,write] = matRad_supportedBinaryFormats() % -% input +% input: % -% output +% output: % read cell array with file filter in first column, name in second % column, and handle to read function in third column % write cell array with file filter in first column, name in second @@ -17,7 +17,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/IO/matRad_writeCube.m b/matRad/IO/matRad_writeCube.m index 7bdadd419..47be88eba 100644 --- a/matRad/IO/matRad_writeCube.m +++ b/matRad/IO/matRad_writeCube.m @@ -1,10 +1,10 @@ function [saved_metadata] = matRad_writeCube(filepath,cube,datatype,metadata) % matRad wrapper for Cube export % -% call +% call: % [saved_metadata] = matRad_writeCube(filepath,cube,meta) % -% input +% input: % filepath: full output path. needs the right extension % to choose the appropriate writer % cube: cube that is to be written @@ -19,12 +19,12 @@ % - dataUnit (i.e. Gy..) % - dataName (i.e. dose, ED, ...) % - compress (true/false) -% (default chosen by writer) +% (default chosen by writer) % % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/IO/matRad_writeMHA.m b/matRad/IO/matRad_writeMHA.m index e640878c8..2f9f58e5b 100644 --- a/matRad/IO/matRad_writeMHA.m +++ b/matRad/IO/matRad_writeMHA.m @@ -1,10 +1,10 @@ function matRad_writeMHA(filepath,cube,metadata) % matRad function to write mha files % -% call +% call: % matRad_writeMHA(filepath,cube,metadata) % -% input +% input: % filepath: full filename (with extension) % cube: 3D array to be written into file % metadata: struct of metadata. Writer will wrap the existing metadata @@ -13,7 +13,7 @@ function matRad_writeMHA(filepath,cube,metadata) % - resolution: [x y z] % - datatype: numeric MATLAB-Datatype % -% output +% output: % file will be written to disk % % References @@ -21,7 +21,7 @@ function matRad_writeMHA(filepath,cube,metadata) % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/IO/matRad_writeMHD.m b/matRad/IO/matRad_writeMHD.m index 4aed532db..6e7f6d728 100644 --- a/matRad/IO/matRad_writeMHD.m +++ b/matRad/IO/matRad_writeMHD.m @@ -1,10 +1,10 @@ function matRad_writeMHD(filepath,cube,metadata) % matRad function to write mha files % -% call +% call: % matRad_writeMHA(filepath,cube,metadata) % -% input +% input: % filepath: full filename (with extension) % cube: 3D array to be written into file % metadata: struct of metadata. Writer will wrap the existing metadata @@ -13,7 +13,7 @@ function matRad_writeMHD(filepath,cube,metadata) % - resolution: [x y z] % - datatype: numeric MATLAB-Datatype % -% output +% output: % file will be written to disk % % References @@ -21,7 +21,7 @@ function matRad_writeMHD(filepath,cube,metadata) % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/IO/matRad_writeNRRD.m b/matRad/IO/matRad_writeNRRD.m index d676fcdc0..333a21db8 100644 --- a/matRad/IO/matRad_writeNRRD.m +++ b/matRad/IO/matRad_writeNRRD.m @@ -1,11 +1,11 @@ function matRad_writeNRRD(filename,cube,metadata) % matRad NRRD writer % -% call +% call: % matRad_writeNRRD(filename,cube,datatype,... % additionalFields,additionalKeyValuePairs) % -% input +% input: % filename: full output path, including the nrrd extension % cube: cube that is to be written % metadata: struct of metadata. Writer will wrap the existing metadata @@ -16,7 +16,7 @@ function matRad_writeNRRD(filename,cube,metadata) % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/IO/matRad_writeNifTI.m b/matRad/IO/matRad_writeNifTI.m index 2986e9a8f..d12edeedb 100644 --- a/matRad/IO/matRad_writeNifTI.m +++ b/matRad/IO/matRad_writeNifTI.m @@ -1,19 +1,19 @@ function [cube, metadata] = matRad_writeNifTI(filepath,cube,metadata) % matRad NifTI reader % -% call +% call: % [cube, metadata] = matRad_writeNifTI(filename) % -% input +% input: % filename: full path to .nii(.gz) file % -% output +% output: % file will be written to disk % % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2024 the matRad development team. +% Copyright 2024-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/IO/matRad_writeVTK.m b/matRad/IO/matRad_writeVTK.m index 6b81c4148..b36f16d96 100644 --- a/matRad/IO/matRad_writeVTK.m +++ b/matRad/IO/matRad_writeVTK.m @@ -1,10 +1,10 @@ function matRad_writeVTK(filepath,cube,metadata) % matRad function to write vtk cubes % -% call +% call: % matRad_writeVTK(filepath,cube,metadata) % -% input +% input: % filepath: full filename (with extension) % cube: 3D array to be written into file % metadata: struct of metadata. Writer will wrap the existing metadata @@ -13,7 +13,7 @@ function matRad_writeVTK(filepath,cube,metadata) % - resolution: [x y z] % - datatype: numeric MATLAB-Datatype % -% output +% output: % file will be written to disk % % References @@ -21,7 +21,7 @@ function matRad_writeVTK(filepath,cube,metadata) % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/MatRad_Config.m b/matRad/MatRad_Config.m index 06e12316b..7b2861fa5 100644 --- a/matRad/MatRad_Config.m +++ b/matRad/MatRad_Config.m @@ -2,12 +2,13 @@ % MatRad_Config MatRad Configuration class % This class is used globally through Matlab to handle default values and % logging and is declared as global matRad_cfg. + % % Usage: % matRad_cfg = MatRad_Config.instance(); % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2019 the matRad development team. + % Copyright 2019-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -124,7 +125,7 @@ function displayToConsole(obj,type,formatSpec,varargin) % Display to console will be called from the public wrapper % functions dispError, dispWarning, dispInfo, dispDebug % - % input + % input: % type: type of the log information. % Needs to be one of 'error', 'warning', 'info' or 'debug'. % formatSpec: string to print using format specifications similar to fprintf @@ -191,8 +192,7 @@ function reset(obj) function setDefaultProperties(obj) %setDefaultProperties set matRad's default computation - % properties - % input + % properties %Default machines obj.defaults.machine.photons = 'Generic'; @@ -232,6 +232,7 @@ function setDefaultProperties(obj) obj.defaults.propDoseCalc.calcLET = true; %calculate LETs for particles obj.defaults.propDoseCalc.selectVoxelsInScenarios = 'all'; obj.defaults.propDoseCalc.airOffsetCorrection = true; + % default properties for fine sampling calculation obj.defaults.propDoseCalc.fineSampling.sigmaSub = 1; obj.defaults.propDoseCalc.fineSampling.N = 2; @@ -246,6 +247,7 @@ function setDefaultProperties(obj) obj.defaults.propOpt.maxIter = 500; obj.defaults.propOpt.runDAO = 0; obj.defaults.propOpt.clearUnusedVoxels = false; + obj.defaults.propOpt.enableGPU = false; %Sequencing Options obj.defaults.propSeq.sequencer = 'siochi'; @@ -349,7 +351,8 @@ function setDefaultGUIProperties(obj) function dispDebug(obj,formatSpec,varargin) %dispDebug print debug messages (log level >= 4) - % input + % + % input: % formatSpec: string to print using format specifications similar to fprintf % varargin: variables according to formatSpec @@ -358,7 +361,8 @@ function dispDebug(obj,formatSpec,varargin) function dispInfo(obj,formatSpec,varargin) %dispInfo print information console output (log level >= 3) - % input + % + % input: % formatSpec: string to print using format specifications similar to fprintf % varargin: variables according to formatSpec obj.displayToConsole('info',formatSpec,varargin{:}); @@ -366,7 +370,8 @@ function dispInfo(obj,formatSpec,varargin) function dispError(obj,formatSpec,varargin) %dispError print errors (forwarded to "error" that will stop the program) (log level >= 1) - % input + % + % input: % formatSpec: string to print using format specifications % similar to 'error' % varargin: variables according to formatSpec @@ -385,7 +390,8 @@ function dispError(obj,formatSpec,varargin) function dispWarning(obj,formatSpec,varargin) %dispError print warning (forwarded to 'warning') (log level >= 2) - % input + % + % input: % formatSpec: string to print using format specifications % similar to 'warning' % varargin: variables according to formatSpec diff --git a/matRad/basedata/matRad_MCemittanceBaseData.m b/matRad/basedata/matRad_MCemittanceBaseData.m index e081dc28c..06f566060 100644 --- a/matRad/basedata/matRad_MCemittanceBaseData.m +++ b/matRad/basedata/matRad_MCemittanceBaseData.m @@ -1,5 +1,4 @@ classdef matRad_MCemittanceBaseData - % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % matRad_MCemmitanceBaseData This is the superclass for MonteCarlo base % data calculation % @@ -11,7 +10,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2020 the matRad development team. + % Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -613,7 +612,7 @@ end end - methods (Access = protected) + methods function obj = getRangeShiftersFromStf(obj,stf) allRays = [stf.ray]; raShis = [allRays.rangeShifter]; diff --git a/matRad/basedata/matRad_getAvailableMachines.m b/matRad/basedata/matRad_getAvailableMachines.m index 245d330a6..9e7149a17 100644 --- a/matRad/basedata/matRad_getAvailableMachines.m +++ b/matRad/basedata/matRad_getAvailableMachines.m @@ -2,13 +2,13 @@ % matRad_loadMachine load a machine base data file from pln struct. % Looks for the machine fileuct. in the basedata folder and in the provided user folders. % -% call +% call: % machine = matRad_loadMachine(pln) % -% input +% input: % pln: matRad plan meta information struct % -% output +% output: % machine: matRad machine struct % % References @@ -16,7 +16,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2024 the matRad development team. +% Copyright 2024-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -55,4 +55,4 @@ else machineList(modalities{i}) = {}; end -end \ No newline at end of file +end diff --git a/matRad/basedata/matRad_loadMachine.m b/matRad/basedata/matRad_loadMachine.m index be7078f07..209567a17 100644 --- a/matRad/basedata/matRad_loadMachine.m +++ b/matRad/basedata/matRad_loadMachine.m @@ -2,13 +2,13 @@ % matRad_loadMachine load a machine base data file from pln struct. % Looks for the machine file from pln in the basedata folder and in the provided user folders. % -% call +% call: % machine = matRad_loadMachine(pln) % -% input +% input: % pln: matRad plan meta information struct % -% output +% output: % machine: matRad machine struct % % References @@ -16,7 +16,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2022 the matRad development team. +% Copyright 2022-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/bioModels/LQbasedModels/LETbasedModels/RBEminMaxModels/matRad_Carabe.m b/matRad/bioModels/LQbasedModels/LETbasedModels/RBEminMaxModels/matRad_Carabe.m index 84b192a40..35b74ec2d 100644 --- a/matRad/bioModels/LQbasedModels/LETbasedModels/RBEminMaxModels/matRad_Carabe.m +++ b/matRad/bioModels/LQbasedModels/LETbasedModels/RBEminMaxModels/matRad_Carabe.m @@ -4,7 +4,7 @@ % (accessed on 21/7/2023) % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2023 the matRad development team. +% Copyright 2023-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -41,4 +41,4 @@ end end -end \ No newline at end of file +end diff --git a/matRad/bioModels/LQbasedModels/LETbasedModels/RBEminMaxModels/matRad_HeliumMairani.m b/matRad/bioModels/LQbasedModels/LETbasedModels/RBEminMaxModels/matRad_HeliumMairani.m index 045906f0e..7ed397f7e 100644 --- a/matRad/bioModels/LQbasedModels/LETbasedModels/RBEminMaxModels/matRad_HeliumMairani.m +++ b/matRad/bioModels/LQbasedModels/LETbasedModels/RBEminMaxModels/matRad_HeliumMairani.m @@ -4,7 +4,7 @@ % https://iopscience.iop.org/article/10.1088/0031-9155/61/2/888 % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2023 the matRad development team. +% Copyright 2023-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -46,4 +46,4 @@ end end -end \ No newline at end of file +end diff --git a/matRad/bioModels/LQbasedModels/LETbasedModels/RBEminMaxModels/matRad_LinearScalingModel.m b/matRad/bioModels/LQbasedModels/LETbasedModels/RBEminMaxModels/matRad_LinearScalingModel.m index e5de28c1d..91870160f 100644 --- a/matRad/bioModels/LQbasedModels/LETbasedModels/RBEminMaxModels/matRad_LinearScalingModel.m +++ b/matRad/bioModels/LQbasedModels/LETbasedModels/RBEminMaxModels/matRad_LinearScalingModel.m @@ -3,7 +3,7 @@ % according to Malte Frese https://www.ncbi.nlm.nih.gov/pubmed/20382482 (FITTED for head and neck patients !) % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2023 the matRad development team. +% Copyright 2023-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -52,4 +52,4 @@ end end -end \ No newline at end of file +end diff --git a/matRad/bioModels/LQbasedModels/LETbasedModels/RBEminMaxModels/matRad_MCNamara.m b/matRad/bioModels/LQbasedModels/LETbasedModels/RBEminMaxModels/matRad_MCNamara.m index 62736be76..e82cc6c4a 100644 --- a/matRad/bioModels/LQbasedModels/LETbasedModels/RBEminMaxModels/matRad_MCNamara.m +++ b/matRad/bioModels/LQbasedModels/LETbasedModels/RBEminMaxModels/matRad_MCNamara.m @@ -3,7 +3,7 @@ % (https://www.ncbi.nlm.nih.gov/pmc/articles/PMC4634882/) (accessed on 21/7/2023) % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2023 the matRad development team. +% Copyright 2023-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -41,4 +41,4 @@ end end -end \ No newline at end of file +end diff --git a/matRad/bioModels/LQbasedModels/LETbasedModels/RBEminMaxModels/matRad_RBEminMax.m b/matRad/bioModels/LQbasedModels/LETbasedModels/RBEminMaxModels/matRad_RBEminMax.m index 4f224097a..92b4e56a8 100644 --- a/matRad/bioModels/LQbasedModels/LETbasedModels/RBEminMaxModels/matRad_RBEminMax.m +++ b/matRad/bioModels/LQbasedModels/LETbasedModels/RBEminMaxModels/matRad_RBEminMax.m @@ -4,7 +4,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2023 the matRad development team. +% Copyright 2023-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -50,4 +50,4 @@ function getRBEminMax(~,~,~,~,~) end -end \ No newline at end of file +end diff --git a/matRad/bioModels/LQbasedModels/LETbasedModels/RBEminMaxModels/matRad_Wedenberg.m b/matRad/bioModels/LQbasedModels/LETbasedModels/RBEminMaxModels/matRad_Wedenberg.m index 213320cde..17891e7c9 100644 --- a/matRad/bioModels/LQbasedModels/LETbasedModels/RBEminMaxModels/matRad_Wedenberg.m +++ b/matRad/bioModels/LQbasedModels/LETbasedModels/RBEminMaxModels/matRad_Wedenberg.m @@ -3,7 +3,7 @@ % (https://www.ncbi.nlm.nih.gov/pubmed/22909391) (accessed on 21/7/2023) % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2023 the matRad development team. +% Copyright 2023-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -39,4 +39,4 @@ end end -end \ No newline at end of file +end diff --git a/matRad/bioModels/LQbasedModels/LETbasedModels/matRad_LQLETbasedModel.m b/matRad/bioModels/LQbasedModels/LETbasedModels/matRad_LQLETbasedModel.m index 2a1608176..acd2ee656 100644 --- a/matRad/bioModels/LQbasedModels/LETbasedModels/matRad_LQLETbasedModel.m +++ b/matRad/bioModels/LQbasedModels/LETbasedModels/matRad_LQLETbasedModel.m @@ -5,7 +5,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2023 the matRad development team. +% Copyright 2023-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -33,4 +33,4 @@ end end -end \ No newline at end of file +end diff --git a/matRad/bioModels/LQbasedModels/RBEtabulatedModels/matRad_LQRBETabulatedModel.m b/matRad/bioModels/LQbasedModels/RBEtabulatedModels/matRad_LQRBETabulatedModel.m index 93a4caf13..df7e5be15 100644 --- a/matRad/bioModels/LQbasedModels/RBEtabulatedModels/matRad_LQRBETabulatedModel.m +++ b/matRad/bioModels/LQbasedModels/RBEtabulatedModels/matRad_LQRBETabulatedModel.m @@ -8,7 +8,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2023 the matRad development team. +% Copyright 2023-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -224,7 +224,7 @@ function updateRBEtable(this) load(fullfile(searchPath{1}, [fileName, '.mat']), 'RBEtable'); catch try - laod(fullfile(searchPath{2}, [fileName, '.mat']), 'RBEtable'); + load(fullfile(searchPath{2}, [fileName, '.mat']), 'RBEtable'); catch matRad_cfg.dispError('Cannot find RBEtable: %s', fileName); end @@ -291,4 +291,4 @@ function checkRBEtableStructure(RBEtable) end end -end \ No newline at end of file +end diff --git a/matRad/bioModels/LQbasedModels/RBEtabulatedModels/matRad_TabulatedSpectralKernelBasedModel.m b/matRad/bioModels/LQbasedModels/RBEtabulatedModels/matRad_TabulatedSpectralKernelBasedModel.m index ff7153a13..fad2c0755 100644 --- a/matRad/bioModels/LQbasedModels/RBEtabulatedModels/matRad_TabulatedSpectralKernelBasedModel.m +++ b/matRad/bioModels/LQbasedModels/RBEtabulatedModels/matRad_TabulatedSpectralKernelBasedModel.m @@ -14,7 +14,7 @@ % with the RBE table (i.e. 'Fluence', 'EnergyDeposit') % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2023 the matRad development team. +% Copyright 2023-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -155,4 +155,4 @@ function updatePropertyValues(this) end end -end \ No newline at end of file +end diff --git a/matRad/bioModels/LQbasedModels/kernelBasedModels/matRad_KernelBasedLEM.m b/matRad/bioModels/LQbasedModels/kernelBasedModels/matRad_KernelBasedLEM.m index 121d02e2f..5ad70a259 100644 --- a/matRad/bioModels/LQbasedModels/kernelBasedModels/matRad_KernelBasedLEM.m +++ b/matRad/bioModels/LQbasedModels/kernelBasedModels/matRad_KernelBasedLEM.m @@ -2,7 +2,7 @@ % This class specifically implements the kernel-based LEMIV model % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2023 the matRad development team. +% Copyright 2023-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -45,4 +45,4 @@ end end end -end \ No newline at end of file +end diff --git a/matRad/bioModels/LQbasedModels/kernelBasedModels/matRad_LQKernelBasedModel.m b/matRad/bioModels/LQbasedModels/kernelBasedModels/matRad_LQKernelBasedModel.m index 6f9c5853a..f6de995da 100644 --- a/matRad/bioModels/LQbasedModels/kernelBasedModels/matRad_LQKernelBasedModel.m +++ b/matRad/bioModels/LQbasedModels/kernelBasedModels/matRad_LQKernelBasedModel.m @@ -6,7 +6,7 @@ % tissue classes. % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2023 the matRad development team. +% Copyright 2023-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -98,4 +98,4 @@ end end -end \ No newline at end of file +end diff --git a/matRad/bioModels/LQbasedModels/matRad_LQBasedModel.m b/matRad/bioModels/LQbasedModels/matRad_LQBasedModel.m index 957c4a84d..ca0f99953 100644 --- a/matRad/bioModels/LQbasedModels/matRad_LQBasedModel.m +++ b/matRad/bioModels/LQbasedModels/matRad_LQBasedModel.m @@ -5,7 +5,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2023 the matRad development team. +% Copyright 2023-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -41,4 +41,4 @@ end end -end \ No newline at end of file +end diff --git a/matRad/bioModels/matRad_BiologicalModel.m b/matRad/bioModels/matRad_BiologicalModel.m index 283648ce4..e79de547e 100644 --- a/matRad/bioModels/matRad_BiologicalModel.m +++ b/matRad/bioModels/matRad_BiologicalModel.m @@ -23,7 +23,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2023 the matRad development team. + % Copyright 2023-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -79,7 +79,7 @@ end methods %(Static) - function [vTissueIndex] = getTissueInformation(this,~,~,~,vAlphaX,~,~,~) %(machine,cst,dij,vAlphaX,vBetaX,VdoseGrid, VdoseGridScenIdx) + function [vTissueIndex] = getTissueInformation(this,~,~,~,vAlphaX,~,~,~) % This is the default, should be masked by the specific model % subclass if needed @@ -261,4 +261,4 @@ end -end \ No newline at end of file +end diff --git a/matRad/bioModels/matRad_ConstantRBE.m b/matRad/bioModels/matRad_ConstantRBE.m index 4f01cdf0a..d1121b1f8 100644 --- a/matRad/bioModels/matRad_ConstantRBE.m +++ b/matRad/bioModels/matRad_ConstantRBE.m @@ -4,7 +4,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2023 the matRad development team. +% Copyright 2023-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -34,4 +34,4 @@ end -end \ No newline at end of file +end diff --git a/matRad/bioModels/matRad_EmptyBiologicalModel.m b/matRad/bioModels/matRad_EmptyBiologicalModel.m index daf7e4e27..822add497 100644 --- a/matRad/bioModels/matRad_EmptyBiologicalModel.m +++ b/matRad/bioModels/matRad_EmptyBiologicalModel.m @@ -4,7 +4,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2023 the matRad development team. +% Copyright 2023-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -26,4 +26,4 @@ this@matRad_BiologicalModel(); end end -end \ No newline at end of file +end diff --git a/matRad/bioModels/matRad_bioModel.m b/matRad/bioModels/matRad_bioModel.m index 4d5bdb725..11622b11d 100644 --- a/matRad/bioModels/matRad_bioModel.m +++ b/matRad/bioModels/matRad_bioModel.m @@ -1,16 +1,15 @@ function model = matRad_bioModel(radiationMode, model, providedQuantities) -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % matRad_bioModel % This is a helper function to instantiate a matRad_BiologicalModel. This % function currently exists for downwards compatability, as the new % Biological Models will follow a polymorphic software architecture % -% call +% call: % matRad_bioModel(radiationMode, model) % % e.g. pln.bioModel = matRad_bioModel('protons','MCN') % -% input +% input: % radiationMode: radiation modality 'photons' 'protons' 'helium' 'carbon' 'brachy' % % model: string to denote which biological model is used @@ -21,14 +20,14 @@ % providedQuantities: optional cell string of provided quantities to % check if the model can be evaluated % -% output +% output: % model: instance of a biological model % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2023 the matRad development team. +% Copyright 2023-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -45,4 +44,4 @@ model = matRad_BiologicalModel.validate(model,radiationMode,providedQuantities); end -end % end function \ No newline at end of file +end % end function diff --git a/matRad/bioModels/matRad_getPhotonLQMParameters.m b/matRad/bioModels/matRad_getPhotonLQMParameters.m index 0b7dacc87..7c2a6c64c 100644 --- a/matRad/bioModels/matRad_getPhotonLQMParameters.m +++ b/matRad/bioModels/matRad_getPhotonLQMParameters.m @@ -1,16 +1,16 @@ function [ax,bx] = matRad_getPhotonLQMParameters(cst,numVoxel,VdoseGrid) % matRad function to receive the photon LQM reference parameter % -% call +% call: % [ax,bx] = matRad_getPhotonLQMParameters(cst,numVoxel,ctScen,VdoseGrid) % -% input +% input: % cst: matRad cst struct % numVoxel: number of voxels of the dose cube % VdoseGrid: optional linear index vector that allows to specify subindices % for which ax and bx will be computed % -% output +% output: % ax: vector containing for each linear voxel index alpha_x % bx: vector containing for each linear voxel index beta_x % @@ -19,7 +19,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2018 the matRad development team. +% Copyright 2018-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -62,4 +62,4 @@ end end -end \ No newline at end of file +end diff --git a/matRad/brachytherapy/additionalScripts/matRad_plot2DFunc.m b/matRad/brachytherapy/additionalScripts/matRad_plot2DFunc.m index b86f86b06..d20d8beeb 100644 --- a/matRad/brachytherapy/additionalScripts/matRad_plot2DFunc.m +++ b/matRad/brachytherapy/additionalScripts/matRad_plot2DFunc.m @@ -3,7 +3,7 @@ % load brachy_HDR machine manually from matRad/basedata! % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2021 the matRad development team. +% Copyright 2021-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -44,4 +44,4 @@ % ylabel('y[mm]') % zlabel('2D approx DoseRate') image(DoseRate,'CDataMapping','scaled') -colorbar \ No newline at end of file +colorbar diff --git a/matRad/brachytherapy/matRad_getDistanceMatrix.m b/matRad/brachytherapy/matRad_getDistanceMatrix.m index 73c15c79b..6b8964145 100644 --- a/matRad/brachytherapy/matRad_getDistanceMatrix.m +++ b/matRad/brachytherapy/matRad_getDistanceMatrix.m @@ -2,28 +2,27 @@ % matRad_getDistanceMatrix gets (seedpoint x dosepoint) matrix of relative % distances % -% call -% [DistanceMatrix,DistanceVector] = getDistanceMatrix(seedPoints,... -% dosePoints) +% call: +% [DistanceMatrix,DistanceVector] = getDistanceMatrix(seedPoints, dosePoints) % normally called within matRad_getBrachyDose % -% input +% +% input: % seedPoints: struct with fields x,y,z % dosePoints: struct with fields x,y,z % -% output +% output: % distance matrix: rows: index of dosepoint % columns: index of deedpoint % entry: distance of seedpoints and dosepoint in mm -% | -% | DistanceMatrix.x/y/z: x/y/z component of -% distance(needed for theta calc) -% | DistanceMatrix.dist: eucledian distance +% DistanceMatrix.x/y/z: x/y/z component of +% distance(needed for theta calc) +% DistanceMatrix.dist: eucledian distance % distance vector: column vector of DistanceMatrix.dist entries % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2021 the matRad development team. +% Copyright 2021-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/brachytherapy/matRad_getTemplateRoot.m b/matRad/brachytherapy/matRad_getTemplateRoot.m index 6eb7470c3..231d9d27e 100644 --- a/matRad/brachytherapy/matRad_getTemplateRoot.m +++ b/matRad/brachytherapy/matRad_getTemplateRoot.m @@ -1,20 +1,20 @@ function templateRoot = matRad_getTemplateRoot(ct,cst) %matRad_getTemplateRoot calculates origin position for template % -% call +% call: % matRad_getTemplateRoot(ct,cst) % -% input +% input: % ct: ct cube % cst: matRad cst struct % -% output +% output: % templateRoot: 1x3 column vector with root position % x,y : center \\ z : bottom of target VOI % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2021 the matRad development team. +% Copyright 2021-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/dicom/@matRad_DicomExporter/matRad_DicomExporter.m b/matRad/dicom/@matRad_DicomExporter/matRad_DicomExporter.m index 6e7831789..5a198c63c 100644 --- a/matRad/dicom/@matRad_DicomExporter/matRad_DicomExporter.m +++ b/matRad/dicom/@matRad_DicomExporter/matRad_DicomExporter.m @@ -14,7 +14,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2020 the matRad development team. + % Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/dicom/@matRad_DicomExporter/matRad_exportDicom.m b/matRad/dicom/@matRad_DicomExporter/matRad_exportDicom.m index 69ceacbbc..a6a702230 100644 --- a/matRad/dicom/@matRad_DicomExporter/matRad_exportDicom.m +++ b/matRad/dicom/@matRad_DicomExporter/matRad_exportDicom.m @@ -2,7 +2,7 @@ % matRad function to export current workspace to DICOM. % Function of matRad_DicomExporter % -% call +% call: % matRad_DicomExporter.matRad_exportDicom() % % References @@ -10,7 +10,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2019 the matRad development team. +% Copyright 2019-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/dicom/@matRad_DicomExporter/matRad_exportDicomCt.m b/matRad/dicom/@matRad_DicomExporter/matRad_exportDicomCt.m index 2dc7841de..721401174 100644 --- a/matRad/dicom/@matRad_DicomExporter/matRad_exportDicomCt.m +++ b/matRad/dicom/@matRad_DicomExporter/matRad_exportDicomCt.m @@ -2,7 +2,7 @@ % matRad function to export ct to dicom. % Class method of matRad_DicomExporter % -% call +% call: % matRad_DicomExporter.matRad_exportDicomCt() % % @@ -11,7 +11,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2019 the matRad development team. +% Copyright 2019-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -110,7 +110,7 @@ obj.ctSliceMetas(i).ImagePositionPatient = [ct.x(1); ct.y(1); ct.z(i)]; - obj.ctSliceMetas(i).SlicePositions = z(i); + obj.ctSliceMetas(i).SliceLocation = z(i); %Create and store unique ID obj.ctSliceMetas(i).SOPClassUID = ClassUID; diff --git a/matRad/dicom/@matRad_DicomExporter/matRad_exportDicomRTDoses.m b/matRad/dicom/@matRad_DicomExporter/matRad_exportDicomRTDoses.m index c0d6380a1..a61047d5c 100644 --- a/matRad/dicom/@matRad_DicomExporter/matRad_exportDicomRTDoses.m +++ b/matRad/dicom/@matRad_DicomExporter/matRad_exportDicomRTDoses.m @@ -2,7 +2,7 @@ % matRad function to exportt resultGUI to dicom RT dose. % Function of matRad_DicomExporter % -% call +% call: % matRad_DicomExporter.matRad_exportDicomRTDoses() % % References @@ -10,7 +10,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/dicom/@matRad_DicomExporter/matRad_exportDicomRTPlan.m b/matRad/dicom/@matRad_DicomExporter/matRad_exportDicomRTPlan.m index 180cef941..69003700c 100644 --- a/matRad/dicom/@matRad_DicomExporter/matRad_exportDicomRTPlan.m +++ b/matRad/dicom/@matRad_DicomExporter/matRad_exportDicomRTPlan.m @@ -7,7 +7,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -120,11 +120,6 @@ %meta.DoseReferenceSquence.Item_1. ... -if obj.pln.propStf.numOfBeams ~= numel(obj.stf) - matRad_cfg.error('Inconsistency in stf! number of beams not matching!'); -end - - %Sequences %Sequences %ToleranceTableSequence - Optional, we do not write this @@ -133,7 +128,7 @@ % meta.FractionGroupSequence.Item_1. meta.FractionGroupSequence.Item_1.FractionGroupNumber = 1; meta.FractionGroupSequence.Item_1.NumberOfFractionsPlanned = obj.pln.numOfFractions; -meta.FractionGroupSequence.Item_1.NumberOfBeams = obj.pln.propStf.numOfBeams; +meta.FractionGroupSequence.Item_1.NumberOfBeams = numel(obj.stf); meta.FractionGroupSequence.Item_1.NumberOfBrachyApplicationSetups = 0; % meta.FractionGroupSequence.Item_1.BeamDoseMeaning = 'FRACTION_LEVEL'; %TODO: This is probably no longer necessary. @@ -152,7 +147,7 @@ end -for iBeam = 1:obj.pln.propStf.numOfBeams +for iBeam = 1:numel(obj.stf) matRad_cfg.dispInfo('\tBeam %d: ',iBeam); %PatientSetupSequence - Required @@ -165,7 +160,7 @@ if strcmp(obj.pln.radiationMode,'photons') BeamParam = 'BeamSequence'; ControlParam = 'ControlPointSequence'; - elseif strcmp(obj.pln.radiationmode, 'protons') ||strcmp(obj.pln.radiationmode, 'helium') || strcmp(obj.pln.radiationmode, 'carbon') + elseif strcmp(obj.pln.radiationMode, 'protons') ||strcmp(obj.pln.radiationMode, 'helium') || strcmp(obj.pln.radiationMode, 'carbon') BeamParam = 'IonBeamSequence'; ControlParam = 'IonControlPointSequence'; else diff --git a/matRad/dicom/@matRad_DicomExporter/matRad_exportDicomRTStruct.m b/matRad/dicom/@matRad_DicomExporter/matRad_exportDicomRTStruct.m index 0de043444..4aba7dbc6 100644 --- a/matRad/dicom/@matRad_DicomExporter/matRad_exportDicomRTStruct.m +++ b/matRad/dicom/@matRad_DicomExporter/matRad_exportDicomRTStruct.m @@ -2,7 +2,7 @@ % matRad function to export dicom RT structure set. % Class method of matRad_DicomExporter % -% call +% call: % matRad_DicomExporter.matRad_exportDicomRTStruct() % % @@ -11,7 +11,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/dicom/@matRad_DicomImporter/matRad_DicomImporter.m b/matRad/dicom/@matRad_DicomImporter/matRad_DicomImporter.m index 1c3368182..04aadbf6d 100644 --- a/matRad/dicom/@matRad_DicomImporter/matRad_DicomImporter.m +++ b/matRad/dicom/@matRad_DicomImporter/matRad_DicomImporter.m @@ -9,7 +9,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2020 the matRad development team. + % Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/dicom/@matRad_DicomImporter/matRad_calcHU.m b/matRad/dicom/@matRad_DicomImporter/matRad_calcHU.m index ea651ee5d..3535adbcc 100644 --- a/matRad/dicom/@matRad_DicomImporter/matRad_calcHU.m +++ b/matRad/dicom/@matRad_DicomImporter/matRad_calcHU.m @@ -9,7 +9,7 @@ % % HU = IV * slope + intercept % -% call +% call: % obj = matRad_calcHU(obj) % % @@ -18,7 +18,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/dicom/@matRad_DicomImporter/matRad_createCst.m b/matRad/dicom/@matRad_DicomImporter/matRad_createCst.m index bb5b202af..782e0b7d8 100644 --- a/matRad/dicom/@matRad_DicomImporter/matRad_createCst.m +++ b/matRad/dicom/@matRad_DicomImporter/matRad_createCst.m @@ -7,7 +7,7 @@ % % Output - matRad cst structure % -% call +% call: % obj = matRad_createCst(obj) % % @@ -16,7 +16,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/dicom/@matRad_DicomImporter/matRad_dummyCst.m b/matRad/dicom/@matRad_DicomImporter/matRad_dummyCst.m index 7534ddc4b..855959290 100644 --- a/matRad/dicom/@matRad_DicomImporter/matRad_dummyCst.m +++ b/matRad/dicom/@matRad_DicomImporter/matRad_dummyCst.m @@ -7,7 +7,7 @@ % % Output - matRad cst structure % -% call +% call: % obj = matRad_dummyCst(obj) % % References @@ -15,7 +15,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/dicom/@matRad_DicomImporter/matRad_importDicom.m b/matRad/dicom/@matRad_DicomImporter/matRad_importDicom.m index 2dcdba1ee..f6a44faad 100644 --- a/matRad/dicom/@matRad_DicomImporter/matRad_importDicom.m +++ b/matRad/dicom/@matRad_DicomImporter/matRad_importDicom.m @@ -2,18 +2,21 @@ function matRad_importDicom(obj) % matRad wrapper function to import a predefined set of dicom files files % into matRad's native data formats % -% In your object, there must be properties that contain: +% In your object, there must be properties that contain: +% % - list of files to be imported. +% % Optional: -% - а boolean; if you don't want to import complete DICOM information, -% set it to false. % -% Next matRad structures are created in the object and saved in the -% workspace: +% - a boolean; if you don't want to import complete DICOM information, set it to false. +% +% Next matRad structures are created in the object and saved in the workspace: +% % - ct, cst, stf, pln, resultGUI. -% *to save them as .mat file you can use matRad_importDicomWidget % -% call +% To save them as .mat file you can use matRad_importDicomWidget +% +% call: % matRad_importDicom(obj) % % References @@ -21,7 +24,7 @@ function matRad_importDicom(obj) % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/dicom/@matRad_DicomImporter/matRad_importDicomCt.m b/matRad/dicom/@matRad_DicomImporter/matRad_importDicomCt.m index 4b4172f8d..ed17b2d74 100644 --- a/matRad/dicom/@matRad_DicomImporter/matRad_importDicomCt.m +++ b/matRad/dicom/@matRad_DicomImporter/matRad_importDicomCt.m @@ -2,12 +2,13 @@ % matRad function to import dicom ct data % % In your object, there must be properties that contain: +% % - list of dicom ct files; -% - resolution of the imported ct cube, i.e. this function will -% interpolate to a different resolution if desired; -% - a boolean, if you don't want to import complete dicom information set -% it false. +% - resolution of the imported ct cube, i.e. this function will interpolate to a different resolution if desired; +% - a boolean, if you don't want to import complete dicom information set it false. +% % Optional: +% % - a priori grid specified for interpolation; % - a boolean to turn off/on visualization. % @@ -16,7 +17,7 @@ % electron denisities. Hounsfield units are converted using a standard % lookup table in matRad_calcWaterEqD % -% call +% call: % matRad_importDicomCt(obj) % % @@ -25,7 +26,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/dicom/@matRad_DicomImporter/matRad_importDicomRTDose.m b/matRad/dicom/@matRad_DicomImporter/matRad_importDicomRTDose.m index 93f49a979..83d587b2d 100644 --- a/matRad/dicom/@matRad_DicomImporter/matRad_importDicomRTDose.m +++ b/matRad/dicom/@matRad_DicomImporter/matRad_importDicomRTDose.m @@ -11,7 +11,7 @@ % Note that the summation (called plan) of the beams is named without % subscripts, e.g. physical_Dose. % -% call +% call: % obj = matRad_importDicomRTDose(obj) % % @@ -20,7 +20,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/dicom/@matRad_DicomImporter/matRad_importDicomRTPlan.m b/matRad/dicom/@matRad_DicomImporter/matRad_importDicomRTPlan.m index 8da336473..940d0ed41 100644 --- a/matRad/dicom/@matRad_DicomImporter/matRad_importDicomRTPlan.m +++ b/matRad/dicom/@matRad_DicomImporter/matRad_importDicomRTPlan.m @@ -2,15 +2,16 @@ % matRad function to import dicom RTPLAN data % % In your object, there must be properties that contain: +% % - ct imported by the matRad_importDicomCt function; % - list of RTPlan Dicom files; -% - a boolean, if you don't want to import whole dicom information set it -% false. +% - a boolean, if you don't want to import whole dicom information set it false. +% % % Output - matRad pln structure with meta information. % Note that bixelWidth is determined via the importSteering function. % -% call +% call: % obj = matRad_importDicomRTPlan(obj) % % @@ -19,7 +20,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -149,7 +150,6 @@ obj.pln.propStf.bixelWidth = NaN; % [mm] / also corresponds to lateral spot spacing for particles obj.pln.propStf.gantryAngles = [gantryAngles{1:length(BeamSeqNames)}]; obj.pln.propStf.couchAngles = [PatientSupportAngle{1:length(BeamSeqNames)}]; % [??] -obj.pln.propStf.numOfBeams = length(BeamSeqNames); numOfVoxels = 1; for i = 1:length(obj.ct.cubeDim) numOfVoxels = numOfVoxels*obj.ct.cubeDim(i); diff --git a/matRad/dicom/@matRad_DicomImporter/matRad_importDicomRtss.m b/matRad/dicom/@matRad_DicomImporter/matRad_importDicomRtss.m index b70bcea79..63c6b0aed 100644 --- a/matRad/dicom/@matRad_DicomImporter/matRad_importDicomRtss.m +++ b/matRad/dicom/@matRad_DicomImporter/matRad_importDicomRtss.m @@ -11,7 +11,7 @@ % Output - structure containing names, numbers, colors and coordinates % of the polygon segmentations. % -% call +% call: % obj = matRad_importDicomRtss(obj) % % @@ -20,7 +20,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/dicom/@matRad_DicomImporter/matRad_importDicomSteeringParticles.m b/matRad/dicom/@matRad_DicomImporter/matRad_importDicomSteeringParticles.m index fa3ed34b8..f6083ceda 100644 --- a/matRad/dicom/@matRad_DicomImporter/matRad_importDicomSteeringParticles.m +++ b/matRad/dicom/@matRad_DicomImporter/matRad_importDicomSteeringParticles.m @@ -1,6 +1,6 @@ function obj = matRad_importDicomSteeringParticles(obj) % matRad function to import a matRad stf struct from dicom RTPLAN data -% +% % In your object, there must be properties that contain: % - ct imported by the matRad_importDicomCt function; % - matRad pln structure with meta information; @@ -9,7 +9,7 @@ % Output - matRad stf and pln structures. % Note: pln is input and output since pln.bixelWidth is determined here. % -% call +% call: % obj = matRad_importDicomSteeringParticles(obj) % % @@ -18,16 +18,16 @@ % % Note % not implemented - compensator. Fixed SAD. -% +% % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. -% -% This file is part of the matRad project. It is subject to the license -% terms in the LICENSE file found in the top-level directory of this -% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part -% of the matRad project, including this file, may be copied, modified, -% propagated, or distributed except according to the terms contained in the +% Copyright 2015-2026 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the % LICENSE file. % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -40,23 +40,23 @@ matRad_cfg = MatRad_Config.instance(); matRad_checkEnvDicomRequirements(matRad_cfg.env); -dlgBaseDataText = ['Import steering information from DICOM Plan.','Choose corresponding matRad base data for ', ... - obj.pln.radiationMode, '.']; +dlgBaseDataText = ['Import steering information from DICOM Plan.', 'Choose corresponding matRad base data for ', ... + obj.pln.radiationMode, '.']; % messagebox only necessary for non windows users if ~ispc - uiwait(helpdlg(dlgBaseDataText,['DICOM import - ', obj.pln.radiationMode, ' base data' ])); + uiwait(helpdlg(dlgBaseDataText, ['DICOM import - ', obj.pln.radiationMode, ' base data'])); end -[fileName,pathName] = uigetfile([matRad_cfg.matRadSrcRoot filesep 'basedata' filesep '*.mat'], dlgBaseDataText); +[fileName, pathName] = uigetfile([matRad_cfg.matRadSrcRoot filesep 'basedata' filesep '*.mat'], dlgBaseDataText); load([pathName filesep fileName]); ix = find(fileName == '_'); -obj.pln.machine = fileName(ix(1)+1:end-4); +obj.pln.machine = fileName(ix(1) + 1:end - 4); % RT Plan consists only on meta information -if matRad_cfg.isOctave || verLessThan('matlab','9') +if matRad_cfg.isOctave || verLessThan('matlab', '9') rtPlanInfo = dicominfo(obj.importFiles.rtplan{1}); else - rtPlanInfo = dicominfo(obj.importFiles.rtplan{1},'UseDictionaryVR',true); + rtPlanInfo = dicominfo(obj.importFiles.rtplan{1}, 'UseDictionaryVR', true); end BeamSeq = rtPlanInfo.IonBeamSequence; BeamSeqNames = fieldnames(BeamSeq); @@ -68,8 +68,8 @@ currBeamSeq = BeamSeq.(BeamSeqNames{i}); try treatDelType = currBeamSeq.TreatmentDeliveryType; - if ~strcmpi(treatDelType,'TREATMENT') - BeamSeq = rmfield(BeamSeq,BeamSeqNames{i}); + if ~strcmpi(treatDelType, 'TREATMENT') + BeamSeq = rmfield(BeamSeq, BeamSeqNames{i}); end catch warning('Something went wrong while determining the type of the beam.'); @@ -79,23 +79,27 @@ % reinitialize the BeamSeqNames and length, as the Seq itself is reduced. BeamSeqNames = fieldnames(BeamSeq); -% remove empty ControlPointSequences +% remove empty ControlPointSequences (PBS beams only) for i = 1:length(BeamSeqNames) currBeamSeq = BeamSeq.(BeamSeqNames{i}); + if strcmp(currBeamSeq.ScanMode, 'NONE') + % Passive scattering beam - no scan spot pruning applicable + continue + end ControlPointSeq = currBeamSeq.IonControlPointSequence; ControlPointSeqNames = fieldnames(ControlPointSeq); numOfContrPointSeq = length(ControlPointSeqNames); for currContr = 1:numOfContrPointSeq currContrSeq = ControlPointSeq.(ControlPointSeqNames{currContr}); if sum(currContrSeq.ScanSpotMetersetWeights) == 0 - ControlPointSeq = rmfield(ControlPointSeq,ControlPointSeqNames{currContr}); + ControlPointSeq = rmfield(ControlPointSeq, ControlPointSeqNames{currContr}); end end BeamSeq.(BeamSeqNames{i}).IonControlPointSequence = ControlPointSeq; end % check if number of beams correspond -if ~isequal(length(BeamSeqNames),numOfBeamsPlan) +if ~isequal(length(BeamSeqNames), numOfBeamsPlan) warning('Number of beams from beamsequences do not correspond to number of Gantry Angles'); end @@ -114,8 +118,7 @@ obj.stf(length(BeamSeqNames)).numOfBixelsPerRay = []; obj.stf(length(BeamSeqNames)).totalNumOfBixels = []; obj.stf(length(BeamSeqNames)).ray = []; - - +obj.stf(length(BeamSeqNames)).deliveryType = []; for i = 1:length(BeamSeqNames) currBeamSeq = BeamSeq.(BeamSeqNames{i}); @@ -124,123 +127,178 @@ obj.stf(i).couchAngle = obj.pln.propStf.couchAngles(i); obj.stf(i).bixelWidth = obj.pln.propStf.bixelWidth; obj.stf(i).radiationMode = obj.pln.radiationMode; - % there might be several SAD's, e.g. compensator? - obj.stf(i).SAD_x = currBeamSeq.VirtualSourceAxisDistances(1); - obj.stf(i).SAD_y = currBeamSeq.VirtualSourceAxisDistances(2); - %stf(i).SAD = machine.meta.SAD; %we write the SAD later when we check machine match - %stf(i).sourcePoint_bev = [0 -stf(i).SAD 0]; - obj.stf(i).isoCenter = obj.pln.propStf.isoCenter(i,:); - - % now loop over ControlPointSequences - ControlPointSeqNames = fieldnames(ControlPointSeq); - numOfContrPointSeq = length(ControlPointSeqNames); - % create empty helper matrix - temporarySteering = zeros(0,8); - for currContr = 1:numOfContrPointSeq - currContrSeq = ControlPointSeq.(ControlPointSeqNames{currContr}); - % get energy, equal for all coming elements in the next loop - currEnergy = currContrSeq.NominalBeamEnergy; - % get focusValue - currFocus = unique(currContrSeq.ScanningSpotSize); - % get the Spotpositions - numOfScanSpots = currContrSeq.NumberOfScanSpotPositions; - % x is 1, 3, 5 ...; y 2, 4, 6, - c1_help = currContrSeq.ScanSpotPositionMap(1:2:(2 * numOfScanSpots)); - c2_help = currContrSeq.ScanSpotPositionMap(2:2:(2 * numOfScanSpots)); - weight_help = currContrSeq.ScanSpotMetersetWeights; - if isfield(currContrSeq, 'RangeShifterSettingsSequence') - % rangeshifter identification - rashiID = currContrSeq.RangeShifterSettingsSequence.Item_1.ReferencedRangeShifterNumber; - % rangeshifter waterequivalent thickness - rashiWeThickness = currContrSeq.RangeShifterSettingsSequence.Item_1.RangeShifterWaterEquivalentThickness; - % rangeshifter isocenter to range shifter distance - rashiIsoRangeDist = currContrSeq.RangeShifterSettingsSequence.Item_1.IsocenterToRangeShifterDistance; - elseif currContr == 1 - rashiID = 0; - rashiWeThickness = 0; - rashiIsoRangeDist = 0; - else - % in this case range shifter settings has not changed between this - % and previous control sequence, so reuse values. - end - temporarySteering = [temporarySteering; c1_help c2_help ... - (currEnergy * ones(numOfScanSpots,1)) weight_help (currFocus * ones(numOfScanSpots,1)) ... - (rashiID * ones(numOfScanSpots,1)) (rashiWeThickness * ones(numOfScanSpots,1)) (rashiIsoRangeDist * ones(numOfScanSpots,1))]; - end - - % finds all unique rays and saves them in to the stf - [RayPosTmp, ~, ic] = unique(temporarySteering(:,1:2), 'rows'); - clear ray; - for j = 1:size(RayPosTmp,1) - obj.stf(i).ray(j).rayPos_bev = double([RayPosTmp(j,1) 0 RayPosTmp(j,2)]); - obj.stf(i).ray(j).energy = []; - obj.stf(i).ray(j).focusFWHM = []; - obj.stf(i).ray(j).focusIx = []; - obj.stf(i).ray(j).weight = []; - obj.stf(i).ray(j).rangeShifter = struct(); - ray(j).ID = []; - ray(j).eqThickness = []; - ray(j).sourceRashiDistance = []; - end - - % saves all energies and weights to their corresponding ray - for j = 1:size(temporarySteering,1) - k = ic(j); - obj.stf(i).ray(k).energy = [obj.stf(i).ray(k).energy double(temporarySteering(j,3))]; - obj.stf(i).ray(k).focusFWHM = [obj.stf(i).ray(k).focusFWHM double(temporarySteering(j,5))]; - obj.stf(i).ray(k).weight = [obj.stf(i).ray(k).weight double(temporarySteering(j,4)) / 1e6]; - % helpers to construct something like a(:).b = c.b(:) after this - % loop - ray(k).ID = [ray(k).ID double(temporarySteering(j,6))]; - ray(k).eqThickness = [ray(k).eqThickness double(temporarySteering(j,7))]; - ray(k).sourceRashiDistance = [ray(k).sourceRashiDistance double(temporarySteering(j,8))]; + % SAD - PBS beams provide two virtual source distances, passive beams a single scalar + if isfield(currBeamSeq, 'VirtualSourceAxisDistances') + obj.stf(i).SAD_x = currBeamSeq.VirtualSourceAxisDistances(1); + obj.stf(i).SAD_y = currBeamSeq.VirtualSourceAxisDistances(2); + elseif isfield(currBeamSeq, 'SourceAxisDistance') + obj.stf(i).SAD_x = currBeamSeq.SourceAxisDistance; + obj.stf(i).SAD_y = currBeamSeq.SourceAxisDistance; + else + matRad_cfg.dispWarning('Could not determine SAD for beam %d. Setting to NaN.', i); + obj.stf(i).SAD_x = NaN; + obj.stf(i).SAD_y = NaN; end - - % reassign to preserve data structure - for j = 1:numel(ray) - for k = 1:numel(ray(j).ID) - obj.stf(i).ray(j).rangeShifter(k).ID = ray(j).ID(k); - obj.stf(i).ray(j).rangeShifter(k).eqThickness = ray(j).eqThickness(k); - obj.stf(i).ray(j).rangeShifter(k).sourceRashiDistance = obj.stf(i).SAD - ray(j).sourceRashiDistance(k); + % stf(i).SAD = machine.meta.SAD; %we write the SAD later when we check machine match + % stf(i).sourcePoint_bev = [0 -stf(i).SAD 0]; + obj.stf(i).isoCenter = obj.pln.propStf.isoCenter(i, :); + + if strcmp(currBeamSeq.ScanMode, 'NONE') + % Passive scattering beam: import geometry and represent as a single central ray/bixel. + % The beam is not yet calculable in matRad, but the stf structure is kept consistent + % so it can be integrated with future compensator-based pencil-beam adaptations. + matRad_cfg.dispWarning('Beam %d uses passive scattering (ScanMode NONE). Imported as single central ray, but not yet calculable in matRad!', i); + obj.stf(i).deliveryType = 'passiveScattering'; + + % Extract energy, total weight and range shifter from control points + ControlPointSeqNames = fieldnames(ControlPointSeq); + beamEnergy = NaN; + beamWeight = 1; + rashiID = 0; + rashiWeThickness = 0; + rashiIsoRangeDist = 0; + for currContr = 1:length(ControlPointSeqNames) + currContrSeq = ControlPointSeq.(ControlPointSeqNames{currContr}); + if isnan(beamEnergy) && isfield(currContrSeq, 'NominalBeamEnergy') + beamEnergy = double(currContrSeq.NominalBeamEnergy); + end + if isfield(currContrSeq, 'FinalCumulativeMetersetWeight') + beamWeight = double(currContrSeq.FinalCumulativeMetersetWeight); + end + if isfield(currContrSeq, 'RangeShifterSettingsSequence') + rashiID = currContrSeq.RangeShifterSettingsSequence.Item_1.ReferencedRangeShifterNumber; + rashiWeThickness = currContrSeq.RangeShifterSettingsSequence.Item_1.RangeShifterWaterEquivalentThickness; + rashiIsoRangeDist = currContrSeq.RangeShifterSettingsSequence.Item_1.IsocenterToRangeShifterDistance; + end end - end - - - % getting some information of the rays - % clean up energies, so they appear only one time per energy - numOfRays = size(obj.stf(i).ray,2); - for l = 1:numOfRays - obj.stf(i).ray(l).energy = unique(obj.stf(i).ray(l).energy); - end - obj.stf(i).numOfRays = numel(obj.stf(i).ray); - - % save total number of bixels - numOfBixels = 0; - for j = 1:numel(obj.stf(i).ray) - numOfBixels = numOfBixels + numel(obj.stf(i).ray(j).energy); - obj.stf(i).numOfBixelsPerRay(j) = numel(obj.stf(i).ray(j).energy); -% w = [w stf(currBeam).ray(j).weight]; - end - - obj.stf(i).totalNumOfBixels = numOfBixels; - - % get bixelwidth - bixelWidth_help = zeros(size(obj.stf(i).ray,2),2); - for j = 1:obj.stf(i).numOfRays - bixelWidth_help(j,1) = obj.stf(i).ray(j).rayPos_bev(1); - bixelWidth_help(j,2) = obj.stf(i).ray(j).rayPos_bev(3); - end - bixelWidth_help1 = unique(round(1e3*bixelWidth_help(:,1))/1e3,'sorted'); - bixelWidth_help2 = unique(round(1e3*bixelWidth_help(:,2))/1e3,'sorted'); - - bixelWidth = unique([unique(diff(bixelWidth_help1))' unique(diff(bixelWidth_help2))']); - - if numel(bixelWidth) == 1 - obj.stf(i).bixelWidth = bixelWidth; + + % Single central ray at isocenter (BEV coordinates) + obj.stf(i).ray(1).rayPos_bev = [0 0 0]; + obj.stf(i).ray(1).energy = beamEnergy; + obj.stf(i).ray(1).focusFWHM = NaN; % not applicable for passive delivery + obj.stf(i).ray(1).focusIx = 1; + obj.stf(i).ray(1).weight = beamWeight / 1e6; + obj.stf(i).ray(1).rangeShifter(1).ID = rashiID; + obj.stf(i).ray(1).rangeShifter(1).eqThickness = rashiWeThickness; + obj.stf(i).ray(1).rangeShifter(1).sourceRashiDistance = obj.stf(i).SAD - rashiIsoRangeDist; + + obj.stf(i).numOfRays = 1; + obj.stf(i).numOfBixelsPerRay = 1; + obj.stf(i).totalNumOfBixels = 1; + obj.stf(i).bixelWidth = NaN; else - obj.stf(i).bixelWidth = NaN; - end - + obj.stf(i).deliveryType = 'scanned'; + + % now loop over ControlPointSequences + ControlPointSeqNames = fieldnames(ControlPointSeq); + numOfContrPointSeq = length(ControlPointSeqNames); + % create empty helper matrix + temporarySteering = zeros(0, 8); + for currContr = 1:numOfContrPointSeq + currContrSeq = ControlPointSeq.(ControlPointSeqNames{currContr}); + % get energy, equal for all coming elements in the next loop + if ~isfield(currContrSeq, 'NominalBeamEnergy') + continue + end + currEnergy = currContrSeq.NominalBeamEnergy; + % get focusValue and spot positions + currFocus = unique(currContrSeq.ScanningSpotSize); + numOfScanSpots = currContrSeq.NumberOfScanSpotPositions; + % x is 1, 3, 5 ...; y 2, 4, 6, + c1_help = currContrSeq.ScanSpotPositionMap(1:2:(2 * numOfScanSpots)); + c2_help = currContrSeq.ScanSpotPositionMap(2:2:(2 * numOfScanSpots)); + weight_help = currContrSeq.ScanSpotMetersetWeights; + + if isfield(currContrSeq, 'RangeShifterSettingsSequence') + % rangeshifter identification + rashiID = currContrSeq.RangeShifterSettingsSequence.Item_1.ReferencedRangeShifterNumber; + % rangeshifter waterequivalent thickness + rashiWeThickness = currContrSeq.RangeShifterSettingsSequence.Item_1.RangeShifterWaterEquivalentThickness; + % rangeshifter isocenter to range shifter distance + rashiIsoRangeDist = currContrSeq.RangeShifterSettingsSequence.Item_1.IsocenterToRangeShifterDistance; + elseif currContr == 1 + rashiID = 0; + rashiWeThickness = 0; + rashiIsoRangeDist = 0; + else + % in this case range shifter settings has not changed between this + % and previous control sequence, so reuse values. + end + temporarySteering = [temporarySteering; c1_help c2_help ... + (currEnergy * ones(numOfScanSpots, 1)) weight_help (currFocus * ones(numOfScanSpots, 1)) ... + (rashiID * ones(numOfScanSpots, 1)) (rashiWeThickness * ones(numOfScanSpots, 1)) (rashiIsoRangeDist * ones(numOfScanSpots, 1))]; + end + + % finds all unique rays and saves them in to the stf + [RayPosTmp, ~, ic] = unique(temporarySteering(:, 1:2), 'rows'); + clear ray; + for j = 1:size(RayPosTmp, 1) + obj.stf(i).ray(j).rayPos_bev = double([RayPosTmp(j, 1) 0 RayPosTmp(j, 2)]); + obj.stf(i).ray(j).energy = []; + obj.stf(i).ray(j).focusFWHM = []; + obj.stf(i).ray(j).focusIx = []; + obj.stf(i).ray(j).weight = []; + obj.stf(i).ray(j).rangeShifter = struct(); + ray(j).ID = []; + ray(j).eqThickness = []; + ray(j).sourceRashiDistance = []; + end + + % saves all energies and weights to their corresponding ray + for j = 1:size(temporarySteering, 1) + k = ic(j); + obj.stf(i).ray(k).energy = [obj.stf(i).ray(k).energy double(temporarySteering(j, 3))]; + obj.stf(i).ray(k).focusFWHM = [obj.stf(i).ray(k).focusFWHM double(temporarySteering(j, 5))]; + obj.stf(i).ray(k).weight = [obj.stf(i).ray(k).weight double(temporarySteering(j, 4)) / 1e6]; + % helpers to construct something like a(:).b = c.b(:) after this loop + ray(k).ID = [ray(k).ID double(temporarySteering(j, 6))]; + ray(k).eqThickness = [ray(k).eqThickness double(temporarySteering(j, 7))]; + ray(k).sourceRashiDistance = [ray(k).sourceRashiDistance double(temporarySteering(j, 8))]; + end + + % reassign to preserve data structure + for j = 1:numel(ray) + for k = 1:numel(ray(j).ID) + obj.stf(i).ray(j).rangeShifter(k).ID = ray(j).ID(k); + obj.stf(i).ray(j).rangeShifter(k).eqThickness = ray(j).eqThickness(k); + obj.stf(i).ray(j).rangeShifter(k).sourceRashiDistance = obj.stf(i).SAD - ray(j).sourceRashiDistance(k); + end + end + + % getting some information of the rays + % clean up energies, so they appear only one time per energy + numOfRays = size(obj.stf(i).ray, 2); + for l = 1:numOfRays + obj.stf(i).ray(l).energy = unique(obj.stf(i).ray(l).energy); + end + obj.stf(i).numOfRays = numel(obj.stf(i).ray); + + % save total number of bixels + numOfBixels = 0; + for j = 1:numel(obj.stf(i).ray) + numOfBixels = numOfBixels + numel(obj.stf(i).ray(j).energy); + obj.stf(i).numOfBixelsPerRay(j) = numel(obj.stf(i).ray(j).energy); + end + obj.stf(i).totalNumOfBixels = numOfBixels; + + % get bixelwidth + bixelWidth_help = zeros(size(obj.stf(i).ray, 2), 2); + for j = 1:obj.stf(i).numOfRays + bixelWidth_help(j, 1) = obj.stf(i).ray(j).rayPos_bev(1); + bixelWidth_help(j, 2) = obj.stf(i).ray(j).rayPos_bev(3); + end + bixelWidth_help1 = unique(round(1e3 * bixelWidth_help(:, 1)) / 1e3, 'sorted'); + bixelWidth_help2 = unique(round(1e3 * bixelWidth_help(:, 2)) / 1e3, 'sorted'); + + bixelWidth = unique([unique(diff(bixelWidth_help1))' unique(diff(bixelWidth_help2))']); + + if numel(bixelWidth) == 1 + obj.stf(i).bixelWidth = bixelWidth; + else + obj.stf(i).bixelWidth = NaN; + end + end % deliveryType / ScanMode branch + end %% check if matching given machine @@ -254,32 +312,32 @@ for j = 1:obj.stf(i).numOfRays % loop over all energies numOfEnergy = length(obj.stf(i).ray(j).energy); - for k = 1:numOfEnergy + for k = 1:numOfEnergy energyTemp = obj.stf(i).ray(j).energy(k); focusFWHM = obj.stf(i).ray(j).focusFWHM(k); - energyIndex = find(abs([machine.data(:).energy]-energyTemp)<10^-2); + energyIndex = find(abs([machine.data(:).energy] - energyTemp) < 10^-2); if isempty(energyIndex) machineNotMatching = true; - break; + break end - focusIndex = find(abs([machine.data(energyIndex).initFocus.SisFWHMAtIso] - focusFWHM )< 10^-3); + focusIndex = find(abs([machine.data(energyIndex).initFocus.SisFWHMAtIso] - focusFWHM) < 10^-3); if isempty(focusIndex) machineNotMatching = true; - break; + break end end - + if machineNotMatching - break; + break end end if machineNotMatching - break; + break end end - - %If the machine matches, format the stf for direct use. Otherwise, - %leave it be + + % If the machine matches, format the stf for direct use. Otherwise, + % leave it be if machineNotMatching matRad_cfg.dispInfo('not matching!\n'); matRad_cfg.dispWarning('The given machine does not match the steering info found in RTPlan. matRad will generate an stf, but it will be incompatible with the given machine and most likely not directly be usable in dose calculation!'); @@ -290,7 +348,7 @@ else matRad_cfg.dispInfo('matching!\n'); matRad_cfg.dispInfo('Formatting stf for use with given machine...'); - + for i = 1:numel(obj.stf) obj.stf(i).SAD = machine.meta.SAD; for j = 1:obj.stf(i).numOfRays @@ -298,50 +356,49 @@ % loop over all energies numOfEnergy = length(obj.stf(i).ray(j).energy); for k = 1:numOfEnergy - %If a corresponding machine was found, check assignment here + % If a corresponding machine was found, check assignment here if ~isempty(machine) energyTemp = obj.stf(i).ray(j).energy(k); focusFWHM = obj.stf(i).ray(j).focusFWHM(k); - energyIndex = find(abs([machine.data(:).energy]-energyTemp)<10^-2); - focusIndex = find(abs([machine.data(energyIndex).initFocus.SisFWHMAtIso] - focusFWHM )< 10^-3); - + energyIndex = find(abs([machine.data(:).energy] - energyTemp) < 10^-2); + focusIndex = find(abs([machine.data(energyIndex).initFocus.SisFWHMAtIso] - focusFWHM) < 10^-3); + obj.stf(i).ray(j).energy(k) = machine.data(energyIndex).energy; obj.stf(i).ray(j).focusIx(k) = focusIndex; obj.stf(i).ray(j).focusFWHM(k) = machine.data(energyIndex).initFocus.SisFWHMAtIso(obj.stf(i).ray(j).focusIx(k)); end - end + end end - end end -end +end %% Finalize geometry for i = 1:numel(obj.stf) % coordinate transformation with rotation matrix. % use transpose matrix because we are working with row vectors - rotMat_vectors_T = transpose(matRad_getRotationMatrix(obj.stf(i).gantryAngle,obj.stf(i).couchAngle)); - + rotMat_vectors_T = transpose(matRad_getRotationMatrix(obj.stf(i).gantryAngle, obj.stf(i).couchAngle)); + % set source point using (average/machine) SAD obj.stf(i).sourcePoint_bev = [0 -obj.stf(i).SAD 0]; % Rotated Source point (1st gantry, 2nd couch) - obj.stf(i).sourcePoint = obj.stf(i).sourcePoint_bev*rotMat_vectors_T; - + obj.stf(i).sourcePoint = obj.stf(i).sourcePoint_bev * rotMat_vectors_T; + % Save ray and target position in lps system. for j = 1:obj.stf(i).numOfRays - obj.stf(i).ray(j).targetPoint_bev = [2*obj.stf(i).ray(j).rayPos_bev(1) obj.stf(i).SAD 2*obj.stf(i).ray(j).rayPos_bev(3)]; - obj.stf(i).ray(j).rayPos = obj.stf(i).ray(j).rayPos_bev*rotMat_vectors_T; - obj.stf(i).ray(j).targetPoint = obj.stf(i).ray(j).targetPoint_bev*rotMat_vectors_T; + obj.stf(i).ray(j).targetPoint_bev = [2 * obj.stf(i).ray(j).rayPos_bev(1) obj.stf(i).SAD 2 * obj.stf(i).ray(j).rayPos_bev(3)]; + obj.stf(i).ray(j).rayPos = obj.stf(i).ray(j).rayPos_bev * rotMat_vectors_T; + obj.stf(i).ray(j).targetPoint = obj.stf(i).ray(j).targetPoint_bev * rotMat_vectors_T; end - + % book keeping & calculate focus index for j = 1:obj.stf(i).numOfRays - obj.stf(i).numOfBixelsPerRay(j) = numel([obj.stf(i).ray(j).energy]); - end - - obj.stf(i).timeStamp = datetime('now'); + obj.stf(i).numOfBixelsPerRay(j) = numel([obj.stf(i).ray(j).energy]); + end + + obj.stf(i).timeStamp = datetime('now'); end if any(isnan([obj.stf(:).bixelWidth])) || numel(unique([obj.stf(:).bixelWidth])) > 1 diff --git a/matRad/dicom/@matRad_DicomImporter/matRad_importDicomSteeringPhotons.m b/matRad/dicom/@matRad_DicomImporter/matRad_importDicomSteeringPhotons.m index c591699f2..52afa5d3d 100644 --- a/matRad/dicom/@matRad_DicomImporter/matRad_importDicomSteeringPhotons.m +++ b/matRad/dicom/@matRad_DicomImporter/matRad_importDicomSteeringPhotons.m @@ -6,7 +6,7 @@ % % Output - matRad stf and pln structures. % -% call +% call: % obj = matRad_importDicomSteeringPhotons(obj) % % @@ -15,7 +15,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/dicom/@matRad_DicomImporter/matRad_interpDicomDoseCube.m b/matRad/dicom/@matRad_DicomImporter/matRad_interpDicomDoseCube.m index 0fd4847f5..da9b6e742 100644 --- a/matRad/dicom/@matRad_DicomImporter/matRad_interpDicomDoseCube.m +++ b/matRad/dicom/@matRad_DicomImporter/matRad_interpDicomDoseCube.m @@ -10,7 +10,7 @@ % Output - structure with different actual current dose cube and several % meta data. % -% call +% call: % obj = matRad_interpDicomDoseCube(obj) % % @@ -19,7 +19,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/dicom/@matRad_DicomImporter/matRad_scanDicomImportFolder.m b/matRad/dicom/@matRad_DicomImporter/matRad_scanDicomImportFolder.m index b5874a40b..5938b3d05 100644 --- a/matRad/dicom/@matRad_DicomImporter/matRad_scanDicomImportFolder.m +++ b/matRad/dicom/@matRad_DicomImporter/matRad_scanDicomImportFolder.m @@ -9,7 +9,7 @@ % infomation (type, series number etc.) % - list of patients with dicom data in the folder % -% call +% call: % obj = matRad_scanDicomImportFolder(obj) % % @@ -18,7 +18,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/dicom/matRad_calcWaterEqD.m b/matRad/dicom/matRad_calcWaterEqD.m index 8f1d7ec80..b325ad7bc 100644 --- a/matRad/dicom/matRad_calcWaterEqD.m +++ b/matRad/dicom/matRad_calcWaterEqD.m @@ -2,15 +2,15 @@ % matRad function to calculate the equivalent densities from a dicom ct % that originally uses intensity values % -% call +% call: % ct = matRad_calcWaterEqD(ct, radiationMode) % -% input +% input: % ct: ct containing a cubeHU to compute rED/rSP values from % radiationMode: radiationMode as character array (e.g. 'photons') since matRad 3. % Can also be a pln-struct for downwards compatibility % -% output +% output: % ct: ct struct with cube with relative _electron_ densities stored in % ct.cube % @@ -19,7 +19,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2018 the matRad development team. +% Copyright 2018-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/dicom/matRad_checkEnvDicomRequirements.m b/matRad/dicom/matRad_checkEnvDicomRequirements.m index f7f3d9f7f..3d0d5ae06 100644 --- a/matRad/dicom/matRad_checkEnvDicomRequirements.m +++ b/matRad/dicom/matRad_checkEnvDicomRequirements.m @@ -2,10 +2,10 @@ % matRad function to check if requirements for dicom import / export are % given. Throws an error if requirements not met % -% call +% call: % matRad_checkEnvDicomRequirements(env) % -% input +% input: % env: folder to be scanned % % References @@ -13,7 +13,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/dicom/matRad_convRtssContours2Indices.m b/matRad/dicom/matRad_convRtssContours2Indices.m index 4d830b310..8f436e19f 100644 --- a/matRad/dicom/matRad_convRtssContours2Indices.m +++ b/matRad/dicom/matRad_convRtssContours2Indices.m @@ -2,15 +2,15 @@ % matRad function to convert a polygon segmentation from an rt structure % set into a binary segmentation as required within matRad's cst struct % -% call +% call: % indices = matRad_convRtssContours2Indices(contPoints,ct) % -% input +% input: % structure: information about a single structure % ct: matRad ct struct where the binary segmentations will % be aligned to % -% output +% output: % indicies: indices of voxels of the ct cube that are inside the % contour % @@ -19,7 +19,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/dicom/matRad_exportDicomGUI.m b/matRad/dicom/matRad_exportDicomGUI.m index f1cc66cef..b84d8023a 100644 --- a/matRad/dicom/matRad_exportDicomGUI.m +++ b/matRad/dicom/matRad_exportDicomGUI.m @@ -1,7 +1,7 @@ function hGUI = matRad_exportDicomGUI() % matRad compatability function to call the dicom export widget % -% call +% call: % matRad_importDicomGUI % % References @@ -9,7 +9,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -25,4 +25,4 @@ else matRad_exportDicomWidget(); end -end \ No newline at end of file +end diff --git a/matRad/dicom/matRad_importDicomGUI.m b/matRad/dicom/matRad_importDicomGUI.m index eb1fb5451..793b992fb 100644 --- a/matRad/dicom/matRad_importDicomGUI.m +++ b/matRad/dicom/matRad_importDicomGUI.m @@ -1,7 +1,7 @@ function hGUI = matRad_importDicomGUI() % matRad compatability function to call the dicom importwidget % -% call +% call: % matRad_importDicomGUI % % References @@ -9,7 +9,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -27,4 +27,4 @@ else matRad_importDicomWidget(); end -end \ No newline at end of file +end diff --git a/matRad/dicom/matRad_importFieldShapes.m b/matRad/dicom/matRad_importFieldShapes.m index 2450e7376..e076d73e7 100644 --- a/matRad/dicom/matRad_importFieldShapes.m +++ b/matRad/dicom/matRad_importFieldShapes.m @@ -1,14 +1,14 @@ function collimation = matRad_importFieldShapes(beamSequence, fractionSequence) % function to import collimator shapes from a DICOM RT plan % -% call +% call: % collimation = matRad_importFieldShapes(beamSequence, fractionSequence) % -% input +% input: % beamSequence: struct containing the beamSequence elements from the RT plan % fractionSequence: struct containing the fractionGroupSequence elements from the RT plan % -% output +% output: % collimation: struct with all meta information about the collimators and % all field shape matrices % @@ -17,7 +17,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/dicom/matRad_interpDicomCtCube.m b/matRad/dicom/matRad_interpDicomCtCube.m index 50cb061c9..f6280cbe8 100644 --- a/matRad/dicom/matRad_interpDicomCtCube.m +++ b/matRad/dicom/matRad_interpDicomCtCube.m @@ -1,18 +1,18 @@ function interpCt = matRad_interpDicomCtCube(origCt, origCtInfo, resolution, doseGrid) % matRad function to interpolate a 3D ct cube to a different resolution % -% call +% call: % interpCt = matRad_interpDicomCtCube(origCt, origCtInfo, resolution) % interpCt = matRad_interpDicomCtCube(origCt, origCtInfo, resolution, doseGrid) % -% input +% input: % origCt: original CT as matlab 3D array % origCtInfo: meta information about the geometry of the orgiCt cube % resolution: target resolution [mm] in x, y, an z direction for the % new cube % doseGrid: optional: externally specified grid vector % -% output +% output: % interpCt: interpolated ct cube as matlab 3D array % % References @@ -20,7 +20,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/dicom/matRad_listAllFiles.m b/matRad/dicom/matRad_listAllFiles.m index 1a649824b..cc7518a26 100644 --- a/matRad/dicom/matRad_listAllFiles.m +++ b/matRad/dicom/matRad_listAllFiles.m @@ -1,17 +1,17 @@ function fileList = matRad_listAllFiles(dirPath,uiInput) % matRad function to get all files in arbitrary deep subfolders % -% call +% call: % fileList = matRad_listAllFiles() % fileList = matRad_listAllFiles(dirPath) % fileList = matRad_listAllFiles(uiInput) % -% input +% input: % dirPath: (optional) initial folder to start searching % uiInput: (optional) if userInteraction is wanted. % Use EITHER dirPath or uiInput % -% output +% output: % fileList: Filelist % % References @@ -24,7 +24,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/dicom/matRad_loadHLUT.m b/matRad/dicom/matRad_loadHLUT.m index 7be93f0e2..23acd8767 100644 --- a/matRad/dicom/matRad_loadHLUT.m +++ b/matRad/dicom/matRad_loadHLUT.m @@ -1,15 +1,15 @@ function hlut = matRad_loadHLUT(ct, radiationMode) % matRad function to load HLUT file based on the provided ct % -% call +% call: % hlut = matRad_loadHLUT(ct, pln) % -% input +% input: % ct: ct with dicom information % radiationMode: radiationMode as character array (e.g. 'photons') since matRad 3. % Can also be a pln-struct for downwards compatibility % -% output +% output: % hlut: lookup table % % References @@ -17,7 +17,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2018 the matRad development team. +% Copyright 2018-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/doseCalc/+DoseEngines/@matRad_DoseEngineBase/initDoseCalc.m b/matRad/doseCalc/+DoseEngines/@matRad_DoseEngineBase/initDoseCalc.m index 8507f521e..bffae4034 100644 --- a/matRad/doseCalc/+DoseEngines/@matRad_DoseEngineBase/initDoseCalc.m +++ b/matRad/doseCalc/+DoseEngines/@matRad_DoseEngineBase/initDoseCalc.m @@ -18,7 +18,7 @@ % dij: matRad dij struct % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2022 the matRad development team. +% Copyright 2022-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -39,6 +39,7 @@ msg = sprintf('Dose influence matrix calculation using ''%s'' Dose Engine...',this.name); end matRad_cfg.dispInfo('%s\n',msg); +matRad_cfg.dispInfo('Dose calculation will prefer ''%s'' where possible!\n', this.precision); % initialize waitbar % TODO: This should be managed from the user interface instead @@ -139,7 +140,7 @@ %Default MU calibration dij.minMU = zeros(this.numOfColumnsDij,1); dij.maxMU = inf(this.numOfColumnsDij,1); -dij.numOfParticlesPerMU = 1e6*ones(this.numOfColumnsDij,1); +dij.numParticlesPerMU = 1e6*ones(this.numOfColumnsDij,1); if isempty(this.voxelSubIx) % take only voxels inside patient @@ -179,10 +180,10 @@ % Convert CT subscripts to world coordinates. -this.voxWorldCoords = matRad_cubeIndex2worldCoords(this.VctGrid,dij.ctGrid); +this.voxWorldCoords = cast(matRad_cubeIndex2worldCoords(this.VctGrid,dij.ctGrid),this.precision); % Convert dosegrid subscripts to world coordinates -this.voxWorldCoordsDoseGrid = matRad_cubeIndex2worldCoords(this.VdoseGrid,dij.doseGrid); +this.voxWorldCoordsDoseGrid = cast(matRad_cubeIndex2worldCoords(this.VdoseGrid,dij.doseGrid),this.precision); %Create helper masks this.VdoseGridMask = false(dij.doseGrid.numOfVoxels,1); diff --git a/matRad/doseCalc/+DoseEngines/@matRad_DoseEngineBase/matRad_DoseEngineBase.m b/matRad/doseCalc/+DoseEngines/@matRad_DoseEngineBase/matRad_DoseEngineBase.m index 1fcd1da6c..c1f9cda17 100644 --- a/matRad/doseCalc/+DoseEngines/@matRad_DoseEngineBase/matRad_DoseEngineBase.m +++ b/matRad/doseCalc/+DoseEngines/@matRad_DoseEngineBase/matRad_DoseEngineBase.m @@ -8,7 +8,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -24,7 +24,7 @@ shortName; % short identifier by which matRad recognizes an engine name; % user readable name for dose engine possibleRadiationModes; % radiation modes the engine is meant to process - %supportedQuantities; % supported (influence) quantities. Does not include quantities that can be derived post-calculation. + %supportedQuantities; % supported (influence) quantities. Does not include quantities that can be derived post-calculation. end % Public properties @@ -33,7 +33,9 @@ multScen; % scenario model to use voxelSubIx; % selection of where to calculate / store dose, empty by default selectVoxelsInScenarios; % which voxels to compute in robustness scenarios - %bioModel; % name of the biological model + precision = 'double'; % floating point precision for the dij and computations. + enableGPU = false; % whether to use GPU arrays (experimental) for dose calculation (if supported by subclass implementation). + %bioModel; % name of the biological model end % Protected properties with public get access @@ -154,8 +156,6 @@ function assignPropertiesFromPln(this,pln,warnWhenPropertyChanged) else plnStruct = struct(); end - - fields = fieldnames(plnStruct); %Set up warning message if warnWhenPropertyChanged @@ -164,49 +164,10 @@ function assignPropertiesFromPln(this,pln,warnWhenPropertyChanged) warningMsg = ''; end - % iterate over all fieldnames and try to set the - % corresponding properties inside the engine - if matRad_cfg.isOctave - c2sWarningState = warning('off','Octave:classdef-to-struct'); - end - - for i = 1:length(fields) - try - field = fields{i}; - if matRad_ispropCompat(this,field) - this.(field) = matRad_recursiveFieldAssignment(this.(field),plnStruct.(field),true,warningMsg); - else - matRad_cfg.dispWarning('Not able to assign property ''%s'' from pln.propDoseCalc to Dose Engine!',field); - end - catch ME - % catch exceptions when the engine has no properties, - % which are defined in the struct. - % When defining an engine with custom setter and getter - % methods, custom exceptions can be caught here. Be - % careful with Octave exceptions! - if ~isempty(warningMsg) - matRad_cfg = MatRad_Config.instance(); - switch ME.identifier - case 'MATLAB:noPublicFieldForClass' - matRad_cfg.dispWarning('Not able to assign property from pln.propDoseCalc to Dose Engine: %s',ME.message); - otherwise - matRad_cfg.dispWarning('Problem while setting up engine from struct:%s %s',field,ME.message); - end - end - end - end - - if matRad_cfg.isOctave - warning(c2sWarningState.state,'Octave:classdef-to-struct'); - end + matRad_assignPropertiesFromStruct(this,plnStruct,true,warningMsg); end function assignBioModelPropertiesFromPln(this, plnModel, warnWhenPropertyChanged) - - - matRad_cfg = MatRad_Config.instance(); - - fields = fieldnames(plnModel); %Set up warning message if warnWhenPropertyChanged @@ -215,33 +176,7 @@ function assignBioModelPropertiesFromPln(this, plnModel, warnWhenPropertyChanged warningMsg = ''; end - % iterate over all fieldnames and try to set the - % corresponding properties inside the engine - for i = 1:length(fields) - try - field = fields{i}; - if isprop(this.bioModel,field) - this.bioModel.(field) = matRad_recursiveFieldAssignment(this.bioModel.(field),plnModel.(field),warningMsg); - else - matRad_cfg.dispWarning('Not able to assign property ''%s'' from pln.bioModel to Biological Model!',field); - end - catch ME - % catch exceptions when the engine has no properties, - % which are defined in the struct. - % When defining an engine with custom setter and getter - % methods, custom exceptions can be caught here. Be - % careful with Octave exceptions! - if ~isempty(warningMsg) - matRad_cfg = MatRad_Config.instance(); - switch ME.identifier - case 'MATLAB:noPublicFieldForClass' - matRad_cfg.dispWarning('Not able to assign property from pln.bioModel to Biological Model: %s',ME.message); - otherwise - matRad_cfg.dispWarning('Problem while setting up Biological Model from struct:%s %s',field,ME.message); - end - end - end - end + matRad_assignPropertiesFromStruct(this,plnModel,true,warningMsg); end function resultGUI = calcDoseForward(this,ct,cst,stf,w) @@ -281,6 +216,7 @@ function assignBioModelPropertiesFromPln(this, plnModel, warnWhenPropertyChanged this.directWeights = w; this.calcDoseDirect = true; dij = this.calcDose(ct,cst,stf); + dij = this.finalizeDose(dij); % calculate cubes; use uniform weights here, weighting with actual fluence % already performed in dij construction @@ -340,6 +276,7 @@ function assignBioModelPropertiesFromPln(this, plnModel, warnWhenPropertyChanged function dij = calcDoseInfluence(this,ct,cst,stf) this.calcDoseDirect = false; dij = this.calcDose(ct,cst,stf); + dij = this.finalizeDose(dij); end function setDefaults(this) % future code for property validation on creation here @@ -366,13 +303,21 @@ function setDefaults(this) function dij = finalizeDose(this,dij) matRad_cfg = MatRad_Config.instance(); + + dijStoragePrecision = matRad_underlyingTypeCompat(dij.physicalDose{1}); + if this.calcDoseDirect + matRad_cfg.dispInfo('Dose stored in ''%s'' precision\n', dijStoragePrecision); + else + matRad_cfg.dispInfo('Dose influence stored in ''%s'' precision\n', dijStoragePrecision); + end + %Close Waitbar if any(ishandle(this.hWaitbar)) delete(this.hWaitbar); end this.timers.full = toc(this.timers.full); - + matRad_cfg.dispInfo('Dose calculation finished in %g seconds!\n',this.timers.full); end @@ -415,6 +360,13 @@ function progressUpdate(this,pos,total) % Reset the timer for the next progress update this.lastProgressUpdate = tic; end + + function allows = allowsSinglePrecisionSparseDij(~) + matRad_cfg = MatRad_Config.instance(); + %single precision sparse is not supported in Octave or Matlab + %older than R2025a. + allows = matRad_cfg.isMatlab & str2double(matRad_cfg.envVersion) >= 25; + end end % Should be abstract methods but in order to satisfy the compatibility @@ -435,14 +387,16 @@ function progressUpdate(this,pos,total) function [available,msg] = isAvailable(pln,machine) % return a boolean if the engine is is available for the given pln % struct. Needs to be implemented in non abstract subclasses + % % input: - % - pln: matRad pln struct - % - machine: optional machine to avoid loading the machine from + % pln: matRad pln struct + % machine: optional machine to avoid loading the machine from % disk (makes sense to use if machine already loaded) + % % output: - % - available: boolean value to check if the dose engine is + % available: boolean value to check if the dose engine is % available for the given pln/machine - % - msg: msg to elaborate on availability. If not available, + % msg: msg to elaborate on availability. If not available, % a msg string indicates an error during the check % if available, indicates a warning that not all % information was present in the machine file and diff --git a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/calcDose.m b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/calcDose.m index 39a188e23..fc81ef069 100644 --- a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/calcDose.m +++ b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/calcDose.m @@ -1,16 +1,16 @@ -function dij = calcDose(this,ct,cst,stf) -% Function to forward dose calculation to FRED and inport the results +function dij = calcDose(this, ct, cst, stf) +% Function to forward dose calculation to FRED and import the results % in matRad % -% call +% call: % dij = this.calcDose(ct,stf,pln,cst) % -% input -% ct: matRad ct struct +% input: +% ct: matRad ct struct % cst: matRad cst struct -% stf: atRad steering information struct -% -% output +% stf: matRad steering information struct +% +% output: % dij: matRad dij struct % % References @@ -18,424 +18,471 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2019 the matRad development team. -% -% This file is part of the matRad project. It is subject to the license -% terms in the LICENSE file found in the top-level directory of this -% distribution and at https://github.com/e0404/matRad/LICENSES.txt. No part -% of the matRad project, including this file, may be copied, modified, -% propagated, or distributed except according to the terms contained in the +% Copyright 2019-2026 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSES.txt. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the % LICENSE file. % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - - matRad_cfg = MatRad_Config.instance(); - - currFolder = pwd; - cd(this.FREDrootFolder); - - %Now we can run initDoseCalc as usual - dij = this.initDoseCalc(ct,cst,stf); - - % Interpolate cube on dose grid - HUcube{1} = matRad_interp3(dij.ctGrid.x, dij.ctGrid.y', dij.ctGrid.z,ct.cubeHU{1}, ... - dij.doseGrid.x,dij.doseGrid.y',dij.doseGrid.z,'linear'); - - % Force HU clamping if values are found outside of available range - switch this.HUtable - case 'internal' - if any(HUcube{1}(:)>this.hLutLimits(2)) || any(HUcube{1}(:) Dose cube coordinate system places the first coordinate point (aka resolution) in - % the center of the first voxel. So the "zero" of the cube - % coordinate system is 0.5*resolution outside of the cube surface. - isoInDoseGridCoord = matRad_world2cubeCoords(stf(i).isoCenter,this.doseGrid); - - % Coordinate of the first voxel in cube system is in the center of the first voxel and is equal to resolution. Thus the zero - % of the cube coordinate system is 0.5*resolution before the surface of - % the phantom. The surface thus starts at 1/2 resolution wrt zero of that system - fredCubeSurfaceInDoseCubeCoords = 0.5*doseGridResolution; - - % Get coordinates of pivot point in FRED cube (center of - % geometrical cube) in dose cube coordinates. This is the distance - % between pivot point and the surface + the position of the - % surface in the cube coord. system. - fredPivotInCubeCoordinates = 0.5*this.doseGrid.dimensions([2 1 3]).*doseGridResolution + fredCubeSurfaceInDoseCubeCoords; - - - % Define the FRED isocenter as the distance between the pivot point - % and the matRad isocenter in the cube coordinate system. - stfFred(i).isoCenter = -(fredPivotInCubeCoordinates - isoInDoseGridCoord); - - % First coordinate is flipped - stfFred(i).isoCenter = stfFred(i).isoCenter.*[-1 1 1]; - - % NOTE on the coordinate system. - % FRED places the pivot point of the component at the center of the - % FRED coordinate system, then applies a translation s.t. the FRED - % isocenter (defined for each field) is in the center of the FRED - % coordinate system. Then applies rotations. This way everything - % is defined in BEV reference. - - nominalEnergies = unique([stf(i).ray.energy]); - [~,nominalEnergiesIdx] = intersect([this.machine.data.energy],nominalEnergies); - - energyIdxInEmittance = ismember(emittanceBaseData.energyIndex, nominalEnergiesIdx); - monteCarloBaseData = emittanceBaseData.monteCarloData(energyIdxInEmittance); - - stfFred(i).nominalEnergies = nominalEnergies; - stfFred(i).energies = [monteCarloBaseData.MeanEnergy].*this.numOfNucleons.*this.primaryMass; % Note for generic baseData: the kernels were simulated with equivalent of primaryMass = 1 - stfFred(i).energySpread = [monteCarloBaseData.EnergySpread]; - stfFred(i).energySpreadMeV = [monteCarloBaseData.EnergySpread].*[monteCarloBaseData.MeanEnergy]/100; - stfFred(i).FWHMs = 2.355*[monteCarloBaseData.SpotSize1x]; - - stfFred(i).energySpreadFWHMMev = 2.355*stfFred(i).energySpreadMeV; - stfFred(i).BAMStoIsoDist = emittanceBaseData.nozzleToIso; - - % Select the parametrs for source model - switch this.sourceModel - - case 'gaussian' - - case 'emittance' - stfFred(i).emittanceX = []; - stfFred(i).twissBetaX = []; - stfFred(i).twissAlphaX = []; - stfFred(i).emittanceRefPlaneDistance = []; - - % Need to get the parameters for the model from MCemittance - for eIdx=emittanceBaseData.energyIndex' - % Only using first focus index for now - tmpOpticsData = emittanceBaseData.fitBeamOpticsForEnergy(eIdx,1); - stfFred(i).emittanceX = [stfFred(i).emittanceX, tmpOpticsData.twissEpsilonX]; - stfFred(i).twissBetaX = [stfFred(i).twissBetaX, tmpOpticsData.twissBetaX]; - stfFred(i).twissAlphaX = [stfFred(i).twissAlphaX, tmpOpticsData.twissAlphaX]; - stfFred(i).emittanceRefPlaneDistance = [stfFred(i).emittanceRefPlaneDistance, this.machine.meta.BAMStoIsoDist]; - end +matRad_cfg = MatRad_Config.instance(); - case 'sigmaSqrModel' - stfFred(i).sSQr_a = []; - stfFred(i).sSQr_b = []; - stfFred(i).sSQr_c = []; +currFolder = pwd; +cd(this.workingDir); - for eIdx=emittanceBaseData.energyIndex' - tmpOpticsData = emittanceBaseData.fitBeamOpticsForEnergy(eIdx,1); - stfFred(i).sSQr_a = [stfFred(i).sSQr_a, tmpOpticsData.sSQ_a]; - stfFred(i).sSQr_b = [stfFred(i).sSQr_b, tmpOpticsData.sSQ_b]; - stfFred(i).sSQr_c = [stfFred(i).sSQr_c, tmpOpticsData.sSQ_c]; - end - otherwise - matRad_cfg.dispWarning('Unrecognized source model, setting gaussian'); +% Now we can run initDoseCalc as usual +dij = this.initDoseCalc(ct, cst, stf); - end +% Interpolate cube on dose grid +for ctIdx = 1:this.multScen.numOfCtScen + this.HUcube{ctIdx} = matRad_interp3(dij.ctGrid.x, dij.ctGrid.y', dij.ctGrid.z, ... + ct.cubeHU{ctIdx}, ... + dij.doseGrid.x, dij.doseGrid.y', dij.doseGrid.z, 'linear'); +end - % Allocate empty layer container - % Rearrange info into separate energy layers - for j = 1:numel(stfFred(i).energies) - - %stfFred(i).energyLayer(j).targetPoints = []; - stfFred(i).energyLayer(j).numOfPrimaries = []; - stfFred(i).energyLayer(j).rayNum = []; - stfFred(i).energyLayer(j).bixelNum = []; - stfFred(i).energyLayer(j).rayDivX = []; - stfFred(i).energyLayer(j).rayDivY = []; - stfFred(i).energyLayer(j).rayPosX = []; - stfFred(i).energyLayer(j).rayPosY = []; +% Force HU clamping if values are found outside of available range +switch this.HUtable + case 'internal' + if any(this.HUcube{1}(:) > this.hLutLimits(2)) || any(this.HUcube{1}(:) < this.hLutLimits(1)) + matRad_cfg.dispWarning('HU outside of boundaries'); + this.HUclamping = true; end + otherwise + matRad_cfg.dispInfo('Using custom HU table: %s\n', this.HUtable); +end + +if this.ignoreOutsideDensities + for ctIdx = 1:this.multScen.numOfCtScen + eraseCtDensMask = ones(prod(ct.cubeDim), 1); + eraseCtDensMask(this.VctGrid(this.VctGridScenIx{ctIdx})) = 0; + this.HUcube{ctIdx}(eraseCtDensMask == 1) = this.hLutLimits(1); + end +end + +% Linear projection of BEV source (x,y) points to plane at BAMStoISO distance +getPointAtBAMS = @(target, source, distance, BAMStoIso) (target - source) * (-BAMStoIso) / distance + source; + +% Loop over the stf to rearrange data +counter = 0; + +% Use the emittance base data class to recover MC information +emittanceBaseData = matRad_MCemittanceBaseData(this.machine, stf); + +% Loop over fields. FRED performs one single simulation for multiple +% fields +for i = 1:length(stf) + + stfFred(i).gantryAngle = stf(i).gantryAngle; + stfFred(i).couchAngle = stf(i).couchAngle; + + % Get dose grid resolution + doseGridResolution = [this.doseGrid.resolution.x, this.doseGrid.resolution.y, this.doseGrid.resolution.z]; + + % get matRad isocenter coordinates in dose cube coordinate system. + % -> Dose cube coordinate system places the first coordinate point (aka resolution) in + % the center of the first voxel. So the "zero" of the cube + % coordinate system is 0.5*resolution outside of the cube surface. + isoInDoseGridCoord = matRad_world2cubeCoords(stf(i).isoCenter, this.doseGrid); + + % Coordinate of the first voxel in cube system is in the center of the first voxel and is equal to resolution. Thus the zero + % of the cube coordinate system is 0.5*resolution before the surface of + % the phantom. The surface thus starts at 1/2 resolution wrt zero of that system + fredCubeSurfaceInDoseCubeCoords = 0.5 * doseGridResolution; + + % Get coordinates of pivot point in FRED cube (center of + % geometrical cube) in dose cube coordinates. This is the distance + % between pivot point and the surface + the position of the + % surface in the cube coord. system. + fredPivotInCubeCoordinates = 0.5 * this.doseGrid.dimensions([2 1 3]) .* doseGridResolution + fredCubeSurfaceInDoseCubeCoords; + + % Define the FRED isocenter as the distance between the pivot point + % and the matRad isocenter in the cube coordinate system. + stfFred(i).isoCenter = -(fredPivotInCubeCoordinates - isoInDoseGridCoord); + + % First coordinate is flipped + stfFred(i).isoCenter = stfFred(i).isoCenter .* [-1 1 1]; + + % NOTE on the coordinate system. + % FRED places the pivot point of the component at the center of the + % FRED coordinate system, then applies a translation s.t. the FRED + % isocenter (defined for each field) is in the center of the FRED + % coordinate system. Then applies rotations. This way everything + % is defined in BEV reference. + + nominalEnergies = unique([stf(i).ray.energy]); + [~, nominalEnergiesIdx] = intersect([this.machine.data.energy], nominalEnergies); + + energyIdxInEmittance = ismember(emittanceBaseData.energyIndex, nominalEnergiesIdx); + monteCarloBaseData = emittanceBaseData.monteCarloData(energyIdxInEmittance); + + stfFred(i).nominalEnergies = nominalEnergies; + % Note for generic baseData: the kernels were simulated with equivalent of primaryMass = 1 + stfFred(i).energies = [monteCarloBaseData.MeanEnergy] .* this.numOfNucleons .* this.primaryMass; + stfFred(i).energySpread = [monteCarloBaseData.EnergySpread]; + stfFred(i).energySpreadMeV = [monteCarloBaseData.EnergySpread] .* [monteCarloBaseData.MeanEnergy] / 100; + stfFred(i).FWHMs = 2.355 * [monteCarloBaseData.SpotSize1x]; + + stfFred(i).energySpreadFWHMMev = 2.355 * stfFred(i).energySpreadMeV; + stfFred(i).BAMStoIsoDist = emittanceBaseData.nozzleToIso; + + % Select the parameters for source model + switch this.sourceModel + + case 'gaussian' + + case 'emittance' + stfFred(i).emittanceX = []; + stfFred(i).twissBetaX = []; + stfFred(i).twissAlphaX = []; + stfFred(i).emittanceRefPlaneDistance = []; + + % Need to get the parameters for the model from MCemittance + for eIdx = emittanceBaseData.energyIndex' + % Only using first focus index for now + tmpOpticsData = emittanceBaseData.fitBeamOpticsForEnergy(eIdx, 1); + stfFred(i).emittanceX = [stfFred(i).emittanceX, tmpOpticsData.twissEpsilonX]; + stfFred(i).twissBetaX = [stfFred(i).twissBetaX, tmpOpticsData.twissBetaX]; + stfFred(i).twissAlphaX = [stfFred(i).twissAlphaX, tmpOpticsData.twissAlphaX]; + stfFred(i).emittanceRefPlaneDistance = [stfFred(i).emittanceRefPlaneDistance, this.machine.meta.BAMStoIsoDist]; + end + + case 'sigmaSqrModel' + stfFred(i).sSQr_a = []; + stfFred(i).sSQr_b = []; + stfFred(i).sSQr_c = []; - for j = 1:stf(i).numOfRays - for k = 1:stf(i).numOfBixelsPerRay(j) - counter = counter + 1; - dij.beamNum(counter,1) = i; - dij.rayNum(counter,1) = j; - dij.bixelNum(counter,1) = k; + for eIdx = emittanceBaseData.energyIndex' + tmpOpticsData = emittanceBaseData.fitBeamOpticsForEnergy(eIdx, 1); + stfFred(i).sSQr_a = [stfFred(i).sSQr_a, tmpOpticsData.sSQ_a]; + stfFred(i).sSQr_b = [stfFred(i).sSQr_b, tmpOpticsData.sSQ_b]; + stfFred(i).sSQr_c = [stfFred(i).sSQr_c, tmpOpticsData.sSQ_c]; end - - for k = 1:numel(stfFred(i).energies) - - if any(stf(i).ray(j).energy == stfFred(i).nominalEnergies(k)) - stfFred(i).energyLayer(k).rayNum = [stfFred(i).energyLayer(k).rayNum j]; - - stfFred(i).energyLayer(k).bixelNum = [stfFred(i).energyLayer(k).bixelNum ... - find(stf(i).ray(j).energy == stfFred(i).nominalEnergies(k))]; - - % Get spot position and divergence - targetX = stf(i).ray(j).targetPoint_bev(1); - targetY = stf(i).ray(j).targetPoint_bev(3); - - % Stf.ray.rayPos_bev is position of ray at the - % IsoCenter plane. - sourceX = stf(i).ray(j).rayPos_bev(1); - sourceY = stf(i).ray(j).rayPos_bev(3); - - distance = stf(i).ray(j).targetPoint_bev(2) - stf(i).ray(j).rayPos_bev(2); - - divergenceX = (targetX - sourceX)/distance; - divergenceY = (targetY - sourceY)/distance; - - %stfFred(i).energyLayer(k).targetPoints = [stfFred(i).energyLayer(k).targetPoints; -targetX targetY]; - - % This is position of the spot at -BAMsToIso distance - % (zero is at IsoCenter depth). - stfFred(i).energyLayer(k).rayPosX = [stfFred(i).energyLayer(k).rayPosX, getPointAtBAMS(targetX,sourceX,distance,stfFred(i).BAMStoIsoDist)]; - stfFred(i).energyLayer(k).rayPosY = [stfFred(i).energyLayer(k).rayPosY, getPointAtBAMS(targetY,sourceY,distance,stfFred(i).BAMStoIsoDist)]; - - stfFred(i).energyLayer(k).rayDivX = [stfFred(i).energyLayer(k).rayDivX, divergenceX]; - stfFred(i).energyLayer(k).rayDivY = [stfFred(i).energyLayer(k).rayDivY, divergenceY]; - - - if this.calcDoseDirect - % Set the bixel weight - stfFred(i).energyLayer(k).numOfPrimaries = [stfFred(i).energyLayer(k).numOfPrimaries ... - stf(i).ray(j).weight(stf(i).ray(j).energy == stfFred(i).nominalEnergies(k))]; - else - stfFred(i).energyLayer(k).numOfPrimaries = [stfFred(i).energyLayer(k).numOfPrimaries, 1]; - end + otherwise + matRad_cfg.dispWarning('Unrecognized source model, setting gaussian'); + + end + + % Allocate empty layer container + % Rearrange info into separate energy layers + for j = 1:numel(stfFred(i).energies) + + % stfFred(i).energyLayer(j).targetPoints = []; + stfFred(i).energyLayer(j).numOfPrimaries = []; + stfFred(i).energyLayer(j).rayNum = []; + stfFred(i).energyLayer(j).bixelNum = []; + stfFred(i).energyLayer(j).rayDivX = []; + stfFred(i).energyLayer(j).rayDivY = []; + stfFred(i).energyLayer(j).rayPosX = []; + stfFred(i).energyLayer(j).rayPosY = []; + end + + for j = 1:stf(i).numOfRays + for k = 1:stf(i).numOfBixelsPerRay(j) + counter = counter + 1; + dij.beamNum(counter, 1) = i; + dij.rayNum(counter, 1) = j; + dij.bixelNum(counter, 1) = k; + end + + for k = 1:numel(stfFred(i).energies) + + if any(stf(i).ray(j).energy == stfFred(i).nominalEnergies(k)) + stfFred(i).energyLayer(k).rayNum = [stfFred(i).energyLayer(k).rayNum j]; + + stfFred(i).energyLayer(k).bixelNum = [stfFred(i).energyLayer(k).bixelNum ... + find(stf(i).ray(j).energy == stfFred(i).nominalEnergies(k))]; + + % Get spot position and divergence + targetX = stf(i).ray(j).targetPoint_bev(1); + targetY = stf(i).ray(j).targetPoint_bev(3); + + % Stf.ray.rayPos_bev is position of ray at the + % IsoCenter plane. + sourceX = stf(i).ray(j).rayPos_bev(1); + sourceY = stf(i).ray(j).rayPos_bev(3); + + distance = stf(i).ray(j).targetPoint_bev(2) - stf(i).ray(j).rayPos_bev(2); + + divergenceX = (targetX - sourceX) / distance; + divergenceY = (targetY - sourceY) / distance; + + % stfFred(i).energyLayer(k).targetPoints = [stfFred(i).energyLayer(k).targetPoints; -targetX targetY]; + + % This is position of the spot at -BAMsToIso distance + % (zero is at IsoCenter depth). + stfFred(i).energyLayer(k).rayPosX = [stfFred(i).energyLayer(k).rayPosX, ... + getPointAtBAMS(targetX, sourceX, distance, stfFred(i).BAMStoIsoDist)]; + stfFred(i).energyLayer(k).rayPosY = [stfFred(i).energyLayer(k).rayPosY, ... + getPointAtBAMS(targetY, sourceY, distance, stfFred(i).BAMStoIsoDist)]; + stfFred(i).energyLayer(k).rayDivX = [stfFred(i).energyLayer(k).rayDivX, divergenceX]; + stfFred(i).energyLayer(k).rayDivY = [stfFred(i).energyLayer(k).rayDivY, divergenceY]; + + if this.calcDoseDirect + % Set the bixel weight + stfFred(i).energyLayer(k).numOfPrimaries = [stfFred(i).energyLayer(k).numOfPrimaries ... + stf(i).ray(j).weight(stf(i).ray(j).energy == stfFred(i).nominalEnergies(k))]; + else + stfFred(i).energyLayer(k).numOfPrimaries = [stfFred(i).energyLayer(k).numOfPrimaries, 1]; end end + end + end - %FRED works in cm - stfFred(i).isoCenter = stfFred(i).isoCenter/10; - stfFred(i).BAMStoIsoDist = stfFred(i).BAMStoIsoDist/10; + % FRED works in cm + stfFred(i).isoCenter = stfFred(i).isoCenter / 10; + stfFred(i).BAMStoIsoDist = stfFred(i).BAMStoIsoDist / 10; - switch this.sourceModel - case 'gaussian' - stfFred(i).FWHMs = stfFred(i).FWHMs/10; - case 'emittance' - stfFred(i).emittanceRefPlaneDistance = stfFred(i).emittanceRefPlaneDistance/10; - case 'sigmaSqrModel' + switch this.sourceModel + case 'gaussian' + stfFred(i).FWHMs = stfFred(i).FWHMs / 10; + case 'emittance' + stfFred(i).emittanceRefPlaneDistance = stfFred(i).emittanceRefPlaneDistance / 10; + case 'sigmaSqrModel' - end + end - stfFred(i).totalNumOfBixels = stf(i).totalNumOfBixels; - for j=1:numel(stfFred(i).nominalEnergies) - stfFred(i).energyLayer(j).rayPosX = stfFred(i).energyLayer(j).rayPosX/10; - stfFred(i).energyLayer(j).rayPosY = stfFred(i).energyLayer(j).rayPosY/10; - stfFred(i).energyLayer(j).nBixels = numel(stfFred(i).energyLayer(j).bixelNum); + stfFred(i).totalNumOfBixels = stf(i).totalNumOfBixels; + for j = 1:numel(stfFred(i).nominalEnergies) + stfFred(i).energyLayer(j).rayPosX = stfFred(i).energyLayer(j).rayPosX / 10; + stfFred(i).energyLayer(j).rayPosY = stfFred(i).energyLayer(j).rayPosY / 10; + stfFred(i).energyLayer(j).nBixels = numel(stfFred(i).energyLayer(j).bixelNum); - if this.calcDoseDirect - stfFred(i).energyLayer(j).numOfPrimaries = this.conversionFactor*stfFred(i).energyLayer(j).numOfPrimaries; - end + if this.calcDoseDirect + stfFred(i).energyLayer(j).numOfPrimaries = this.conversionFactor * stfFred(i).energyLayer(j).numOfPrimaries; end end - - counterFred = 0; - fredOrder = NaN * ones(dij.totalNumOfBixels,1); - for i = 1:length(stf) - for j = 1:numel(stfFred(i).nominalEnergies) - for k = 1:numel(stfFred(i).energyLayer(j).numOfPrimaries) - counterFred = counterFred + 1; - ix = find(i == dij.beamNum & ... - stfFred(i).energyLayer(j).rayNum(k) == dij.rayNum & ... - stfFred(i).energyLayer(j).bixelNum(k) == dij.bixelNum); - - fredOrder(ix) = counterFred; - end +end + +counterFred = 0; +fredOrder = NaN * ones(dij.totalNumOfBixels, 1); +for i = 1:length(stf) + for j = 1:numel(stfFred(i).nominalEnergies) + for k = 1:numel(stfFred(i).energyLayer(j).numOfPrimaries) + counterFred = counterFred + 1; + ix = find(i == dij.beamNum & ... + stfFred(i).energyLayer(j).rayNum(k) == dij.rayNum & ... + stfFred(i).energyLayer(j).bixelNum(k) == dij.bixelNum); + + fredOrder(ix) = counterFred; end end - - if any(isnan(fredOrder)) - matRad_cfg.dispError('Invalid ordering of Beamlets for FRED computation!'); - end - - % %% MC computation and dij filling - this.writeFredInputAllFiles(stfFred); - - switch this.externalCalculation - - case 'write' % Write simulation files for external calculation (no FRED installation required) - - matRad_cfg.dispInfo('All files have been generated\n'); - dijFieldsToOverride = {'numOfBeams','beamNum','bixelNum','rayNum','totalNumOfBixels','totalNumOfRays','numOfRaysPerBeam'}; - - for fieldName=dijFieldsToOverride - dij.(fieldName{1}) = this.numOfColumnsDij; - end +end + +if any(isnan(fredOrder)) + matRad_cfg.dispError('Invalid ordering of Beamlets for FRED computation!'); +end + +switch this.externalCalculation + + case 'write' % Write simulation files for external calculation (no FRED installation required) + % %% MC computation and dij filling + + this.writeTreeDirectory(); + this.writeCTs(); + this.writeFredInputAllFiles(stfFred); + + matRad_cfg.dispInfo('All files have been generated\n'); + dijFieldsToOverride = {'numOfBeams', 'beamNum', 'bixelNum', 'rayNum', 'totalNumOfBixels', 'totalNumOfRays', 'numOfRaysPerBeam'}; + + for fieldName = dijFieldsToOverride + dij.(fieldName{1}) = this.numOfColumnsDij; + end + + doseCube = []; + + case 'off' % Run FRED simulation (requires installation) + + this.writeTreeDirectory(); + this.writeCTs(); + this.writeFredInputAllFiles(stfFred); + + % Check consistency of installation + if this.checkExec() + + for scenIdx = 1:this.multScen.totNumScen + matRad_cfg.dispInfo('calling FRED for scenario %d/%d', scenIdx, this.multScen.totNumScen); + + [~, runFolderName] = fileparts(this.MCrunFolder); + if this.multScen.totNumScen > 1 + tailRun = sprintf('_%d', scenIdx); + else + tailRun = ''; + end + + cd(strrep(this.MCrunFolder, runFolderName, sprintf('%s%s', runFolderName, tailRun))); + + flags = '-V1 -f fred.inp'; - doseCube = []; - - case 'off' % Run FRED simulation (requires installation) - - % Check consistency of installation - if this.checkExec() - - matRad_cfg.dispInfo('calling FRED'); - - cd(this.MCrunFolder); - - systemCall = [this.cmdCall, '-f fred.inp']; if ~this.useGPU - systemCall = [this.cmdCall, ' -nogpu -f fred.inp']; + flags = ['-nogpu ' flags]; end - + systemCall = [this.cmdCall, flags]; + % printOutput to matlab console if this.printOutput - [status,~] = system(systemCall,'-echo'); + [status, ~] = system(systemCall, '-echo'); else - [status,~] = system(systemCall); + [status, ~] = system(systemCall); end - cd(this.FREDrootFolder); - else - matRad_cfg.dispError('FRED setup incorrect for this plan simulation'); + + cd(this.workingDir); + end + else + matRad_cfg.dispError('FRED setup incorrect for this plan simulation'); + end + + if status == 0 + matRad_cfg.dispInfo(' done\n'); + end - if status==0 - matRad_cfg.dispInfo(' done\n'); + for scenIdx = 1:this.multScen.totNumScen + [~, runFolderName] = fileparts(this.MCrunFolder); + if this.multScen.totNumScen > 1 + tailRun = sprintf('_%d', scenIdx); + else + tailRun = ''; end - + % read simulation output - [doseCube, letdCube] = this.readSimulationOutput(this.MCrunFolder,this.calcDoseDirect, 'calcLET', logical(this.calcLET), 'readFunctionHandle', this.dijReaderHandle); + [doseCube{scenIdx}, letdCube{scenIdx}] = ... + this.readSimulationOutput(strrep(this.MCrunFolder, ... + runFolderName, sprintf('%s%s', runFolderName, tailRun)), ... + this.calcDoseDirect, ... + logical(this.calcLET)); + end - otherwise % A path for loading has been provided - - matRad_cfg.dispInfo(['Reading simulation data from: ', strrep(this.MCrunFolder,'\','\\'), '\n']); + otherwise % A path for loading has been provided + + for scenIdx = 1:this.multScen.totNumScen + [~, runFolderName] = fileparts(this.MCrunFolder); + if this.multScen.totNumScen > 1 + tailRun = sprintf('_%d', scenIdx); + else + tailRun = ''; + end + + scenarioRunFolder = strrep(this.MCrunFolder, runFolderName, sprintf('%s%s', runFolderName, tailRun)); + + matRad_cfg.dispInfo(['Reading simulation data from: ', strrep(scenarioRunFolder, '\', '\\'), '\n']); % read simulation output - [doseCube, letdCube, loadFileName] = this.readSimulationOutput(this.MCrunFolder,this.calcDoseDirect, 'calcLET',logical(this.calcLET),'readFunctionHandle', this.dijReaderHandle); + [doseCube{scenIdx}, letdCube{scenIdx}, loadFileName] = ... + this.readSimulationOutput(scenarioRunFolder, this.calcDoseDirect, logical(this.calcLET)); - dij.externalCalculationLodPath = loadFileName; + dij.externalCalculationLodPath{scenIdx} = loadFileName; + end - end +end - if ~isempty(doseCube) - - % Fill dij - if this.calcDoseDirect - % Dose cube - if isequal(size(doseCube), this.doseGrid.dimensions) - dij.physicalDose{1} = sparse(this.VdoseGrid, ones(numel(this.VdoseGrid),1),doseCube(this.VdoseGrid), this.doseGrid.numOfVoxels,1); - end - - % LETd cube - if this.calcLET - if isequal(size(letdCube), this.doseGrid.dimensions) - dij.mLETd{1} = sparse(this.VdoseGrid, ones(numel(this.VdoseGrid),1),letdCube(this.VdoseGrid)./10, this.doseGrid.numOfVoxels,1); - - % We need LETd * dose as well - dij.mLETDose{1} = sparse(this.VdoseGrid, ones(numel(this.VdoseGrid),1),(letdCube(this.VdoseGrid)./10).*doseCube(this.VdoseGrid), this.doseGrid.numOfVoxels,1); - end - end - - % Needed for calcCubes - dijFieldsToOverride = {'numOfBeams','beamNum','bixelNum','rayNum','totalNumOfBixels','totalNumOfRays','numOfRaysPerBeam'}; - - for fieldName=dijFieldsToOverride - dij.(fieldName{1}) = 1; - end +if ~isempty(doseCube) + scenMaskIdx = num2cell(this.multScen.linearMask(scenIdx, :)); - else - % Dose cube - if isequal(size(doseCube), [dij.doseGrid.numOfVoxels,dij.totalNumOfBixels]) - %When scoring dij, FRED internaly normalizes to 1 - dij.physicalDose{1}(this.VdoseGrid,:) = this.conversionFactor*doseCube(this.VdoseGrid,fredOrder); + % Fill dij + if this.calcDoseDirect + % Dose cube + if all(cellfun(@(cube) isequal(size(cube), this.doseGrid.dimensions), doseCube)) + for scenIdx = 1:this.multScen.totNumScen + dij.physicalDose{scenMaskIdx{:}} = doseCube{scenIdx}(:); end + end - % LET cube - if this.calcLET - if isequal(size(letdCube), [dij.doseGrid.numOfVoxels,dij.totalNumOfBixels]) - % Need to divide by 10, FRED scores in MeV * cm^2 / g - dij.mLETd{1}(this.VdoseGrid,:) = letdCube(this.VdoseGrid,fredOrder)./10; + % LETd cube + if this.calcLET + if all(cellfun(@(cube) isequal(size(cube), this.doseGrid.dimensions), letdCube)) + + for scenIdx = 1:this.multScen.totNumScen + dij.mLETd{scenMaskIdx{:}} = letdCube{scenIdx}(:); end - % We need LETd * dose as well - dij.mLETDose{1} = sparse(dij.physicalDose{1}.*dij.mLETd{1}); + letdCube = cellfun(@(letScen, doseScen) letScen(:) .* doseScen(:), letdCube, doseCube, 'UniformOutput', false); + + for scenIdx = 1:this.multScen.totNumScen + dij.mLETDose{scenMaskIdx{:}} = letdCube{scenIdx}(:); + end end end + % Needed for calcCubes + dijFieldsToOverride = {'numOfBeams', 'beamNum', 'bixelNum', 'rayNum', 'totalNumOfBixels', 'totalNumOfRays', 'numOfRaysPerBeam'}; + + for fieldName = dijFieldsToOverride + dij.(fieldName{1}) = 1; + end - % Calc Biological quantities - if this.calcBioDose - % recover alpha and beta maps - tmpBixel.radDepths = zeros(size(this.VdoseGrid,1),1); - - tmpBixel.vAlphaX = dij.ax{1}(this.VdoseGrid); - tmpBixel.vBetaX = dij.bx{1}(this.VdoseGrid); - tmpBixel.vABratio = dij.ax{1}(this.VdoseGrid)./dij.bx{1}(this.VdoseGrid); + else + doseCube = cellfun(@(doseScen) doseScen(:, fredOrder), doseCube, 'UniformOutput', false); + % Dose cube + % When scoring dij, FRED internally normalizes to 1 + dij.physicalDose = cellfun(@(doseScen) this.conversionFactor * doseScen, doseCube, 'UniformOutput', false); - if this.calcDoseDirect - tmpKernel.LET = dij.mLETd{1}(this.VdoseGrid); + % LET cube + if this.calcLET + letdCube = cellfun(@(letdScen) letdScen(:, fredOrder), letdCube, 'UniformOutput', false); + + % We need LETd * dose as well + dij.mLETDose = cellfun(@(dose, letd) dose .* letd, dij.physicalDose, letdCube, 'UniformOutput', false); + end + end + + % Calc Biological quantities + if this.calcBioDose + + if this.calcDoseDirect + for scenIdx = 1:this.multScen.totNumScen + + tmpKernel.LET = letdCube{scenIdx}(this.VdoseGrid); + + % recover alpha and beta maps + tmpBixel.radDepths = zeros(size(this.VdoseGrid, 1), 1); + + tmpBixel.vAlphaX = dij.ax{scenIdx}(this.VdoseGrid); + tmpBixel.vBetaX = dij.bx{scenIdx}(this.VdoseGrid); + tmpBixel.vABratio = dij.ax{scenIdx}(this.VdoseGrid) ./ dij.bx{scenIdx}(this.VdoseGrid); + + tmpBixel = this.bioModel.calcBiologicalQuantitiesForBixel(tmpBixel, tmpKernel); - tmpBixel = this.bioModel.calcBiologicalQuantitiesForBixel(tmpBixel,tmpKernel); - tmpBixel.alpha(isnan(tmpBixel.alpha)) = 0; tmpBixel.beta(isnan(tmpBixel.beta)) = 0; - dij.mAlphaDose{1} = sparse(this.VdoseGrid, ones(numel(this.VdoseGrid),1),tmpBixel.alpha.*dij.physicalDose{1}(this.VdoseGrid), this.doseGrid.numOfVoxels,1); - dij.mSqrtBetaDose{1} = sparse(this.VdoseGrid, ones(numel(this.VdoseGrid),1),sqrt(tmpBixel.beta).*dij.physicalDose{1}(this.VdoseGrid), this.doseGrid.numOfVoxels,1); - else - % Loop over all bixels - for bxlIdx = 1:dij.totalNumOfBixels - bixelLET = full(dij.mLETd{1}(:,bxlIdx)); - tmpKernel.LET = bixelLET(this.VdoseGrid); - - tmpBixel = this.bioModel.calcBiologicalQuantitiesForBixel(tmpBixel,tmpKernel); - - tmpBixel.alpha(isnan(tmpBixel.alpha)) = 0; - tmpBixel.beta(isnan(tmpBixel.beta)) = 0; - - dij.mAlphaDose{1}(:,bxlIdx) = sparse(this.VdoseGrid, ones(numel(this.VdoseGrid),1),tmpBixel.alpha.*dij.physicalDose{1}(this.VdoseGrid,bxlIdx), this.doseGrid.numOfVoxels,1); - dij.mSqrtBetaDose{1}(:,bxlIdx) = sparse(this.VdoseGrid, ones(numel(this.VdoseGrid),1),sqrt(tmpBixel.beta).*dij.physicalDose{1}(this.VdoseGrid,bxlIdx), this.doseGrid.numOfVoxels,1); - end - end + dij.mAlphaDose{scenIdx} = zeros(size(letdCube{scenIdx})); + dij.mSqrtBetaDose{scenIdx} = zeros(size(letdCube{scenIdx})); + dij.mAlphaDose{scenIdx}(this.VdoseGrid) = tmpBixel.alpha .* dij.physicalDose{scenIdx}(this.VdoseGrid); + dij.mSqrtBetaDose{scenIdx}(this.VdoseGrid) = sqrt(tmpBixel.beta) .* dij.physicalDose{scenIdx}(this.VdoseGrid); + end + else + for scenIdx = 1:this.multScen.totNumScen + + currLetdCube = letdCube{scenIdx}; + indices = find(currLetdCube); + matSize = size(currLetdCube); + [voxels, bixels] = ind2sub(size(currLetdCube), indices); + tmpKernel.LET = nonzeros(currLetdCube); + + tmpBixel.radDepths = zeros(size(voxels), "logical"); + tmpBixel.vAlphaX = dij.ax{scenIdx}(voxels); + tmpBixel.vBetaX = dij.bx{scenIdx}(voxels); + tmpBixel.vABratio = tmpBixel.vAlphaX ./ tmpBixel.vBetaX; + + tmpBixel = this.bioModel.calcBiologicalQuantitiesForBixel(tmpBixel, tmpKernel); + + tmpBixel.alpha(~isfinite(tmpBixel.alpha)) = 0; + tmpBixel.beta(~isfinite(tmpBixel.beta)) = 0; + + dij.mAlphaDose{scenIdx} = sparse(voxels, bixels, tmpBixel.alpha, matSize(1), matSize(2)); + dij.mSqrtBetaDose{scenIdx} = sparse(voxels, bixels, sqrt(tmpBixel.beta), matSize(1), matSize(2)); + dij.mAlphaDose{scenIdx} = dij.mAlphaDose{scenIdx} .* dij.physicalDose{scenIdx}; + dij.mSqrtBetaDose{scenIdx} = dij.mSqrtBetaDose{scenIdx} .* dij.physicalDose{scenIdx}; + end end end +end - dij = this.finalizeDose(dij); - - cd(currFolder); -end \ No newline at end of file +cd(currFolder); diff --git a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/matRad_ParticleFREDEngine.m b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/matRad_ParticleFREDEngine.m index ddc10e684..1deb506e2 100644 --- a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/matRad_ParticleFREDEngine.m +++ b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/matRad_ParticleFREDEngine.m @@ -1,118 +1,119 @@ classdef matRad_ParticleFREDEngine < DoseEngines.matRad_MonteCarloEngineAbstract -% Engine for particle dose calculation using FRED Monte Carlo algorithm -% for more informations see superclass -% DoseEngines.matRad_MonteCarloEngineAbstract -% -% -% The following parameters for the FRED engine can be tuned by the user. In -% order to do so, specify the desired value in: pln.propDoseCalc. -% [s]: string/character array -% [b]: boolean -% [i]: integer -% [f]: float/double/any non strictly integer number -% -% -% HUclamping: [b] allows for clamping of HU table. Default: true -% HUtable: [s] HU table name. Example: 'internal', 'matRad_default_FRED' -% externalCalculation [b/s] off (default): run FRED -% t/'write' : Only write simulation paramter files -% 'path' : read simulation files from 'path' -% -% sourceModel [s] see AvailableSourceModels, {'gaussian', 'emittance', 'sigmaSqrModel'} -% useGPU [b] trigger use of GPU (if available) -% roomMaterial [s] material of the patient surroundings. Example: -% 'vacuum', 'Air' -% printOutput [b] 't: FRED output is mirrored to Matlab console, f: no output is printed' -% numHistoriesDirect [i] -% numHistoriesPerBeamlet [i] -% scorers [c] cell array with specified scorers. Example: -% 'Dose', 'LETd' -% primaryMass [f] mass of the primary ion (in Da). Default value for -% protons: 1.0727 -% numOfNucleons [i] number of nucleons. Default for protons: 1 - -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% -% Copyright 2023 the matRad development team. -% -% This file is part of the matRad project. It is subject to the license -% terms in the LICENSE file found in the top-level directory of this -% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part -% of the matRad project, including this file, may be copied, modified, -% propagated, or distributed except according to the terms contained in the -% LICENSE file. -% -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - + % Engine for particle dose calculation using FRED Monte Carlo algorithm + % for more information see superclass + % DoseEngines.matRad_MonteCarloEngineAbstract + % + % + % The following parameters for the FRED engine can be tuned by the user. In + % order to do so, specify the desired value in: pln.propDoseCalc. + % [s]: string/character array + % [b]: boolean + % [i]: integer + % [f]: float/double/any non strictly integer number + % + % + % HUclamping: [b] allows for clamping of HU table. Default: true + % HUtable: [s] HU table name. Example: 'internal', 'matRad_default_FRED' + % externalCalculation [b/s] off (default): run FRED + % t/'write' : Only write simulation parameter files + % 'path' : read simulation files from 'path' + % + % sourceModel [s] see AvailableSourceModels, {'gaussian', 'emittance', 'sigmaSqrModel'} + % useGPU [b] trigger use of GPU (if available) + % roomMaterial [s] material of the patient surroundings. Example: + % 'vacuum', 'Air' + % printOutput [b] 't: FRED output is mirrored to Matlab console, f: no output is printed' + % numHistoriesDirect [i] + % numHistoriesPerBeamlet [i] + % scorers [c] cell array with specified scorers. Example: + % 'Dose', 'LETd' + % primaryMass [f] mass of the primary ion (in Da). Default value for + % protons: 1.0727 + % numOfNucleons [i] number of nucleons. Default for protons: 1 + + % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + % + % Copyright 2023-2026 the matRad development team. + % + % This file is part of the matRad project. It is subject to the license + % terms in the LICENSE file found in the top-level directory of this + % distribution and at https://github.com/e0404/matRad/LICENSE.md. No part + % of the matRad project, including this file, may be copied, modified, + % propagated, or distributed except according to the terms contained in the + % LICENSE file. + % + % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + properties (Constant) - possibleRadiationModes = {'protons'}; - name = 'FRED'; - shortName = 'FRED'; + possibleRadiationModes = {'protons'} + name = 'FRED' + shortName = 'FRED' end properties (SetAccess = protected, GetAccess = public) - - defaultHUtable = 'matRad_default_FredMaterialConverter'; - AvailableSourceModels = {'gaussian', 'emittance', 'sigmaSqrModel'}; - defaultDijFormatVersion = '20'; - - calcBioDose; - currentVersion; - availableVersions = {'3.70.0'}; % Or higher. - radiationMode; + + defaultHUtable = 'matRad_default_FredMaterialConverter' + availableSourceModels = {'gaussian', 'emittance', 'sigmaSqrModel'} + calcBioDose + currentVersion + availableVersions = {'3.70.0'} % Or higher. + radiationMode end properties - dijFormatVersion; - externalCalculation; - useGPU; - calcLET; - constantRBE; - HUclamping; - scorers; - HUtable; - sourceModel; - roomMaterial; - printOutput; - primaryMass; - numOfNucleons; - ignoreOutsideDensities; + externalCalculation = 'write' + useGPU = true + calcLET = false + constantRBE + HUclamping + scorers + HUtable + sourceModel + roomMaterial + printOutput + primaryMass + numOfNucleons + ignoreOutsideDensities + workingDir + forceDijFormatVersion end + properties (Dependent) + dijFormatVersion + end - properties (SetAccess = private, Hidden) - patientFilename = 'CTpatient.mhd'; - runInputFilename = 'fred.inp'; - regionsFilename = 'regions.inp'; - funcsFilename = 'funcs.inp'; - planFilename = 'plan.inp'; - fieldsFilename = 'fields.inp'; - layersFilename = 'layers.inp'; - beamletsFilename = 'beamlets.inp'; - planDeliveryFilename = 'planDelivery.inp'; - - hLutLimits = [-1000,1375]; % Default FRED values - - conversionFactor = 1e6; % Used to scale the FRED dose to matRad normalization - - FREDrootFolder; - - MCrunFolder; - inputFolder; - regionsFolder; - planFolder; - dijReaderHandle; + properties (SetAccess = protected, Hidden) + patientFilename = 'CTpatient.mhd' + runInputFilename = 'fred.inp' + regionsFilename = 'regions.inp' + funcsFilename = 'funcs.inp' + planFilename = 'plan.inp' + fieldsFilename = 'fields.inp' + layersFilename = 'layers.inp' + beamletsFilename = 'beamlets.inp' + planDeliveryFilename = 'planDelivery.inp' + + hLutLimits = [-1000, 1375] % Default FRED values + + conversionFactor = 1e6 % Used to scale the FRED dose to matRad normalization + MCrunFolder + inputFolder + regionsFolder + planFolder + + HUcube end - + methods + function this = matRad_ParticleFREDEngine(pln) % Constructor % - % call + % call: % engine = DoseEngines.matRad_DoseEngineFRED(ct,stf,pln,cst) % - + matRad_cfg = MatRad_Config.instance(); if nargin < 1 pln = []; @@ -127,34 +128,36 @@ end end - if isempty(this.FREDrootFolder) - this.FREDrootFolder = fullfile(matRad_cfg.primaryUserFolder, 'FRED'); + if isempty(this.workingDir) + this.workingDir = fullfile(matRad_cfg.primaryUserFolder, 'FRED'); end - - if ~exist(this.FREDrootFolder, 'dir') - mkdir(this.FREDrootFolder); + + if ~exist(this.workingDir, 'dir') + mkdir(this.workingDir); matRad_cfg.dispWarning('FRED root folder not found, this should not happen!'); end - end end - methods(Access = protected) + methods (Access = protected) - dij = calcDose(this,ct,cst,stf) + dij = calcDose(this, ct, cst, stf) - function dij = initDoseCalc(this,ct,cst,stf) + function dij = initDoseCalc(this, ct, cst, stf) matRad_cfg = MatRad_Config.instance(); - dij = initDoseCalc@DoseEngines.matRad_MonteCarloEngineAbstract(this,ct,cst,stf); - + dij = initDoseCalc@DoseEngines.matRad_MonteCarloEngineAbstract(this, ct, cst, stf); + dij = this.allocateQuantityMatrixContainers(dij, {'physicalDose'}); - %Issue a warning when we have more than 1 scenario + % Issue a warning when we have more than 1 scenario if dij.numOfScenarios ~= 1 - matRad_cfg.dispWarning('FRED is only implemented for single scenario use at the moment. Will only use the first Scenario for Monte Carlo calculation!'); + matRad_cfg.dispWarning( ... + ['FRED is only implemented for single scenario use at the moment. '... + 'Will only use the first Scenario for Monte Carlo calculation!'] ... + ); end % Check for model consistency @@ -163,7 +166,7 @@ else this.calcBioDose = 0; end - + % Limit RBE calculation to proton models for the time being if this.calcBioDose @@ -171,10 +174,10 @@ case 'protons' - dij = this.loadBiologicalData(cst,dij); - dij = this.allocateQuantityMatrixContainers(dij,{'mAlphaDose', 'mSqrtBetaDose'}); + dij = this.loadBiologicalData(cst, dij); + dij = this.allocateQuantityMatrixContainers(dij, {'mAlphaDose', 'mSqrtBetaDose'}); - % Only considering LET based models + % Only considering LET based models this.calcLET = true; otherwise matRad_cfg.dispWarning('biological dose calculation not supported for radiation modality: %s', this.radiationMode); @@ -195,106 +198,321 @@ end - function writeTreeDirectory(this) + function writeTreeDirectory(this) + + % Loop over the scenarios + if this.multScen.totNumScen > 1 + for scenIdx = 1:this.multScen.totNumScen + this.writeTreeDirectoryForRun(scenIdx); + end + else + + this.writeTreeDirectoryForRun(0); + end + end + + function writeTreeDirectoryForRun(this, scenIdx) - if ~exist(this.MCrunFolder, 'dir') - mkdir(this.MCrunFolder); + [~, runFolderName] = fileparts(this.MCrunFolder); + if scenIdx == 0 + tailRun = ''; + else + tailRun = sprintf('_%d', scenIdx); + end + + folderName = sprintf('%s%s', this.MCrunFolder, tailRun); + if ~exist(folderName, 'dir') + mkdir(folderName); end % write input folder - if ~exist(this.inputFolder, 'dir') - mkdir(this.inputFolder); + folderName = strrep(this.inputFolder, runFolderName, sprintf('%s%s', runFolderName, tailRun)); + if ~exist(folderName, 'dir') + mkdir(folderName); end % build MCrun/inp/regions - if ~exist(this.regionsFolder, 'dir') - mkdir(this.regionsFolder); + folderName = strrep(this.regionsFolder, runFolderName, sprintf('%s%s', runFolderName, tailRun)); + if ~exist(folderName, 'dir') + mkdir(folderName); end % build MCrun/inp/plan - if ~exist(this.planFolder, 'dir') - mkdir(this.planFolder); + folderName = strrep(this.planFolder, runFolderName, sprintf('%s%s', runFolderName, tailRun)); + if ~exist(folderName, 'dir') + mkdir(folderName); end end - %% Write files functions + writeRunFile(this, fName) - writeRunFile(~, fName) - - writeRegionsFile(this,fName, stf) + writeRegionsFile(this, fName, stf) writePlanDeliveryFile(this, fName, stf) - - writePlanFile(this,fName, stf) - - function writeFredInputAllFiles(this,stf) - - %write fred.inp file - runFilename = fullfile(this.MCrunFolder, this.runInputFilename); - this.writeRunFile(runFilename); - - %write region/region.inp file - regionFilename = fullfile(this.regionsFolder, this.regionsFilename); - this.writeRegionsFile(regionFilename); - - %write plan file - planFile = fullfile(this.planFolder, this.planFilename); - this.writePlanFile(planFile,stf); - - %write planDelivery file - - planDeliveryFile = fullfile(this.planFolder,this.planDeliveryFilename); - this.writePlanDeliveryFile(planDeliveryFile); + + writePlanFile(this, fName, stf, scenIdx) + + function writeFredInputAllFiles(this, stf) + + % write fred.inp file + for scenIdx = 1:this.multScen.totNumScen + [~, runFolderName] = fileparts(this.MCrunFolder); + + if this.multScen.totNumScen > 1 + tailRun = sprintf('_%d', scenIdx); + else + tailRun = ''; + end + + runFilename = fullfile(strrep(this.MCrunFolder, runFolderName, sprintf('%s%s', runFolderName, tailRun)), this.runInputFilename); + this.writeRunFile(runFilename); + + % write region/region.inp file + regionFilename = fullfile(strrep(this.regionsFolder, runFolderName, sprintf('%s%s', runFolderName, tailRun)), this.regionsFilename); + this.writeRegionsFile(regionFilename); + + if ~strcmp(this.HUtable, 'internal') + hlutFilename = fullfile(strrep(this.regionsFolder, runFolderName, sprintf('%s%s', runFolderName, tailRun)), 'hLut.inp'); + this.writeHlutFile(hlutFilename, scenIdx); + end + + % write plan file + planFile = fullfile(strrep(this.planFolder, runFolderName, sprintf('%s%s', runFolderName, tailRun)), this.planFilename); + this.writePlanFile(planFile, stf, scenIdx); + + % write planDelivery file + planDeliveryFile = fullfile(strrep(this.planFolder, runFolderName, sprintf('%s%s', runFolderName, tailRun)), ... + this.planDeliveryFilename); + this.writePlanDeliveryFile(planDeliveryFile); + end + end + + function writeCTs(this) + + patientMetadata.imageOrigin = [0 0 0]; + patientMetadata.resolution = [this.doseGrid.resolution.x, this.doseGrid.resolution.y, this.doseGrid.resolution.z]; + patientMetadata.datatype = 'int16'; + + if this.multScen.totNumScen > 1 + [~, runFolderName] = fileparts(this.MCrunFolder); + + for scenIdx = 1:this.multScen.totNumScen + fileNamePatient = fullfile(strrep(this.regionsFolder, runFolderName, sprintf('%s_%d', runFolderName, scenIdx)), ... + this.patientFilename); + ctIdx = this.multScen.linearMask(scenIdx, 1); + matRad_writeMHD(fileNamePatient, this.HUcube{ctIdx}, patientMetadata); + end + + else + fileNamePatient = fullfile(this.regionsFolder, this.patientFilename); + matRad_writeMHD(fileNamePatient, this.HUcube{1}, patientMetadata); + end + + end + + function writeHlutFile(this, fileName, scenIdx) + + matRad_cfg = MatRad_Config.instance(); + + if ~exist('scenIdx', 'var') || isempty(scenIdx) + scenIdx = 1; + end + + mainFolder = fullfile(matRad_cfg.matRadSrcRoot, 'hluts'); + userDefinedFolder = fullfile(matRad_cfg.primaryUserFolder, 'hluts'); + fredDefinedFolder = fullfile(matRad_cfg.matRadSrcRoot, 'doseCalc', 'FRED', 'hluts'); + + % Collect all the subfolders + + searchPath = [strsplit(genpath(mainFolder), pathsep)'; ... + strsplit(genpath(userDefinedFolder), pathsep)'; ... + strsplit(genpath(fredDefinedFolder), pathsep)']; + + searchPath(cellfun(@isempty, searchPath)) = []; + + % Check for existence of folder paths + searchPath = searchPath(cellfun(@isfolder, searchPath)); + + availableHLUTs = cellfun(@(x) dir([x, filesep, '*.txt']), searchPath, 'UniformOutput', false); + availableHLUTs = cell2mat(availableHLUTs); + + hLUTindex = find(strcmp([this.HUtable, '.txt'], {availableHLUTs.name})); + + if isempty(hLUTindex) + errString = sprintf('Cannot open hLut: %s. Available hLut files are: ', hLutFile); + errString = [errString, sprintf('\ninternal')]; + for hLUTindex = 1:numel(availableHLUTs) + errString = [errString, sprintf('\n%s', strrep(fullfile(availableHLUTs(hLUTindex).folder, ... + availableHLUTs(hLUTindex).name), '\', '\\'))]; + end + matRad_cfg.dispError(errString); + + else + selectedHlutfile = fullfile(availableHLUTs(hLUTindex).folder, availableHLUTs(hLUTindex).name); + selectedHlut = this.readHlutFileToStruct(selectedHlutfile); + end + + % Apply relative range shift + selectedHlut.RSP = selectedHlut.RSP * (1 + this.multScen.relRangeShift(scenIdx)); + + % How do we handle absolute shift? Do we insert a slab of water + % in front of the patient? + + % Write the hlut back + this.writeHlutFileFromStruct(fileName, selectedHlut); + + end + + function materials = readHlutFileToStruct(this, fileName) + + matRad_cfg = MatRad_Config.instance(); + fid = fopen(fileName, 'r'); + if fid == -1 + matRad_cfg.dispError('Cannot open file.'); + end + + % --- Read header line --- + headerLine = fgetl(fid); + + % Remove "matColumns:" and split column names + headerLine = strrep(headerLine, 'matColumns:', ''); + colNames = strsplit(strtrim(headerLine)); + + % --- Read data lines --- + data = []; + while ~feof(fid) + line = strtrim(fgetl(fid)); + if strncmp(line, 'mat:', length('mat:')) + line = strrep(line, 'mat:', ''); + values = sscanf(line, '%f')'; + data = [data; values]; + end + end + + fclose(fid); + + % --- Assign main fields --- + materials = struct(); + + materials.HU = data(:, strcmp(colNames, 'HU'))'; + materials.rho = data(:, strcmp(colNames, 'rho'))'; + materials.RSP = data(:, strcmp(colNames, 'RSP'))'; + materials.Ipot = data(:, strcmp(colNames, 'Ipot'))'; + materials.Lrad = data(:, strcmp(colNames, 'Lrad'))'; + + % --- Composition fields --- + compStartIdx = find(strcmp(colNames, 'C')); % first composition column + compNames = colNames(compStartIdx:end); + + materials.materialComposition = struct(); + + for i = 1:length(compNames) + materials.materialComposition.(compNames{i}) = ... + data(:, compStartIdx + i - 1)'; + end + + end + + function writeHlutFileFromStruct(this, fileName, materials) + + matRad_cfg = MatRad_Config.instance(); + + fid = fopen(fileName, 'w'); + if fid == -1 + matRad_cfg.dispError('Cannot open file.'); + end + + % --- Main fields --- + mainFields = {'HU', 'rho', 'RSP', 'Ipot', 'Lrad'}; + + % --- Composition fields --- + compFields = fieldnames(materials.materialComposition); + + % --- Write header --- + fprintf(fid, 'matColumns: '); + fprintf(fid, '%s ', mainFields{:}); + fprintf(fid, '%s ', compFields{:}); + fprintf(fid, '\n'); + + % --- Number of materials --- + n = numel(materials.HU); + + % --- Write rows --- + for i = 1:n + fprintf(fid, 'mat: '); + + % Write main properties + for k = 1:numel(mainFields) + value = materials.(mainFields{k})(i); + fprintf(fid, '%g ', value); + end + + % Write composition values + for k = 1:numel(compFields) + value = materials.materialComposition.(compFields{k})(i); + fprintf(fid, '%g ', value); + end + + fprintf(fid, '\n'); + end + + fclose(fid); + end - function dij = loadBiologicalData(this,cst,dij) - matRad_cfg = MatRad_Config.instance(); - + function dij = loadBiologicalData(this, cst, dij) + matRad_cfg = MatRad_Config.instance(); + matRad_cfg.dispInfo('Initializing biological dose calculation...\n'); - - dij.ax = zeros(dij.doseGrid.numOfVoxels,1); - dij.bx = zeros(dij.doseGrid.numOfVoxels,1); - + + dij.ax = zeros(dij.doseGrid.numOfVoxels, 1); + dij.bx = zeros(dij.doseGrid.numOfVoxels, 1); + cstDownsampled = matRad_setOverlapPriorities(cst); - + % resizing cst to dose cube resolution - cstDownsampled = matRad_resizeCstToGrid(cstDownsampled,dij.ctGrid.x,dij.ctGrid.y,dij.ctGrid.z,... - dij.doseGrid.x,dij.doseGrid.y,dij.doseGrid.z); - + cstDownsampled = matRad_resizeCstToGrid(cstDownsampled, dij.ctGrid.x, dij.ctGrid.y, dij.ctGrid.z, ... + dij.doseGrid.x, dij.doseGrid.y, dij.doseGrid.z); + % retrieve photon LQM parameter for the current dose grid voxels - [dij.ax,dij.bx] = matRad_getPhotonLQMParameters(cstDownsampled,dij.doseGrid.numOfVoxels,this.VdoseGrid); - - end + [dij.ax, dij.bx] = matRad_getPhotonLQMParameters(cstDownsampled, dij.doseGrid.numOfVoxels, this.VdoseGrid); + end - function dij = allocateQuantityMatrixContainers(this,dij,names) + function dij = allocateQuantityMatrixContainers(this, dij, names) % if this.calcDoseDirect % numOfBixelsContainer = 1; % else % numOfBixelsContainer = dij.totalNumOfBixels; % end - - %Loop over all requested quantities + + % Loop over all requested quantities for n = 1:numel(names) dij.(names{n}) = cell(size(this.multScen.scenMask)); - - %Now preallocate a matrix in each active scenario using the - %scenmask + + % Now preallocate a matrix in each active scenario using the + % scenmask if this.calcDoseDirect - dij.(names{n})(this.multScen.scenMask) = {zeros(dij.doseGrid.numOfVoxels,this.numOfColumnsDij)}; + dij.(names{n})(this.multScen.scenMask) = {zeros(dij.doseGrid.numOfVoxels, this.numOfColumnsDij)}; else - %We preallocate a sparse matrix with sparsity of - %1e-3 to make the filling slightly faster - %TODO: the preallocation could probably - %have more accurate estimates - dij.(names{n})(this.multScen.scenMask) = {spalloc(dij.doseGrid.numOfVoxels,this.numOfColumnsDij,round(prod(dij.doseGrid.numOfVoxels,this.numOfColumnsDij)*1e-3))}; + % We preallocate a sparse matrix with sparsity of + % 1e-3 to make the filling slightly faster + % TODO: the preallocation could probably + % have more accurate estimates + + dij.(names{n})(this.multScen.scenMask) = {spalloc(dij.doseGrid.numOfVoxels, ... + this.numOfColumnsDij, ... + round(prod(dij.doseGrid.numOfVoxels, ... + this.numOfColumnsDij) * 1e-3)) ... + }; end end end + end methods @@ -308,7 +526,7 @@ function setDefaults(this) this.HUtable = this.defaultHUtable; this.externalCalculation = 'off'; this.useGPU = false; - this.sourceModel = this.AvailableSourceModels{1}; + this.sourceModel = this.availableSourceModels{1}; this.roomMaterial = 'Air'; this.printOutput = true; this.numHistoriesDirect = matRad_cfg.defaults.propDoseCalc.numHistoriesDirect; @@ -319,49 +537,50 @@ function setDefaults(this) this.outputMCvariance = false; this.constantRBE = NaN; this.ignoreOutsideDensities = false; - this.dijFormatVersion = this.defaultDijFormatVersion; end - function writeHlut(this,hLutFile) + function writeHlut(this, hLutFile, fileName) matRad_cfg = MatRad_Config.instance(); - fileName = fullfile(this.regionsFolder, 'hLut.inp'); - mainFolder = fullfile(matRad_cfg.matRadSrcRoot,'hluts'); + mainFolder = fullfile(matRad_cfg.matRadSrcRoot, 'hluts'); userDefinedFolder = fullfile(matRad_cfg.primaryUserFolder, 'hluts'); fredDefinedFolder = fullfile(matRad_cfg.matRadSrcRoot, 'doseCalc', 'FRED', 'hluts'); % Collect all the subfolders - searchPath = [strsplit(genpath(mainFolder), pathsep)';... - strsplit(genpath(userDefinedFolder), pathsep)';... - strsplit(genpath(fredDefinedFolder),pathsep)']; - + searchPath = [strsplit(genpath(mainFolder), pathsep)'; ... + strsplit(genpath(userDefinedFolder), pathsep)'; ... + strsplit(genpath(fredDefinedFolder), pathsep)']; + searchPath(cellfun(@isempty, searchPath)) = []; % Check for existence of folder paths searchPath = searchPath(cellfun(@isfolder, searchPath)); - - availableHLUTs = cellfun(@(x) dir([x, filesep, '*.txt']), searchPath, 'UniformOutput',false); + + availableHLUTs = cellfun(@(x) dir([x, filesep, '*.txt']), searchPath, 'UniformOutput', false); availableHLUTs = cell2mat(availableHLUTs); - hLUTindex = find(strcmp([hLutFile,'.txt'], {availableHLUTs.name})); + hLUTindex = find(strcmp([hLutFile, '.txt'], {availableHLUTs.name})); if ~isempty(hLUTindex) selectedHlutfile = fullfile(availableHLUTs(hLUTindex).folder, availableHLUTs(hLUTindex).name); - + template = fileread(selectedHlutfile); - + newLut = fopen(fileName, 'w'); fprintf(newLut, template); fclose(newLut); else - - errString = sprintf('Cannot open hLut: %s. Available hLut files are: ',hLutFile); + + errString = sprintf('Cannot open hLut: %s. Available hLut files are: ', hLutFile); errString = [errString, sprintf('\ninternal')]; - for hLUTindex=1:numel(availableHLUTs) - errString = [errString, sprintf('\n%s', strrep(fullfile(availableHLUTs(hLUTindex).folder, availableHLUTs(hLUTindex).name), '\', '\\'))]; + for hLUTindex = 1:numel(availableHLUTs) + + errString = [errString, sprintf('\n%s', ... + strrep(fullfile(availableHLUTs(hLUTindex).folder, availableHLUTs(hLUTindex).name), '\', '\\'))]; + end matRad_cfg.dispError(errString); end @@ -371,20 +590,20 @@ function writeHlut(this,hLutFile) end methods (Static) + function cmdString = cmdCall(newCmdString) - persistent fredCmdCall; + persistent fredCmdCall if nargin > 0 - fredCmdCall = newCmdString; + fredCmdCall = newCmdString; elseif isempty(fredCmdCall) if ispc -% fredCmdCall = 'wsl if [ -f ~/.fredenv.sh ] ; then source ~/.fredenv.sh ; fi; fred'; fredCmdCall = 'fred '; elseif isunix fredCmdCall = 'if [ -f ~/.fredenv.sh ] ; then source ~/.fredenv.sh ; fi; fred'; else matRad_cfg = MatRad_Config.instance(); matRad_cfg.dispError('OS not supported for FRED!'); - end + end end cmdString = fredCmdCall; end @@ -401,21 +620,21 @@ function writeHlut(this,hLutFile) [status, cmdOut] = system([currCmdCall, ' -listVers']); if status == 0 - nLidx = regexp(cmdOut, '\n')+6; %6 because of tab - nVersions = numel(nLidx)-1; + nLidx = regexp(cmdOut, '\n') + 6; % 6 because of tab + nVersions = numel(nLidx) - 1; - for versIdx=1:nVersions - availableVersions = [availableVersions,{cmdOut(nLidx(versIdx):nLidx(versIdx)+5)}]; + for versIdx = 1:nVersions + availableVersions = [availableVersions, {cmdOut(nLidx(versIdx):nLidx(versIdx) + 5)}]; end else - matRad_cfg.dispError('Something wrong occured in checking FRED available version. Please check correct FRED installation'); + matRad_cfg.dispError('Something wrong occurred in checking FRED available version. Please check correct FRED installation'); end end - function [available,msg] = isAvailable(pln,machine) + function [available, msg] = isAvailable(pln, machine) % see superclass for information - + msg = []; available = false; @@ -423,21 +642,21 @@ function writeHlut(this,hLutFile) machine = matRad_loadMachine(pln); end - %checkBasic + % checkBasic try - checkBasic = isfield(machine,'meta') && isfield(machine,'data'); + checkBasic = isfield(machine, 'meta') && isfield(machine, 'data'); - %check modality + % check modality checkModality = any(strcmp(DoseEngines.matRad_ParticleFREDEngine.possibleRadiationModes, machine.meta.radiationMode)); - + preCheck = checkBasic && checkModality; if ~preCheck - return; + return end catch msg = 'Your machine file is invalid and does not contain the basic field (meta/data/radiationMode)!'; - return; + return end available = preCheck; end @@ -445,24 +664,31 @@ function writeHlut(this,hLutFile) function execCheck = checkExec() matRad_cfg = MatRad_Config.instance(); - - %Check if I can obtain FRED version + + % Check if I can obtain FRED version try ver = DoseEngines.matRad_ParticleFREDEngine.getVersion(); if ~isempty(ver) execCheck = true; else execCheck = false; - msg = sprintf('Couldn''t call FRED executable. Please set the correct system call with DoseEngines.matRad_ParticleFREDEngine.cmdCall(''path/to/executable''). Current value is ''%s''',DoseEngines.matRad_ParticleFREDEngine.cmdCall); + + msg = sprintf(['Couldn''t call FRED executable. ' ... + 'Please set the correct path with DoseEngines.matRad_ParticleFREDEngine.cmdCall(''path/to/executable''). ' ... + 'Current value is ''%s'''], DoseEngines.matRad_ParticleFREDEngine.cmdCall); + matRad_cfg.dispError(msg); end catch execCheck = false; - msg = sprintf('Couldn''t call FRED executable. Please set the correct system call with DoseEngines.matRad_ParticleFREDEngine.cmdCall(''path/to/executable''). Current value is ''%s''',DoseEngines.matRad_ParticleFREDEngine.cmdCall); + + msg = sprintf(['Couldn''t call FRED executable. ' ... + 'Please set the correct path with DoseEngines.matRad_ParticleFREDEngine.cmdCall(''path/to/executable''). ' ... + 'Current value is ''%s'''], DoseEngines.matRad_ParticleFREDEngine.cmdCall); matRad_cfg.dispError(msg); end end - + function version = getVersion() % Function to get current default FRED version matRad_cfg = MatRad_Config.instance(); @@ -484,234 +710,139 @@ function writeHlut(this,hLutFile) version = []; end catch - matRad_cfg.dispWarning('Something wrong occured in checking FRED installation. Please check correct FRED installation'); + matRad_cfg.dispWarning('Something wrong occurred in checking FRED installation. Please check correct FRED installation'); version = []; end end % end - function dijMatrix = readSparseDijBin(fName) - % FRED function to read sparseDij in .bin format - % call - % readSparseDijBin(fName) - % - % input - % fName: filename to read - % - % output - % dijMatrix: dij structure - + function dijMats = readSparseDataV2(fID, fileFormatVersion, cubeDim, numberOfBixels, nComponents) matRad_cfg = MatRad_Config.instance(); - - f = fopen(fName,'r','l'); - try - %Header - fileFormatVerison = fread(f,1,"int32"); - dims = fread(f,3,"int32"); - res = fread(f,3,"float32"); - offset = fread(f,3,"float32"); - nComponents = fread(f,1,"int32"); - numberOfBixels = fread(f,1,"int32"); - - values = []; - valuesDen = []; - voxelIndices = []; - colIndices = []; - valuesNom = []; - - matRad_cfg.dispInfo("Reading %d number of beamlets in %d voxels (%dx%dx%d)\n",numberOfBixels,prod(dims),dims(1),dims(2),dims(3)); - - bixelCounter = 0; - for i = 1:numberOfBixels - %Read Beamlet - bixNum = fread(f,1,"int32"); - numVox = fread(f,1,"int32"); - - bixelCounter = bixelCounter +1; - - colIndices(end+1:end+numVox) = bixelCounter; - currVoxelIndices = fread(f,numVox,"uint32") + 1; - tmpValues = fread(f,numVox*nComponents,"float32"); - valuesNom = tmpValues(1:nComponents:end); - - if nComponents == 2 - valuesDen = tmpValues(nComponents:nComponents:end); - values(end+1:end+numVox) = valuesNom./valuesDen; - else - values(end+1:end+numVox) = valuesNom; - end - - % x and y components have been permuted in CT - [indY, indX, indZ] = ind2sub(dims, currVoxelIndices); - - voxelIndices(end+1:end+numVox) = sub2ind(dims([2,1,3]), indX, indY, indZ); - matRad_cfg.dispInfo("\tRead beamlet %d, %d voxels...\n",bixNum,numVox); + values = zeros([nComponents, 0]); + voxelIndices = []; + colIndices = []; + + for i = 1:numberOfBixels + % Read Beamlet + bixNum = fread(fID, 1, "int32"); + numVox = fread(fID, 1, "int32"); + + colIndices(end + 1:end + numVox) = i; + currVoxelIndices = fread(fID, numVox, "uint32") + 1; + values(:, end + 1:end + numVox) = fread(fID, [nComponents numVox], "float32"); + + % x and y components have been permuted in CT + [indY, indX, indZ] = ind2sub(cubeDim, currVoxelIndices); + + voxelIndices(end + 1:end + numVox) = sub2ind(cubeDim([2, 1, 3]), indX, indY, indZ); + if matRad_cfg.logLevel > 2 + matRad_progress(i, numberOfBixels); end - dijMatrix = sparse(voxelIndices,colIndices,values,prod(dims),numberOfBixels); - - fclose(f); - catch - fclose(f); - matRad_cfg.dispError('unable to load file: %s',fName); + end + for c = 1:nComponents + dijMats{c} = sparse(voxelIndices, colIndices, values(c, :), prod(cubeDim), numberOfBixels); end end - function dijMatrix = readSparseDijBin_v21(fName) - + function dijMats = readSparseDataV3(fID, fileFormatVersion, cubeDim, numberOfBixels, nComponents) matRad_cfg = MatRad_Config.instance(); - - f = fopen(fName,'r','l'); - try - %Header - fileFormatVerison = fread(f,1,"int32"); - dims = fread(f,3,"int32"); - res = fread(f,3,"float32"); - offset = fread(f,3,"float32"); - orientation = fread(f,9,"float32"); - nComponents = fread(f,1,"int32"); - numberOfBixels = fread(f,1,"int32"); - - values = []; - valuesDen = []; - voxelIndices = []; - colIndices = []; - valuesNom = []; - - matRad_cfg.dispInfo("Reading %d number of beamlets in %d voxels (%dx%dx%d)\n",numberOfBixels,prod(dims),dims(1),dims(2),dims(3)); - - bixelCounter = 0; - for i = 1:numberOfBixels - %Read Beamlet - bixNum = fread(f,1,"uint32"); - numVox = fread(f,1,"int32"); - - bixelCounter = bixelCounter +1; - - colIndices(end+1:end+numVox) = bixelCounter; - currVoxelIndices = fread(f,numVox,"uint32") + 1; - tmpValues = fread(f,numVox*nComponents,"float32"); - valuesNom = tmpValues(1:nComponents:end); - - if nComponents == 2 - valuesDen = tmpValues(nComponents:nComponents:end); - values(end+1:end+numVox) = valuesNom./valuesDen; - else - values(end+1:end+numVox) = valuesNom; - end - - % x and y components have been permuted in CT - [indY, indX, indZ] = ind2sub(dims, currVoxelIndices); - - voxelIndices(end+1:end+numVox) = sub2ind(dims([2,1,3]), indX, indY, indZ); - matRad_cfg.dispInfo("\tRead beamlet %d, %d voxels...\n",bixNum,numVox); - end - dijMatrix = sparse(voxelIndices,colIndices,values,prod(dims),numberOfBixels); - - fclose(f); - catch - fclose(f); - matRad_cfg.dispError('unable to load file: %s',fName); + allBixelMeta = fread(fID, [3 numberOfBixels], "uint32"); % (#bixels, PBidx, FID, PBID) + + % Size information for each component + componentDataSize = fread(f, nComponents, "uint32"); + + % Data + dijMatrices = cell(1, nComponents); + for c = 1:nComponents + PBidxs = fread(fID, componentDataSize(c), "uint32") + 1; + voxelIndices = fread(fID, componentDataSize(c), "uint32") + 1; + values = fread(fID, componentDataSize(c), "float32"); + + % x and y components have been permuted in CT + [indY, indX, indZ] = ind2sub(cubeDim, voxelIndices); + voxelIndices = sub2ind(cubeDim([2, 1, 3]), indX, indY, indZ); + + dijMatrices{c} = sparse(voxelIndices, PBidxs, values, prod(cubeDim), numberOfBixels); end end - function dijMatrix = readSparseDijBin_v31(fName) - + function dijMatrices = readSparseDijBin(fName) + % FRED function to read sparseDij in .bin format + % call: + % readSparseDijBin(fName) + % + % input: + % fName: filename to read + % + % output: + % dijMatrix: dij structure matRad_cfg = MatRad_Config.instance(); - f = fopen(fName,'r','l'); + f = fopen(fName, 'r', 'l'); try - fileFormatVerison = fread(f,1,"int32"); - dims = fread(f,3,"int32"); - res = fread(f,3,"float32"); - offset = fread(f,3,"float32"); - orientation = fread(f,9,"float32"); - nComponents = fread(f,1,"uint32"); - numberOfBixels = fread(f,1,"uint32"); - - values = []; - valuesDen = []; - voxelIndices = []; - colIndices = []; - valuesNom = []; - - matRad_cfg.dispInfo("Reading %d number of beamlets in %d voxels (%dx%dx%d)\n",numberOfBixels,prod(dims),dims(1),dims(2),dims(3)); - - allBixelMeta = fread(f, 3*numberOfBixels, "uint32"); - allBixelMeta = reshape(allBixelMeta, 3,numberOfBixels)'; % (#bixels, PBidx, FID, PBID) - - % if nComponents>1 - % matRad_cfg.dispWarning('!! Only last component will be read!!') - % end - % Components header - for i = 1:nComponents - componentDataSize(i) = fread(f,1,"uint32"); + % Header + fileFormatVersion = fread(f, 1, "int32"); + dims = fread(f, 3, "int32"); + res = fread(f, 3, "float32"); + offset = fread(f, 3, "float32"); + if fileFormatVersion > 20 + orientation = fread(f, 9, "float32"); end + nComponents = fread(f, 1, "int32"); + numberOfBixels = fread(f, 1, "int32"); - % Data - for compIdx=1:nComponents - % PBidx and voxelIndices should be always the same for - % each component? - PBidxs = fread(f, componentDataSize(compIdx), "uint32")+1; - voxelIndices = fread(f, componentDataSize(compIdx), "uint32")+1; - tmpValues{compIdx} = fread(f, componentDataSize(compIdx), "float32"); - - end - - % For now we only have Dose and LET scorers, if nComponents - % == 2, then it's LET - - if nComponents>1 - values = tmpValues{1}./tmpValues{2}; - else - values = tmpValues{1}; - end - - % x and y components have been permuted in CT - [indY, indX, indZ] = ind2sub(dims, voxelIndices); - - voxelIndices = sub2ind(dims([2,1,3]), indX, indY, indZ); % + (nComponents-1)*prod(dims); - % This will probably not work for multiple components - dijMatrix = sparse(voxelIndices,PBidxs,values,prod(dims), numberOfBixels); - - fclose(f); - - catch + matRad_cfg.dispInfo('Reading FRED dij with %d components of size %dx%d (voxels x beamlets) with cubeDim = %dx%dx%d\n', ... + nComponents, prod(dims), numberOfBixels, dims(1), dims(2), dims(3)); + + if fileFormatVersion < 30 + dijMatrices = DoseEngines.matRad_ParticleFREDEngine.readSparseDataV2(f, fileFormatVersion, dims, numberOfBixels, nComponents); + else + dijMatrices = DoseEngines.matRad_ParticleFREDEngine.readSparseDataV3(f, fileFormatVersion, dims, numberOfBixels, nComponents); + end + + fclose(f); + catch ME fclose(f); - matRad_cfg.dispError('unable to load file: %s',fName); - + matRad_cfg.dispError('unable to load file %s: %s', fName, ME.message); end end - [doseCubeV, letdCubeV, fileName] = readSimulationOutput(runFolder,calcDoseDirect, varargin); + % Used to check against a machine file if a specific quantity can be + % computed. + function q = providedQuantities(machine) + q = {'physicalDose', 'LET'}; + end - end + [doseCubeV, letdCubeV, fileName] = readSimulationOutput(runFolder, calcDoseDirect, calcLET) + end - methods (Access = private) + methods (Access = private) function updatePaths(obj, rootFolder) - if ~strcmp(rootFolder, obj.FREDrootFolder) - obj.FREDrootFolder = rootFolder; + if ~strcmp(rootFolder, obj.workingDir) + obj.workingDir = rootFolder; end - - obj.MCrunFolder = fullfile(obj.FREDrootFolder, 'MCrun'); + + obj.MCrunFolder = fullfile(obj.workingDir, 'MCrun'); obj.inputFolder = fullfile(obj.MCrunFolder, 'inp'); obj.regionsFolder = fullfile(obj.inputFolder, 'regions'); obj.planFolder = fullfile(obj.inputFolder, 'plan'); end - function [radiationMode] = updateRadiationMode(this,value) - % This function also resets the values for primary mass and numebr + function [radiationMode] = updateRadiationMode(this, value) + + % This function also resets the values for primary mass and number % of nucleons. Used for possible future extension to multiple % ion species matRad_cfg = MatRad_Config.instance(); - + if any(strcmp(value, this.possibleRadiationModes)) radiationMode = value; else @@ -727,132 +858,108 @@ function updatePaths(obj, rootFolder) matRad_cfg.dispError('Only proton dose calculation available with this version of FRED'); end - + matRad_cfg.dispWarning('Selected radiation modality: %s with primary mass: %2.3f', radiationMode, this.primaryMass); end - function isHigher = isVersionHigher(this,version) - isHigher = false; - + function isLower = isVersionLower(this, version) % This function directly looks at FRED installation, not at % the current FRED version stored in the class property. fredVersion = this.getVersion(); + isLower = false; + if ~isempty(fredVersion) % Decompose the current version for comparison - v1 = sscanf(fredVersion, '%d.%d.%d')'; - v2 = sscanf(version, '%d.%d.%d')'; - - if (v1(1) >= v2(1)) && (v1(2) >= v2(2)) && (v1(3) > v2(3)) - isHigher = true; - end - end - + vdiff = sscanf(fredVersion, '%d.%d.%d') - sscanf(version, '%d.%d.%d'); + firstdiff = find(vdiff, 1, 'first'); + isLower = ~isempty(firstdiff) && vdiff(firstdiff) < 0; + end end - - end - + end + methods function set.sourceModel(this, value) matRad_cfg = MatRad_Config.instance(); - valid = ischar(value) && any(strcmp(value, this.AvailableSourceModels)); + valid = ischar(value) && any(strcmp(value, this.availableSourceModels)); if valid this.sourceModel = value; else - matRad_cfg.dispWarning('Unable to set source model:%s, setting default:%s', value, this.AvailableSourceModels{1}) - this.sourceModel = this.AvailableSourceModels{1}; + + matRad_cfg.dispWarning('Unable to set source model:%s, setting default:%s', value, this.availableSourceModels{1}); + this.sourceModel = this.availableSourceModels{1}; + end - end + end - function version = get.currentVersion(this) + function version = get.currentVersion(this) - if isempty(this.currentVersion) + if isempty(this.currentVersion) version = this.getVersion(); this.currentVersion = version; - else + else version = this.currentVersion; - end + end + + end - end + function v = get.dijFormatVersion(this) - function set.dijFormatVersion(this,value) + matRad_cfg = MatRad_Config.instance(); - matRad_cfg = MatRad_Config.instance(); + if ~isempty(this.forceDijFormatVersion) + v = this.forceDijFormatVersion; + elseif this.isVersionLower('3.76.0') + v = 20; + else + v = 31; + end - if ~this.isVersionHigher('3.70.0') - % FRED version < 3.70.0 does not allow dij version - % selection and only works with ifFormatVersion < 21 - - this.dijFormatVersion = this.defaultDijFormatVersion; - - if ~strcmp(value, this.defaultDijFormatVersion) - matRad_cfg.dispWarning(sprintf('ijFormat: %s not available for FRED version < 3.70.0', value)); + % FRED version <= 3.70.0 does not allow dij version + % selection and only works with ifFormatVersion < 21 + if this.isVersionLower('3.76.0') + if v > 20 + matRad_cfg.dispWarning('FRED version %s does not support ijFormatVersions>20. Version 20 will be used!'); + v = 20; end - - else - %if this.useGPU - if strcmp(value, '20') - this.dijFormatVersion = '21'; - else - this.dijFormatVersion = value; - end - %else - % this.dijFormatVersion = '20'; - %end - end - end - - function readerHandle = get.dijReaderHandle(this) - - matRad_cfg = MatRad_Config.instance(); - - switch this.dijFormatVersion - - case '20' - readerHandle = @(lFile) DoseEngines.matRad_ParticleFREDEngine.readSparseDijBin(lFile); - case '21' - readerHandle = @(lFile) DoseEngines.matRad_ParticleFREDEngine.readSparseDijBin_v21(lFile); - case '31' - readerHandle = @(lFile) DoseEngines.matRad_ParticleFREDEngine.readSparseDijBin_v31(lFile); - otherwise - matRad_cfg.dispWarning(sprintf('Unable to read dij format version: %s, using default: %s', this.dijFormatVersion, this.defaultDijFormatVersion)); - readerHandle = @(lFile) DoseEngines.matRad_ParticleFREDEngine.readSparseDijBin(lFile); - end - - - end - - function set.radiationMode(this,value) - if ischar(value) - if ~isempty(this.radiationMode) && ~strcmp(this.radiationMode, value) + else + if v == 20 + matRad_cfg.dispWarning('FRED version %s does no longer support ijFormatVersions 20. Version 21 will be used!'); + v = 21; + end + end + + end + + function set.radiationMode(this, value) + if ischar(value) + if ~isempty(this.radiationMode) && ~strcmp(this.radiationMode, value) this.radiationMode = value; this.updateRadiationMode(this.radiationMode); - elseif isempty(this.radiationMode) + elseif isempty(this.radiationMode) this.radiationMode = value; - end - end + end + end - end + end - function set.FREDrootFolder(obj, pathValue) - obj.FREDrootFolder = pathValue; + function set.workingDir(obj, pathValue) + obj.workingDir = pathValue; obj.updatePaths(pathValue); end - - function set.externalCalculation(this, value) - % Set exportCalculation value, available options are: - % - false: (default) runs the FRED simulation (requires FRED installation) - % - write/1: triggers the file export - % - 'path': simulation data will be loaded from the specified - % path. Full simulation directory path should be provided. - % Example: 'matRadRoot/userdata/FRED/' - + function set.externalCalculation(this, value) + % Set exportCalculation value, available options are: + % - false: (default) runs the FRED simulation (requires FRED installation) + % - write/1: triggers the file export + % - 'path': simulation data will be loaded from the specified + % path. Full simulation directory path should be provided. + % Example: 'matRadRoot/userdata/FRED/' if isnumeric(value) || islogical(value) switch value case 1 @@ -860,18 +967,16 @@ function updatePaths(obj, rootFolder) case 0 this.externalCalculation = 'off'; end - elseif ischar(value) - - if any(strcmp(value, {'write', 'off'})) - this.externalCalculation = value; - elseif isfolder(value) + elseif ischar(value) + if any(strcmp(value, {'write', 'off'})) + this.externalCalculation = value; + elseif isfolder(value) this.externalCalculation = value; - + this.updatePaths(value); - end - end - end + end + end + end - end + end end - diff --git a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/readSimulationOutput.m b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/readSimulationOutput.m index 0ae5da683..a9b2ee65f 100644 --- a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/readSimulationOutput.m +++ b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/readSimulationOutput.m @@ -1,18 +1,15 @@ -function [doseCube, letCube, loadFileName] = readSimulationOutput(runFolder,calcDoseDirect,varargin) +function [doseCube, letCube, loadFileName] = readSimulationOutput(runFolder,calcDoseDirect,calcLET) % FRED helper to read simulation output -% call +% call: % readSimulationOutput(runFolder,calcDoseDirect, varargin) % -% input +% input: % runFolder: path to folder containing the simulation files % calcDoseDirect: boolean to trigger dij or .mhd reading % -% optional: -% calLET: addirional boolean to trigger loading of LETd -% readFunctionHandle: handle to readout function for ij-scorer %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2023 the matRad development team. +% Copyright 2023-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -27,10 +24,9 @@ p = inputParser(); addRequired(p, 'runFolder', @ischar); addRequired(p, 'calcDoseDirect', @islogical); -addParameter(p, 'calcLET',0,@islogical); -addParameter(p, 'readFunctionHandle', @(lFile) DoseEngines.matRad_ParticleFREDEngine.readSparseDijBin(lFile)) +addOptional(p, 'calcLET',false,@islogical); -parse(p, runFolder,calcDoseDirect, varargin{:}); +parse(p, runFolder,calcDoseDirect, calcLET); runFolder = p.Results.runFolder; calcDoseDirect = p.Results.calcDoseDirect; @@ -38,43 +34,63 @@ doseCube = []; letCube = []; -loadFileName = []; + +outFolder = fullfile(runFolder,'out'); if ~calcDoseDirect + letCompatible = true; - doseDijFolder = fullfile(runFolder, 'out', 'scoreij'); + scoreIjFolder = fullfile(outFolder, 'scoreij'); doseDijFile = 'Phantom.Dose.bin'; - loadFileName = fullfile(doseDijFolder,doseDijFile); + letdDijFile = 'Phantom.LETd.bin'; + if ~exist(scoreIjFolder,"dir") + scoreIjFolder = outFolder; + doseDijFile = 'Dij.bin'; + letCompatible = false; + end + loadFileName = fullfile(scoreIjFolder,doseDijFile); - matRad_cfg.dispInfo(sprintf('Looking for scorer-ij output in sub folder: %s\n', strrep(doseDijFolder, '\', '\\'))); + matRad_cfg.dispInfo('Looking for scorer-ij output in sub folder: %s\n', strrep(scoreIjFolder, '\', '\\')); % read dij matrix if isfile(loadFileName) -% doseCube = DoseEngines.matRad_ParticleFREDEngine.readSparseDijBin(loadFileName); - doseCube = p.Results.readFunctionHandle(loadFileName); + dijMatrices = DoseEngines.matRad_ParticleFREDEngine.readSparseDijBin(loadFileName); + doseCube = dijMatrices{1}; else matRad_cfg.dispError(sprintf('Unable to find file: %s', strrep(loadFileName, '\', '\\'))); end - if calcLET + if calcLET && letCompatible + - letdDijFile = 'Phantom.LETd.bin'; - letdDijFileName = fullfile(doseDijFolder,letdDijFile); + letdDijFileName = fullfile(scoreIjFolder,letdDijFile); try -% letCube = DoseEngines.matRad_ParticleFREDEngine.readSparseDijBin(letdDijFileName); - letCube = p.Results.readFunctionHandle(letdDijFileName); + dijMatrices = DoseEngines.matRad_ParticleFREDEngine.readSparseDijBin(letdDijFileName); + % TODO: store nominator and denominator in dij and allow this + % structure for LETd calculation in calcCubes and + % backProjection. + dijNom = dijMatrices{1}; + dijDen = dijMatrices{2}; + letCube = spfun(@(x) 1./x,dijDen); + letCube = letCube .* dijNom ./ 10; %divided by 10 to have keV/um catch matRad_cfg.dispError('unable to load file: %s',letdDijFileName); end end else - - doseCubeFolder = fullfile(runFolder, 'out', 'score'); + scoreFolder = fullfile(outFolder, 'score'); doseCubeFileName = 'Phantom.Dose.mhd'; - loadFileName = fullfile(doseCubeFolder, doseCubeFileName); + letdCubeFileName = 'Phantom.LETd.mhd'; + if ~exist(scoreFolder,"dir") + scoreFolder = outFolder; + doseCubeFileName = 'Dose.mhd'; + letdCubeFileName = 'LETd.mhd'; + end + + loadFileName = fullfile(scoreFolder, doseCubeFileName); - matRad_cfg.dispInfo(sprintf('Looking for scorer file in sub folder: %s\n', strrep(doseCubeFolder, '\', '\\'))); + matRad_cfg.dispInfo(sprintf('Looking for scorer file in sub folder: %s\n', strrep(scoreFolder, '\', '\\'))); if isfile(loadFileName) doseCube = matRad_readMHD(loadFileName); @@ -83,12 +99,8 @@ end if calcLET - - letdDijFolder = doseCubeFolder; - letdCubeFileName = 'Phantom.LETd.mhd'; - try - letCube = matRad_readMHD(fullfile(letdDijFolder, letdCubeFileName)); + letCube = matRad_readMHD(fullfile(scoreFolder, letdCubeFileName)); catch matRad_cfg.dispError('Unable to load file: %s',fullfile(letdDijFolder, letdCubeFileName)); end @@ -97,4 +109,4 @@ end matRad_cfg.dispInfo('Loading succesful!\n'); -end \ No newline at end of file +end diff --git a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writePlanDeliveryFile.m b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writePlanDeliveryFile.m index 6cca35019..0457e1fac 100644 --- a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writePlanDeliveryFile.m +++ b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writePlanDeliveryFile.m @@ -1,14 +1,14 @@ function writePlanDeliveryFile(this, fName) % FRED helper to write file for plan delivery routine -% call +% call: % writePlanDeliveryFile(fName) -% -% input +% +% input: % fName: tring specifying the file path and name for saving the data. % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2023 the matRad development team. +% Copyright 2023-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -25,138 +25,137 @@ function writePlanDeliveryFile(this, fName) fprintf(fID, '#Include file defining fields and layers geometry\n'); fprintf(fID, 'include: inp/plan/plan.inp\n'); fprintf(fID, '\n'); - fprintf(fID, '#Define the fields\n'); fprintf(fID, 'for(currField in plan.get(''Fields''))<\n'); - fprintf(fID, '\tfield<\n'); - fprintf(fID, '\t\tID = ${currField.get(''fieldNumber'')}\n'); - fprintf(fID, '\t\tO = [0,${plan.get(''SAD'')},0]\n'); - fprintf(fID, '\t\tL = ${currField.get(''dim'')}\n'); - fprintf(fID, '\t\tpivot = [0.5,0.5,0.5]\n'); - - fprintf(fID, '\t\tl = [0, 0, -1]\n'); - fprintf(fID, '\t\tu = [1, 0 ,0]\n'); - - fprintf(fID, '\tfield>\n'); - - fprintf(fID, '\n'); - fprintf(fID, '\t#Deactivate the fields to avoid geometrical overlap\n'); - fprintf(fID, '\tdeactivate: field_${currField.get(''fieldNumber'')}\n'); + fprintf(fID, '\tfield<\n'); + fprintf(fID, '\t\tID = ${currField.get(''fieldNumber'')}\n'); + fprintf(fID, '\t\tO = [0,${plan.get(''SAD'')},0]\n'); + fprintf(fID, '\t\tL = ${currField.get(''dim'')}\n'); + fprintf(fID, '\t\tpivot = [0.5,0.5,0.5]\n'); + + fprintf(fID, '\t\tl = [0, 0, -1]\n'); + fprintf(fID, '\t\tu = [1, 0 ,0]\n'); + + fprintf(fID, '\tfield>\n'); + + fprintf(fID, '\n'); + fprintf(fID, '\t#Deactivate the fields to avoid geometrical overlap\n'); + fprintf(fID, '\tdeactivate: field_${currField.get(''fieldNumber'')}\n'); fprintf(fID, 'for>\n\n'); - %loop over fields + % loop over fields fprintf(fID, 'for(currField in plan.get(''Fields''))<\n'); - fprintf(fID,'\n'); - fprintf(fID, '\tdef: fieldIdx = currField.get(''fieldNumber'')\n'); - fprintf(fID,'\n'); - - % activate current filed - fprintf(fID, '\t#Activate current field\n'); - fprintf(fID, '\tactivate: field_$fieldIdx\n'); - fprintf(fID,'\n'); - - % Gantry/COunch angles - fprintf(fID,'\t#Collect Gantry and Couch angles\n'); - fprintf(fID, '\tdef: GA = currField.get(''GA'')\n'); - fprintf(fID, '\tdef: CA = currField.get(''CA'')\n'); - fprintf(fID,'\n'); - - % Isocenter - fprintf(fID, '\t#Collect Isocenter\n'); - fprintf(fID, '\tdef: ISO = currField.get(''ISO'')\n'); - fprintf(fID, '\n'); - - fprintf(fID, '\t#First move the patient so that the Isocenter is now in the center of the Room coordinate system\n'); - fprintf(fID, '\ttransform: Phantom move_to ${ISO.item(0)} ${ISO.item(1)} ${ISO.item(2)} Room\n'); - fprintf(fID, '\n'); - - fprintf(fID, '\t#Second rotate the patient according to the gantry and couch angles.\n'); - fprintf(fID, '\t#In this configuration the fileds are always fixed in +SAD in y direction and the patient is rotated accordingly\n'); - fprintf(fID, '\ttransform: Phantom rotate y ${CA} Room\n'); - fprintf(fID, '\ttransform: Phantom rotate z ${GA} Room\n'); - fprintf(fID, '\n'); - - fprintf(fID, '\tfor(layer in currField.get(''Layers''))<\n'); - fprintf(fID, '\n'); - - fprintf(fID, '\t\t#Recover parameters of the current energy layer\n'); - fprintf(fID, '\t\tdef: currEnergy = layer.get(''Energy'')\n'); - fprintf(fID, '\t\tdef: currEspread = layer.get(''Espread'')\n'); - - switch this.sourceModel - case 'gaussian' - fprintf(fID, '\t\tdef: currFWHM = layer.get(''FWHM'')\n'); - - case 'emittance' - fprintf(fID, '\t\tdef: currEmittanceX = layer.get(''emittanceX'')\n'); - - fprintf(fID, '\t\tdef: currTwissAlphaX = layer.get(''twissAlphaX'')\n'); - fprintf(fID, '\t\tdef: currTwissBetaX = layer.get(''twissBetaX'')\n'); - fprintf(fID, '\t\tdef: currReferencePlane = layer.get(''emittanceRefPlaneDistance'')\n'); - case 'sigmaSqrModel' - fprintf(fID, '\t\tdef: currSQr_a = layer.get(''sSQr_a'')\n'); - fprintf(fID, '\t\tdef: currSQr_b = layer.get(''sSQr_b'')\n'); - fprintf(fID, '\t\tdef: currSQr_c = layer.get(''sSQr_c'')\n'); - end - - fprintf(fID, '\n'); - fprintf(fID, '\t\tfor(beamlet in layer.get(''beamlets''))<\n'); - fprintf(fID, '\t\t\tpb<\n'); - fprintf(fID, '\t\t\t\tID = ${beamlet.get(''beamletID'')}\n'); - fprintf(fID, '\t\t\t\tfieldID = $fieldIdx\n'); - switch this.machine.meta.radiationMode - case 'protons' - fprintf(fID, '\t\t\t\tparticle = proton\n'); - case 'carbon' - fprintf(fID, '\t\t\t\tparticle = C12\n'); - end - fprintf(fID, '\t\t\t\tT = $currEnergy\n'); - fprintf(fID, '\t\t\t\tEFWHM = $currEspread\n'); - - switch this.sourceModel - case 'gaussian' - - fprintf(fID, '\t\t\t\tXsec = gauss\n'); - fprintf(fID, '\t\t\t\tFWHM = $currFWHM\n'); - case 'emittance' - fprintf(fID, '\t\t\t\tXsec = emittance\n'); - fprintf(fID, '\t\t\t\temittanceX = $currEmittanceX\n'); - fprintf(fID, '\t\t\t\ttwissAlphaX = $currTwissAlphaX\n'); - fprintf(fID, '\t\t\t\ttwissBetaX = $currTwissBetaX\n'); - fprintf(fID, '\t\t\t\temittanceRefPlaneDistance = 100\n'); - - case 'sigmaSqrModel' - fprintf(fID, '\t\t\t\tXsec = emittance\n'); - fprintf(fID, '\t\t\t\tsigmaSqrModel = [${plan.get(''SAD'')},${currSQr_a},${currSQr_b}, ${currSQr_c}]\n'); - end - - fprintf(fID, '\n'); - fprintf(fID, '\t\t\t\tP = ${beamlet.get(''P'')}\n'); - fprintf(fID, '\t\t\t\tv = ${beamlet.get(''v'')}\n'); - fprintf(fID, '\t\t\t\tN = ${beamlet.get(''w'')}\n'); - fprintf(fID, '\t\t\tpb>\n'); - fprintf(fID, '\t\tfor>\n'); - fprintf(fID, '\tfor>\n'); - fprintf(fID, '\n'); - - fprintf(fID, '\t#Deliver all the pecil beams in this field\n'); - fprintf(fID, '\tdeliver: field_$fieldIdx\n'); - fprintf(fID, '\n'); - - fprintf(fID, '\t#Deactivate the current field\n'); - fprintf(fID, '\tdeactivate: field_$fieldIdx\n'); - fprintf(fID, '\n'); - - fprintf(fID, '\t#Restore the patient to original position\n'); - fprintf(fID, '\ttransform: Phantom rotate z ${-1*GA} Room\n'); - fprintf(fID, '\ttransform: Phantom rotate y ${-1*CA} Room\n'); - fprintf(fID, '\ttransform: Phantom move_to 0 0 0 Room\n'); - - fprintf(fID, 'for>\n\n'); + fprintf(fID, '\n'); + fprintf(fID, '\tdef: fieldIdx = currField.get(''fieldNumber'')\n'); + fprintf(fID, '\n'); + + % activate current filed + fprintf(fID, '\t#Activate current field\n'); + fprintf(fID, '\tactivate: field_$fieldIdx\n'); + fprintf(fID, '\n'); + + % Gantry/COunch angles + fprintf(fID, '\t#Collect Gantry and Couch angles\n'); + fprintf(fID, '\tdef: GA = currField.get(''GA'')\n'); + fprintf(fID, '\tdef: CA = currField.get(''CA'')\n'); + fprintf(fID, '\n'); + + % Isocenter + fprintf(fID, '\t#Collect Isocenter\n'); + fprintf(fID, '\tdef: ISO = currField.get(''ISO'')\n'); + fprintf(fID, '\n'); + + fprintf(fID, '\t#First move the patient so that the Isocenter is now in the center of the Room coordinate system\n'); + fprintf(fID, '\ttransform: Phantom move_to ${ISO.item(0)} ${ISO.item(1)} ${ISO.item(2)} Room\n'); + fprintf(fID, '\n'); + + fprintf(fID, '\t#Second rotate the patient according to the gantry and couch angles.\n'); + fprintf(fID, '\t#In this configuration the fileds are always fixed in +SAD in y direction and the patient is rotated accordingly\n'); + fprintf(fID, '\ttransform: Phantom rotate y ${CA} Room\n'); + fprintf(fID, '\ttransform: Phantom rotate z ${GA} Room\n'); + fprintf(fID, '\n'); + + fprintf(fID, '\tfor(layer in currField.get(''Layers''))<\n'); + fprintf(fID, '\n'); + + fprintf(fID, '\t\t#Recover parameters of the current energy layer\n'); + fprintf(fID, '\t\tdef: currEnergy = layer.get(''Energy'')\n'); + fprintf(fID, '\t\tdef: currEspread = layer.get(''Espread'')\n'); + + switch this.sourceModel + case 'gaussian' + fprintf(fID, '\t\tdef: currFWHM = layer.get(''FWHM'')\n'); + + case 'emittance' + fprintf(fID, '\t\tdef: currEmittanceX = layer.get(''emittanceX'')\n'); + + fprintf(fID, '\t\tdef: currTwissAlphaX = layer.get(''twissAlphaX'')\n'); + fprintf(fID, '\t\tdef: currTwissBetaX = layer.get(''twissBetaX'')\n'); + fprintf(fID, '\t\tdef: currReferencePlane = layer.get(''emittanceRefPlaneDistance'')\n'); + case 'sigmaSqrModel' + fprintf(fID, '\t\tdef: currSQr_a = layer.get(''sSQr_a'')\n'); + fprintf(fID, '\t\tdef: currSQr_b = layer.get(''sSQr_b'')\n'); + fprintf(fID, '\t\tdef: currSQr_c = layer.get(''sSQr_c'')\n'); + end + + fprintf(fID, '\n'); + fprintf(fID, '\t\tfor(beamlet in layer.get(''beamlets''))<\n'); + fprintf(fID, '\t\t\tpb<\n'); + fprintf(fID, '\t\t\t\tID = ${beamlet.get(''beamletID'')}\n'); + fprintf(fID, '\t\t\t\tfieldID = $fieldIdx\n'); + switch this.machine.meta.radiationMode + case 'protons' + fprintf(fID, '\t\t\t\tparticle = proton\n'); + case 'carbon' + fprintf(fID, '\t\t\t\tparticle = C12\n'); + end + fprintf(fID, '\t\t\t\tT = $currEnergy\n'); + fprintf(fID, '\t\t\t\tEFWHM = $currEspread\n'); + + switch this.sourceModel + case 'gaussian' + + fprintf(fID, '\t\t\t\tXsec = gauss\n'); + fprintf(fID, '\t\t\t\tFWHM = $currFWHM\n'); + case 'emittance' + fprintf(fID, '\t\t\t\tXsec = emittance\n'); + fprintf(fID, '\t\t\t\temittanceX = $currEmittanceX\n'); + fprintf(fID, '\t\t\t\ttwissAlphaX = $currTwissAlphaX\n'); + fprintf(fID, '\t\t\t\ttwissBetaX = $currTwissBetaX\n'); + fprintf(fID, '\t\t\t\temittanceRefPlaneDistance = 100\n'); + + case 'sigmaSqrModel' + fprintf(fID, '\t\t\t\tXsec = emittance\n'); + fprintf(fID, '\t\t\t\tsigmaSqrModel = [${plan.get(''SAD'')},${currSQr_a},${currSQr_b}, ${currSQr_c}]\n'); + end + + fprintf(fID, '\n'); + fprintf(fID, '\t\t\t\tP = ${beamlet.get(''P'')}\n'); + fprintf(fID, '\t\t\t\tv = ${beamlet.get(''v'')}\n'); + fprintf(fID, '\t\t\t\tN = ${beamlet.get(''w'')}\n'); + fprintf(fID, '\t\t\tpb>\n'); + fprintf(fID, '\t\tfor>\n'); + fprintf(fID, '\tfor>\n'); + fprintf(fID, '\n'); + + fprintf(fID, '\t#Deliver all the pecil beams in this field\n'); + fprintf(fID, '\tdeliver: field_$fieldIdx\n'); + fprintf(fID, '\n'); + + fprintf(fID, '\t#Deactivate the current field\n'); + fprintf(fID, '\tdeactivate: field_$fieldIdx\n'); + fprintf(fID, '\n'); + + fprintf(fID, '\t#Restore the patient to original position\n'); + fprintf(fID, '\ttransform: Phantom rotate z ${-1*GA} Room\n'); + fprintf(fID, '\ttransform: Phantom rotate y ${-1*CA} Room\n'); + fprintf(fID, '\ttransform: Phantom move_to 0 0 0 Room\n'); + + fprintf(fID, 'for>\n\n'); catch matRad_cfg.dispError('Failed to write planDelivery file'); end fclose(fID); -end \ No newline at end of file +end diff --git a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writePlanFile.m b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writePlanFile.m index 76059d814..bb71536b0 100644 --- a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writePlanFile.m +++ b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writePlanFile.m @@ -1,16 +1,16 @@ -function writePlanFile(this, fName, stf) +function writePlanFile(this, fName, stf, scenIdx) % FRED helper to write data to plan.inp file -% call +% call: % writePlanFile(fName, stf) -% -% input +% +% input: % fName: string specifying the file path and name for saving the data. % stf: Fred stf struct % % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2023 the matRad development team. +% Copyright 2023-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -22,40 +22,44 @@ function writePlanFile(this, fName, stf) % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% matRad_cfg = MatRad_Config.instance(); +if ~exist('scenIdx', 'var') || isempty(scenIdx) + scenIdx = 1; +end + fID = fopen(fName, 'w'); try totalNumOfBixels = sum([stf.totalNumOfBixels]); if this.calcDoseDirect - simulatedPrimariesPerBixel = max([1, floor(this.numHistoriesDirect/totalNumOfBixels)]); + simulatedPrimariesPerBixel = max([1, floor(this.numHistoriesDirect / totalNumOfBixels)]); else simulatedPrimariesPerBixel = this.numHistoriesPerBeamlet; end - + fprintf(fID, 'nprim = %i\n', simulatedPrimariesPerBixel); layerCounter = 0; bixelCounter = 0; % loop oever the fields - for i=1:numel(stf) + for i = 1:numel(stf) - %Loop over energy layers - for j=1:numel(stf(i).energies) + % Loop over energy layers + for j = 1:numel(stf(i).energies) - fprintf(fID, '#Bixels Field%i, Layer%i\n', i-1,layerCounter+j-1); + fprintf(fID, '#Bixels Field%i, Layer%i\n', i - 1, layerCounter + j - 1); % Print bixel info (ID, Position, Direction, Weight) - for k=1:stf(i).energyLayer(j).nBixels - currBixel.beamletID = num2str(bixelCounter+k-1); - currBixel.P = arrayfun(@(idx) num2str(idx, '%2.3f'), [stf(i).energyLayer(j).rayPosY(k),stf(i).energyLayer(j).rayPosX(k),0], 'UniformOutput', false); - currBixel.v = arrayfun(@(idx) num2str(idx, '%2.5f'), [stf(i).energyLayer(j).rayDivY(k),stf(i).energyLayer(j).rayDivX(k),1], 'UniformOutput', false); + for k = 1:stf(i).energyLayer(j).nBixels + currBixel.beamletID = num2str(bixelCounter + k - 1); + currBixel.P = arrayfun(@(idx) num2str(idx, '%2.3f'), [stf(i).energyLayer(j).rayPosY(k), stf(i).energyLayer(j).rayPosX(k), 0], 'UniformOutput', false); + currBixel.v = arrayfun(@(idx) num2str(idx, '%2.5f'), [stf(i).energyLayer(j).rayDivY(k), stf(i).energyLayer(j).rayDivX(k), 1], 'UniformOutput', false); currBixel.w = num2str(stf(i).energyLayer(j).numOfPrimaries(k), '%2.7f'); - printStructToDictionary(fID, currBixel, ['S', num2str(bixelCounter+k-1)],2); - + printStructToDictionary(fID, currBixel, ['S', num2str(bixelCounter + k - 1)], 2); + end - + currLayer.Energy = num2str(stf(i).energies(j)); currLayer.Espread = num2str(stf(i).energySpreadFWHMMev(j)); @@ -65,7 +69,7 @@ function writePlanFile(this, fName, stf) currLayer.FWHM = num2str(stf(i).FWHMs(j)); case 'emittance' currLayer.emittanceX = num2str(stf(i).emittanceX(j), '%1.10f'); - currLayer.twissAlphaX = num2str(stf(i).twissAlphaX(j),'%1.10f'); + currLayer.twissAlphaX = num2str(stf(i).twissAlphaX(j), '%1.10f'); currLayer.twissBetaX = num2str(stf(i).twissBetaX(j), '%1.10f'); case 'sigmaSqrModel' currLayer.sSQr_a = num2str(stf(i).sSQr_a(j)); @@ -74,28 +78,31 @@ function writePlanFile(this, fName, stf) end % Specify the beamlets in current layer - currLayer.beamlets = arrayfun(@(idx) ['S', num2str(idx)], bixelCounter:stf(i).energyLayer(j).nBixels+bixelCounter-1, 'UniformOutput', false); + currLayer.beamlets = arrayfun(@(idx) ['S', num2str(idx)], bixelCounter:stf(i).energyLayer(j).nBixels + bixelCounter - 1, 'UniformOutput', false); - %Print layer - printStructToDictionary(fID, currLayer, ['L', num2str(layerCounter+j-1)],1); + % Print layer + printStructToDictionary(fID, currLayer, ['L', num2str(layerCounter + j - 1)], 1); fprintf(fID, '\n'); bixelCounter = bixelCounter + stf(i).energyLayer(j).nBixels; end - + % Estimate field dimension - fieldLim = max(abs([stf(i).energyLayer.rayPosX,stf(i).energyLayer.rayPosY])) + 10*max([stf(i).FWHMs]); + fieldLim = max(abs([stf(i).energyLayer.rayPosX, stf(i).energyLayer.rayPosY])) + 10 * max([stf(i).FWHMs]); - %Write field parameters - currF.fieldNumber = i-1; + % Write field parameters + + fieldIsocenter = stf(i).isoCenter + this.multScen.isoShift(scenIdx, :) .* [-1 1 1] ./ 10; + + currF.fieldNumber = i - 1; currF.GA = num2str(stf(i).gantryAngle); currF.CA = num2str(stf(i).couchAngle); - currF.ISO = arrayfun(@num2str, stf(i).isoCenter, 'UniformOutput', false); + currF.ISO = arrayfun(@num2str, fieldIsocenter, 'UniformOutput', false); currF.dim = arrayfun(@num2str, [fieldLim, fieldLim, 0.1], 'UniformOutput', false); - currF.Layers = arrayfun(@(idx) ['L', num2str(idx)], layerCounter:numel(stf(i).energies)+layerCounter-1, 'UniformOutput', false); + currF.Layers = arrayfun(@(idx) ['L', num2str(idx)], layerCounter:numel(stf(i).energies) + layerCounter - 1, 'UniformOutput', false); layerCounter = layerCounter + numel(stf(i).energies); - printStructToDictionary(fID, currF, ['F', num2str(i-1)]); + printStructToDictionary(fID, currF, ['F', num2str(i - 1)]); fprintf(fID, '\n'); end @@ -103,10 +110,10 @@ function writePlanFile(this, fName, stf) % BAMsToIso is the same for all fields plan.SAD = stf(1).BAMStoIsoDist; - plan.Fields = arrayfun(@(i) ['F', num2str(i)], 0:numel(stf)-1, 'UniformOutput', false); - + plan.Fields = arrayfun(@(i) ['F', num2str(i)], 0:numel(stf) - 1, 'UniformOutput', false); + printStructToDictionary(fID, plan, 'plan'); - + catch matRad_cfg.dispError('Failed to write plan file'); end @@ -115,11 +122,11 @@ function writePlanFile(this, fName, stf) end function printStructToDictionary(fID, S, sName, indentTabs) -% Helper function to convert struct fields into FRED specific python dictionary -% call +% Helper function to convert struct fields into FRED specific python dictionary +% call: % printStructToDictionary(fID, S, sName, indentTabs) -% -% input +% +% input: % fID: ID of file to write % S: struct to convert % sName: variable name of the printed dictionary @@ -142,8 +149,7 @@ function printStructToDictionary(fID, S, sName, indentTabs) indentTabs = 0; end -indentString = repmat('\t',1,indentTabs); - +indentString = repmat('\t', 1, indentTabs); fprintf(fID, indentString); @@ -153,20 +159,19 @@ function printStructToDictionary(fID, S, sName, indentTabs) % Get all fields to print sFields = fieldnames(S); - -for sFieldIdx =1:numel(sFields) +for sFieldIdx = 1:numel(sFields) currField = sFields{sFieldIdx}; % write field name - fprintf(fID, '''%s'': ',currField); + fprintf(fID, '''%s'': ', currField); % write one or multiple values if ~iscell(S.(currField)) fprintf(fID, '%s', num2str(S.(currField))); else fprintf(fID, '['); - for elementIdx=1:numel(S.(currField))-1 + for elementIdx = 1:numel(S.(currField)) - 1 fprintf(fID, '%s, ', S.(currField){elementIdx}); end fprintf(fID, '%s]', S.(currField){end}); @@ -180,4 +185,4 @@ function printStructToDictionary(fID, S, sName, indentTabs) % Close dictionary defintion fprintf(fID, '}\n'); -end \ No newline at end of file +end diff --git a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writeRegionsFile.m b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writeRegionsFile.m index 849304e9e..eb64ca472 100644 --- a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writeRegionsFile.m +++ b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writeRegionsFile.m @@ -1,14 +1,14 @@ -function writeRegionsFile(this,fName) +function writeRegionsFile(this, fName) % FRED helper to write file for geometrical components -% call +% call: % writeRegionsFile(fName) -% -% input +% +% input: % fName: string specifying the file path and name for saving the data. % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2023 the matRad development team. +% Copyright 2023-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -23,56 +23,57 @@ function writeRegionsFile(this,fName) fID = fopen(fName, 'w'); try % Write patient component - fprintf(fID,'region<\n'); - fprintf(fID,'\tID=Phantom\n'); - fprintf(fID,'\tCTscan=inp/regions/%s\n', this.patientFilename); - fprintf(fID,'\tO=[%i,%i,%i]\n', 0,0,0); - fprintf(fID,'\tpivot=[0.5,0.5,0.5]\n'); + fprintf(fID, 'region<\n'); + fprintf(fID, '\tID=Phantom\n'); + fprintf(fID, '\tCTscan=inp/regions/%s\n', this.patientFilename); + fprintf(fID, '\tO=[%i,%i,%i]\n', 0, 0, 0); + fprintf(fID, '\tpivot=[0.5,0.5,0.5]\n'); % l=e1; u=e2; % x in Room coordinates is x in patient frame % y in Romm coordinates is -y in patient frame - % Voxels in y-direection in matRad grow in -y direction in FRED Room reference - fprintf(fID, '\tl=[%1.1f,%1.1f,%1.1f]\n', 1,0,0); - fprintf(fID, '\tu=[%1.1f,%1.1f,%1.1f]\n', 0,-1,0); + % Voxels in y-direection in matRad grow in -y direction in FRED Room reference + fprintf(fID, '\tl=[%1.1f,%1.1f,%1.1f]\n', 1, 0, 0); + fprintf(fID, '\tu=[%1.1f,%1.1f,%1.1f]\n', 0, -1, 0); % Syntax changes for scorers according to direct or ij calculation - if this.calcDoseDirect - fprintf(fID,'\tscore=['); + + if this.calcDoseDirect || this.isVersionLower('3.70.0') + fprintf(fID, '\tscore=['); else - fprintf(fID,'\tscoreij=['); + fprintf(fID, '\tscoreij=['); end - if numel(this.scorers)>1 - for k=1:size(this.scorers,2)-1 - fprintf(fID,'%s,', this.scorers{k}); + if numel(this.scorers) > 1 + for k = 1:size(this.scorers, 2) - 1 + fprintf(fID, '%s,', this.scorers{k}); end end - fprintf(fID,'%s]\n', this.scorers{end}); - - fprintf(fID,'region>\n'); + fprintf(fID, '%s]\n', this.scorers{end}); + + fprintf(fID, 'region>\n'); % Write Room parameters fprintf(fID, 'region<\n'); fprintf(fID, '\tID=Room\n'); fprintf(fID, '\tmaterial=%s\n', this.roomMaterial); fprintf(fID, 'region>\n'); - + % Write HU table if needed switch this.HUtable case 'internal' fprintf(fID, 'lUseInternalHU2Mat=t\n'); + otherwise fprintf(fID, 'include: inp/regions/hLut.inp\n'); - this.writeHlut(this.HUtable); end - + % Toogle HU clamping if requested if this.HUclamping fprintf(fID, 'lAllowHUClamping=t\n'); end - if ~isempty(this.dijFormatVersion) && this.isVersionHigher('3.70.0') + if ~isempty(this.dijFormatVersion) && ~this.isVersionLower('3.76.0') fprintf(fID, 'ijFormatVersion = %s\n', this.dijFormatVersion); end @@ -82,4 +83,4 @@ function writeRegionsFile(this,fName) end fclose(fID); -end \ No newline at end of file +end diff --git a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writeRunFile.m b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writeRunFile.m index b3f0d3814..ee6baa7d7 100644 --- a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writeRunFile.m +++ b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writeRunFile.m @@ -1,14 +1,14 @@ -function writeRunFile(~, fName) +function writeRunFile(this, fName) % FRED helper to write file for simulation -% call +% call: % writeRunFile(fName) -% -% input +% +% input: % fName: string specifying the file path and name for saving the data. % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2023 the matRad development team. +% Copyright 2023-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -26,9 +26,13 @@ function writeRunFile(~, fName) % Include regions and plan delivery routine fprintf(fID, 'include: inp/regions/regions.inp\n'); fprintf(fID, 'include: inp/plan/planDelivery.inp\n'); + if ~this.calcDoseDirect && this.isVersionLower('3.70.0') + fprintf(fID, 'lwriteDij_bin = True\n'); + end catch + fclose(fID); matRad_cfg.dispError('Failed to write run file'); end fclose(fID); -end \ No newline at end of file +end diff --git a/matRad/doseCalc/+DoseEngines/matRad_MonteCarloEngineAbstract.m b/matRad/doseCalc/+DoseEngines/matRad_MonteCarloEngineAbstract.m index 0219b202a..5182ca7fc 100644 --- a/matRad/doseCalc/+DoseEngines/matRad_MonteCarloEngineAbstract.m +++ b/matRad/doseCalc/+DoseEngines/matRad_MonteCarloEngineAbstract.m @@ -6,7 +6,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2019 the matRad development team. +% Copyright 2019-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/doseCalc/+DoseEngines/matRad_ParticleAnalyticalBortfeldEngine.m b/matRad/doseCalc/+DoseEngines/matRad_ParticleAnalyticalBortfeldEngine.m index df2bcf3aa..90cec6aca 100644 --- a/matRad/doseCalc/+DoseEngines/matRad_ParticleAnalyticalBortfeldEngine.m +++ b/matRad/doseCalc/+DoseEngines/matRad_ParticleAnalyticalBortfeldEngine.m @@ -2,11 +2,10 @@ % matRad_DoseEngineParticlePB: % Implements an engine for particle based dose calculation % For detailed information see superclass matRad_DoseEngine - % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2022 the matRad development team. + % Copyright 2022-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -56,10 +55,10 @@ function this = matRad_ParticleAnalyticalBortfeldEngine(pln) % Constructor % - % call + % call: % engine = DoseEngines.matRad_ParticleAnalyticalPencilBeamDoseEngine(ct,stf,pln,cst) % - % input + % input: % pln: matRad plan meta information struct if nargin < 1 @@ -74,28 +73,25 @@ methods (Access = public) function [doseVector,hatD] = calcAnalyticalBragg(this, primaryEnergy, depthZ, energySpread) - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - % call - % this.calcAnalyticalBragg(PrimaryEnergy, depthz, WidthMod) - % =========================================================== - % Purpose: Compute depth-dose curve i.e. the Bragg Peak - % in 'Bortfeld 1998' formalism. + % call: + % this.calcAnalyticalBragg(PrimaryEnergy, depthz, WidthMod) + % + % Purpose: Compute depth-dose curve i.e. the Bragg Peak + % in 'Bortfeld 1998' formalism. + % + % input: + % primaryEnergy: Parameter (primaryEnergy > 0, it + % is the primary energy of the beam) + % depthZ: Argument (depthZ > 0, + % depth in the target material). + % energySpread: Energy Spread + % + % output: + % doseVector: Depth dose curve; same size of depthZ % - % Input : primaryEnergy -- Parameter (primaryEnergy > 0, it - % is the primary energy of the beam - % ) - % depthZ --------- Argument (depthZ > 0, - % depth in the target material). - % energySpread ------- - % Energy Spread - % Output : doseVector ----- Depth dose curve; same size of - % depthZ - % =========================================================== - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - % This function was inspired by the paper from - % Thomas Bortfeld (1997) "An analytical approximation of the - % Bragg curve for therapeutic proton beams". - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + % This function was inspired by the paper from + % Thomas Bortfeld (1997) "An analytical approximation of the + % Bragg curve for therapeutic proton beams". numberDensity = this.massDensity*this.avogadroNum/this.MM; % Number density of molecules per cm^3 alphaPrime = this.electronCharge^2*numberDensity*this.Z/(4*pi*this.epsilon0^2)/10^8; % Bohr's formula for d(sigmaE)^2/dz @@ -179,22 +175,23 @@ end function sigmaMCS = calcSigmaLatMCS(this, depthZ, primaryEnergy) - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - %call + % call: % this.SigmaLatMSC_H(depthz, En) - % =================================================================== - % Purpose: Compute the lateral displacement of a particle beam due to - % Multiple Coulomb Scattering, as function of the depth in - % the target material and in Highland approximation. - % Input : PrimaryEnergy -- Parameter (PrimaryEnergy > 0, it is the - % primary energy of the beam) - % z -------------- Argument (z > 0, it is the actual - % depth in the target material) - % Output : displ ---------- SigmaLatMCS_H(z, E) - % =================================================================== - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - % This function was inspired by the paper from Gottschalk et al.1992. - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + % + % Purpose: Compute the lateral displacement of a particle beam due to + % Multiple Coulomb Scattering, as function of the depth in + % the target material and in Highland approximation. + % + % input: + % primaryEnergy: Parameter (PrimaryEnergy > 0, it is the + % primary energy of the beam) + % depthZ: Argument (z > 0, it is the actual + % depth in the target material) + % + % output: + % sigmaMCS: SigmaLatMCS_H(z, E) + % + % This function was inspired by the paper from Gottschalk et al.1992. % Conversion of depth value from mm to cm diff --git a/matRad/doseCalc/+DoseEngines/matRad_ParticleFineSamplingPencilBeamEngine.m b/matRad/doseCalc/+DoseEngines/matRad_ParticleFineSamplingPencilBeamEngine.m index fbe852d5a..27ef25de8 100644 --- a/matRad/doseCalc/+DoseEngines/matRad_ParticleFineSamplingPencilBeamEngine.m +++ b/matRad/doseCalc/+DoseEngines/matRad_ParticleFineSamplingPencilBeamEngine.m @@ -2,11 +2,10 @@ % matRad_ParticlePencilBeamEngineAbstractFineSampling: % Implements an engine for particle based dose calculation % For detailed information see superclass matRad_DoseEngine -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2022 the matRad development team. +% Copyright 2022-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -34,10 +33,10 @@ function this = matRad_ParticleFineSamplingPencilBeamEngine(pln) % Constructor % - % call + % call: % engine = DoseEngines.matRad_ParticleAnalyticalPencilBeamDoseEngine(ct,stf,pln,cst) % - % input + % input: % pln: matRad plan meta information struct if nargin < 1 @@ -149,17 +148,17 @@ function setDefaults(this) % Method for initializing the beams for analytical pencil beam % dose calculation % - % call + % call: % this.initBeam(dij,ct,cst,stf,i) % - % input + % input: % dij: matRad dij struct % ct: matRad ct struct % cst: matRad cst struct % stf: matRad steering information struct % i: index of beam % - % output + % output: % dij: updated dij struct if ~this.keepRadDepthCubes @@ -287,14 +286,14 @@ function setDefaults(this) % This function creates a Gaussian Mixture Model on a Gaussian % for Fine-Sampling % - % call + % call: % [finalWeight, sigmaBeamlet, posX, posY, numOfSub] = ... % this.calcFineSamplingMixture(sigmaTot) % - % input + % input: % sigmaTot: the standard deviation of the lateral spread of the pencil % beam - % output + % output: % finalWeight: is the array of the weights of the sub-pencil beams. It % runs over the same index as posx and posy % diff --git a/matRad/doseCalc/+DoseEngines/matRad_ParticleHongPencilBeamEngine.m b/matRad/doseCalc/+DoseEngines/matRad_ParticleHongPencilBeamEngine.m index 4ccb4a904..bc6c4847c 100644 --- a/matRad/doseCalc/+DoseEngines/matRad_ParticleHongPencilBeamEngine.m +++ b/matRad/doseCalc/+DoseEngines/matRad_ParticleHongPencilBeamEngine.m @@ -1,11 +1,10 @@ classdef matRad_ParticleHongPencilBeamEngine < DoseEngines.matRad_ParticlePencilBeamEngineAbstract % matRad_ParticleHongPencilBeamEngine: % Implements the Hong pencil-beam engine -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2022 the matRad development team. +% Copyright 2022-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -29,10 +28,10 @@ function this = matRad_ParticleHongPencilBeamEngine(pln) % Constructor % - % call + % call: % engine = DoseEngines.matRad_ParticleAnalyticalPencilBeamDoseEngine(ct,stf,pln,cst) % - % input + % input: % pln: matRad plan meta information struct if nargin < 1 diff --git a/matRad/doseCalc/+DoseEngines/matRad_ParticleMCsquareEngine.m b/matRad/doseCalc/+DoseEngines/matRad_ParticleMCsquareEngine.m index 5d653adcc..bfd193e62 100644 --- a/matRad/doseCalc/+DoseEngines/matRad_ParticleMCsquareEngine.m +++ b/matRad/doseCalc/+DoseEngines/matRad_ParticleMCsquareEngine.m @@ -1,46 +1,48 @@ classdef matRad_ParticleMCsquareEngine < DoseEngines.matRad_MonteCarloEngineAbstract -% Engine for particle dose calculation using monte carlo calculation -% specificly the mc square method -% for more informations see superclass -% DoseEngines.matRad_MonteCarloEngineAbstract -% -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% -% Copyright 2019 the matRad development team. -% -% This file is part of the matRad project. It is subject to the license -% terms in the LICENSE file found in the top-level directory of this -% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part -% of the matRad project, including this file, may be copied, modified, -% propagated, or distributed except according to the terms contained in the -% LICENSE file. -% -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + % Engine for particle dose calculation using monte carlo calculation + % specifically the mc square method + % for more information see superclass + % DoseEngines.matRad_MonteCarloEngineAbstract + % + % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + % + % Copyright 2019-2026 the matRad development team. + % + % This file is part of the matRad project. It is subject to the license + % terms in the LICENSE file found in the top-level directory of this + % distribution and at https://github.com/e0404/matRad/LICENSE.md. No part + % of the matRad project, including this file, may be copied, modified, + % propagated, or distributed except according to the terms contained in the + % LICENSE file. + % + % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% properties (Constant) - possibleRadiationModes = {'protons'}; - name = 'MCsquare'; - shortName = 'MCsquare'; + possibleRadiationModes = {'protons'} + name = 'MCsquare' + shortName = 'MCsquare' end properties - config; %Holds an instance of all configurable parameters (matRad_MCsquareConfig) - MCsquareFolder; %Folder to the MCsquare installation - workingDir; %Working directory for simulation - forceBDL = []; %Specify an existing BDL file to load + config % Holds an instance of all configurable parameters (matRad_MCsquareConfig) + MCsquareFolder % Folder to the MCsquare installation + workingDir % Working directory for simulation + forceBDL = [] % Specify an existing BDL file to load - %Other Dose Calculation Properties - calcLET = true; + % Other Dose Calculation Properties + calcLET = true + + externalCalculation = 'off' end properties (SetAccess = protected, GetAccess = public) - currFolder = pwd; %folder path when set + currFolder = pwd % folder path when set - mcSquareBinary; %Executable for mcSquare simulation - nbThreads; %number of threads for MCsquare, 0 is all available + mcSquareBinary % Executable for mcSquare simulation + nbThreads % number of threads for MCsquare, 0 is all available - constantRBE = NaN; % constant RBE value + constantRBE = NaN % constant RBE value end methods @@ -48,10 +50,10 @@ function this = matRad_ParticleMCsquareEngine(pln) % Constructor % - % call + % call: % engine = DoseEngines.matRad_DoseEngineMCsquare(ct,stf,pln,cst) % - % input + % input: % ct: matRad ct struct % stf: matRad steering information struct % pln: matRad plan meta information struct @@ -64,23 +66,29 @@ % call superclass constructor this = this@DoseEngines.matRad_MonteCarloEngineAbstract(pln); + if this.enableGPU + matRad_cfg = MatRad_Config.instance(); + matRad_cfg.dispWarning('Set enableGPU ot true but MCsquare does not support GPU computation! Setting back to false!'); + this.enableGPU = false; + end + this.config = matRad_MCsquareConfig(); % check if bio optimization is needed and set the - % coresponding boolean accordingly + % corresponding boolean accordingly % TODO: % This should not be handled here as an optimization property % We should rather make optimization dependent on what we have % decided to calculate here. - if nargin > 0 - if isfield(pln,'bioModel') - if isa(pln.bioModel,'matRad_LQBasedModel') + if nargin > 0 + if isfield(pln, 'bioModel') + if isa(pln.bioModel, 'matRad_LQBasedModel') this.calcBioDose = true; - elseif isa(pln.bioModel,'matRad_ConstantRBE') - this.constantRBE = 1.1; + elseif isa(pln.bioModel, 'matRad_ConstantRBE') + this.constantRBE = 1.1; else - %Physical Dose calculation + % Physical Dose calculation end end end @@ -91,33 +99,34 @@ function setDefaults(this) % future code for property validation on creation here matRad_cfg = MatRad_Config.instance(); - %Set Default MCsquare path - %Set folder - this.workingDir = fullfile(matRad_cfg.primaryUserFolder,'MCsquare'); - this.MCsquareFolder = fullfile(matRad_cfg.matRadRoot,'thirdParty','MCsquare','bin'); + % Set Default MCsquare path + % Set folder + this.workingDir = fullfile(matRad_cfg.primaryUserFolder, 'MCsquare'); + this.MCsquareFolder = fullfile(matRad_cfg.matRadRoot, 'thirdParty', 'MCsquare', 'bin'); end + end - methods(Access = protected) + methods (Access = protected) - function dij = calcDose(this,ct,cst,stf) + function dij = calcDose(this, ct, cst, stf) % matRad MCsqaure monte carlo photon dose calculation wrapper - % can be automaticly called through matRad_calcDose or + % can be automatically called through matRad_calcDose or % matRad_calcParticleDoseMC % % nCase per Bixel and be either set by hand after creating the % engine or over the matRad_calcPhotonDoseMC function while % calling the calculation % - % call + % call: % dij = this.calcDose(ct,stf,pln,cst) % - % input - % ct: matRad ct struct + % input: + % ct: matRad ct struct % cst: matRad cst struct - % stf: atRad steering information struct + % stf: matRad steering information struct % - % output + % output: % dij: matRad dij struct % % References @@ -140,8 +149,8 @@ function setDefaults(this) matRad_cfg = MatRad_Config.instance(); - %Now we can run initDoseCalc as usual - dij = this.initDoseCalc(ct,cst,stf); + % Now we can run initDoseCalc as usual + dij = this.initDoseCalc(ct, cst, stf); % switch for using existing BDL file (e.g. to fit matRad basedata), % or generate BDL file from matRad base data using MCsquareBDL @@ -151,8 +160,8 @@ function setDefaults(this) else % Newer machine files have "name" instead of "machine" - if ~isfield(this.machine.meta,'machine') - machineName = this.machine.meta.name; + if ~isfield(this.machine.meta, 'machine') + machineName = this.machine.meta.name; else machineName = this.machine.meta.machine; end @@ -162,52 +171,51 @@ function setDefaults(this) % Calculate MCsquare base data % Argument stf is optional, if given, calculation only for energies given in stf MCsquareBDL = matRad_MCsquareBaseData(this.machine); + MCsquareBDL = MCsquareBDL.getRangeShiftersFromStf(stf); - %matRad_createMCsquareBaseDataFile(bdFile,machine,1); - bdlFolder = fullfile(this.workingDir,'BDL'); - if ~exist(bdlFolder,'dir') + % matRad_createMCsquareBaseDataFile(bdFile,machine,1); + bdlFolder = fullfile(this.workingDir, 'BDL'); + if ~exist(bdlFolder, 'dir') mkdir(bdlFolder); end - bdFile = fullfile(bdlFolder,bdFile); + bdFile = fullfile(bdlFolder, bdFile); MCsquareBDL = MCsquareBDL.writeMCsquareData(bdFile); MCsquareBDL = MCsquareBDL.saveMatradMachine('savedMatRadMachine'); end - % The offset of the dose grid of MCsquare - mcSquareAddIsoCenterOffset = [dij.doseGrid.resolution.x/2 dij.doseGrid.resolution.y/2 dij.doseGrid.resolution.z/2] ... + mcSquareAddIsoCenterOffset = [dij.doseGrid.resolution.x / 2 dij.doseGrid.resolution.y / 2 dij.doseGrid.resolution.z / 2] ... - [dij.ctGrid.resolution.x dij.ctGrid.resolution.y dij.ctGrid.resolution.z]; % for MCsquare we explicitly downsample the ct to the dose grid (might not % be necessary in future MCsquare versions with separated grids) for s = 1:dij.numOfScenarios - HUcube{s} = matRad_interp3(dij.ctGrid.x, dij.ctGrid.y', dij.ctGrid.z,ct.cubeHU{s}, ... - dij.doseGrid.x,dij.doseGrid.y',dij.doseGrid.z,'linear'); + HUcube{s} = matRad_interp3(dij.ctGrid.x, dij.ctGrid.y', dij.ctGrid.z, ct.cubeHU{s}, ... + dij.doseGrid.x, dij.doseGrid.y', dij.doseGrid.z, 'linear'); end % set absolute calibration factor % convert from eV/g/primary to Gy 1e6 primaries absCalibrationFactorMC2 = 1.602176e-19 * 1.0e+9; - MCsquareConfigFile = fullfile(this.workingDir,'MCsquareConfig.txt'); - plnFile = fullfile(this.workingDir,'currBixels.txt'); - ctFile = fullfile(this.workingDir,'MC2patientCT.mhd'); - outputDir = fullfile(this.workingDir,'output'); - HU_Density_Conversion_File = fullfile(this.MCsquareFolder,'Scanners','matRad_default','HU_Density_Conversion.txt'); % Name of the file containing HU to density conversion data. Default: HU_Density_Conversion.txt - HU_Material_Conversion_File = fullfile(this.MCsquareFolder,'Scanners','matRad_default','HU_Material_Conversion.txt'); % Name of the filecontaining HU to material conversion data. Default: HU_Material_Conversion.txt - - - %Format paths to always have slashes - if isequal(filesep,'\') - bdFileWrite = strrep(bdFile,'\','/'); - plnFileWrite = strrep(plnFile,'\','/'); - ctFileWrite = strrep(ctFile,'\','/'); - MCsquareConfigFileWrite = strrep(MCsquareConfigFile,'\','/'); - outputDirWrite = strrep(outputDir,'\','/'); - HU_Density_Conversion_File_write = strrep(HU_Density_Conversion_File,'\','/'); - HU_Material_Conversion_File_write = strrep(HU_Material_Conversion_File,'\','/'); + MCsquareConfigFile = fullfile(this.workingDir, 'MCsquareConfig.txt'); + plnFile = fullfile(this.workingDir, 'currBixels.txt'); + ctFile = fullfile(this.workingDir, 'MC2patientCT.mhd'); + outputDir = fullfile(this.workingDir, 'output'); + HU_Density_Conversion_File = fullfile(this.MCsquareFolder, 'Scanners', 'matRad_default', 'HU_Density_Conversion.txt'); % Name of the file containing HU to density conversion data. Default: HU_Density_Conversion.txt + HU_Material_Conversion_File = fullfile(this.MCsquareFolder, 'Scanners', 'matRad_default', 'HU_Material_Conversion.txt'); % Name of the filecontaining HU to material conversion data. Default: HU_Material_Conversion.txt + + % Format paths to always have slashes + if isequal(filesep, '\') + bdFileWrite = strrep(bdFile, '\', '/'); + plnFileWrite = strrep(plnFile, '\', '/'); + ctFileWrite = strrep(ctFile, '\', '/'); + MCsquareConfigFileWrite = strrep(MCsquareConfigFile, '\', '/'); + outputDirWrite = strrep(outputDir, '\', '/'); + HU_Density_Conversion_File_write = strrep(HU_Density_Conversion_File, '\', '/'); + HU_Material_Conversion_File_write = strrep(HU_Material_Conversion_File, '\', '/'); else bdFileWrite = bdFile; plnFileWrite = plnFile; @@ -245,78 +253,126 @@ function setDefaults(this) % set threshold of sparse matrix generation this.config.Dose_Sparse_Threshold = 1 - this.relativeDosimetricCutOff; - %Matrices for LET + % Matrices for LET if this.calcLET - this.config.LET_MHD_Output = this.calcDoseDirect; - this.config.LET_Sparse_Output = ~this.calcDoseDirect; + this.config.LET_MHD_Output = this.calcDoseDirect; + this.config.LET_Sparse_Output = ~this.calcDoseDirect; end - %Create X Y Z vectors if not present + % Create X Y Z vectors if not present ct = matRad_getWorldAxes(ct); - if this.multScen.totNumRangeScen > 1 matRad_cfg.dispWarning('Range shift scenarios are not yet implemented for Monte Carlo simulations.'); end - for scenarioIx = 1:this.multScen.totNumScen - %For direct dose calculation + % For direct dose calculation totalWeights = 0; % manipulate isocenter - isoCenterShift = this.multScen.isoShift(scenarioIx,:) + mcSquareAddIsoCenterOffset; + isoCenterShift = this.multScen.isoShift(scenarioIx, :) + mcSquareAddIsoCenterOffset; - ctScen = this.multScen.linearMask(scenarioIx,1); - shiftScen = this.multScen.linearMask(scenarioIx,2); - rangeShiftScen = this.multScen.linearMask(scenarioIx,3); - - if this.multScen.scenMask(ctScen,shiftScen,rangeShiftScen) + ctScen = this.multScen.linearMask(scenarioIx, 1); + shiftScen = this.multScen.linearMask(scenarioIx, 2); + rangeShiftScen = this.multScen.linearMask(scenarioIx, 3); + if this.multScen.scenMask(ctScen, shiftScen, rangeShiftScen) counter = 0; + stfMCsquare = []; + for i = 1:length(stf) - %Create new stf for MCsquare with energy layer ordering and - %shifted scenario isocenter - stfMCsquare(i).isoCenter = matRad_world2cubeCoords(stf(i).isoCenter, ct) + isoCenterShift; %MCsquare uses the isoCenter in cubeCoords - stfMCsquare(i).gantryAngle = mod(180-stf(i).gantryAngle,360); %Different MCsquare geometry - stfMCsquare(i).couchAngle = stf(i).couchAngle; - stfMCsquare(i).energies = unique([stf(i).ray.energy]); - stfMCsquare(i).SAD = stf(i).SAD; - - %Let's check if we have a unique or no range shifter, because MCsquare - %only allows one range shifter type per field which can be IN or OUT - %per spot + + stfFieldMCsquare = []; + + stfFieldMCsquare.isoCenter = matRad_world2cubeCoords(stf(i).isoCenter, ct) + isoCenterShift; % MCsquare uses the isoCenter in cubeCoords + stfFieldMCsquare.gantryAngle = mod(180 - stf(i).gantryAngle, 360); % Different MCsquare geometry + stfFieldMCsquare.couchAngle = stf(i).couchAngle; + stfFieldMCsquare.energies = unique([stf(i).ray.energy]); + stfFieldMCsquare.SAD = stf(i).SAD; + stfFieldMCsquare.originalStfFieldIndex = i; % Required for ordering later + + stfFieldMCsquare.rangeShifterID = 0; + stfFieldMCsquare.rangeShifterType = 'binary'; + + % Let's check if we have a unique or no range shifter, because MCsquare + % only allows one range shifter type per field which can be IN or OUT + % per spot raShiField = []; for j = 1:stf(i).numOfRays - if isfield(stf(i).ray(j),'rangeShifter') + if isfield(stf(i).ray(j), 'rangeShifter') raShiField = [raShiField stf(i).ray(j).rangeShifter(:).ID]; else raShiField = [raShiField zeros(size(stf(i).ray(j).energies))]; end end - raShiField = unique(raShiField); %unique range shifter - raShiField(raShiField == 0) = []; %no range shifter + raShiField = unique(raShiField); % unique range shifter + raShiField(raShiField == 0) = []; % no range shifter if numel(raShiField) > 1 matRad_cfg.dispError('MCsquare does not support different range shifter IDs per field! Aborting.\n'); end + % Create new stf for MCsquare with energy layer ordering and + % shifted scenario isocenter + % Need to split the current stf field into two separate fields for MCsquare, with and without RaSh + stfFieldMCsquareRaShi = []; if ~isempty(raShiField) - stfMCsquare(i).rangeShifterID = raShiField; - stfMCsquare(i).rangeShifterType = 'binary'; - else - stfMCsquare(i).rangeShifterID = 0; - stfMCsquare(i).rangeShifterType = 'binary'; + % Copy the field information + stfFieldMCsquareRaShi = stfFieldMCsquare; + + stfFieldMCsquareRaShi.rangeShifterID = raShiField; + stfFieldMCsquareRaShi.rangeShifterType = 'binary'; + + % Select the energies that have a RaShi for + % this stf field + raShiLayers = []; + for j = 1:stf(i).numOfRays + currentRay = stf(i).ray(j); + raShiLayers = [raShiLayers, currentRay.energy([currentRay.rangeShifter.ID] == stfFieldMCsquareRaShi.rangeShifterID)]; + end + stfFieldMCsquareRaShi.energies = unique(raShiLayers); + + % Need to delete an energy layer from non rashi + % field if the layer is only delivered with + % rashi + + % Extract all single bixel RaShiIDs + allRangeShifterIDs = arrayfun(@(rashi) rashi.ID, [stf(i).ray.rangeShifter]); + allEnergies = [stf(i).ray.energy]; + for layerEnergy = stfFieldMCsquareRaShi.energies + raShiIDSinLayer = unique(allRangeShifterIDs(allEnergies == layerEnergy)); + + % If there is only one raShiId for this + % layer, and its the current range shifter, + % we can eliminate the layer from the + % non-RaShi field + if isscalar(raShiIDSinLayer) && raShiIDSinLayer == stfFieldMCsquareRaShi.rangeShifterID + stfFieldMCsquare(stfFieldMCsquare.energies == layerEnergy) = []; + end + end + + % allocate empty target point container + for j = 1:numel(stfFieldMCsquareRaShi.energies) + stfFieldMCsquareRaShi.energyLayer(j).targetPoints = []; + stfFieldMCsquareRaShi.energyLayer(j).numOfPrimaries = []; + stfFieldMCsquareRaShi.energyLayer(j).MU = []; + stfFieldMCsquareRaShi.energyLayer(j).rayNum = []; + stfFieldMCsquareRaShi.energyLayer(j).bixelNum = []; + end + end % allocate empty target point container - for j = 1:numel(stfMCsquare(i).energies) - stfMCsquare(i).energyLayer(j).targetPoints = []; - stfMCsquare(i).energyLayer(j).numOfPrimaries = []; - stfMCsquare(i).energyLayer(j).MU = []; - stfMCsquare(i).energyLayer(j).rayNum = []; - stfMCsquare(i).energyLayer(j).bixelNum = []; + if ~isempty(stfFieldMCsquare) + for j = 1:numel(stfFieldMCsquare.energies) + stfFieldMCsquare.energyLayer(j).targetPoints = []; + stfFieldMCsquare.energyLayer(j).numOfPrimaries = []; + stfFieldMCsquare.energyLayer(j).MU = []; + stfFieldMCsquare.energyLayer(j).rayNum = []; + stfFieldMCsquare.energyLayer(j).bixelNum = []; + end end for j = 1:stf(i).numOfRays @@ -327,59 +383,114 @@ function setDefaults(this) dij.bixelNum(counter) = k; end - for k = 1:numel(stfMCsquare(i).energies) - %Check if ray has a spot in the current energy layer - if any(stf(i).ray(j).energy == stfMCsquare(i).energies(k)) - energyIx = find(stf(i).ray(j).energy == stfMCsquare(i).energies(k)); - stfMCsquare(i).energyLayer(k).rayNum = [stfMCsquare(i).energyLayer(k).rayNum j]; - stfMCsquare(i).energyLayer(k).bixelNum = [stfMCsquare(i).energyLayer(k).bixelNum energyIx]; - stfMCsquare(i).energyLayer(k).targetPoints = [stfMCsquare(i).energyLayer(k).targetPoints; ... - -stf(i).ray(j).rayPos_bev(1) stf(i).ray(j).rayPos_bev(3)]; - - %Number of primaries depending on beamlet-wise or field-based compuation (direct dose calculation) - if this.calcDoseDirect - stfMCsquare(i).energyLayer(k).numOfPrimaries = [stfMCsquare(i).energyLayer(k).numOfPrimaries ... - round(stf(i).ray(j).weight(stf(i).ray(j).energy == stfMCsquare(i).energies(k))*this.numHistoriesDirect)]; - - stfMCsquare(i).energyLayer(k).MU = [stfMCsquare(i).energyLayer(k).MU ... - round(stf(i).ray(j).weight(stf(i).ray(j).energy == stfMCsquare(i).energies(k))*this.numHistoriesDirect)]; - - totalWeights = totalWeights + stf(i).ray(j).weight(stf(i).ray(j).energy == stfMCsquare(i).energies(k)); - else - stfMCsquare(i).energyLayer(k).numOfPrimaries = [stfMCsquare(i).energyLayer(k).numOfPrimaries ... - this.numHistoriesPerBeamlet]; - - stfMCsquare(i).energyLayer(k).MU = [stfMCsquare(i).energyLayer(k).MU ... - this.numHistoriesPerBeamlet]; - end + if ~isempty(stfFieldMCsquare) + for k = 1:numel(stfFieldMCsquare.energies) + % Check if ray has a spot in the current energy layer + if any(stf(i).ray(j).energy == stfFieldMCsquare.energies(k)) + energyIx = find(stf(i).ray(j).energy == stfFieldMCsquare.energies(k)); + + % If more than one energy layer is + % found, one of them is for the + % RaShiField + energyIx = energyIx([stf(i).ray(j).rangeShifter(energyIx).ID] == 0); % Select the one with no RaShi; - %Now add the range shifter - raShis = stf(i).ray(j).rangeShifter(energyIx); + if isempty(energyIx) + continue + end - %sanity check range shifters - raShiIDs = unique([raShis.ID]); - %raShiIDs = raShiIDs(raShiIDs ~= 0); + stfFieldMCsquare.energyLayer(k).rayNum = [stfFieldMCsquare.energyLayer(k).rayNum j]; + stfFieldMCsquare.energyLayer(k).bixelNum = [stfFieldMCsquare.energyLayer(k).bixelNum energyIx]; + stfFieldMCsquare.energyLayer(k).targetPoints = [stfFieldMCsquare.energyLayer(k).targetPoints; ... + -stf(i).ray(j).rayPos_bev(1) stf(i).ray(j).rayPos_bev(3)]; + + % Number of primaries depending on beamlet-wise or field-based computation (direct dose calculation) + if this.calcDoseDirect + stfFieldMCsquare.energyLayer(k).numOfPrimaries = [stfFieldMCsquare.energyLayer(k).numOfPrimaries ... + round(stf(i).ray(j).weight(energyIx) * this.numHistoriesDirect)]; + + stfFieldMCsquare.energyLayer(k).MU = [stfFieldMCsquare.energyLayer(k).MU ... + round(stf(i).ray(j).weight(energyIx) * this.numHistoriesDirect)]; + + totalWeights = totalWeights + stf(i).ray(j).weight(energyIx); + else + stfFieldMCsquare.energyLayer(k).numOfPrimaries = [stfFieldMCsquare.energyLayer(k).numOfPrimaries ... + this.numHistoriesPerBeamlet]; + + stfFieldMCsquare.energyLayer(k).MU = [stfFieldMCsquare.energyLayer(k).MU ... + this.numHistoriesPerBeamlet]; + end - if ~isscalar(raShiIDs) - matRad_cfg.dispError('MCsquare only supports one range shifter setting (on or off) per energy! Aborting.\n'); end + end + end + % Add the bixels to the RaShi field if any + if ~isempty(raShiField) + for k = 1:numel(stfFieldMCsquareRaShi.energies) + if any(stf(i).ray(j).energy == stfFieldMCsquareRaShi.energies(k)) + + energyIx = find(stf(i).ray(j).energy == stfFieldMCsquareRaShi.energies(k)); + + % If more than one energy layer is + % found, one of them is for the + % RaShiField + energyIx = energyIx([stf(i).ray(j).rangeShifter(energyIx).ID] == stfFieldMCsquareRaShi.rangeShifterID); % Select the one with no RaShi; + + stfFieldMCsquareRaShi.energyLayer(k).rayNum = [stfFieldMCsquareRaShi.energyLayer(k).rayNum j]; + stfFieldMCsquareRaShi.energyLayer(k).bixelNum = [stfFieldMCsquareRaShi.energyLayer(k).bixelNum energyIx]; + stfFieldMCsquareRaShi.energyLayer(k).targetPoints = [stfFieldMCsquareRaShi.energyLayer(k).targetPoints; ... + -stf(i).ray(j).rayPos_bev(1) stf(i).ray(j).rayPos_bev(3)]; + + % Number of primaries depending on beamlet-wise or field-based computation (direct dose calculation) + if this.calcDoseDirect + stfFieldMCsquareRaShi.energyLayer(k).numOfPrimaries = [stfFieldMCsquareRaShi.energyLayer(k).numOfPrimaries ... + round(stf(i).ray(j).weight(energyIx) * this.numHistoriesDirect)]; + + stfFieldMCsquareRaShi.energyLayer(k).MU = [stfFieldMCsquareRaShi.energyLayer(k).MU ... + round(stf(i).ray(j).weight(energyIx) * this.numHistoriesDirect)]; + + totalWeights = totalWeights + stf(i).ray(j).weight(energyIx); + else + stfFieldMCsquareRaShi.energyLayer(k).numOfPrimaries = [stfFieldMCsquareRaShi.energyLayer(k).numOfPrimaries ... + this.numHistoriesPerBeamlet]; + + stfFieldMCsquareRaShi.energyLayer(k).MU = [stfFieldMCsquareRaShi.energyLayer(k).MU ... + this.numHistoriesPerBeamlet]; + end + + % Now add the range shifter + raShis = stf(i).ray(j).rangeShifter(energyIx); - stfMCsquare(i).energyLayer(k).rangeShifter = raShis(1); + % sanity check range shifters + raShiIDs = unique([raShis.ID]); + % raShiIDs = raShiIDs(raShiIDs ~= 0); + + if ~isscalar(raShiIDs) + matRad_cfg.dispError('MCsquare only supports one range shifter setting (on or off) per energy! Aborting.\n'); + end + + stfFieldMCsquareRaShi.energyLayer(k).rangeShifter = raShis(1); + end end end end + + % Fill the stfMCsquare + if isempty(stfFieldMCsquare) + stfFieldMCsquare = []; + end + stfMCsquare = [stfMCsquare, stfFieldMCsquare, stfFieldMCsquareRaShi]; end % remember order counterMCsquare = 0; - MCsquareOrder = NaN * ones(dij.totalNumOfBixels,1); - for i = 1:length(stf) + MCsquareOrder = NaN * ones(dij.totalNumOfBixels, 1); + for i = 1:length(stfMCsquare) for j = 1:numel(stfMCsquare(i).energies) for k = 1:numel(stfMCsquare(i).energyLayer(j).numOfPrimaries) counterMCsquare = counterMCsquare + 1; - ix = find(i == dij.beamNum & ... - stfMCsquare(i).energyLayer(j).rayNum(k) == dij.rayNum & ... - stfMCsquare(i).energyLayer(j).bixelNum(k) == dij.bixelNum); + ix = find(stfMCsquare(i).originalStfFieldIndex == dij.beamNum & ... + stfMCsquare(i).energyLayer(j).rayNum(k) == dij.rayNum & ... + stfMCsquare(i).energyLayer(j).bixelNum(k) == dij.bixelNum); MCsquareOrder(ix) = counterMCsquare; end @@ -393,36 +504,55 @@ function setDefaults(this) %% Write config files % write patient data MCsquareBinCubeResolution = [dij.doseGrid.resolution.x ... - dij.doseGrid.resolution.y ... - dij.doseGrid.resolution.z]; + dij.doseGrid.resolution.y ... + dij.doseGrid.resolution.z]; - this.writeMhd(HUcube{ctScen},MCsquareBinCubeResolution); + %% MC computation and dij filling - % write config file - this.writeInputFiles(MCsquareConfigFile,stfMCsquare); + switch this.externalCalculation - %% MC computation and dij filling + case 'write' + this.writeMhd(HUcube{ctScen}, MCsquareBinCubeResolution); - % run MCsquare - mcSquareCall = [this.mcSquareBinary ' ' MCsquareConfigFile]; - matRad_cfg.dispInfo(['Calling Monte Carlo Engine: ' mcSquareCall]); - if matRad_cfg.logLevel >= 3 - [status,cmdout] = system(mcSquareCall,'-echo'); - else - [status,cmdout] = system(mcSquareCall); - matRad_cfg.dispInfo(cmdout); - end - if status == 0 - matRad_cfg.dispInfo('MCsquare exited successfully with status %d!',status); - else - matRad_cfg.dispInfo('MCsquare did not exit successfully with status %d! Results might be compromised!',status); + % write config file + this.writeInputFiles(MCsquareConfigFile, stfMCsquare); + matRad_cfg.dispInfo(['MCsquare simulation skipped for external calculation\nFiles have been written to: "', strrep(this.workingDir, '\', '\\'), '"']); + cd(this.currFolder); + continue + case 'off' + this.writeMhd(HUcube{ctScen}, MCsquareBinCubeResolution); + + % write config file + this.writeInputFiles(MCsquareConfigFile, stfMCsquare); + + % run MCsquare + mcSquareCall = [this.mcSquareBinary ' ' sprintf('"%s"', MCsquareConfigFile)]; + matRad_cfg.dispInfo(['Calling Monte Carlo Engine: ' mcSquareCall]); + if matRad_cfg.logLevel >= 3 + [status, cmdout] = system(mcSquareCall, '-echo'); + else + [status, cmdout] = system(mcSquareCall); + matRad_cfg.dispInfo(cmdout); + end + if status == 0 + matRad_cfg.dispInfo('MCsquare exited successfully with status %d!', status); + else + matRad_cfg.dispInfo('MCsquare did not exit successfully with status %d! Results might be compromised!', status); + end + otherwise + if isfolder(this.externalCalculation) + this.config.Output_Directory = fullfile(this.externalCalculation, 'output'); + matRad_cfg.dispInfo('Trying to load simulation results from folder:%s', this.config.Output_Directory); + end end - mask = false(dij.doseGrid.numOfVoxels,1); + % if strcmp(this.externalCalculation, 'write') + + mask = false(dij.doseGrid.numOfVoxels, 1); mask(this.VdoseGrid) = true; if this.calcDoseDirect - if abs(totalWeights-sum(this.directWeights)) > 1e-2 + if abs(totalWeights - sum(this.directWeights)) > 1e-2 matRad_cfg.dispWarning('Sum of provided weights and weights used in MCsquare inconsistent!'); end finalResultWeight = absCalibrationFactorMC2 * totalWeights; @@ -432,43 +562,43 @@ function setDefaults(this) % read sparse matrix if ~this.calcDoseDirect - dij.physicalDose{ctScen,shiftScen,rangeShiftScen} = finalResultWeight * matRad_sparseBeamletsReaderMCsquare ( ... - [this.config.Output_Directory filesep 'Sparse_Dose.bin'], ... - dij.doseGrid.dimensions, ... - dij.totalNumOfBixels, ... - mask); + dij.physicalDose{ctScen, shiftScen, rangeShiftScen} = finalResultWeight * matRad_sparseBeamletsReaderMCsquare ( ... + [this.config.Output_Directory filesep 'Sparse_Dose.bin'], ... + dij.doseGrid.dimensions, ... + dij.totalNumOfBixels, ... + mask); - %Read sparse LET + % Read sparse LET if this.calcLET - dij.mLETDose{ctScen,shiftScen,rangeShiftScen} = dij.physicalDose{ctScen,shiftScen,rangeShiftScen} .* matRad_sparseBeamletsReaderMCsquare ( ... - [this.config.Output_Directory filesep 'Sparse_LET.bin'], ... - dij.doseGrid.dimensions, ... - dij.totalNumOfBixels, ... - mask); + dij.mLETDose{ctScen, shiftScen, rangeShiftScen} = dij.physicalDose{ctScen, shiftScen, rangeShiftScen} .* matRad_sparseBeamletsReaderMCsquare ( ... + [this.config.Output_Directory filesep 'Sparse_LET.bin'], ... + dij.doseGrid.dimensions, ... + dij.totalNumOfBixels, ... + mask); end % reorder influence matrix to comply with matRad default ordering - dij.physicalDose = cellfun(@(mx) mx(:,MCsquareOrder),dij.physicalDose,'UniformOutput',false); + dij.physicalDose = cellfun(@(mx) mx(:, MCsquareOrder), dij.physicalDose, 'UniformOutput', false); if this.calcLET - dij.mLETDose = cellfun(@(mx) mx(:,MCsquareOrder),dij.mLETDose,'UniformOutput',false); + dij.mLETDose = cellfun(@(mx) mx(:, MCsquareOrder), dij.mLETDose, 'UniformOutput', false); end else cube = this.readMhd('Dose.mhd'); - dij.physicalDose{ctScen,shiftScen,rangeShiftScen} = sparse(this.VdoseGrid,ones(numel(this.VdoseGrid),1), ... - finalResultWeight * cube(this.VdoseGrid), ... - dij.doseGrid.numOfVoxels,1); + dij.physicalDose{ctScen, shiftScen, rangeShiftScen} = sparse(this.VdoseGrid, ones(numel(this.VdoseGrid), 1), ... + finalResultWeight * cube(this.VdoseGrid), ... + dij.doseGrid.numOfVoxels, 1); - %Read LET cube + % Read LET cube if this.calcLET cube = this.readMhd('LET.mhd'); - dij.mLETDose{ctScen,shiftScen,rangeShiftScen} = dij.physicalDose{ctScen,shiftScen,rangeShiftScen} .* sparse(this.VdoseGrid,ones(numel(this.VdoseGrid),1), ... - cube(this.VdoseGrid), ... - dij.doseGrid.numOfVoxels,1); + dij.mLETDose{ctScen, shiftScen, rangeShiftScen} = dij.physicalDose{ctScen, shiftScen, rangeShiftScen} .* sparse(this.VdoseGrid, ones(numel(this.VdoseGrid), 1), ... + cube(this.VdoseGrid), ... + dij.doseGrid.numOfVoxels, 1); end % Postprocessing for dij: % This is already the combined dose over all bixels, so all parameters are 1 in this case - dij = rmfield(dij,'MCsquareCalcOrder'); + dij = rmfield(dij, 'MCsquareCalcOrder'); dij.numOfBeams = 1; dij.beamNum = 1; @@ -479,28 +609,29 @@ function setDefaults(this) dij.numOfRaysPerBeam = 1; end - if this.config.Beamlet_Mode end - matRad_cfg.dispInfo('Scenario %d of %d finished!\n',scenarioIx,this.multScen.totNumScen); + matRad_cfg.dispInfo('Scenario %d of %d finished!\n', scenarioIx, this.multScen.totNumScen); %% clear all data - %could also be moved to the "finalize" function - delete([this.config.CT_File(1:end-4) '.*']); - fullfile(this.workingDir,'currBixels.txt'); - fullfile(this.workingDir,'MCsquareConfig.txt'); - - %For Octave temporarily disable confirmation for recursive rmdir - if strcmp(matRad_cfg.env,'OCTAVE') - rmdirConfirmState = confirm_recursive_rmdir(0); - end - rmdir(this.config.Output_Directory,'s'); + % could also be moved to the "finalize" function + if strcmp(this.externalCalculation, 'off') + delete([this.config.CT_File(1:end - 4) '.*']); + fullfile(this.workingDir, 'currBixels.txt'); + fullfile(this.workingDir, 'MCsquareConfig.txt'); + + % For Octave temporarily disable confirmation for recursive rmdir + if strcmp(matRad_cfg.env, 'OCTAVE') + rmdirConfirmState = confirm_recursive_rmdir(0); + end + rmdir(this.config.Output_Directory, 's'); - %Reset to old confirmatoin state - if strcmp(matRad_cfg.env,'OCTAVE') - confirm_recursive_rmdir(rmdirConfirmState); + % Reset to old confirmatoin state + if strcmp(matRad_cfg.env, 'OCTAVE') + confirm_recursive_rmdir(rmdirConfirmState); + end end % cd back @@ -509,8 +640,30 @@ function setDefaults(this) end matRad_cfg.dispInfo('matRad: Simulation finished!\n'); - %Finalize dose calculation - dij = this.finalizeDose(dij); + + if strcmp(this.externalCalculation, 'write') + dij.beamNum = 1; + dij.bixelNum = 1; + dij.doseGrid = this.doseGrid; + dij.numOfBeams = 1; + dij.numOfRaysPerBeam = 1; + dij.numOfScenarios = this.multScen.totNumScen; + for i = 1:this.multScen.numOfCtScen + for j = 1:this.multScen.totNumShiftScen + for k = 1:this.multScen.totNumRangeScen + if this.multScen.scenMask(i, j, k) + % TODO: loop over all expected output quantities + dij.physicalDose{i, j, k} = zeros(dij.doseGrid.numOfVoxels, 1); + dij.physicalDose_std{i, j, k} = zeros(dij.doseGrid.numOfVoxels, 1); + end + + end + end + end + dij.rayNum = 1; + dij.totalNumOfBixels = 1; + dij.totalNumOfRays = 1; + end end @@ -519,41 +672,41 @@ function setBinaries(this) % machine and sets to the mcsquarebinary object property % - [~,binaryFile] = this.checkBinaries(); + [~, binaryFile] = this.checkBinaries(); this.mcSquareBinary = binaryFile; end - function dij = initDoseCalc(this,ct,cst,stf) - %% Assingn and check parameters + function dij = initDoseCalc(this, ct, cst, stf) + %% Assign and check parameters matRad_cfg = MatRad_Config.instance(); % check if binaries are available % Executables for simulation this.setBinaries(); - %Mex interface for import of sparse matrix + % Mex interface for import of sparse matrix if ~this.calcDoseDirect && ~matRad_checkMexFileExists('matRad_sparseBeamletsReaderMCsquare') matRad_cfg.dispWarning('Compiled sparse reader interface not found. Trying to compile it on the fly!'); try matRad_compileMCsquareSparseReader(); catch MException - matRad_cfg.dispError('Could not find/generate mex interface for reading the sparse matrix. \nCause of error:\n%s\n Please compile it yourself.',MException.message); + matRad_cfg.dispError('Could not find/generate mex interface for reading the sparse matrix. \nCause of error:\n%s\n Please compile it yourself.', MException.message); end end % set and change to MCsquare binary folder this.currFolder = pwd; - %fullfilename = mfilename('fullpath'); + % fullfilename = mfilename('fullpath'); % cd to MCsquare folder (necessary for binary) % TODO: Could be checked in a property setter function - if ~exist(this.MCsquareFolder,'dir') + if ~exist(this.MCsquareFolder, 'dir') matRad_cfg.dispError('MCsquare Folder does not exist!'); end cd(this.MCsquareFolder); - %Check Materials - if ~exist([this.MCsquareFolder filesep 'Materials'],'dir') || ~exist(fullfile(this.MCsquareFolder,'Materials','list.dat'),'file') + % Check Materials + if ~exist([this.MCsquareFolder filesep 'Materials'], 'dir') || ~exist(fullfile(this.MCsquareFolder, 'Materials', 'list.dat'), 'file') matRad_cfg.dispInfo('First call of MCsquare: unzipping Materials...'); unzip('Materials.zip'); matRad_cfg.dispInfo('Done'); @@ -568,40 +721,40 @@ function setBinaries(this) if this.doseGrid.resolution.x ~= this.doseGrid.resolution.y this.doseGrid.resolution.x = mean([this.doseGrid.resolution.x this.doseGrid.resolution.y]); this.doseGrid.resolution.y = this.doseGrid.resolution.x; - matRad_cfg.dispWarning('Anisotropic resolution in axial plane for dose calculation with MCsquare not possible\nUsing average x = y = %g mm\n',this.doseGrid.resolution.x); + matRad_cfg.dispWarning('Anisotropic resolution in axial plane for dose calculation with MCsquare not possible\nUsing average x = y = %g mm\n', this.doseGrid.resolution.x); end - dij = initDoseCalc@DoseEngines.matRad_MonteCarloEngineAbstract(this,ct,cst,stf); + dij = initDoseCalc@DoseEngines.matRad_MonteCarloEngineAbstract(this, ct, cst, stf); %% Validate and preset some additional dij variables % Explicitly setting the number of threads for MCsquare, 0 is all available this.nbThreads = 0; - %Issue a warning when we have more than 1 scenario + % Issue a warning when we have more than 1 scenario if dij.numOfScenarios ~= 1 matRad_cfg.dispWarning('MCsquare is only implemented for single scenario use at the moment. Will only use the first Scenario for Monte Carlo calculation!'); end % prefill ordering of MCsquare bixels - dij.MCsquareCalcOrder = NaN*ones(dij.totalNumOfBixels,1); + dij.MCsquareCalcOrder = NaN * ones(dij.totalNumOfBixels, 1); if ~isnan(this.constantRBE) dij.RBE = this.constantRBE; end end - function writeInputFiles(obj,filename,stf) - % generate input files for MCsquare dose calcualtion from matRad + function writeInputFiles(obj, filename, stf) + % generate input files for MCsquare dose calculation from matRad % - % call + % call: % obj.writeInputFiles(filename,filename,stf) % - % input + % input: % filename: filename of the Configuration file % stf: matRad steering information struct % - % output + % output: % - % % References @@ -620,9 +773,8 @@ function writeInputFiles(obj,filename,stf) % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - %% write overall configuration file - fileHandle = fopen(filename,'w'); + fileHandle = fopen(filename, 'w'); obj.config.write(fileHandle); fclose(fileHandle); @@ -631,7 +783,7 @@ function writeInputFiles(obj,filename,stf) if obj.config.Beamlet_Mode totalMetersetWeightOfAllFields = 1; else - totalMetersetWeightOfFields = NaN*ones(numOfFields,1); + totalMetersetWeightOfFields = NaN * ones(numOfFields, 1); for i = 1:numOfFields totalMetersetWeightOfFields(i) = sum([stf(i).energyLayer.numOfPrimaries]); end @@ -640,88 +792,88 @@ function writeInputFiles(obj,filename,stf) %% write steering file - fileHandle = fopen(obj.config.BDL_Plan_File,'w'); - - fprintf(fileHandle,'#TREATMENT-PLAN-DESCRIPTION\n'); - fprintf(fileHandle,'#PlanName\n'); - fprintf(fileHandle,'matRad_bixel\n'); - fprintf(fileHandle,'#NumberOfFractions\n'); - fprintf(fileHandle,'1\n'); - fprintf(fileHandle,'##FractionID\n'); - fprintf(fileHandle,'1\n'); - fprintf(fileHandle,'##NumberOfFields\n'); - fprintf(fileHandle,[num2str(numOfFields) '\n']); + fileHandle = fopen(obj.config.BDL_Plan_File, 'w'); + + fprintf(fileHandle, '#TREATMENT-PLAN-DESCRIPTION\n'); + fprintf(fileHandle, '#PlanName\n'); + fprintf(fileHandle, 'matRad_bixel\n'); + fprintf(fileHandle, '#NumberOfFractions\n'); + fprintf(fileHandle, '1\n'); + fprintf(fileHandle, '##FractionID\n'); + fprintf(fileHandle, '1\n'); + fprintf(fileHandle, '##NumberOfFields\n'); + fprintf(fileHandle, [num2str(numOfFields) '\n']); for i = 1:numOfFields - fprintf(fileHandle,'###FieldsID\n'); - fprintf(fileHandle,[num2str(i) '\n']); + fprintf(fileHandle, '###FieldsID\n'); + fprintf(fileHandle, [num2str(i) '\n']); end - fprintf(fileHandle,'\n#TotalMetersetWeightOfAllFields\n'); - fprintf(fileHandle,[num2str(totalMetersetWeightOfAllFields) '\n']); + fprintf(fileHandle, '\n#TotalMetersetWeightOfAllFields\n'); + fprintf(fileHandle, [num2str(totalMetersetWeightOfAllFields) '\n']); for i = 1:numOfFields - fprintf(fileHandle,'\n#FIELD-DESCRIPTION\n'); - fprintf(fileHandle,'###FieldID\n'); - fprintf(fileHandle,[num2str(i) '\n']); - fprintf(fileHandle,'###FinalCumulativeMeterSetWeight\n'); + fprintf(fileHandle, '\n#FIELD-DESCRIPTION\n'); + fprintf(fileHandle, '###FieldID\n'); + fprintf(fileHandle, [num2str(i) '\n']); + fprintf(fileHandle, '###FinalCumulativeMeterSetWeight\n'); if obj.config.Beamlet_Mode - finalCumulativeMeterSetWeight = 1/numOfFields; + finalCumulativeMeterSetWeight = 1 / numOfFields; else finalCumulativeMeterSetWeight = totalMetersetWeightOfFields(i); end - fprintf(fileHandle,[num2str(finalCumulativeMeterSetWeight) '\n']); - fprintf(fileHandle,'###GantryAngle\n'); - fprintf(fileHandle,[num2str(stf(i).gantryAngle) '\n']); - fprintf(fileHandle,'###PatientSupportAngle\n'); - fprintf(fileHandle,[num2str(stf(i).couchAngle) '\n']); - fprintf(fileHandle,'###IsocenterPosition\n'); - fprintf(fileHandle,[num2str(stf(i).isoCenter) '\n']); - fprintf(fileHandle,'###NumberOfControlPoints\n'); + fprintf(fileHandle, [num2str(finalCumulativeMeterSetWeight) '\n']); + fprintf(fileHandle, '###GantryAngle\n'); + fprintf(fileHandle, [num2str(stf(i).gantryAngle) '\n']); + fprintf(fileHandle, '###PatientSupportAngle\n'); + fprintf(fileHandle, [num2str(stf(i).couchAngle) '\n']); + fprintf(fileHandle, '###IsocenterPosition\n'); + fprintf(fileHandle, [num2str(stf(i).isoCenter) '\n']); + fprintf(fileHandle, '###NumberOfControlPoints\n'); numOfEnergies = numel(stf(i).energies); - fprintf(fileHandle,[num2str(numOfEnergies) '\n']); + fprintf(fileHandle, [num2str(numOfEnergies) '\n']); - %Range shfiter + % Range shfiter if stf(i).rangeShifterID ~= 0 - fprintf(fileHandle,'###RangeShifterID\n%d\n',stf(i).rangeShifterID); - fprintf(fileHandle,'###RangeShifterType\n%s\n',stf(i).rangeShifterType); + fprintf(fileHandle, '###RangeShifterID\n%d\n', stf(i).rangeShifterID); + fprintf(fileHandle, '###RangeShifterType\n%s\n', stf(i).rangeShifterType); end metersetOffset = 0; - fprintf(fileHandle,'\n#SPOTS-DESCRIPTION\n'); + fprintf(fileHandle, '\n#SPOTS-DESCRIPTION\n'); for j = 1:numOfEnergies - fprintf(fileHandle,'####ControlPointIndex\n'); - fprintf(fileHandle,[num2str(j) '\n']); - fprintf(fileHandle,'####SpotTunnedID\n'); - fprintf(fileHandle,['1\n']); - fprintf(fileHandle,'####CumulativeMetersetWeight\n'); + fprintf(fileHandle, '####ControlPointIndex\n'); + fprintf(fileHandle, [num2str(j) '\n']); + fprintf(fileHandle, '####SpotTunnedID\n'); + fprintf(fileHandle, ['1\n']); + fprintf(fileHandle, '####CumulativeMetersetWeight\n'); if obj.config.Beamlet_Mode - cumulativeMetersetWeight = j/numOfEnergies * 1/numOfFields; + cumulativeMetersetWeight = j / numOfEnergies * 1 / numOfFields; else cumulativeMetersetWeight = metersetOffset + sum([stf(i).energyLayer(j).numOfPrimaries]); metersetOffset = cumulativeMetersetWeight; end - fprintf(fileHandle,[num2str(cumulativeMetersetWeight) '\n']); - fprintf(fileHandle,'####Energy (MeV)\n'); - fprintf(fileHandle,[num2str(stf(i).energies(j)) '\n']); + fprintf(fileHandle, [num2str(cumulativeMetersetWeight) '\n']); + fprintf(fileHandle, '####Energy (MeV)\n'); + fprintf(fileHandle, [num2str(stf(i).energies(j)) '\n']); - %Range shfiter + % Range shfiter if stf(i).rangeShifterID ~= 0 rangeShifter = stf(i).energyLayer(j).rangeShifter; if rangeShifter.ID ~= 0 - fprintf(fileHandle,'####RangeShifterSetting\n%s\n','IN'); - pmma_rsp = 1.165; %TODO: hardcoded for now + fprintf(fileHandle, '####RangeShifterSetting\n%s\n', 'IN'); + pmma_rsp = 1.165; % TODO: hardcoded for now rsWidth = rangeShifter.eqThickness / pmma_rsp; - isoToRaShi = stf(i).SAD - rangeShifter.sourceRashiDistance + rsWidth; - fprintf(fileHandle,'####IsocenterToRangeShifterDistance\n%f\n',-isoToRaShi/10); %in cm - fprintf(fileHandle,'####RangeShifterWaterEquivalentThickness\n%f\n',rangeShifter.eqThickness); + isoToRaShi = stf(i).SAD - rangeShifter.sourceRashiDistance - rsWidth; + fprintf(fileHandle, '####IsocenterToRangeShifterDistance\n%f\n', isoToRaShi / 10); % in cm + fprintf(fileHandle, '####RangeShifterWaterEquivalentThickness\n%f\n', rangeShifter.eqThickness); else - fprintf(fileHandle,'####RangeShifterSetting\n%s\n','OUT'); + fprintf(fileHandle, '####RangeShifterSetting\n%s\n', 'OUT'); end end - fprintf(fileHandle,'####NbOfScannedSpots\n'); - numOfSpots = size(stf(i).energyLayer(j).targetPoints,1); - fprintf(fileHandle,[num2str(numOfSpots) '\n']); - fprintf(fileHandle,'####X Y Weight\n'); + fprintf(fileHandle, '####NbOfScannedSpots\n'); + numOfSpots = size(stf(i).energyLayer(j).targetPoints, 1); + fprintf(fileHandle, [num2str(numOfSpots) '\n']); + fprintf(fileHandle, '####X Y Weight\n'); for k = 1:numOfSpots %{ if obj.config.Beamlet_Mode @@ -731,7 +883,7 @@ function writeInputFiles(obj,filename,stf) end %} n = stf(i).energyLayer(j).numOfPrimaries(k); - fprintf(fileHandle,[num2str(stf(i).energyLayer(j).targetPoints(k,:)) ' ' num2str(n) '\n']); + fprintf(fileHandle, [num2str(stf(i).energyLayer(j).targetPoints(k, :)) ' ' num2str(n) '\n']); end end end @@ -740,19 +892,19 @@ function writeInputFiles(obj,filename,stf) end - function cube = readMhd(obj,filename) + function cube = readMhd(obj, filename) % TODO: This should become a binary export function in matRads - % IO folde + % IO folder % matRad mhd file reader % - % call + % call: % cube = matRad_readMhd(folder,filename) % - % input + % input: % folder: folder where the *raw and *mhd file are located % filename: filename % - % output + % output: % cube: 3D array % % References @@ -770,37 +922,36 @@ function writeInputFiles(obj,filename,stf) % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - %% read header - headerFileHandle = fopen([obj.config.Output_Directory, filesep filename],'r'); + headerFileHandle = fopen([obj.config.Output_Directory, filesep filename], 'r'); s = textscan(headerFileHandle, '%s', 'delimiter', '\n'); % read dimensions - idx = find(~cellfun(@isempty,strfind(s{1}, 'DimSize')),1,'first'); - dimensions = cell2mat(textscan(s{1}{idx},'DimSize = %f %f %f')); + idx = find(~cellfun(@isempty, strfind(s{1}, 'DimSize')), 1, 'first'); + dimensions = cell2mat(textscan(s{1}{idx}, 'DimSize = %f %f %f')); % read filename of data - idx = find(~cellfun(@isempty,strfind(s{1}, 'ElementDataFile')),1,'first'); - tmp = textscan(s{1}{idx},'ElementDataFile = %s'); + idx = find(~cellfun(@isempty, strfind(s{1}, 'ElementDataFile')), 1, 'first'); + tmp = textscan(s{1}{idx}, 'ElementDataFile = %s'); dataFilename = cell2mat(tmp{1}); % get data type - idx = find(~cellfun(@isempty,strfind(s{1}, 'ElementType')),1,'first'); - tmp = textscan(s{1}{idx},'ElementType = MET_%s'); + idx = find(~cellfun(@isempty, strfind(s{1}, 'ElementType')), 1, 'first'); + tmp = textscan(s{1}{idx}, 'ElementType = MET_%s'); type = lower(cell2mat(tmp{1})); fclose(headerFileHandle); %% read data - dataFileHandle = fopen([obj.config.Output_Directory filesep dataFilename],'r'); - cube = reshape(fread(dataFileHandle,inf,type),dimensions); - cube = permute(cube,[2 1 3]); - cube = flip(cube,2); + dataFileHandle = fopen([obj.config.Output_Directory filesep dataFilename], 'r'); + cube = reshape(fread(dataFileHandle, inf, type), dimensions); + cube = permute(cube, [2 1 3]); + cube = flip(cube, 2); fclose(dataFileHandle); end - function writeMhd(obj,cube,resolution) + function writeMhd(obj, cube, resolution) % TODO: This should become a binary export function in matRads % IO folder % References @@ -820,40 +971,41 @@ function writeMhd(obj,cube,resolution) % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% write header file - fileHandle = fopen(obj.config.CT_File,'w'); - - fprintf(fileHandle,'ObjectType = Image\n'); - fprintf(fileHandle,'NDims = 3\n'); - fprintf(fileHandle,'BinaryData = True\n'); - fprintf(fileHandle,'BinaryDataByteOrderMSB = False\n'); - fprintf(fileHandle,'CompressedData = False\n'); - fprintf(fileHandle,'TransformMatrix = 1 0 0 0 1 0 0 0 1\n'); - fprintf(fileHandle,'Offset = 0 0 0\n'); - fprintf(fileHandle,'CenterOfRotation = 0 0 0\n'); - fprintf(fileHandle,'AnatomicalOrientation = RAI\n'); - fprintf(fileHandle,'ElementSpacing = %f %f %f\n',resolution); - fprintf(fileHandle,'DimSize = %d %d %d\n',size(cube,2),size(cube,1),size(cube,3)); - fprintf(fileHandle,'ElementType = MET_DOUBLE\n'); - [fPath,fName,~] = fileparts(obj.config.CT_File); + fileHandle = fopen(obj.config.CT_File, 'w'); + + fprintf(fileHandle, 'ObjectType = Image\n'); + fprintf(fileHandle, 'NDims = 3\n'); + fprintf(fileHandle, 'BinaryData = True\n'); + fprintf(fileHandle, 'BinaryDataByteOrderMSB = False\n'); + fprintf(fileHandle, 'CompressedData = False\n'); + fprintf(fileHandle, 'TransformMatrix = 1 0 0 0 1 0 0 0 1\n'); + fprintf(fileHandle, 'Offset = 0 0 0\n'); + fprintf(fileHandle, 'CenterOfRotation = 0 0 0\n'); + fprintf(fileHandle, 'AnatomicalOrientation = RAI\n'); + fprintf(fileHandle, 'ElementSpacing = %f %f %f\n', resolution); + fprintf(fileHandle, 'DimSize = %d %d %d\n', size(cube, 2), size(cube, 1), size(cube, 3)); + fprintf(fileHandle, 'ElementType = MET_DOUBLE\n'); + [fPath, fName, ~] = fileparts(obj.config.CT_File); filenameRaw = [fName '.raw']; - fprintf(fileHandle,'ElementDataFile = %s\n',filenameRaw); + fprintf(fileHandle, 'ElementDataFile = %s\n', filenameRaw); fclose(fileHandle); %% write data file - filenameRaw = fullfile(fPath,filenameRaw); - dataFileHandle = fopen(filenameRaw,'w'); + filenameRaw = fullfile(fPath, filenameRaw); + dataFileHandle = fopen(filenameRaw, 'w'); - cube = flip(cube,2); - cube = permute(cube,[2 1 3]); + cube = flip(cube, 2); + cube = permute(cube, [2 1 3]); - fwrite(dataFileHandle,cube(:),'double'); + fwrite(dataFileHandle, cube(:), 'double'); fclose(dataFileHandle); end end methods (Access = private) - function gain = mcSquare_magicFudge(~,energy) + + function gain = mcSquare_magicFudge(~, energy) % mcSquare will scale the spot intensities in % https://gitlab.com/openmcsquare/MCsquare/blob/master/src/data_beam_model.c#L906 % by this factor so we need to divide up front to make things work. The @@ -862,7 +1014,7 @@ function writeMhd(obj,cube,resolution) K = 35.87; % in eV (other value 34.23 ?) % // Air stopping power (fit ICRU) multiplied by air density - SP = (9.6139e-9*energy^4 - 7.0508e-6*energy^3 + 2.0028e-3*energy^2 - 2.7615e-1*energy + 2.0082e1) * 1.20479E-3 * 1E6; % // in eV / cm + SP = (9.6139e-9 * energy^4 - 7.0508e-6 * energy^3 + 2.0028e-3 * energy^2 - 2.7615e-1 * energy + 2.0082e1) * 1.20479E-3 * 1E6; % // in eV / cm % // Temp & Pressure correction PTP = 1.0; @@ -872,18 +1024,18 @@ function writeMhd(obj,cube,resolution) C = 3.0E-9; % // in C / cm % // Gain: 1eV = 1.602176E-19 J - gain = (C*K) / (SP*PTP*1.602176E-19); + gain = (C * K) / (SP * PTP * 1.602176E-19); % divide by 1e7 to not get tiny numbers... - gain = gain/1e7; + gain = gain / 1e7; end - end + end methods (Static) - function [binaryFound,binaryFile] = checkBinaries() + function [binaryFound, binaryFile] = checkBinaries() % checkBinaries check if the binaries are available on the current % machine and sets to the mcsquarebinary object property % @@ -894,20 +1046,20 @@ function writeMhd(obj,cube,resolution) binaryFound = false; if ispc - if exist('MCSquare_windows.exe','file') ~= 2 + if exist('MCSquare_windows.exe', 'file') ~= 2 matRad_cfg.dispWarning('Could not find MCsquare binary.\n'); else binaryFile = 'MCSquare_windows.exe'; end elseif ismac - if exist('MCsquare_mac','file') ~= 2 + if exist('MCsquare_mac', 'file') ~= 2 matRad_cfg.dispWarning('Could not find MCsquare binary.\n'); else binaryFile = './MCsquare_mac'; end - %error('MCsquare binaries not available for mac OS.\n'); + % error('MCsquare binaries not available for mac OS.\n'); elseif isunix - if exist('MCsquare_linux','file') ~= 2 + if exist('MCsquare_linux', 'file') ~= 2 matRad_cfg.dispWarning('Could not find MCsquare binary.\n'); else binaryFile = './MCsquare_linux'; @@ -920,7 +1072,7 @@ function writeMhd(obj,cube,resolution) end - function [available,msg] = isAvailable(pln,machine) + function [available, msg] = isAvailable(pln, machine) % see superclass for information msg = []; @@ -930,35 +1082,35 @@ function writeMhd(obj,cube,resolution) machine = matRad_loadMachine(pln); end - %checkBasic + % checkBasic try - checkBasic = isfield(machine,'meta') && isfield(machine,'data'); + checkBasic = isfield(machine, 'meta') && isfield(machine, 'data'); - %check modality + % check modality checkModality = any(strcmp(DoseEngines.matRad_ParticleMCsquareEngine.possibleRadiationModes, machine.meta.radiationMode)); preCheck = checkBasic && checkModality; if ~preCheck - return; + return end catch msg = 'Your machine file is invalid and does not contain the basic field (meta/data/radiationMode)!'; - return; + return end - %Check the binaries + % Check the binaries hasBinaries = DoseEngines.matRad_ParticleMCsquareEngine.checkBinaries(); available = preCheck & hasBinaries; end - %Used to check against a machine file if a specific quantity can be - %computed. + % Used to check against a machine file if a specific quantity can be + % computed. function q = providedQuantities(machine) - %A dose engine will, by definition, return dose - q = {'physicalDose','LET'}; + % A dose engine will, by definition, return dose + q = {'physicalDose', 'LET'}; end + end end - diff --git a/matRad/doseCalc/+DoseEngines/matRad_ParticlePencilBeamEngineAbstract.m b/matRad/doseCalc/+DoseEngines/matRad_ParticlePencilBeamEngineAbstract.m index 64749b5f2..27a263970 100644 --- a/matRad/doseCalc/+DoseEngines/matRad_ParticlePencilBeamEngineAbstract.m +++ b/matRad/doseCalc/+DoseEngines/matRad_ParticlePencilBeamEngineAbstract.m @@ -2,11 +2,10 @@ % matRad_DoseEngineParticlePB: % Implements an engine for particle based dose calculation % For detailed information see superclass matRad_DoseEngine -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2022 the matRad development team. +% Copyright 2022-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -149,7 +148,7 @@ function chooseLateralModel(this) function bixel = initBixel(this,currRay,k) % matRad initialize general bixel geometry for particle dose calc % - % call + % call: % bixel = this.initBixel(currRay,k) bixel = struct(); @@ -374,7 +373,7 @@ function chooseLateralModel(this) end % compute! - sigmaRashi = matRad_calcSigmaRashi(currBixel.baseData.energy, ... + sigmaRashi = matRad_calcSigmaRashi(currBixel.baseData, ... currBixel.rangeShifter, ... currBixel.SSD); @@ -532,15 +531,15 @@ function calcLateralParticleCutOff(this,cutOffLevel,stfElement) % matRad function to calculate a depth dependend lateral cutoff % for each pristine particle beam % - % call + % call: % this.calcLateralParticleCutOff(cutOffLevel,stf) % - % input + % input: % this: current engine object includes machine base data file % cutOffLevel: cut off level - number between 0 and 1 % stfElement: matRad steering information struct for a single beam % - % output + % output: % machine: changes in the object property machine base data file including an additional field representing the lateral % cutoff % @@ -629,7 +628,7 @@ function calcLateralParticleCutOff(this,cutOffLevel,stfElement) if strcmp(this.machine.meta.radiationMode,'protons') && rangeShifterLUT(i).eqThickness > 0 %get max range shift - sigmaRashi = matRad_calcSigmaRashi(this.machine.data(energyIx).energy, ... + sigmaRashi = matRad_calcSigmaRashi(this.machine.data(energyIx), ... rangeShifterLUT(i), ... energySigmaLUT(i,3)); @@ -818,7 +817,7 @@ function calcLateralParticleCutOff(this,cutOffLevel,stfElement) if rangeShifter.eqThickness > 0 && strcmp(pln.radiationMode,'protons') % compute! - sigmaRashi = matRad_calcSigmaRashi(this.machine.data(energyIx).energy,rangeShifter,maxSSD); + sigmaRashi = matRad_calcSigmaRashi(this.machine.data(energyIx),rangeShifter,maxSSD); % add to initial sigma in quadrature sigmaIni_sq = sigmaIni_sq + sigmaRashi^2; diff --git a/matRad/doseCalc/+DoseEngines/matRad_PencilBeamEngineAbstract.m b/matRad/doseCalc/+DoseEngines/matRad_PencilBeamEngineAbstract.m index 6d647af0e..c835449cd 100644 --- a/matRad/doseCalc/+DoseEngines/matRad_PencilBeamEngineAbstract.m +++ b/matRad/doseCalc/+DoseEngines/matRad_PencilBeamEngineAbstract.m @@ -7,7 +7,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2019 the matRad development team. + % Copyright 2019-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -29,10 +29,13 @@ ignoreOutsideDensities; % Ignore densities outside of cst contours numOfDijFillSteps = 10; % Number of times during dose calculation the temporary containers are moved to a sparse matrix + + traceOnDoseGrid = false; % perform raytracing on dose grid instead of CT grid end properties (SetAccess = protected) effectiveLateralCutOff; %internal cutoff to be used, computed from machine/pencil-beam kernel properties and geometric/dosimetric cutoff settings + rayTracer; end properties (SetAccess = protected, GetAccess = public) @@ -97,7 +100,7 @@ function setDefaults(this) scenStf = stf; % manipulate isocenter for k = 1:numel(scenStf) - scenStf(k).isoCenter = scenStf(k).isoCenter + this.multScen.isoShift(ixShiftScen,:); + scenStf(k).isoCenter = cast(scenStf(k).isoCenter + this.multScen.isoShift(ixShiftScen,:),this.precision); end if this.multScen.totNumShiftScen > 1 @@ -152,9 +155,6 @@ function setDefaults(this) end end end - - %Finalize dose calculation - dij = this.finalizeDose(dij); end function dij = initDoseCalc(this,ct,cst,stf) @@ -162,9 +162,14 @@ function setDefaults(this) % containing intialization which are specificly needed for % pencil beam calculation and not for other engines - dij = initDoseCalc@DoseEngines.matRad_DoseEngineBase(this,ct,cst,stf); - matRad_cfg = MatRad_Config.instance(); + if this.enableGPU + ct = matRad_moveCtToGPU(ct); + cst = matRad_moveCstToGPU(cst); + end + + + dij = initDoseCalc@DoseEngines.matRad_DoseEngineBase(this,ct,cst,stf); % calculate rED or rSP from HU or take provided wedCube if this.useGivenEqDensityCube && ~isfield(ct,'cube') @@ -178,9 +183,9 @@ function setDefaults(this) ct = matRad_calcWaterEqD(ct, stf); % Maybe we can avoid duplicating the CT here? end - this.cubeWED = ct.cube; + this.cubeWED = cellfun(@(x) cast(x,this.precision),ct.cube, 'UniformOutput',false); if isfield(ct,'hlut') - this.hlut = ct.hlut; + this.hlut = cast(ct.hlut,this.precision); end % ignore densities outside of contours @@ -193,7 +198,18 @@ function setDefaults(this) end % Allocate memory for quantity containers - dij = this.allocateQuantityMatrixContainers(dij,{'physicalDose'}); + dij = this.allocateQuantityMatrixContainers(dij,{'physicalDose'}); + + if this.traceOnDoseGrid + cubesForTracing = cellfun(@(cube) matRad_interp3(... + dij.ctGrid.x,dij.ctGrid.y,dij.ctGrid.z, ... + cube, ... + dij.doseGrid.x, dij.doseGrid.y', dij.doseGrid.z, ... + 'linear'), this.cubeWED, 'UniformOutput', false); + this.rayTracer = matRad_RayTracerSiddon(cubesForTracing, dij.doseGrid); + else + this.rayTracer = matRad_RayTracerSiddon(this.cubeWED,dij.ctGrid); + end end function dij = allocateQuantityMatrixContainers(this,dij,names) @@ -213,13 +229,17 @@ function setDefaults(this) %Now preallocate a matrix in each active scenario using the %scenmask if this.calcDoseDirect - dij.(names{n})(this.multScen.scenMask) = {zeros(dij.doseGrid.numOfVoxels,this.numOfColumnsDij)}; + dij.(names{n})(this.multScen.scenMask) = {zeros(dij.doseGrid.numOfVoxels,this.numOfColumnsDij,this.precision)}; else %We preallocate a sparse matrix with sparsity of %1e-3 to make the filling slightly faster %TODO: the preallocation could probably - %have more accurate estimates - dij.(names{n})(this.multScen.scenMask) = {spalloc(dij.doseGrid.numOfVoxels,this.numOfColumnsDij,round(prod(dij.doseGrid.numOfVoxels,this.numOfColumnsDij)*1e-3))}; + %have more accurate estimates + if this.allowsSinglePrecisionSparseDij() + dij.(names{n})(this.multScen.scenMask) = {spalloc(dij.doseGrid.numOfVoxels,this.numOfColumnsDij,round(prod(dij.doseGrid.numOfVoxels,this.numOfColumnsDij)*1e-3),this.precision)}; + else + dij.(names{n})(this.multScen.scenMask) = {spalloc(dij.doseGrid.numOfVoxels,this.numOfColumnsDij,round(prod(dij.doseGrid.numOfVoxels,this.numOfColumnsDij)*1e-3))}; + end end end end @@ -228,16 +248,16 @@ function setDefaults(this) % Method for initializing the beams for analytical pencil beam % dose calculation % - % call + % call: % this.initBeam(ct,stf,dij,i) % - % input + % input: % ct: matRad ct struct % cst: matRad cst struct % stf: matRad steering information struct % i: index of beam % - % output + % output: % dij: updated dij struct matRad_cfg = MatRad_Config.instance(); @@ -256,7 +276,7 @@ function setDefaults(this) % Do not transpose matrix since we usage of row vectors & % transformation of the coordinate system need double transpose - currBeam.rotMat_system_T = matRad_getRotationMatrix(currBeam.gantryAngle,currBeam.couchAngle); + currBeam.rotMat_system_T = cast(matRad_getRotationMatrix(currBeam.gantryAngle,currBeam.couchAngle),this.precision); % Rotate coordinates (1st couch around Y axis, 2nd gantry movement) rot_coordsV = coordsV*currBeam.rotMat_system_T; @@ -269,21 +289,41 @@ function setDefaults(this) geoDistVdoseGrid(1:ct.numOfCtScen)= {sqrt(sum(rot_coordsVdoseGrid.^2,2))}; % Calculate radiological depth cube - matRad_cfg.dispInfo('matRad: calculate radiological depth cube... '); + tRayTracingStart = tic; + if this.traceOnDoseGrid + matRad_cfg.dispInfo('matRad: calculate radiological depth (on dose grid)... '); + voxForRayTracer = this.VdoseGrid; + rotCoordsForRayTracer = rot_coordsVdoseGrid; + else + matRad_cfg.dispInfo('matRad: calculate radiological depth (on CT grid)... '); + voxForRayTracer = this.VctGrid; + rotCoordsForRayTracer = rot_coordsV; + end + + this.rayTracer.lateralCutOff = this.effectiveLateralCutOff; - ct.cube = this.cubeWED; if this.keepRadDepthCubes - [radDepthVctGrid, currBeam.radDepthCube] = matRad_rayTracing(currBeam,ct,this.VctGrid,rot_coordsV,this.effectiveLateralCutOff); + [radDepths, currBeam.radDepthCube] = this.rayTracer.traceCube(currBeam,voxForRayTracer,rotCoordsForRayTracer); + + if ~this.traceOnDoseGrid + currBeam.radDepthCube = cellfun(@(rD) matRad_interp3(dij.ctGrid.x, dij.ctGrid.y, dij.ctGrid.z, rD, ... + dij.doseGrid.x,dij.doseGrid.y',dij.doseGrid.z,'nearest'),currBeam.radDepthCube,'UniformOutput',false); + end - currBeam.radDepthCube = cellfun(@(rD) matRad_interp3(dij.ctGrid.x, dij.ctGrid.y, dij.ctGrid.z, rD, ... - dij.doseGrid.x,dij.doseGrid.y',dij.doseGrid.z,'nearest'),currBeam.radDepthCube,'UniformOutput',false); this.radDepthCubes(i,:) = currBeam.radDepthCube(:); else - radDepthVctGrid = matRad_rayTracing(currBeam,ct,this.VctGrid,rot_coordsV,this.effectiveLateralCutOff); + radDepths = this.rayTracer.traceCube(currBeam,voxForRayTracer,rotCoordsForRayTracer); end - + + ct.cube = this.cubeWED; % interpolate radiological depth cube to dose grid resolution - radDepthVdoseGrid = this.interpRadDepth(ct,1:ct.numOfCtScen,this.VctGrid,this.VdoseGrid,dij.ctGrid,dij.doseGrid,radDepthVctGrid); + if this.traceOnDoseGrid + radDepthVdoseGrid = radDepths; + else + radDepthVdoseGrid = this.interpRadDepth(ct,1:ct.numOfCtScen,this.VctGrid,this.VdoseGrid,dij.ctGrid,dij.doseGrid,radDepths); + end + + matRad_cfg.dispInfo('done in %fs.\n',toc(tRayTracingStart)); % limit rotated coordinates to positions where ray tracing is availabe %radDepthsMat = cellfun(@(radDepthCube) matRad_interp3(dij.ctGrid.x, dij.ctGrid.y, dij.ctGrid.z,radDepthCube,dij.doseGrid.x,dij.doseGrid.y',dij.doseGrid.z,'nearest'),radDepthsMat,'UniformOutput',false); @@ -299,18 +339,19 @@ function setDefaults(this) % compute SSDs currBeam = matRad_computeSSD(currBeam,ct,'densityThreshold',this.ssdDensityThreshold); - - matRad_cfg.dispInfo('done.\n'); %Reinitialize Progress: %matRad_progress(1,1000); end - function radDepthVdoseGrid = interpRadDepth(~,ct,ctScen,V,Vcoarse,ctGrid,doseGrid,radDepthVctGrid) + function radDepthVdoseGrid = interpRadDepth(this,ct,ctScen,V,Vcoarse,ctGrid,doseGrid,radDepthVctGrid) for i = 1:numel(ctScen) ctScenNum = ctScen(i); - radDepthCube = NaN*ones(ct.cubeDim); + radDepthCube = NaN*ones(ct.cubeDim,this.precision); + if isa(radDepthVctGrid{i},'gpuArray') + radDepthCube = gpuArray(radDepthCube); + end radDepthCube(V(~isnan(radDepthVctGrid{1}))) = radDepthVctGrid{ctScenNum}(~isnan(radDepthVctGrid{1})); % interpolate cube - cube is now stored in Y X Z @@ -428,6 +469,9 @@ function setDefaults(this) %this.tmpMatrixContainers.(qName){bixelContainerColIx,1} = zeros(dij.doseGrid.numOfVoxels,1); %this.tmpMatrixContainers.(qName){bixelContainerColIx,1}(this.VdoseGrid(bixel.ix)) = bixel.(qName); else + if ~this.allowsSinglePrecisionSparseDij() + bixel.(qName) = double(bixel.(qName)); + end this.tmpMatrixContainers.(qName){bixelContainerColIx,subScenIdx{:}} = sparse(bixel.ix,1,bixel.(qName),dij.doseGrid.numOfVoxels,1); end end @@ -517,7 +561,7 @@ function setDefaults(this) % matRad calculation of lateral distances from central ray % used for dose calculation % - % call + % call: % [ix,rad_distancesSq,isoLatDistsX,isoLatDistsZ] = ... % this.calcGeoDists(rot_coords_bev, ... % sourcePoint_bev, ... @@ -526,7 +570,7 @@ function setDefaults(this) % radDepthIx, ... % lateralCutOff) % - % input + % input: % rot_coords_bev: coordinates in bev of the voxels with index V, % where also ray tracing results are availabe % sourcePoint_bev: source point in voxel coordinates in beam's eye view @@ -537,7 +581,7 @@ function setDefaults(this) % lateralCutOff: lateral cutoff specifying the neighbourhood for % which dose calculations will actually be performed % - % output + % output: % ix: indices of voxels where we want to compute dose % influence data % rad_distancesSq: squared radial distance to the central ray (where the diff --git a/matRad/doseCalc/+DoseEngines/matRad_PhotonOmpMCEngine.m b/matRad/doseCalc/+DoseEngines/matRad_PhotonOmpMCEngine.m index fc5d53ca2..053b8860f 100644 --- a/matRad/doseCalc/+DoseEngines/matRad_PhotonOmpMCEngine.m +++ b/matRad/doseCalc/+DoseEngines/matRad_PhotonOmpMCEngine.m @@ -5,7 +5,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2019 the matRad development team. + % Copyright 2019-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -52,10 +52,10 @@ function this = matRad_PhotonOmpMCEngine(pln) % Constructor % - % call + % call: % engine = DoseEngines.matRad_DoseEnginePhotonsOmpMCct,stf,pln,cst) % - % input + % input: % pln: matRad plan meta information struct if nargin < 1 @@ -65,6 +65,12 @@ % call superclass constructor this = this@DoseEngines.matRad_MonteCarloEngineAbstract(pln); + if this.enableGPU + matRad_cfg = MatRad_Config.instance(); + matRad_cfg.dispWarning('Set enableGPU ot true but ompMC does not support GPU computation! Setting back to false!'); + this.enableGPU = false; + end + matRad_cfg = MatRad_Config.instance(); this.omcFolder = [matRad_cfg.matRadRoot filesep 'thirdParty' filesep 'ompMC']; @@ -85,14 +91,14 @@ % can be automaticly called through matRad_calcDose or % matRad_calcPhotonDoseMC % - % call + % call: % dij = this.calcDose(ct,stf,pln,cst) % - % input + % input: % ct: matRad ct struct % stf: matRad steering information struct % cst: matRad cst struct - % output + % output: % dij: matRad dij struct % % References @@ -201,9 +207,6 @@ end end end - - %Finalize dose calculation - dij = this.finalizeDose(dij); end function dij = initDoseCalc(this,ct,cst,stf) @@ -523,10 +526,11 @@ function getOmpMCsource(obj,stf) function compileOmpMCInterface(dest,omcFolder) % Compiles the ompMC interface (integrated as submodule) % - % call + % call: % matRad_OmpConfig.compileOmpMCInterface() % matRad_OmpConfig.compileOmpMCInterface(dest) % matRad_OmpConfig.compileOmpMCInterface(dest,sourceFolder) + % % if an object is instantiated, matRad_OmpConfig can be replaced by the % object handle % diff --git a/matRad/doseCalc/+DoseEngines/matRad_PhotonPencilBeamSVDEngine.m b/matRad/doseCalc/+DoseEngines/matRad_PhotonPencilBeamSVDEngine.m index dd273c235..1d3354357 100644 --- a/matRad/doseCalc/+DoseEngines/matRad_PhotonPencilBeamSVDEngine.m +++ b/matRad/doseCalc/+DoseEngines/matRad_PhotonPencilBeamSVDEngine.m @@ -5,11 +5,10 @@ % % References % [1] http://www.ncbi.nlm.nih.gov/pubmed/8497215 + % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - % - % Copyright 2022 the matRad development team. + % Copyright 2022-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -77,10 +76,10 @@ function this = matRad_PhotonPencilBeamSVDEngine(pln) % Constructor % - % call + % call: % engine = DoseEngines.matRad_PhotonPencilBeamSVDEngine(pln) % - % input + % input: % ct: matRad ct struct % stf: matRad steering information struct % pln: matRad plan meta information struct @@ -175,7 +174,7 @@ function setDefaults(this) end % calculate field size and distances - fieldLimit = ceil(this.fieldWidth/(2*this.intConvResolution)); + fieldLimit = cast(ceil(this.fieldWidth/(2*this.intConvResolution)),this.precision); [this.F_X,this.F_Z] = meshgrid(-fieldLimit*this.intConvResolution: ... this.intConvResolution: ... (fieldLimit-1)*this.intConvResolution); @@ -184,7 +183,7 @@ function setDefaults(this) sigmaGauss = this.penumbraFWHM / sqrt(8*log(2)); % [mm] % use 5 times sigma as the limits for the gaussian convolution - gaussLimit = ceil(5*sigmaGauss/this.intConvResolution); + gaussLimit = cast(ceil(5*sigmaGauss/this.intConvResolution),this.precision); [gaussFilterX,gaussFilterZ] = meshgrid(-gaussLimit*this.intConvResolution: ... this.intConvResolution: ... (gaussLimit-1)*this.intConvResolution); @@ -193,7 +192,7 @@ function setDefaults(this) % get kernel size and distances - kernelLimit = ceil(this.kernelCutOff/this.intConvResolution); + kernelLimit = cast(ceil(this.kernelCutOff/this.intConvResolution),this.precision); [this.kernelX, this.kernelZ] = meshgrid(-kernelLimit*this.intConvResolution: ... this.intConvResolution: ... (kernelLimit-1)*this.intConvResolution); @@ -215,7 +214,7 @@ function setDefaults(this) % convolution if we use a uniform fluence if ~this.isFieldBasedDoseCalc % Create fluence matrix - this.Fpre = ones(floor(this.fieldWidth/this.intConvResolution)); + this.Fpre = ones(floor(this.fieldWidth/this.intConvResolution),this.precision); if ~this.useCustomPrimaryPhotonFluence % gaussian convolution of field to model penumbra @@ -242,16 +241,16 @@ function setDefaults(this) % Method for initializing the beams for analytical pencil beam % dose calculation % - % call + % call: % this.initBeam(ct,stf,dij,i) % - % input + % input: % ct: matRad ct struct % stf: matRad steering information struct % dij: matRad dij struct % i: index of beam % - % output + % output: % dij: updated dij struct currBeam = initBeam@DoseEngines.matRad_PencilBeamEngineAbstract(this,currBeam,ct,cst,stf,i); @@ -290,7 +289,7 @@ function setDefaults(this) function [bixel] = computeBixel(this,currRay,k) % matRad photon dose calculation for an individual bixel % - % call + % call: % bixel = this.computeBixel(currRay,k) bixel = struct(); @@ -347,18 +346,18 @@ function setDefaults(this) % matRad dij sampling function % This function samples. % - % call + % call: % [ixNew,bixelDoseNew] = % this.sampleDij(ix,bixelDose,radDepthV,rad_distancesSq,sType,Param) % - % input + % input: % ix: indices of voxels where we want to compute dose influence data % bixelDose: dose at specified locations as linear vector % radDepthV: radiological depth vector % rad_distancesSq: squared radial distance to the central ray % bixelWidth: bixelWidth as set in pln (optional) % - % output + % output: % ixNew: reduced indices of voxels where we want to compute dose influence data % bixelDoseNew reduced dose at specified locations as linear vector % @@ -548,12 +547,12 @@ function setDefaults(this) % called individually for certain applications without having % a fully defined dose engine % - % call + % call: % dose = this.calcPhotonDoseBixel(SAD,m,betas,Interp_kernel1,... % Interp_kernel2,Interp_kernel3,radDepths,geoDists,... % isoLatDistsX,isoLatDistsZ) % - % input + % input: % SAD: source to axis distance % m: absorption in water (part of the dose calc base % data) @@ -567,7 +566,7 @@ function setDefaults(this) % isoLatDistsZ: lateral distance in Z direction in BEV from central % ray at iso center plane % - % output + % output: % dose: photon dose at specified locations as linear vector % % References diff --git a/matRad/doseCalc/+DoseEngines/matRad_TG43BrachyEngine.m b/matRad/doseCalc/+DoseEngines/matRad_TG43BrachyEngine.m index 13d44deca..391992665 100644 --- a/matRad/doseCalc/+DoseEngines/matRad_TG43BrachyEngine.m +++ b/matRad/doseCalc/+DoseEngines/matRad_TG43BrachyEngine.m @@ -8,7 +8,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2024 the matRad development team. +% Copyright 2024-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -140,9 +140,6 @@ function setDefaults(this) % update waitbar, delete waitbar matRad_cfg.dispInfo('Brachytherapy dose calculation finished in %f s!\n',toc(startTime)); this.progressUpdate(1,1); - - %Finalize dose calculation - dij = this.finalizeDose(dij); end end @@ -157,14 +154,14 @@ function setDefaults(this) % 1D dose rate formalism from Rivard et al. (2004): AAPM TG-43 update, % page 639, Eq. 11: % - % call + % call: % DoseRate = matRad_TG43BrachyEngine.getDoseRate1D_poly(machine,r_mm) % - % input + % input: % machine: TG43 information about the used seeds % r: radial distance array, given in mm! % - % output + % output: % DoseRate: size(r) array of dose Rate in cGy/h % % comment on dimensions / units @@ -239,15 +236,15 @@ function setDefaults(this) % 2D dose rate formalism from Rivard et al. (2004): AAPM TG-43 update, % page 637, eq. 1 % - % call + % call: % DoseRate = matRad_TG43BrachyEngine.getDoseRate2D_poly(machine,r_mm) % - % input + % input: % machine: TG43 information about the used seeds % r: radial distance array, given in mm! % theta: polar angle in degree % - % output + % output: % DoseRate: size(r) array of dose Rate in cGy/h % % comment on dimensions / units @@ -342,19 +339,20 @@ function setDefaults(this) function [ThetaMatrix,ThetaVector] = getThetaMatrix(this,templateNormal,DistanceMatrix) % getThetaMatrix gets (seed x dosepoint) matrix of relative polar angles % - % call + % call: % [ThetaMatrix,ThetaVector] = matRad_TG43BrachyEngine.getThetaMatrix(templateNormal,... % DistanceMatrix) + % % normally called within matRad_TG43BrachyEngine.getBrachyDose % !!getDistanceMatrix needs to be called first!! % - % input + % input: % DistanceMatrix: [dosePoint x seedPoint] struct with fields 'x','y', % 'z' and total distance 'dist' % templateNormal: normal vector of template (its assumed that this is % the dir all seeds point to) % - % output + % output: % angle matrix: rows: index of dosepoint % columns: index of deedpoint % entry: polar angles betreen seedpoints and @@ -382,17 +380,17 @@ function setDefaults(this) % AAPM Task Group No. 43 Report Eq. (2). % Normally called within matRad_TG43BrachyEngine.getDoseRate(...) % - % call + % call: % PhiAn = matRad_TG43BrachyEngine.anisotropyFactor1D(r,PhiAnTab, L) % - % input + % input: % r: array of radial distances in cm! % PhiAnTab: tabulated consensus data of gL according to the following % cell structure: % PhiAnTab{1} = AnisotropyFactorRadialDistance % PhiAnTab{2} = AnisotropyFactorValue % - % output + % output: % PhiAn: array of the same shape as r and thet containing the % interpolated and extrapolated values rmin = PhiAnTab{1}(1); @@ -413,10 +411,10 @@ function setDefaults(this) % % This function requires the multiPolyRegress code : https://de.mathworks.com/matlabcentral/fileexchange/34918-multivariate-polynomial-regression % - % call + % call: % F = matRad_TG43BrachyEngine.anisotropyFunction2D(r,thet,FTab) % - % input + % input: % r: array of radial distances in cm % thet: array of azimuthal angles in ° % FTab: tabulated consensus data of F according to the @@ -425,7 +423,7 @@ function setDefaults(this) % FTab{2} = AnisotropyPolarAngles % FTab{3} = AnisotropyFunctionValue % - % output + % output: % F: array of the same shape as r and thet containing the % interpolated and extrapolated values @@ -457,15 +455,15 @@ function setDefaults(this) % according to Rivard et al.: AAPM TG-43, p 638 update Eq. (4) % Normally called within matRad_TG43BrachyEngine.getDoseRate(...) % - % call + % call: % GL = matRad_TG43BrachyEngine.geometryFunction(r,thet,L) % - % inputs + % input: % r: array of radial distances in cm! % thet: array of azimual angles in ° % Length: length of radiation source in cm % - % outputs + % output: % GL(r,theta): geometry function output % calculate solution @@ -500,17 +498,17 @@ function setDefaults(this) % according to Rivard et al.: AAPM TG-43 update, p.669, Eq. (C1). % Normally called within matRad_TG43BrachyEngine.TG43BrachyEngine.getDoseRate(...) % - % call + % call: % matRad_TG43BrachyEngine.TG43BrachyEngine.radialDoseFuncrion(r,gLTab) % - % input + % input: % r: array of radial distances in cm! % gLTab: tabulated consensus data of gL according to the % following cell structure: % gLTab{1} = RadialDoseDistance % gLTab{2} = RadialDoseValue % - % output + % output: % gL: array of the same shape as r containing the interpolated % and extrapolated values rmin = gLTab{1}(1); @@ -529,10 +527,10 @@ function setDefaults(this) % data using interp2 ( interp technique TBD) % Normally called within matRad_TG43BrachyEngine.TG43BrachyEngine.getDoseRate(...) % - % call + % call: % F = matRad_TG43BrachyEngine.TG43BrachyEngine.anisotropyFunction2D(r,thet,FTab) % - % input + % input: % r: array of radial distances in cm % thet: array of azimuthal angles in ?? % FTab: tabulated consensus data of F according to the @@ -541,7 +539,7 @@ function setDefaults(this) % FTab{2} = AnisotropyPolarAngles % FTab{3} = AnisotropyFunctionValue % - % output + % output: % F: array of the same shape as r and thet containing the % interpolated and extrapolated values [DataRGrid,DataThetGrid] = meshgrid(FTab{1},FTab{2}); diff --git a/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m b/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m index 58ab20995..bf339955e 100644 --- a/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m +++ b/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m @@ -7,7 +7,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2023 the matRad development team. + % Copyright 2023-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -170,14 +170,20 @@ end methods - function obj = matRad_TopasMCEngine(pln) + function this = matRad_TopasMCEngine(pln) if nargin < 1 pln = []; end % call superclass constructor - obj = obj@DoseEngines.matRad_MonteCarloEngineAbstract(pln); + this = this@DoseEngines.matRad_MonteCarloEngineAbstract(pln); + + if this.enableGPU + matRad_cfg = MatRad_Config.instance(); + matRad_cfg.dispWarning('Set enableGPU ot true but TOPAS does not support GPU computation! Setting back to false!'); + this.enableGPU = false; + end end function setDefaults(this) @@ -212,10 +218,10 @@ function setDefaults(this) function writeAllFiles(obj,ct,cst,stf,machine,w) % constructor to write all TOPAS fils for local or external simulation % - % call + % call: % topasConfig.writeAllFiles(ct,pln,stf,machine,w) % - % input + % input: % ct: Path to folder where TOPAS files are in (as string) % pln: matRad plan struct % stf: matRad steering struct @@ -246,14 +252,30 @@ function writeAllFiles(obj,ct,cst,stf,machine,w) end end - % Get alpha beta parameters from bioParam struct - if isfield(obj.bioParameters, 'tissuseAlphaX') - obj.bioParameters.AlphaX = obj.bioModel.tissueAlphaX(1); - obj.bioParameters.BetaX = obj.bioModel.tissueBetaX(1); - end - if numel(obj.bioParameters.AlphaX)>1 - matRad_cfg.dispWarning('!!! Only a unique alpha/beta ratio supported at the moment. Found multiple, only the first one will be used !!!!'); + tmpAlphaX = []; + tmpBetaX = []; + for idx = 1:size(cst, 1) + if ~isempty(cst{idx,5}) && isfield(cst{idx,5}, 'alphaX') + tmpAlphaX = [tmpAlphaX cst{idx,5}.alphaX]; + tmpBetaX = [tmpBetaX cst{idx,5}.betaX]; + end end + abX = [tmpAlphaX(:) tmpBetaX(:)]; + unique_abX = unique(abX, 'rows', 'stable'); + obj.bioParameters.AlphaX = unique_abX(:, 1); + obj.bioParameters.BetaX = unique_abX(:, 2); + + % We don't need this part if we assign the alpha beta from + % the cst + + % Get alpha beta parameters from bioParam struct + %if isfield(obj.bioParameters, 'tissueAlphaX') + % obj.bioParameters.AlphaX = obj.bioModel.tissueAlphaX; + % obj.bioParameters.BetaX = obj.bioModel.tissueBetaX; + %end + %if numel(obj.bioParameters.AlphaX)>1 + % matRad_cfg.dispWarning('!!! Only a unique alpha/beta ratio supported at the moment. Found multiple, only the first one will be used !!!!'); + %end end if obj.scorer.LET @@ -342,15 +364,15 @@ function writeAllFiles(obj,ct,cst,stf,machine,w) function dij = readFiles(obj,folder) % function to read out TOPAS data % - % call + % call: % topasCube = topasConfig.readFiles(folder,dij) % topasCube = obj.readFiles(folder,dij) % - % input + % input: % folder: Path to folder where TOPAS files are in (as string) % dij: dij struct (this part needs update) % - % output + % output: % topasCube: struct with all read out subfields @@ -428,14 +450,14 @@ function writeAllFiles(obj,ct,cst,stf,machine,w) function dij = readExternal(obj,folder) % function to read out complete TOPAS simulation from single folder % - % call + % call: % topasCube = topasConfig.readExternal(folder) % topasCube = obj.readExternal(folder) % - % input + % input: % folder: Path to folder where TOPAS files are in (as string) % - % output + % output: % topasCube: struct with all read out subfields % % EXAMPLE calls: @@ -508,10 +530,20 @@ function writeAllFiles(obj,ct,cst,stf,machine,w) end % Get photon parameters for RBExDose calculation - if this.calcBioDose + if this.calcBioDose || this.scorer.RBE this.scorer.RBE = true; - [dij.ax,dij.bx] = matRad_getPhotonLQMParameters(cst,dij.doseGrid.numOfVoxels,1,VdoseGrid); - dij.abx(dij.bx>0) = dij.ax(dij.bx>0)./dij.bx(dij.bx>0); + this.calcBioDose = true; + [dij.ax,dij.bx] = matRad_getPhotonLQMParameters(cst,dij.doseGrid.numOfVoxels,this.VdoseGrid); + numCtScen = numel(dij.ax); + dij.abx = cell(numCtScen,1); + for ctScenIdx = 1:numCtScen + ax = dij.ax{ctScenIdx}; + bx = dij.bx{ctScenIdx}; + abx = zeros(size(ax)); + mask = bx > 0; + abx(mask) = ax(mask) ./ bx(mask); + dij.abx{ctScenIdx} = abx; + end end % save current directory to revert back to later @@ -670,9 +702,6 @@ function writeAllFiles(obj,ct,cst,stf,machine,w) dij.totalNumOfRays = 1; dij.meta.TOPASworkingDir = this.workingDir; end - - this.finalizeDose(); - end @@ -768,15 +797,15 @@ function writeAllFiles(obj,ct,cst,stf,machine,w) function topasCube = readTopasCubes(obj,folder) % function to read out TOPAS data % - % call + % call: % topasCube = topasConfig.readTopasCubes(folder,dij) % topasCube = obj.readTopasCubes(folder,dij) % - % input + % input: % folder: Path to folder where TOPAS files are in (as string) % dij: dij struct (this part needs update) % - % output + % output: % topasCube: struct with all read out subfields matRad_cfg = MatRad_Config.instance(); %Instance of matRad configuration class @@ -1171,16 +1200,39 @@ function writeAllFiles(obj,ct,cst,stf,machine,w) end % Handle RBE-related quantities (not multiplied by sum(w)!) elseif ~isempty(strfind(lower(topasCubesTallies{j}),'alpha')) - modelName = strsplit(topasCubesTallies{j},'_'); - modelName = modelName{end}; + talliesFlags = strsplit(topasCubesTallies{j},'_'); + modelName = talliesFlags{end}; if isfield(topasCubes,[topasCubesTallies{j} '_beam' num2str(d)]) && iscell(topasCubes.([topasCubesTallies{j} '_beam' num2str(d)])) - dij.(['mAlphaDose_' modelName]){ctScen}(:,d) = reshape(topasCubes.([topasCubesTallies{j} '_beam',num2str(d)]){ctScen},[],1) .* dij.physicalDose{ctScen}(:,d); + + if contains(topasCubesTallies{j}, 'CellType') + ab_idx = str2double(talliesFlags{2}); + organAlpha = obj.bioParameters.AlphaX(ab_idx); + organBeta = obj.bioParameters.BetaX(ab_idx); + mask = find( (dij.ax{1} == organAlpha) & (dij.bx{1} == organBeta)); + topasCube_values = reshape(topasCubes.([topasCubesTallies{j} '_beam',num2str(d)]){ctScen},[],1); + topasCube_values = topasCube_values(mask); + dij.(['mAlphaDose_' modelName]){ctScen}(mask,d) = topasCube_values .* dij.physicalDose{ctScen}(mask,d); + else + + dij.(['mAlphaDose_' modelName]){ctScen}(:,d) = reshape(topasCubes.([topasCubesTallies{j} '_beam',num2str(d)]){ctScen},[],1) .* dij.physicalDose{ctScen}(:,d); + end end elseif ~isempty(strfind(lower(topasCubesTallies{j}),'beta')) - modelName = strsplit(topasCubesTallies{j},'_'); - modelName = modelName{end}; + talliesFlags = strsplit(topasCubesTallies{j},'_'); + modelName = talliesFlags{end}; if isfield(topasCubes,[topasCubesTallies{j} '_beam' num2str(d)]) && iscell(topasCubes.([topasCubesTallies{j} '_beam' num2str(d)])) - dij.(['mSqrtBetaDose_' modelName]){ctScen}(:,d) = sqrt(reshape(topasCubes.([topasCubesTallies{j} '_beam',num2str(d)]){ctScen},[],1)) .* dij.physicalDose{ctScen}(:,d); + if contains(topasCubesTallies{j}, 'CellType') + ab_idx = str2double(talliesFlags{2}); + organAlpha = obj.bioParameters.AlphaX(ab_idx); + organBeta = obj.bioParameters.BetaX(ab_idx); + mask = find( (dij.ax{ctScen} == organAlpha) & (dij.bx{ctScen} == organBeta)); + topasCube_values = reshape(topasCubes.([topasCubesTallies{j} '_beam',num2str(d)]){ctScen},[],1); + topasCube_values = topasCube_values(mask); + dij.(['mSqrtBetaDose_' modelName]){ctScen}(mask,d) = sqrt(topasCube_values) .* dij.physicalDose{ctScen}(mask,d); + else + + dij.(['mSqrtBetaDose_' modelName]){ctScen}(:,d) = sqrt(reshape(topasCubes.([topasCubesTallies{j} '_beam',num2str(d)]){ctScen},[],1)) .* dij.physicalDose{ctScen}(:,d); + end end elseif ~isempty(strfind(topasCubesTallies{j},'LET')) if isfield(topasCubes,[topasCubesTallies{j} '_beam' num2str(d)]) && iscell(topasCubes.([topasCubesTallies{j} '_beam' num2str(d)])) @@ -1355,7 +1407,31 @@ function writeScorers(obj,fID,beamIx) % Read appropriate scorer from file and write to config file matRad_cfg.dispDebug('Reading RBE Scorer from %s\n',fname); scorerName = fileread(fname); - fprintf(fID,'\n%s\n\n',scorerName); + generalScorer = scorerName; + + if length(obj.bioParameters.AlphaX) ==1 + fprintf(fID,'\n%s\n\n',scorerName); + else + for idxCell = 1:length(obj.bioParameters.AlphaX) + scorerName = generalScorer; + insertText = ['_CellType_' num2str(idxCell)]; + scorerName = strrep(scorerName, ... + 'Alpha/', ['Alpha' insertText '/']); + scorerName = strrep(scorerName, ... + 'Beta/', ['Beta' insertText '/']); + scorerName = strrep(scorerName, ... + 'Sc/PrescribedDose Gy', ['Sc/PrescribedDose' insertText ' Gy']); + scorerName = strrep(scorerName, ... + 'Sc/CellLines', ['Sc/CellLines' insertText]); + scorerName = strrep(scorerName, ... + 'Sc/SimultaneousExposure', ['Sc/SimultaneousExposure' insertText]); + scorerName = strrep(scorerName, ... + 'Sim/ScoreLabel + "', ['Sim/ScoreLabel + "' insertText]); + + fprintf(fID,'\n%s\n\n',scorerName); + end + + end if obj.calc4DInterplay for PhaseNum = obj.MCparam.Phases{beamIx}' @@ -1402,28 +1478,60 @@ function writeScorers(obj,fID,beamIx) % Begin writing biological scorer components: cell lines switch obj.radiationMode case 'protons' - fprintf(fID,'\n### Biological Parameters ###\n'); - fprintf(fID,'sv:Sc/CellLines = 1 "CellLineGeneric"\n'); - fprintf(fID,'d:Sc/CellLineGeneric/Alphax = Sc/AlphaX /Gy\n'); - fprintf(fID,'d:Sc/CellLineGeneric/Betax = Sc/BetaX /Gy2\n'); - fprintf(fID,'d:Sc/CellLineGeneric/AlphaBetaRatiox = Sc/AlphaBetaX Gy\n\n'); + if length(obj.bioParameters.AlphaX) ==1 + fprintf(fID,'\n### Biological Parameters ###\n'); + fprintf(fID,'sv:Sc/CellLines = 1 "CellLineGeneric"\n'); + fprintf(fID,'d:Sc/CellLineGeneric/Alphax = Sc/AlphaX /Gy\n'); + fprintf(fID,'d:Sc/CellLineGeneric/Betax = Sc/BetaX /Gy2\n'); + fprintf(fID,'d:Sc/CellLineGeneric/AlphaBetaRatiox = Sc/AlphaBetaX Gy\n\n'); + else + for idxCell = 1:length(obj.bioParameters.AlphaX) + insertText = ['_CellType_' num2str(idxCell)]; + fprintf(fID,'\n### Biological Parameters ###\n'); + fprintf(fID, ['sv:Sc/CellLines' insertText ' = 1 "CellLineGeneric' insertText '"\n']); + fprintf(fID, ['d:Sc/CellLineGeneric' insertText '/Alphax = Sc/AlphaX' insertText ' /Gy\n']); + fprintf(fID, ['d:Sc/CellLineGeneric' insertText '/Betax = Sc/BetaX' insertText ' /Gy2\n']); + fprintf(fID, ['d:Sc/CellLineGeneric' insertText '/AlphaBetaRatiox = Sc/AlphaBetaX' insertText ' Gy\n\n']); + end + end case {'carbon','helium'} - fprintf(fID,'\n### Biological Parameters ###\n'); - fprintf(fID,'sv:Sc/CellLines = 1 "CellGeneric_abR2"\n'); - fprintf(fID,'d:Sc/CellGeneric_abR2/Alphax = Sc/AlphaX /Gy\n'); - fprintf(fID,'d:Sc/CellGeneric_abR2/Betax = Sc/BetaX /Gy2\n\n'); - % fprintf(fID,'d:Sc/CellGeneric_abR2/AlphaBetaRatiox = Sc/AlphaBetaX Gy\n'); + if length(obj.bioParameters.AlphaX) ==1 + fprintf(fID,'\n### Biological Parameters ###\n'); + fprintf(fID,'sv:Sc/CellLines = 1 "CellGeneric_abR2"\n'); + fprintf(fID,'d:Sc/CellGeneric_abR2/Alphax = Sc/AlphaX /Gy\n'); + fprintf(fID,'d:Sc/CellGeneric_abR2/Betax = Sc/BetaX /Gy2\n\n'); + % fprintf(fID,'d:Sc/CellGeneric_abR2/AlphaBetaRatiox = Sc/AlphaBetaX Gy\n'); + else + for idxCell = 1:length(obj.bioParameters.AlphaX) + insertText = ['_CellType_' num2str(idxCell)]; + fprintf(fID,'\n### Biological Parameters ###\n'); + fprintf(fID, ['sv:Sc/CellLines' insertText ' = 1 "CellLineGeneric_abR2' insertText '"\n']); + fprintf(fID, ['d:Sc/CellLineGeneric_abR2' insertText '/Alphax = Sc/AlphaX' insertText ' /Gy\n']); + fprintf(fID, ['d:Sc/CellLineGeneric_abR2' insertText '/Betax = Sc/BetaX' insertText ' /Gy2\n\n']); + end + end otherwise matRad_cfg.dispError([obj.radiationMode ' not implemented']); end % write biological scorer components: dose parameters matRad_cfg.dispDebug('Writing Biologial Scorer components.\n'); - fprintf(fID,'d:Sc/PrescribedDose = %.4f Gy\n',obj.bioParameters.PrescribedDose); - fprintf(fID,'b:Sc/SimultaneousExposure = %s\n',obj.bioParameters.SimultaneousExposure); - fprintf(fID,'d:Sc/AlphaX = %.4f /Gy\n',obj.bioParameters.AlphaX); - fprintf(fID,'d:Sc/BetaX = %.4f /Gy2\n',obj.bioParameters.BetaX); - fprintf(fID,'d:Sc/AlphaBetaX = %.4f Gy\n',obj.bioParameters.AlphaX/obj.bioParameters.BetaX); + if length(obj.bioParameters.AlphaX) ==1 + fprintf(fID,'d:Sc/PrescribedDose = %.4f Gy\n',obj.bioParameters.PrescribedDose); + fprintf(fID,'b:Sc/SimultaneousExposure = %s\n',obj.bioParameters.SimultaneousExposure); + fprintf(fID,'d:Sc/AlphaX = %.4f /Gy\n',obj.bioParameters.AlphaX); + fprintf(fID,'d:Sc/BetaX = %.4f /Gy2\n',obj.bioParameters.BetaX); + fprintf(fID,'d:Sc/AlphaBetaX = %.4f Gy\n',obj.bioParameters.AlphaX/obj.bioParameters.BetaX); + else + for idxCell = 1:length(obj.bioParameters.AlphaX) + insertText = ['_CellType_' num2str(idxCell)]; + fprintf(fID, ['d:Sc/PrescribedDose' insertText ' = %.4f Gy\n'],obj.bioParameters.PrescribedDose); + fprintf(fID, ['b:Sc/SimultaneousExposure' insertText ' = %s\n'],obj.bioParameters.SimultaneousExposure); + fprintf(fID, ['d:Sc/AlphaX' insertText ' = %.4f /Gy\n'],obj.bioParameters.AlphaX(idxCell)); + fprintf(fID, ['d:Sc/BetaX' insertText ' = %.4f /Gy2\n'],obj.bioParameters.BetaX(idxCell)); + fprintf(fID, ['d:Sc/AlphaBetaX' insertText ' = %.4f Gy\n\n'],obj.bioParameters.AlphaX(idxCell)/obj.bioParameters.BetaX(idxCell)); + end + end % Update MCparam.tallies with processed scorer for i = 1:length(obj.scorer.RBE_model) @@ -1451,9 +1559,23 @@ function writeScorers(obj,fID,beamIx) % Write subscorer to config files for s = 1:length(scorerNames) if strcmp(obj.radiationMode,'protons') - fprintf(fID,'s:Sc/%s%s/ReferencedSubScorer_LET = "ProtonLET"\n',scorerPrefix,scorerNames{s}); + if length(obj.bioParameters.AlphaX) ==1 + fprintf(fID,'s:Sc/%s%s/ReferencedSubScorer_LET = "ProtonLET"\n',scorerPrefix,scorerNames{s}); + else + for idxCell = 1:length(obj.bioParameters.AlphaX) + insertText = ['_CellType_' num2str(idxCell)]; + fprintf(fID,['s:Sc/%s%s' insertText '/ReferencedSubScorer_LET = "ProtonLET"\n'],scorerPrefix,scorerNames{s}); + end + end end + if length(obj.bioParameters.AlphaX) ==1 fprintf(fID,'s:Sc/%s%s/ReferencedSubScorer_Dose = "Tally_DoseToWater"\n',scorerPrefix,scorerNames{s}); + else + for idxCell = 1:length(obj.bioParameters.AlphaX) + insertText = ['_CellType_' num2str(idxCell)]; + fprintf(fID,['s:Sc/%s%s' insertText '/ReferencedSubScorer_Dose = "Tally_DoseToWater"\n'],scorerPrefix,scorerNames{s}); + end + end if obj.calc4DInterplay for PhaseNum = obj.MCparam.Phases{beamIx}' if strcmp(obj.radiationMode,'protons') @@ -2360,17 +2482,15 @@ function writeStfFields(obj,ct,stf,w,baseData) end function writePatient(obj,ct,pln) - % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % matRad export CT RSP data for TOPAS simulation % - % call + % call: % obj.writePatient(ct, path, material) % - % input + % input: % ct: ct cube % pln: plan structure containing doseCalc classes % - % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % the image cube contains the indexing of materials % since at the moment TOPAS does not support ushort % the materials should have indexes between 0 and 32767 @@ -2684,7 +2804,7 @@ function writeRangeShifter(~,fID,rangeShifter,sourceToNozzleDistance) fprintf(fID,'d:Ge/%s/HLZ = %f mm\n',rangeShifter.topasID,rsWidth/2); fprintf(fID,'d:Ge/%s/TransX = 500 mm * Tf/Beam/%sOut/Value\n',rangeShifter.topasID,rangeShifter.topasID); fprintf(fID,'d:Ge/%s/TransY = 0 mm\n',rangeShifter.topasID); - fprintf(fID,'d:Ge/%s/TransZ = %f mm\n',rangeShifter.topasID,rangeShifter.sourceRashiDistance - sourceToNozzleDistance); + fprintf(fID,'d:Ge/%s/TransZ = %f mm\n',rangeShifter.topasID,rangeShifter.sourceRashiDistance - sourceToNozzleDistance + rsWidth/2); end diff --git a/matRad/doseCalc/MCsquare/matRad_MCsquareBaseData.m b/matRad/doseCalc/MCsquare/matRad_MCsquareBaseData.m index e51222d17..47760fb68 100644 --- a/matRad/doseCalc/MCsquare/matRad_MCsquareBaseData.m +++ b/matRad/doseCalc/MCsquare/matRad_MCsquareBaseData.m @@ -4,7 +4,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2020 the matRad development team. + % Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/doseCalc/MCsquare/matRad_MCsquareConfig.m b/matRad/doseCalc/MCsquare/matRad_MCsquareConfig.m index e550504f8..1e9834751 100644 --- a/matRad/doseCalc/MCsquare/matRad_MCsquareConfig.m +++ b/matRad/doseCalc/MCsquare/matRad_MCsquareConfig.m @@ -6,7 +6,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2019 the matRad development team. +% Copyright 2019-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/doseCalc/MCsquare/matRad_compileMCsquareSparseReader.m b/matRad/doseCalc/MCsquare/matRad_compileMCsquareSparseReader.m index 0a938c8fd..6d2790826 100644 --- a/matRad/doseCalc/MCsquare/matRad_compileMCsquareSparseReader.m +++ b/matRad/doseCalc/MCsquare/matRad_compileMCsquareSparseReader.m @@ -2,7 +2,7 @@ function matRad_compileMCsquareSparseReader(dest,sourceFolder) % Compiles the sparse mcsquare reader as mex interface % for the current platform % -% call +% call: % matRad_compileMCsquareSparseReader() % matRad_compileMCsquareSparseReader(dest) % matRad_compileMCsquareSparseReader(dest,sourceFolder) @@ -20,7 +20,7 @@ function matRad_compileMCsquareSparseReader(dest,sourceFolder) % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2020 the matRad development team. +% Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/doseCalc/matRad_calcDoseDirect.m b/matRad/doseCalc/matRad_calcDoseDirect.m index d0c669b16..d0bfccc23 100644 --- a/matRad/doseCalc/matRad_calcDoseDirect.m +++ b/matRad/doseCalc/matRad_calcDoseDirect.m @@ -2,11 +2,11 @@ % matRad function to bypass dij calculation % Should not be used directly anymore as it is deprecated. Use matRad_calcDoseForward instead. % - % call + % call: % resultGUI = matRad_calcDoseDirec(ct,stf,pln,cst) % resultGUI = matRad_calcDoseDirec(ct,stf,pln,cst,w) % - % input + % input: % ct: ct cube % stf: matRad steering information struct % pln: matRad plan meta information struct @@ -14,7 +14,7 @@ % w: (optional, if no weights available in stf): bixel weight % vector % - % output + % output: % resultGUI: matRad result struct % % References @@ -22,7 +22,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2024 the matRad development team. + % Copyright 2024-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -58,4 +58,4 @@ - \ No newline at end of file + diff --git a/matRad/doseCalc/matRad_calcDoseDirectMC.m b/matRad/doseCalc/matRad_calcDoseDirectMC.m index ba286c7b5..96578b064 100644 --- a/matRad/doseCalc/matRad_calcDoseDirectMC.m +++ b/matRad/doseCalc/matRad_calcDoseDirectMC.m @@ -3,13 +3,13 @@ % matRad dose calculation wrapper for MC dose calculation algorithms % bypassing dij calculation for MC dose calculation algorithms. % -% call +% call: % resultGUI = matRad_calcDoseDirecMC(ct,stf,pln,cst) % resultGUI = matRad_calcDoseDirecMC(ct,stf,pln,cst,w) % resultGUI = matRad_calcDoseDirectMC(ct,stf,pln,cst,nHistories) % resultGUI = matRad_calcDoseDirectMC(ct,stf,pln,cst,w,nHistories) % -% input +% input: % ct: ct cube % stf: matRad steering information struct % pln: matRad plan meta information struct @@ -18,7 +18,7 @@ % vector % nHistories: (optional) number of histories % -% output +% output: % resultGUI: matRad result struct % % References @@ -26,7 +26,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2019 the matRad development team. +% Copyright 2019-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/doseCalc/matRad_calcLQParameter.m b/matRad/doseCalc/matRad_calcLQParameter.m index b63a4d7de..0e5e32c65 100644 --- a/matRad/doseCalc/matRad_calcLQParameter.m +++ b/matRad/doseCalc/matRad_calcLQParameter.m @@ -1,15 +1,15 @@ function [vAlpha, vBeta] = matRad_calcLQParameter(vRadDepths,mTissueClass,baseData) % matRad inverse planning wrapper function % -% call +% call: % [vAlpha, vBeta] = matRad_calcLQParameter(vRadDepths,mTissueClass,baseData) % -% input +% input: % vRadDepths: radiological depths of voxels % mTissueClass: tissue classes of voxels % baseData: biological base data % -% output +% output: % vAlpha: alpha values for voxels interpolated from base data % vBeta: beta values for voxels interpolated from base data % @@ -18,7 +18,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/doseCalc/matRad_calcParticleDose.m b/matRad/doseCalc/matRad_calcParticleDose.m index 0465ad535..20f2c09b1 100644 --- a/matRad/doseCalc/matRad_calcParticleDose.m +++ b/matRad/doseCalc/matRad_calcParticleDose.m @@ -1,10 +1,10 @@ function dij = matRad_calcParticleDose(ct,stf,pln,cst,calcDoseDirect) % matRad particle dose calculation wrapper % -% call +% call: % dij = matRad_calcParticleDose(ct,stf,pln,cst,calcDoseDirect) % -% input +% input: % ct: ct cube % stf: matRad steering information struct % pln: matRad plan meta information struct @@ -13,7 +13,7 @@ % computation and directly calculate dose; only makes % sense in combination with matRad_calcDoseDirect.m % -% output +% output: % dij: matRad dij struct % % References @@ -21,7 +21,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -54,4 +54,4 @@ % call calcDose from engine dij = matRad_calcDoseInfluence(ct,cst,stf,pln); -end \ No newline at end of file +end diff --git a/matRad/doseCalc/matRad_calcParticleDoseMC.m b/matRad/doseCalc/matRad_calcParticleDoseMC.m index 4e3fab9d1..10cd2be04 100644 --- a/matRad/doseCalc/matRad_calcParticleDoseMC.m +++ b/matRad/doseCalc/matRad_calcParticleDoseMC.m @@ -1,10 +1,10 @@ function dij = matRad_calcParticleDoseMC(ct,stf,pln,cst,nCasePerBixel,calcDoseDirect) % matRad MCsqaure monte carlo photon dose calculation wrapper % -% call +% call: % dij = matRad_calcParticleDoseMc(ct,stf,pln,cst,calcDoseDirect) % -% input +% input: % ct: matRad ct struct % stf: matRad steering information struct % pln: matRad plan meta information struct @@ -13,7 +13,7 @@ % calcDoseDirect: binary switch to enable forward dose % calcualtion % -% output +% output: % dij: matRad dij struct % % References @@ -23,7 +23,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2019 the matRad development team. +% Copyright 2019-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/doseCalc/matRad_calcPhotonDose.m b/matRad/doseCalc/matRad_calcPhotonDose.m index 3cdc630be..2459e9f92 100644 --- a/matRad/doseCalc/matRad_calcPhotonDose.m +++ b/matRad/doseCalc/matRad_calcPhotonDose.m @@ -1,10 +1,10 @@ function dij = matRad_calcPhotonDose(ct,stf,pln,cst,calcDoseDirect) % matRad photon dose calculation wrapper % -% call +% call: % dij = matRad_calcPhotonDose(ct,stf,pln,cst,calcDoseDirect) % -% input +% input: % ct: ct cube % stf: matRad steering information struct % pln: matRad plan meta information struct @@ -13,7 +13,7 @@ % computation and directly calculate dose; only makes % sense in combination with matRad_calcDoseDirect.m % -% output +% output: % dij: matRad dij struct % % References @@ -21,7 +21,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/doseCalc/matRad_calcPhotonDoseMC.m b/matRad/doseCalc/matRad_calcPhotonDoseMC.m index 40cb52e4c..069c26359 100644 --- a/matRad/doseCalc/matRad_calcPhotonDoseMC.m +++ b/matRad/doseCalc/matRad_calcPhotonDoseMC.m @@ -1,16 +1,16 @@ function dij = matRad_calcPhotonDoseMC(ct,stf,pln,cst,nCasePerBixel,visBool) % matRad ompMC monte carlo photon dose calculation wrapper % -% call +% call: % dij = matRad_calcPhotonDoseMc(ct,stf,pln,cst,visBool) % -% input +% input: % ct: matRad ct struct % stf: matRad steering information struct % pln: matRad plan meta information struct % cst: matRad cst struct % visBool: binary switch to enable visualization -% output +% output: % dij: matRad dij struct % % References @@ -18,7 +18,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2018 the matRad development team. +% Copyright 2018-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -59,4 +59,4 @@ % call the calcDose from engine dij = matRad_calcDoseInfluence(ct,cst,stf,pln); -end \ No newline at end of file +end diff --git a/matRad/doseCalc/matRad_calcSigmaIni.m b/matRad/doseCalc/matRad_calcSigmaIni.m index 0b5c68828..d90811651 100644 --- a/matRad/doseCalc/matRad_calcSigmaIni.m +++ b/matRad/doseCalc/matRad_calcSigmaIni.m @@ -1,17 +1,16 @@ function [sigmaIni] = matRad_calcSigmaIni(baseData,rays,SSD) -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % This function evaluates simultaneously the initial sigma of the beam for % one or more energies % -% call +% call: % sigmaIni = matRad_calcSigmaIni(machine.data,stf(i).ray,stf(i).ray(j).SSD); % -% input +% input: % baseData: 'machine.data' file % rays: 'stf.ray' file % SSD: source-surface difference % -% output +% output: % sigmaIni: initial sigma of the ray at certain energy (or % energies). The data is given in 1xP dimensions, % where 'P' represents the number of different @@ -24,7 +23,7 @@ % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2017 the matRad development team. +% Copyright 2017-2026 the matRad development team. % % This file is not part of the offical matRad release % diff --git a/matRad/doseCalc/matRad_calcSigmaRashi.m b/matRad/doseCalc/matRad_calcSigmaRashi.m index 26676ff88..d15df226c 100644 --- a/matRad/doseCalc/matRad_calcSigmaRashi.m +++ b/matRad/doseCalc/matRad_calcSigmaRashi.m @@ -2,15 +2,15 @@ % calculation of additional beam broadening due to the use of range shifters % (only for protons) % -% call +% call: % sigmaRashi = matRad_calcSigmaRashi(bdEntry,rangeShifter,SSD) % -% input +% input: % bdEntry: base data entry for energy % rangeShifter: structure defining range shifter geometry % SSD: source to surface distance % -% output +% output: % sigmaRashi: sigma of range shifter (to be added ^2) in mm % % References @@ -19,7 +19,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/doseCalc/matRad_calculateProbabilisticQuantities.m b/matRad/doseCalc/matRad_calculateProbabilisticQuantities.m index d39e6aee8..ef5bccb91 100644 --- a/matRad/doseCalc/matRad_calculateProbabilisticQuantities.m +++ b/matRad/doseCalc/matRad_calculateProbabilisticQuantities.m @@ -3,10 +3,10 @@ % dose influence and variance omega matrices for probabilistic % optimization % -% call +% call: % [dij,cst] = matRad_calculateProbabilisticQuantities(dij,cst,pln) % -% input +% input: % dij: matRad dij struct % cst: matRad cst struct (in dose grid resolution) % pln: matRad pln struct @@ -14,7 +14,7 @@ % - 'all' : include 4D scen in statistic % - 'phase' : create statistics per phase (default) % -% output +% output: % dij: dij with added probabilistic quantities % % References @@ -22,7 +22,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2020 the matRad development team. +% Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/doseCalc/matRad_computeSSD.m b/matRad/doseCalc/matRad_computeSSD.m index 6b07721bf..704eb4188 100644 --- a/matRad/doseCalc/matRad_computeSSD.m +++ b/matRad/doseCalc/matRad_computeSSD.m @@ -1,11 +1,11 @@ function stf = matRad_computeSSD(stf,ct,varargin) % matRad SSD calculation % -% call +% call: % stf = matRad_computeSSD(stf,ct) % stf = matRad_computeSSD(stf,ct,Name,Value) % -% input +% input: % ct: ct cube % stf: matRad steering information struct % @@ -15,7 +15,7 @@ % % densityThreshold: value determining the skin threshold. % -% output +% output: % stf: matRad steering information struct % % References @@ -23,7 +23,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2017 the matRad development team. +% Copyright 2017-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -54,17 +54,14 @@ if strcmp(mode,'first') + rayTracer = matRad_RayTracerSiddon(ct.cube(1),ct);%Is this correct for multiple scenarios? + for i = 1:size(stf,2) SSD = cell(1,stf(i).numOfRays); for j = 1:stf(i).numOfRays - cubeIsoCenter = matRad_world2cubeCoords(stf(i).isoCenter,ct); + [alpha,~,rho,d12,~] = rayTracer.traceRay(stf(i).isoCenter,stf(i).sourcePoint,stf(i).ray(j).targetPoint); - [alpha,~,rho,d12,~] = matRad_siddonRayTracer(cubeIsoCenter, ... - ct.resolution, ... - stf(i).sourcePoint, ... - stf(i).ray(j).targetPoint, ... - {ct.cube{1}}); %Is this correct for multiple scenarios? ixSSD = find(rho{1} > densityThreshold,1,'first'); if boolShowWarning @@ -115,4 +112,4 @@ matRad_cfg.dispError('Error in SSD calculation: Could not fix SSD calculation by using closest neighbouring ray.'); end -end \ No newline at end of file +end diff --git a/matRad/doseCalc/matRad_interpRadDepth.m b/matRad/doseCalc/matRad_interpRadDepth.m index eba1b23c9..f9e566fc7 100644 --- a/matRad/doseCalc/matRad_interpRadDepth.m +++ b/matRad/doseCalc/matRad_interpRadDepth.m @@ -1,11 +1,10 @@ function radDepthVcoarse = matRad_interpRadDepth(ct,V,Vcoarse,vXgrid,vYgrid,vZgrid,radDepthV) -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % down/up sampling the radiological depth dose cubes % -% call +% call: % radDepthVcoarse = matRad_interpRadDepth(ct,V,Vcoarse,vXgrid,vYgrid,vZgrid,radDepthV) % -% input +% input: % ct: matRad ct structure % V: linear voxel indices of the cst % Vcoarse: linear voxel indices of the down sampled grid resolution @@ -14,7 +13,7 @@ % vZgrid: query points of now location in z dimension % radDepthV: radiological depth of radDepthIx % -% output +% output: % radDepthVcoarse: interpolated radiological depth of radDepthIx % % References @@ -22,7 +21,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2018 the matRad development team. +% Copyright 2018-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/doseCalc/matRad_projectOnComponents.m b/matRad/doseCalc/matRad_projectOnComponents.m index 97388c837..730172f39 100644 --- a/matRad/doseCalc/matRad_projectOnComponents.m +++ b/matRad/doseCalc/matRad_projectOnComponents.m @@ -1,14 +1,13 @@ function [projCoord,idx,targetPoint, sourcePoint] = matRad_projectOnComponents(initIx,dim,sourcePoint_bev,targetPoint_bev,isoCenter, res, Dx, Dz, rotMat) -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % This function projects a point on a certain ray and returns both the index % of the projected point in the reference system of the ct cube and its % coordinates % -% call +% call: % [projCoord,idx,targetPoint, sourcePoint] = % matRad_projectOnComponents(initIx,dim,sourcePoint_bev,targetPoint_bev,isoCenter, res, Dx, Dz, rotMat) % -% input +% input: % initIx: initial indices of the points % cubeDim: dimension of the ct cube (i.e. ct.cubeDim) % sourcePoint_bev: source point of the ray in bev @@ -18,7 +17,7 @@ % Dx: displacement on x axis % Dz: displacement on z axis % -% output +% output: % idx: projected indeces % projCoord: projected coordinates % @@ -29,7 +28,7 @@ % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2017 the matRad development team. +% Copyright 2017-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/geometry/matRad_addMargin.m b/matRad/geometry/matRad_addMargin.m index b2ee6b374..4c4895373 100644 --- a/matRad/geometry/matRad_addMargin.m +++ b/matRad/geometry/matRad_addMargin.m @@ -1,10 +1,10 @@ function mVOIEnlarged = matRad_addMargin(mVOI,cst,vResolution,vMargin,bDiaElem) % matRad add margin function % -% call +% call: % mVOIEnlarged = matRad_addMargin(mVOI,cst,vResolution,vMargin,bDiaElem) % -% input +% input: % mVOI: image stack in dimensions of X x Y x Z holding ones for % object and zeros otherwise % cst: matRad cst struct @@ -12,7 +12,7 @@ % vMargin: margin in mm % bDiaElem if true 26-connectivity is used otherwise 6-connectivity % -% output +% output: % mVOIEnlarged: enlarged VOI % % References @@ -20,7 +20,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/geometry/matRad_cubeCoords2worldCoords.m b/matRad/geometry/matRad_cubeCoords2worldCoords.m index d39743a84..c65672672 100644 --- a/matRad/geometry/matRad_cubeCoords2worldCoords.m +++ b/matRad/geometry/matRad_cubeCoords2worldCoords.m @@ -1,23 +1,23 @@ function coord = matRad_cubeCoords2worldCoords(cCoord, gridStruct, allowOutside) % matRad function to convert cube coordinates to world coordinates % -% call +% call: % coord = matRad_worldToCubeCoordinates(vCoord, gridStruct) % -% inputs +% input: % cCoord: cube coordinates [vx vy vz] (Nx3 in mm) % gridStruct: matRad ct struct or dij.doseGrid/ctGrid struct % allowOutside: indices not within the image bounds will be calculated % optional, default is true % -% outputs +% output: % coord: worldCoordinates [x y z] (Nx3 in mm) % References % - % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2024 the matRad development team. +% Copyright 2024-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -52,4 +52,4 @@ % find the closest world coord index in gridStruct.x/y/z % calc absolute differences and locate smallest difference -coord = cCoord + translation; \ No newline at end of file +coord = cCoord + translation; diff --git a/matRad/geometry/matRad_cubeIndex2worldCoords.m b/matRad/geometry/matRad_cubeIndex2worldCoords.m index 99bb7432a..d5dfabfb0 100644 --- a/matRad/geometry/matRad_cubeIndex2worldCoords.m +++ b/matRad/geometry/matRad_cubeIndex2worldCoords.m @@ -1,51 +1,51 @@ function coord = matRad_cubeIndex2worldCoords(cubeIx, gridStruct) % matRad function to convert cube indices to world coordinates -% -% call +% +% call: % coord = matRad_cubeIndex2worldCoords(vCoord, gridStruct) -% -% inputs +% +% input: % cCoord: cube indices [i j k] (Nx3) or [linIx] (Nx1) % gridStruct: matRad ct struct or dij.doseGrid/ctGrid struct % allowOutside: indices not within the image bounds will be calculated % optional, default is true % -% outputs +% output: % coord: worldCoordinates [x y z] (Nx3 in mm) % References % - % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2024 the matRad development team. -% -% This file is part of the matRad project. It is subject to the license -% terms in the LICENSE file found in the top-level directory of this -% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part -% of the matRad project, including this file, may be copied, modified, -% propagated, or distributed except according to the terms contained in the +% Copyright 2024-2026 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the % LICENSE file. % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%Sanitize grid / cube dimensions -if isfield(gridStruct,'cubeDim') +% Sanitize grid / cube dimensions +if isfield(gridStruct, 'cubeDim') gridStruct.dimensions = gridStruct.cubeDim; end -%Check if we have linear indices -if size(cubeIx,2) == 1 - [cubeIx(:,1),cubeIx(:,2),cubeIx(:,3)] = ind2sub(gridStruct.dimensions,cubeIx); -end - -%Check if we have the right dimensions -if size(cubeIx,2) ~= 3 +% Check if we have linear indices +if size(cubeIx, 2) == 1 + [s1, s2, s3] = ind2sub(gridStruct.dimensions, cubeIx); + cubeIx = [s2, s1, s3]; % ijk/xyz -> jik +elseif size(cubeIx, 2) == 3 % if we have subscript coordinates, permute ijk/xyz -> jik + cubeIx = cubeIx(:, [2 1 3]); +else % we have the wrong dimensions matRad_cfg = MatRad_Config.instance(); matRad_cfg.dispError('voxel coordinates must be Nx3 (subscript indices) or Nx1 (linear indices)!'); end -%First create cube coordinates -coord = cubeIx(:,[2 1 3]) .* [gridStruct.resolution.x gridStruct.resolution.y gridStruct.resolution.z]; -coord = matRad_cubeCoords2worldCoords(coord,gridStruct,false); - -end \ No newline at end of file +% First create cube coordinates +coord = double(cubeIx) .* [gridStruct.resolution.x gridStruct.resolution.y gridStruct.resolution.z]; +coord = matRad_cubeCoords2worldCoords(coord, gridStruct, false); + +end diff --git a/matRad/geometry/matRad_getIsoCenter.m b/matRad/geometry/matRad_getIsoCenter.m index 16a0670b7..6ff4471b0 100644 --- a/matRad/geometry/matRad_getIsoCenter.m +++ b/matRad/geometry/matRad_getIsoCenter.m @@ -3,15 +3,15 @@ % of all volumes of interest that are labeled as target within the cst % struct % -% call +% call: % isoCenter = matRad_getIsoCenter(cst,ct,visBool) % -% input +% input: % cst: matRad cst struct % ct: ct cube % visBool: toggle on/off visualization (optional) % -% output +% output: % isoCenter: isocenter in [mm] % % References @@ -19,7 +19,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -69,7 +69,7 @@ % Calculated isocenter. -isoCenter = mean(coord); +isoCenter = mean(coord, 1); % Visualization diff --git a/matRad/geometry/matRad_getRotationMatrix.m b/matRad/geometry/matRad_getRotationMatrix.m index e47a0ad68..77c3b72f9 100644 --- a/matRad/geometry/matRad_getRotationMatrix.m +++ b/matRad/geometry/matRad_getRotationMatrix.m @@ -7,10 +7,10 @@ % matrix is required. % % -% call +% call: % rotMat = matRad_getRotationMatrix(gantryAngle,couchAngle,type,system) % -% input +% input: % gantryAngle: beam/gantry angle % couchAngle: couch angle % @@ -18,7 +18,7 @@ % requested for. So far, only the default option 'LPS' is % supported (right handed system). % -% output +% output: % rotMat: 3x3 matrix that performs an active rotation around the % patient system origin via rotMat * x % @@ -27,7 +27,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/geometry/matRad_getWorldAxes.m b/matRad/geometry/matRad_getWorldAxes.m index 062dd5886..661278441 100644 --- a/matRad/geometry/matRad_getWorldAxes.m +++ b/matRad/geometry/matRad_getWorldAxes.m @@ -1,7 +1,7 @@ function gridStruct = matRad_getWorldAxes(gridStruct) % matRad function to compute and store world coordinates into ct.x % -% call +% call: % gridStruct = matRad_getWorldAxes(gridStruct) % % gridStruct: can be ct, dij.doseGrid,dij.ctGrid @@ -11,7 +11,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/geometry/matRad_resizeCstToGrid.m b/matRad/geometry/matRad_resizeCstToGrid.m index 5206ca2fc..f8c187f0b 100644 --- a/matRad/geometry/matRad_resizeCstToGrid.m +++ b/matRad/geometry/matRad_resizeCstToGrid.m @@ -1,10 +1,10 @@ function cst = matRad_resizeCstToGrid(cst,vXgridOld,vYgridOld,vZgridOld,vXgridNew,vYgridNew,vZgridNew) % matRad function to resize the cst to a given resolution % -% call +% call: % cst = matRad_resizeCstToGrid(cst,vXgridOld,vYgridOld,vZgridOld,vXgridNew,vYgridNew,vZgridNew) % -% input +% input: % cst: matRad cst struct % vXgridOld: vector containing old spatial grid points in x [mm] % vYgridOld: vector containing old spatial grid points in y [mm] @@ -13,7 +13,7 @@ % vYgridNew: vector containing new spatial grid points in y [mm] % vZgridNew: vector containing new spatial grid points in z [mm] % -% output +% output: % cst: updated matRad cst struct containing new linear voxel indices % % References @@ -21,7 +21,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2018 the matRad development team. +% Copyright 2018-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/geometry/matRad_selectVoxelsFromCst.m b/matRad/geometry/matRad_selectVoxelsFromCst.m index fb8c57d51..5bf50af46 100644 --- a/matRad/geometry/matRad_selectVoxelsFromCst.m +++ b/matRad/geometry/matRad_selectVoxelsFromCst.m @@ -2,29 +2,23 @@ % matRad function to get mask of the voxels (on dose grid) that are % included in cst structures specified by selectionMode. % -% call +% call: % includeMask = matRad_getVoxelsOnCstStructs(cst,doseGrid,VdoseGrid,selectionMode) % -% input +% input: % cstOnDoseGrid: cstOnDoseGrid (voxel indexes included in cst{:,4} are referred to a cube of dimensions doseGrid.dimensions) % doseGrid: doseGrid struct containing field doseGrid.dimensions -% selectionMode: define wich method to apply to select the cst -% structures to include. Choices are: -% all all voxels will be included -% targetOnly only includes the voxels in structures labeld as target -% oarsOnly only includes the voxels in structures labeld as oars -% objectivesOnly only includes the voxels in structures with at least one objective/constraint -% robustnessOnly only includes the voxels in structures with robustness objectives/constraints -% [indexes] only includes the voxels in structures specified by index array (i.e. [1,2,3] includes first three structures) -% +% selectionMode: define which method to apply to select cst structures. +% Choices: all, targetOnly, oarsOnly, objectivesOnly, +% robustnessOnly, [indexes] % -% output +% output: % % includeMask: logical array #voxels in dose grid % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2023 the matRad development team. +% Copyright 2023-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -130,4 +124,4 @@ end %for loop over cst end -end \ No newline at end of file +end diff --git a/matRad/geometry/matRad_setOverlapPriorities.m b/matRad/geometry/matRad_setOverlapPriorities.m index c1d3c7cea..4d136afa4 100644 --- a/matRad/geometry/matRad_setOverlapPriorities.m +++ b/matRad/geometry/matRad_setOverlapPriorities.m @@ -4,15 +4,15 @@ % volumes of interest you need to inform matRad to which volume(s) the % intersection voxels belong. % -% call +% call: % cst = matRad_considerOverlap(cst) % [cst, overlapPriorityCube] = matRad_setOverlapPriorities(cst,ctDim) % -% input +% input: % cst: cst file % ctDim: (optional) dimension of the ct for overlap cube claculation % -% output +% output: % cst: updated cst file considering overlap priorities % overlapPriorityCube:(optional) cube visualizing the overlap priority % @@ -21,7 +21,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/geometry/matRad_world2cubeCoords.m b/matRad/geometry/matRad_world2cubeCoords.m index 02bcf04d1..78016ed4c 100644 --- a/matRad/geometry/matRad_world2cubeCoords.m +++ b/matRad/geometry/matRad_world2cubeCoords.m @@ -1,23 +1,24 @@ function coord = matRad_world2cubeCoords(wCoord, gridStruct , allowOutside) % matRad function to convert world coordinates to cube coordinates % -% call +% call: % coord = world2cubeCoords(wCoord, ct) % -% +% input: % wCoord: world coordinates array Nx3 (x,y,z) [mm] % gridStruct: can be matRad ct, dij.doseGrid, or the ctGrid % required fields x,y,x,dimensions,resolution % allowOutside: indices not within the image bounds will be calculated % optional, default is true % -% coord : cube coordinates (x,y,z) - Nx3 in mm +% output: +% coord: cube coordinates (x,y,z) - Nx3 in mm % References % - % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2024 the matRad development team. +% Copyright 2024-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/geometry/matRad_world2cubeIndex.m b/matRad/geometry/matRad_world2cubeIndex.m index 3c047b1bf..810649b7e 100644 --- a/matRad/geometry/matRad_world2cubeIndex.m +++ b/matRad/geometry/matRad_world2cubeIndex.m @@ -1,22 +1,23 @@ function indices = matRad_world2cubeIndex(wCoord, gridStruct,allowOutside) % matRad function to convert world coordinates to cube indices % -% call +% call: % coord = world2cubeCoords(wCoord, ct) % -% +% input: % wCoord: world coordinates array Nx3 (x,y,z) [mm] % gridStruct: can be matRad ct, dij.doseGrid, or the ctGrid % required fields x,y,x,dimensions,resolution % allowOutside: If coordinates outside are allowed. False default. % +% output: % index: cube index (i,j,k) honoring Matlab permuatation of i,j % References % - % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2024 the matRad development team. +% Copyright 2024-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/gpu/matRad_getCstFromGPU.m b/matRad/gpu/matRad_getCstFromGPU.m new file mode 100644 index 000000000..9868c50bb --- /dev/null +++ b/matRad/gpu/matRad_getCstFromGPU.m @@ -0,0 +1,47 @@ +function cst = matRad_getCstFromGPU(cst, indexType) +% matRad_getCstFromGPU transfers cst dose influence data from GPU to host. +% Gathers all cell arrays stored in column 4 of the cst (dose influence +% data) from GPU memory back to host memory. Optionally casts the data to +% the requested numeric precision after gathering. +% +% call: +% cst = matRad_getCstFromGPU(cst) +% cst = matRad_getCstFromGPU(cst, precision) +% +% input: +% cst matRad cst cell array with GPU arrays in column 4 +% indexType (optional) target index type as integer, e.g. +% 'int32' or 'uint64'. If empty or omitted, it will be cast +% to Matlab's standard double. +% +% output: +% cst matRad cst cell array with host arrays in column 4 +% +% References +% - +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2026 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +if nargin < 2 + indexType = 'double'; +end + +for i = 1:size(cst, 1) + cst{i, 4} = cellfun(@matRad_gatherCompat, cst{i, 4}, 'UniformOutput', false); + if ~isempty(indexType) + cst{i, 4} = cellfun(@(x) cast(x, indexType), cst{i, 4}, 'UniformOutput', false); + end +end + +end diff --git a/matRad/gpu/matRad_getCtFromGPU.m b/matRad/gpu/matRad_getCtFromGPU.m new file mode 100644 index 000000000..6c77f4b94 --- /dev/null +++ b/matRad/gpu/matRad_getCtFromGPU.m @@ -0,0 +1,54 @@ +function ct = matRad_getCtFromGPU(ct, precision) +% matRad_getCtFromGPU transfers ct cube data from GPU to host. +% Gathers ct.cubeHU and ct.cube cell arrays from GPU memory back to host +% memory. Optionally casts the data to the requested numeric precision +% after gathering. +% +% call: +% ct = matRad_getCtFromGPU(ct) +% ct = matRad_getCtFromGPU(ct, precision) +% +% input: +% ct matRad ct struct with GPU arrays in cubeHU and/or cube +% precision (optional) target numeric precision as string, e.g. +% 'single' or 'double'. If empty or omitted, no cast is +% performed. +% +% output: +% ct matRad ct struct with host arrays in cubeHU and/or cube +% +% References +% - +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2026 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +if nargin < 2 + precision = []; +end + +if isfield(ct, 'cubeHU') + ct.cubeHU = cellfun(@matRad_gatherCompat, ct.cubeHU, 'UniformOutput', false); + if ~isempty(precision) + ct.cubeHU = cellfun(@(x) cast(x, precision), ct.cubeHU, 'UniformOutput', false); + end +end + +if isfield(ct, 'cube') + ct.cube = cellfun(@matRad_gatherCompat, ct.cube, 'UniformOutput', false); + if ~isempty(precision) + ct.cube = cellfun(@(x) cast(x, precision), ct.cube, 'UniformOutput', false); + end +end + +end diff --git a/matRad/gpu/matRad_getDijFromGPU.m b/matRad/gpu/matRad_getDijFromGPU.m new file mode 100644 index 000000000..82590ab8a --- /dev/null +++ b/matRad/gpu/matRad_getDijFromGPU.m @@ -0,0 +1,52 @@ +function dij = matRad_getDijFromGPU(dij, precision) +% matRad_getDijFromGPU transfers dij dose influence matrix data from GPU to host. +% Gathers the dose influence quantities (physicalDose, mAlphaDose, +% mSqrtBetaDose, mLETDose) from GPU memory back to host memory. +% Optionally casts the data to the requested numeric precision after +% gathering. +% +% call: +% dij = matRad_getDijFromGPU(dij) +% dij = matRad_getDijFromGPU(dij, precision) +% +% input: +% dij matRad dij struct with GPU arrays in dose influence fields +% precision (optional) target numeric precision as string, e.g. +% 'single' or 'double'. If empty or omitted, no cast is +% performed. +% +% output: +% dij matRad dij struct with host arrays in dose influence fields +% +% References +% - +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2026 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +if nargin < 2 + precision = []; +end + +quantities = {'physicalDose', 'mAlphaDose', 'mSqrtBetaDose', 'mLETDose'}; + +for i = 1:numel(quantities) + if isfield(dij, quantities{i}) + dij.(quantities{i}) = cellfun(@matRad_gatherCompat, dij.(quantities{i}), 'UniformOutput', false); + if ~isempty(precision) + dij.(quantities{i}) = cellfun(@(x) cast(x, precision), dij.(quantities{i}), 'UniformOutput', false); + end + end +end + +end diff --git a/matRad/gpu/matRad_moveCstToGPU.m b/matRad/gpu/matRad_moveCstToGPU.m new file mode 100644 index 000000000..4c799fcd3 --- /dev/null +++ b/matRad/gpu/matRad_moveCstToGPU.m @@ -0,0 +1,49 @@ +function cst = matRad_moveCstToGPU(cst, indexType) +% matRad_moveCstToGPU transfers cst dose influence data to the GPU. +% Moves all cell arrays stored in column 4 of the cst (dose influence +% data) to GPU memory. Optionally casts the data to the requested numeric +% precision before uploading. +% +% call: +% cst = matRad_moveCstToGPU(cst) +% cst = matRad_moveCstToGPU(cst, precision) +% +% input: +% cst matRad cst cell array with host arrays in column 4 +% indexType (optional) target index type as integer, e.g. +% 'int32' or 'uint64'. If empty or omitted, no cast is +% performed. +% +% output: +% cst matRad cst cell array with GPU arrays in column 4 +% +% References +% - +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2026 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +if nargin < 2 + indexType = []; +end + +for i = 1:size(cst, 1) + + if ~isempty(indexType) + cst{i, 4} = cellfun(@(x) cast(x, indexType), cst{i, 4}, 'UniformOutput', false); + end + cst{i, 4} = cellfun(@gpuArray, cst{i, 4}, 'UniformOutput', false); + +end + +end diff --git a/matRad/gpu/matRad_moveCtToGPU.m b/matRad/gpu/matRad_moveCtToGPU.m new file mode 100644 index 000000000..43b77629b --- /dev/null +++ b/matRad/gpu/matRad_moveCtToGPU.m @@ -0,0 +1,56 @@ +function ct = matRad_moveCtToGPU(ct, precision) +% matRad_moveCtToGPU transfers ct cube data to the GPU. +% Moves ct.cubeHU and ct.cube cell arrays to GPU memory. Optionally +% casts the data to the requested numeric precision before uploading. +% +% call: +% ct = matRad_moveCtToGPU(ct) +% ct = matRad_moveCtToGPU(ct, precision) +% +% input: +% ct matRad ct struct with host arrays in cubeHU and/or cube +% precision (optional) target numeric precision as string, e.g. +% 'single' or 'double'. If empty or omitted, no cast is +% performed. +% +% output: +% ct matRad ct struct with GPU arrays in cubeHU and/or cube +% +% References +% - +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2026 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +if nargin < 2 + precision = []; +end + +if ~isempty(precision) + if isfield(ct, 'cubeHU') + ct.cubeHU = cellfun(@(x) cast(x, precision), ct.cubeHU, 'UniformOutput', false); + end + if isfield(ct, 'cube') + ct.cube = cellfun(@(x) cast(x, precision), ct.cube, 'UniformOutput', false); + end +end + +if isfield(ct, 'cubeHU') + ct.cubeHU = cellfun(@gpuArray, ct.cubeHU, 'UniformOutput', false); +end + +if isfield(ct, 'cube') + ct.cube = cellfun(@gpuArray, ct.cube, 'UniformOutput', false); +end + +end diff --git a/matRad/gpu/matRad_moveDijToGPU.m b/matRad/gpu/matRad_moveDijToGPU.m new file mode 100644 index 000000000..f20fd7c59 --- /dev/null +++ b/matRad/gpu/matRad_moveDijToGPU.m @@ -0,0 +1,52 @@ +function dij = matRad_moveDijToGPU(dij, precision) +% matRad_moveDijToGPU transfers dij dose influence matrix data to the GPU. +% Moves the dose influence quantities (physicalDose, mAlphaDose, +% mSqrtBetaDose, mLETDose) to GPU memory. Optionally casts the data to +% the requested numeric precision before uploading. +% +% call: +% dij = matRad_moveDijToGPU(dij) +% dij = matRad_moveDijToGPU(dij, precision) +% +% input: +% dij matRad dij struct with host arrays in dose influence fields +% precision (optional) target numeric precision as string, e.g. +% 'single' or 'double'. If empty or omitted, no cast is +% performed. +% +% output: +% dij matRad dij struct with GPU arrays in dose influence fields +% +% References +% - +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2026 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +if nargin < 2 + precision = []; +end + +quantities = {'physicalDose', 'mAlphaDose', 'mSqrtBetaDose', 'mLETDose'}; + +for i = 1:numel(quantities) + if isfield(dij, quantities{i}) + if ~isempty(precision) + dij.(quantities{i}) = cellfun(@(x) gpuArray(cast(x, precision)), dij.(quantities{i}), 'UniformOutput', false); + else + dij.(quantities{i}) = cellfun(@(x) gpuArray(x), dij.(quantities{i}), 'UniformOutput', false); + end + end +end + +end diff --git a/matRad/gui/matRad_InfoWidget_uiwrapper.m b/matRad/gui/matRad_InfoWidget_uiwrapper.m index 6da0f18dc..7f7b85b26 100644 --- a/matRad/gui/matRad_InfoWidget_uiwrapper.m +++ b/matRad/gui/matRad_InfoWidget_uiwrapper.m @@ -4,7 +4,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2019 the matRad development team. +% Copyright 2019-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/gui/matRad_MainGUI.m b/matRad/gui/matRad_MainGUI.m index 9111e67f8..dcaf3e6c2 100644 --- a/matRad/gui/matRad_MainGUI.m +++ b/matRad/gui/matRad_MainGUI.m @@ -6,7 +6,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2019 the matRad development team. + % Copyright 2019-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/gui/matRad_Widget.m b/matRad/gui/matRad_Widget.m index fe9a63bff..57b405451 100644 --- a/matRad/gui/matRad_Widget.m +++ b/matRad/gui/matRad_Widget.m @@ -8,7 +8,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2020 the matRad development team. + % Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/gui/matRad_WorkspaceChangedEvent.m b/matRad/gui/matRad_WorkspaceChangedEvent.m index 1fa7ba7c2..164812fd3 100644 --- a/matRad/gui/matRad_WorkspaceChangedEvent.m +++ b/matRad/gui/matRad_WorkspaceChangedEvent.m @@ -4,7 +4,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2019 the matRad development team. +% Copyright 2019-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/gui/matRad_WorkspaceChangedEventData.m b/matRad/gui/matRad_WorkspaceChangedEventData.m index 700c5a57d..9f36e448d 100644 --- a/matRad/gui/matRad_WorkspaceChangedEventData.m +++ b/matRad/gui/matRad_WorkspaceChangedEventData.m @@ -5,7 +5,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2019 the matRad development team. +% Copyright 2019-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/gui/matRad_addListenerOctave.m b/matRad/gui/matRad_addListenerOctave.m index 4793d99d8..95aa68969 100644 --- a/matRad/gui/matRad_addListenerOctave.m +++ b/matRad/gui/matRad_addListenerOctave.m @@ -4,7 +4,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2019 the matRad development team. +% Copyright 2019-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/gui/matRad_applyThemeToDlg.m b/matRad/gui/matRad_applyThemeToDlg.m index ffdf0d674..2721abd94 100644 --- a/matRad/gui/matRad_applyThemeToDlg.m +++ b/matRad/gui/matRad_applyThemeToDlg.m @@ -4,7 +4,7 @@ function matRad_applyThemeToDlg(hDlgBox) % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2024 the matRad development team. +% Copyright 2024-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -32,4 +32,4 @@ function matRad_applyThemeToDlg(hDlgBox) set(okBtn,'BackgroundColor',matRad_cfg.gui.elementColor,'ForegroundColor',matRad_cfg.gui.textColor); catch matRad_cfg.dispWarning('Theme could not be applied to dialog!'); -end \ No newline at end of file +end diff --git a/matRad/gui/matRad_applyThemeToWaitbar.m b/matRad/gui/matRad_applyThemeToWaitbar.m index 3c43266de..7ef3fe1e2 100644 --- a/matRad/gui/matRad_applyThemeToWaitbar.m +++ b/matRad/gui/matRad_applyThemeToWaitbar.m @@ -3,7 +3,7 @@ function matRad_applyThemeToWaitbar(hWaitbar) % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2024 the matRad development team. +% Copyright 2024-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/gui/matRad_getLogo.m b/matRad/gui/matRad_getLogo.m index fc7830982..28ec5c72f 100644 --- a/matRad/gui/matRad_getLogo.m +++ b/matRad/gui/matRad_getLogo.m @@ -1,7 +1,7 @@ function [im,alpha] = matRad_getLogo(scale) % matRad function to obtain matRad logo image data adhering to theme % -% call +% call: % matRad_getLogoDKFZ() % matRad_getLogoDKFZ(scale) % @@ -18,7 +18,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2024 the matRad development team. +% Copyright 2024-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/gui/matRad_getLogoDKFZ.m b/matRad/gui/matRad_getLogoDKFZ.m index e3483cd03..20f8597e3 100644 --- a/matRad/gui/matRad_getLogoDKFZ.m +++ b/matRad/gui/matRad_getLogoDKFZ.m @@ -1,7 +1,7 @@ function [im,alpha] = matRad_getLogoDKFZ(scale) % matRad function to obtain DKFZ logo image data adhering to theme % -% call +% call: % matRad_getLogoDKFZ() % matRad_getLogoDKFZ(scale) % @@ -18,7 +18,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2024 the matRad development team. +% Copyright 2024-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/gui/matRad_isUifigure.m b/matRad/gui/matRad_isUifigure.m index 41540bb77..a15a17f3b 100644 --- a/matRad/gui/matRad_isUifigure.m +++ b/matRad/gui/matRad_isUifigure.m @@ -3,7 +3,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2024 the matRad development team. +% Copyright 2024-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/gui/matRad_notifyOctave.m b/matRad/gui/matRad_notifyOctave.m index eb58f492c..9841a1d64 100644 --- a/matRad/gui/matRad_notifyOctave.m +++ b/matRad/gui/matRad_notifyOctave.m @@ -4,7 +4,7 @@ function matRad_notifyOctave(hObject,eventName,evt) % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2019 the matRad development team. +% Copyright 2019-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/gui/widgets/matRad_3DWidget.m b/matRad/gui/widgets/matRad_3DWidget.m index d520d4a07..96402f8e0 100644 --- a/matRad/gui/widgets/matRad_3DWidget.m +++ b/matRad/gui/widgets/matRad_3DWidget.m @@ -8,7 +8,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2020 the matRad development team. + % Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -213,4 +213,4 @@ function plot3D(this) end end -end \ No newline at end of file +end diff --git a/matRad/gui/widgets/matRad_DVHStatsWidget.m b/matRad/gui/widgets/matRad_DVHStatsWidget.m index 0f6646dab..ed0abe241 100644 --- a/matRad/gui/widgets/matRad_DVHStatsWidget.m +++ b/matRad/gui/widgets/matRad_DVHStatsWidget.m @@ -9,7 +9,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2020 the matRad development team. + % Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/gui/widgets/matRad_DVHWidget.m b/matRad/gui/widgets/matRad_DVHWidget.m index dc8fe26e5..4dc468150 100644 --- a/matRad/gui/widgets/matRad_DVHWidget.m +++ b/matRad/gui/widgets/matRad_DVHWidget.m @@ -7,7 +7,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2020 the matRad development team. + % Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -141,4 +141,4 @@ function showDVH(this) end end -end \ No newline at end of file +end diff --git a/matRad/gui/widgets/matRad_GammaWidget.m b/matRad/gui/widgets/matRad_GammaWidget.m index f352636f7..16618c429 100644 --- a/matRad/gui/widgets/matRad_GammaWidget.m +++ b/matRad/gui/widgets/matRad_GammaWidget.m @@ -7,7 +7,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2020 the matRad development team. + % Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/gui/widgets/matRad_InfoWidget.m b/matRad/gui/widgets/matRad_InfoWidget.m index 18a9bc30b..6121726af 100644 --- a/matRad/gui/widgets/matRad_InfoWidget.m +++ b/matRad/gui/widgets/matRad_InfoWidget.m @@ -8,7 +8,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2020 the matRad development team. + % Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/gui/widgets/matRad_LogoWidget.m b/matRad/gui/widgets/matRad_LogoWidget.m index 9ed8aa5de..13c127668 100644 --- a/matRad/gui/widgets/matRad_LogoWidget.m +++ b/matRad/gui/widgets/matRad_LogoWidget.m @@ -7,7 +7,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2020 the matRad development team. + % Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/gui/widgets/matRad_OptimizationWidget.m b/matRad/gui/widgets/matRad_OptimizationWidget.m index baee5555a..1100a3840 100644 --- a/matRad/gui/widgets/matRad_OptimizationWidget.m +++ b/matRad/gui/widgets/matRad_OptimizationWidget.m @@ -7,7 +7,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2020 the matRad development team. + % Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -350,7 +350,7 @@ 'UserData',{[i,j],classNames(1,:)}, ... 'Callback',@(hObject,eventdata)changeRobustness_Callback(this,hObject,eventdata)); - if isfield(cst{i,6}{j},'robustness') + if (isa(cst{i,6}{j},'matRad_DoseOptimizationFunction') && matRad_ispropCompat(cst{i,6}{j},'robustness')) || isfield(cst{i,6}{j},'robustness') set(h,'Value',find(strcmp(cst{i,6}{j}.robustness,robustObj))); else set(h, 'Value',find(strcmp('none',robustObj))); diff --git a/matRad/gui/widgets/matRad_PlanWidget.m b/matRad/gui/widgets/matRad_PlanWidget.m index e0b3a2a20..b684d8a40 100644 --- a/matRad/gui/widgets/matRad_PlanWidget.m +++ b/matRad/gui/widgets/matRad_PlanWidget.m @@ -7,7 +7,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2020 the matRad development team. + % Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -391,8 +391,8 @@ h36 = uicontrol(... 'Parent',h12,... 'Units','normalized',... - 'String','3D conformal',... - 'TooltipString','Check this if you want to execute 3D conformal planning',... + 'String','3D conformal/SFUD',... + 'TooltipString','Check this if you want to execute 3D conformal or Single Field Uniform Dose (SFUD) planning',... 'Style','radiobutton',... 'Position',pos,... 'BackgroundColor',matRad_cfg.gui.backgroundColor,... @@ -822,11 +822,17 @@ selectedEngineIx = get(handles.popUpMenuDoseEngine,'Value'); selectedEngine = availableEngines(selectedEngineIx); + if matRad_ispropCompat(stfGen,'numOfBeams') + numOfBeams = stfGen.numOfBeams; + else + numOfBeams = 1; + end + if isfield(pln.propStf,'isoCenter') % sanity check of isoCenter - if size(pln.propStf.isoCenter,1) ~= pln.propStf.numOfBeams && size(pln.propStf.isoCenter,1) == 1 - pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * pln.propStf.isoCenter(1,:); - elseif size(pln.propStf.isoCenter,1) ~= pln.propStf.numOfBeams && size(pln.propStf.isoCenter,1) ~= 1 + if size(pln.propStf.isoCenter,1) ~= numOfBeams && size(pln.propStf.isoCenter,1) == 1 + pln.propStf.isoCenter = ones(numOfBeams,1) * pln.propStf.isoCenter(1,:); + elseif size(pln.propStf.isoCenter,1) ~= numOfBeams && size(pln.propStf.isoCenter,1) ~= 1 this.showError('Isocenter in plan file are inconsistent.'); end @@ -984,7 +990,7 @@ function updatePlnInWorkspace(this,hObject,evtData) this.plotPlan = true; end - pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); + numOfBeams = numel(pln.propStf.gantryAngles); isoStr = get(handles.editIsoCenter,'String'); if ~isequal(isoStr,'multiple isoCenter') @@ -1052,7 +1058,7 @@ function updatePlnInWorkspace(this,hObject,evtData) end tmpIsoCenter = matRad_getIsoCenter(evalin('base','cst'),evalin('base','ct')); if ~isequal(tmpIsoCenter,pln.propStf.isoCenter) - pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1)*tmpIsoCenter; + pln.propStf.isoCenter = ones(numOfBeams,1)*tmpIsoCenter; %handles.State = 1; %UpdateState(handles); end @@ -1078,11 +1084,11 @@ function updatePlnInWorkspace(this,hObject,evtData) cst = evalin('base','cst'); if (sum(strcmp('TARGET',cst(:,3))) > 0 && get(handles.checkIsoCenter,'Value')) || ... (sum(strcmp('TARGET',cst(:,3))) > 0 && ~isfield(pln.propStf,'isoCenter')) - pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct); + pln.propStf.isoCenter = ones(numOfBeams,1) * matRad_getIsoCenter(cst,ct); set(handles.checkIsoCenter,'Value',1); else if ~strcmp(get(handles.editIsoCenter,'String'),'multiple isoCenter') - pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * str2num(get(handles.editIsoCenter,'String')); + pln.propStf.isoCenter = ones(numOfBeams,1) * str2num(get(handles.editIsoCenter,'String')); end end catch ME @@ -1156,7 +1162,7 @@ function switchEnables(this) set(handles.btnRunSequencing,'Enable','off'); set(handles.btnRunDAO,'Enable','off'); - set(handles.radiobutton3Dconf,'Enable','off'); + set(handles.radiobutton3Dconf,'Enable','on'); set(handles.txtSequencing,'Enable','off'); set(handles.editSequencingLevel,'Enable','off'); set(handles.popUpMenuSequencer,'Enable','off'); @@ -1171,7 +1177,7 @@ function switchEnables(this) set(handles.btnRunSequencing,'Enable','off'); set(handles.btnRunDAO,'Enable','off'); - set(handles.radiobutton3Dconf,'Enable','off'); + set(handles.radiobutton3Dconf,'Enable','on'); set(handles.txtSequencing,'Enable','off'); set(handles.editSequencingLevel,'Enable','off'); set(handles.popUpMenuSequencer,'Enable','off'); @@ -1344,10 +1350,11 @@ function editIsocenter_Callback(this, hObject, eventdata) % editIsoCenter textbox tmpIsoCenter = str2num(get(handles.editIsoCenter,'String')); + numOfBeams = numel(pln.propStf.gantryAngles); if length(tmpIsoCenter) == 3 if sum(any(unique(pln.propStf.isoCenter,'rows')~=tmpIsoCenter)) - pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1)*tmpIsoCenter; + pln.propStf.isoCenter = ones(numOfBeams,1)*tmpIsoCenter; end else @@ -1628,21 +1635,7 @@ function popMenuMultScen_Callback(this, hObject, eventdata) end function popMenuQuantityOpt_Callback(this, hObject, eventdata) - % handles = this.handles; - % - % pln = evalin('base','pln'); - % contentQuantityOpt = get(handles.popMenuQuantityOpt,'String'); - % NewQuantityOpt = contentQuantityOpt(get(handles.popMenuQuantityOpt,'Value'),:); - % - % % if (strcmp(pln.propOpt.bioOptimization,'LEMIV_effect') && strcmp(NewBioOptimization,'LEMIV_RBExDose')) ||... - % % (strcmp(pln.propOpt.bioOptimization,'LEMIV_RBExDose') && strcmp(NewBioOptimization,'LEMIV_effect')) - % % % do nothing - re-optimization is still possible - % % elseif ((strcmp(pln.propOpt.bioOptimization,'const_RBE') && strcmp(NewBioOptimization,'none')) ||... - % % (strcmp(pln.propOpt.bioOptimization,'none') && strcmp(NewBioOptimization,'const_RBE'))) && isequal(pln.radiationMode,'protons') - % % % do nothing - re-optimization is still possible - % % end - % % - % this.handles = handles; + % Callback for the quantity optimization popup menu. updatePlnInWorkspace(this); end diff --git a/matRad/gui/widgets/matRad_StatisticsWidget.m b/matRad/gui/widgets/matRad_StatisticsWidget.m index 508c2d8ea..297f71281 100644 --- a/matRad/gui/widgets/matRad_StatisticsWidget.m +++ b/matRad/gui/widgets/matRad_StatisticsWidget.m @@ -8,7 +8,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2020 the matRad development team. + % Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -28,7 +28,7 @@ methods - function this = matRad_StatisticsWidget(handleParent) % use (varargin) ? + function this = matRad_StatisticsWidget(handleParent) matRad_cfg = MatRad_Config.instance(); if nargin < 1 @@ -110,4 +110,4 @@ function showStatistics(this) end end -end \ No newline at end of file +end diff --git a/matRad/gui/widgets/matRad_StructureVisibilityWidget.m b/matRad/gui/widgets/matRad_StructureVisibilityWidget.m index 5083129dc..99491eea3 100644 --- a/matRad/gui/widgets/matRad_StructureVisibilityWidget.m +++ b/matRad/gui/widgets/matRad_StructureVisibilityWidget.m @@ -8,7 +8,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2020 the matRad development team. + % Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/gui/widgets/matRad_ViewerOptionsWidget.m b/matRad/gui/widgets/matRad_ViewerOptionsWidget.m index 5a2523dec..6a714f30d 100644 --- a/matRad/gui/widgets/matRad_ViewerOptionsWidget.m +++ b/matRad/gui/widgets/matRad_ViewerOptionsWidget.m @@ -7,7 +7,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2020 the matRad development team. + % Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -639,10 +639,10 @@ function edit_windowWidth_Callback(this, hObject, eventdata) % H109 function sliderOpacity_Callback(this,hObject, eventdata) - % hObject handle to sliderOpacity (see GCBO) - % eventdata reserved - to be defined in a future version of MATLAB - % handles structure with handles and user data (see GUIDATA) - %handles = this.handles; + % Callback for the opacity slider. + % + % hObject: handle to sliderOpacity (see GCBO) + % eventdata: reserved - to be defined in a future version of MATLAB this.viewingWidgetHandle.doseOpacity = get(hObject,'Value'); diff --git a/matRad/gui/widgets/matRad_ViewingWidget.m b/matRad/gui/widgets/matRad_ViewingWidget.m index 2f8920e94..3870a62e5 100644 --- a/matRad/gui/widgets/matRad_ViewingWidget.m +++ b/matRad/gui/widgets/matRad_ViewingWidget.m @@ -1,20 +1,20 @@ classdef matRad_ViewingWidget < matRad_Widget % matRad_ViewingWidget class to generate GUI widget to display plan % dose distributions and ct - % + % % % References % - % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2020 the matRad development team. - % - % This file is part of the matRad project. It is subject to the license - % terms in the LICENSE file found in the top-level directory of this - % distribution and at https://github.com/e0404/matRad/LICENSE.md. No part - % of the matRad project, including this file, may be copied, modified, - % propagated, or distributed except according to the terms contained in the + % Copyright 2020-2026 the matRad development team. + % + % This file is part of the matRad project. It is subject to the license + % terms in the LICENSE file found in the top-level directory of this + % distribution and at https://github.com/e0404/matRad/LICENSE.md. No part + % of the matRad project, including this file, may be copied, modified, + % propagated, or distributed except according to the terms contained in the % LICENSE file. % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -60,13 +60,13 @@ %plotlegend=false; evt; end - + properties (SetAccess=private) - + IsoDose_Contours; %only updated from within this class VOIPlotFlag; DispInfo; - AxesHandlesVOI; + AxesHandlesVOI; cst; vIsoCenter; sliceContourLegend; @@ -75,16 +75,16 @@ properties (SetAccess = protected) initialized = false; end - + events plotUpdated end - + methods function this = matRad_ViewingWidget(handleParent) matRad_cfg = MatRad_Config.instance(); - if nargin < 1 + if nargin < 1 handleParent = figure(... 'Units','normalized',... 'Position',[0.3 0.2 0.4 0.6],... @@ -97,17 +97,17 @@ 'NumberTitle','off',... 'HandleVisibility','callback',... 'Tag','figure1'); - + end - + this = this@matRad_Widget(handleParent); - + matRad_cfg = MatRad_Config.instance(); - + if nargin < 1 % create the handle objects if there's no parent this.scrollHandle = this.widgetHandle; - + % only available in MATLAB if matRad_cfg.isMatlab this.dcmHandle = datacursormode(this.widgetHandle); @@ -122,7 +122,7 @@ function initialize(this) evt = matRad_WorkspaceChangedEvent('image_display'); this.update(evt); end - + function notifyPlotUpdated(obj) % handle environment matRad_cfg = MatRad_Config.instance(); @@ -132,82 +132,82 @@ function notifyPlotUpdated(obj) case 'OCTAVE' matRad_notifyOctave(obj, 'plotUpdated'); end - + end - + %% SET FUNCTIONS function set.plane(this,value) this.plane=value; this.update(); end - + function set.slice(this,value) % project to allowed set (between min and max value) newSlice = max(value,1); - if evalin('base','exist(''ct'')') + if evalin('base','exist(''ct'')') ct=evalin('base','ct'); newSlice = min(newSlice,ct.cubeDim(this.plane)); else newSlice=1; end - + this.slice=newSlice; this.update(); end - + function set.selectedBeam(this,value) this.selectedBeam=value; evt = matRad_WorkspaceChangedEvent('image_display'); this.update(evt); end - + function set.numOfBeams(this,value) this.numOfBeams=value; this.update(); end - + function set.profileOffset(this,value) this.profileOffset=value; evt = matRad_WorkspaceChangedEvent('image_display'); this.update(evt); end - + function set.OffsetSliderStep(this,value) this.OffsetSliderStep=value; this.update(); end - + function set.OffsetMinMax(this,value) this.OffsetMinMax=value; this.update(); end - - + + function set.typeOfPlot(this,value) - this.typeOfPlot=value; + this.typeOfPlot=value; evt = matRad_WorkspaceChangedEvent('image_display'); cla(this.handles.axesFig,'reset'); this.update(evt); end - + function set.colorData(this,value) this.colorData=value; evt = matRad_WorkspaceChangedEvent('image_display'); this.update(evt); end - + function set.doseColorMap(this,value) this.doseColorMap=value; evt = matRad_WorkspaceChangedEvent('image_display'); this.update(evt); end - + function set.ctColorMap(this,value) this.ctColorMap=value; evt = matRad_WorkspaceChangedEvent('image_display'); this.update(evt); end - + function set.cMapSize(this,value) this.cMapSize=value; this.update(); @@ -218,121 +218,121 @@ function notifyPlotUpdated(obj) evt = matRad_WorkspaceChangedEvent('image_display'); this.update(evt); end - + function set.plotCT(this,value) this.plotCT=value; evt = matRad_WorkspaceChangedEvent('image_display'); this.update(evt); end - + function set.plotContour(this,value) this.plotContour=value; evt = matRad_WorkspaceChangedEvent('image_display'); this.update(evt); end - + function set.plotIsoCenter(this,value) this.plotIsoCenter=value; evt = matRad_WorkspaceChangedEvent('image_display'); this.update(evt); end - + function set.plotPlan(this,value) this.plotPlan=value; evt = matRad_WorkspaceChangedEvent('image_display'); this.update(evt); end - + function set.plotDose(this,value) this.plotDose=value; evt = matRad_WorkspaceChangedEvent('image_display'); this.update(evt); end - + function set.plotIsoDoseLines(this,value) this.plotIsoDoseLines=value; evt = matRad_WorkspaceChangedEvent('image_display'); this.update(evt); end - + function set.plotIsoDoseLinesLabels(this,value) this.plotIsoDoseLinesLabels=value; evt = matRad_WorkspaceChangedEvent('image_display'); this.update(evt); end - + function set.plotLegend(this,value) this.plotLegend=value; evt = matRad_WorkspaceChangedEvent('image_display'); this.update(evt); end - + function set.plotColorBar(this,value) this.plotColorBar=value; evt = matRad_WorkspaceChangedEvent('image_display'); this.update(evt); end - + function set.ProfileType(this,value) this.ProfileType=value; evt = matRad_WorkspaceChangedEvent('image_display'); this.update(evt); end - + function set.SelectedDisplayOption(this,value) this.SelectedDisplayOption=value; evt = matRad_WorkspaceChangedEvent('image_display'); this.update(evt); end - + function set.SelectedDisplayAllOptions(this,value) this.SelectedDisplayAllOptions=value; this.update(); end - - + + function set.CutOffLevel(this,value) this.CutOffLevel=value; this.update(); end - + function set.dispWindow(this,value) this.dispWindow=value; evt = matRad_WorkspaceChangedEvent('viewer_options'); this.update(evt); end - + function set.doseOpacity(this,value) this.doseOpacity=value; evt = matRad_WorkspaceChangedEvent('image_display'); this.update(evt); end - + function set.IsoDose_Levels(this,value) this.IsoDose_Levels=value; evt = matRad_WorkspaceChangedEvent('viewer_options'); this.update(evt); end - + function set.IsoDose_Contours(this,value) this.IsoDose_Contours=value; evt = matRad_WorkspaceChangedEvent('image_display'); this.update(evt); end - + function set.NewIsoDoseFlag(this,value) this.NewIsoDoseFlag=value; evt = matRad_WorkspaceChangedEvent('image_display'); this.update(evt); end - + function set.dcmHandle(this,value) this.dcmHandle=value; set(this.dcmHandle,'DisplayStyle','window'); %Add the callback for the datacursor display set(this.dcmHandle,'UpdateFcn',@(hObject, eventdata)dataCursorUpdateFunction(this, hObject, eventdata)); end - + function set.scrollHandle(this,value) this.scrollHandle=value; % set callback for scroll wheel function @@ -342,11 +342,11 @@ function notifyPlotUpdated(obj) %% methods(Access = protected) function this = createLayout(this) - %Viewer Widget + %Viewer Widget h88 = this.widgetHandle; matRad_cfg = MatRad_Config.instance(); - + h89 = axes(... 'Parent',h88,... 'XTick',[0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1],... @@ -361,11 +361,11 @@ function notifyPlotUpdated(obj) 'Color',matRad_cfg.gui.elementColor,... 'Box','on',... 'BoxStyle','full',... - 'Tag','axesFig'); - + 'Tag','axesFig'); + %Title h90 = get(h89,'title'); - + set(h90, ... 'Parent',h89,... 'Visible','on',... @@ -398,7 +398,7 @@ function notifyPlotUpdated(obj) %X Label h91 = get(h89,'xlabel'); - + set(h91,... 'Parent',h89,... 'Units','data',... @@ -428,10 +428,10 @@ function notifyPlotUpdated(obj) 'BusyAction','queue',... 'Interruptible','on',... 'HitTest','on'); - + %Y label h92 = get(h89,'ylabel'); - + set(h92,... 'Parent',h89,... 'Units','data',... @@ -461,10 +461,10 @@ function notifyPlotUpdated(obj) 'BusyAction','queue',... 'Interruptible','on',... 'HitTest','on'); - + %Z label h93 = get(h89,'zlabel'); - + set(h93,... 'Parent',h89,... 'Units','data',... @@ -493,12 +493,12 @@ function notifyPlotUpdated(obj) 'HandleVisibility','off',... 'BusyAction','queue',... 'Interruptible','on',... - 'HitTest','on'); - + 'HitTest','on'); + this.createHandles(); - + end - + function this=doUpdate(this,evt) if ~this.initialized this.initValues(); @@ -506,11 +506,11 @@ function notifyPlotUpdated(obj) end if ~this.updateLock - + %doUpdate = false; if nargin == 2 this.evt = evt; - %At pln changes and at cst/cst (for Isocenter and new settings) + %At pln changes and at cst/cst (for Isocenter and new settings) %we need to update if this.checkUpdateNecessary({'ct','cst'},evt) this.initValues(); @@ -520,18 +520,18 @@ function notifyPlotUpdated(obj) if this.checkUpdateNecessary({'resultGUI','image_display','viewer_options','pln_angles'},evt) this.updateIsoDoseLineCache(); this.UpdatePlot(); - end + end else this.updateValues(); this.UpdatePlot(); end - + this.evt =[]; end - + end end - + methods function UpdatePlot(this) if this.updateLock @@ -539,23 +539,22 @@ function UpdatePlot(this) end matRad_cfg = MatRad_Config.instance(); - + handles = this.handles; - + %profile on; - axes(handles.axesFig); - + %axes(handles.axesFig); + % this is necessary to prevent multiple callbacks of update plot drawing on % top of each other in matlab <2014 drawnow; - + defaultFontSize = matRad_cfg.gui.fontSize; - currAxes = axis(handles.axesFig); axesHandlesVOI = cell(0); - + axesHandlesCT_Dose = cell(0); axesHandlesIsoDose = cell(0); - + if evalin('base','exist(''ct'')') ct = evalin('base','ct'); else @@ -568,9 +567,9 @@ function UpdatePlot(this) else pln = []; end - + %% If resultGUI exists, then an optimization has been performed - if evalin('base','exist(''resultGUI'')') + if evalin('base','exist(''resultGUI'')') result = evalin('base','resultGUI'); end @@ -581,13 +580,13 @@ function UpdatePlot(this) if this.typeOfPlot==1 %get(handles.popupTypeOfPlot,'Value')==1 set(handles.axesFig,'YDir','Reverse'); end - + selectIx = this.colorData; %get(handles.popupmenu_chooseColorData,'Value'); - + cla(handles.axesFig); %% plot ct - if a ct cube is available and type of plot is set to 1 and not 2; 1 indicate cube plotting and 2 profile plotting if ~isempty(ct) && this.typeOfPlot==1 - + if selectIx == 2 ctIx = 1; else @@ -599,26 +598,26 @@ function UpdatePlot(this) plotCtCube = ct.cube; end ctMap = matRad_getColormap(this.ctColorMap,this.cMapSize); - + % if isempty(this.dispWindow{ctIx,2}) % this.dispWindow{ctIx,2} = [min(reshape([ct.cubeHU{:}],[],1)) max(reshape([ct.cubeHU{:}],[],1))]; % end - + if this.plotCT %get(handles.radiobtnCT,'Value') [axesHandlesCT_Dose{end+1},~,~] = matRad_plotCtSlice(handles.axesFig,plotCtCube,this.ctScen,this.plane,this.slice,ctMap,this.dispWindow{ctIx,1}); - + % plot colorbar? If 1 the user asked for the CT. - % not available in octave + % not available in octave if strcmp(matRad_cfg.env,'MATLAB') && ~isempty(this.colorData) && this.colorData == 1 %Plot the colorbar this.cBarHandle = matRad_plotColorbar(handles.axesFig,ctMap,this.dispWindow{ctIx,1},'FontSize',defaultFontSize,'Color',matRad_cfg.gui.textColor); - + if this.plotColorBar set(this.cBarHandle,'Visible','on') else set(this.cBarHandle,'Visible','off') end - + %adjust lables if isfield(ct,'cubeHU') set(get(this.cBarHandle,'ylabel'),'String', 'Hounsfield Units','fontsize',defaultFontSize); @@ -630,27 +629,27 @@ function UpdatePlot(this) end end end - + %% plot dose cube - if this.typeOfPlot== 1 && exist('result','var') % handles.State >= 1 && + if this.typeOfPlot== 1 && exist('result','var') % handles.State >= 1 && doseMap = matRad_getColormap(this.doseColorMap,this.cMapSize); doseIx = 2; - + dose = result.(this.SelectedDisplayOption); - + % dose colorwash if ~isempty(dose) && ~isvector(dose) - - if this.plotDose + + if this.plotDose [doseHandle,~,~] = matRad_plotDoseSlice(handles.axesFig,dose,this.plane,this.slice,this.CutOffLevel,this.doseOpacity,doseMap,this.dispWindow{doseIx,1}); axesHandlesCT_Dose{end+1} = doseHandle; end - + % plot colorbar - if matRad_cfg.isMatlab && ~isempty(this.colorData) && this.colorData > 1 + if matRad_cfg.isMatlab && ~isempty(this.colorData) && this.colorData > 1 %Plot the colorbar this.cBarHandle = matRad_plotColorbar(handles.axesFig,doseMap,this.dispWindow{selectIx,1},'fontsize',defaultFontSize,'Color',matRad_cfg.gui.textColor); - + if this.plotColorBar set(this.cBarHandle,'Visible','on') else @@ -663,34 +662,34 @@ function UpdatePlot(this) set(get(this.cBarHandle,'ylabel'),'interpreter','none'); end end - - + + %% plot iso dose lines - if this.plotIsoDoseLines - plotLabels = this.plotIsoDoseLinesLabels; + if this.plotIsoDoseLines + plotLabels = this.plotIsoDoseLinesLabels; axesHandlesIsoDose = matRad_plotIsoDoseLines(handles.axesFig,dose,this.IsoDose_Contours,this.IsoDose_Levels,plotLabels,this.plane,this.slice,doseMap,this.dispWindow{doseIx,1},'LineWidth',1.5); end end - + %% plot VOIs if this.plotContour && this.typeOfPlot==1 && exist('ct','var') %&& get(handles.radiobtnContour,'Value') && handles.State>0 [AxVOI, this.sliceContourLegend] = matRad_plotVoiContourSlice(handles.axesFig,this.cst,ct,this.ctScen,this.VOIPlotFlag,this.plane,this.slice,[],'LineWidth',2); axesHandlesVOI = [axesHandlesVOI AxVOI]; end this.AxesHandlesVOI=axesHandlesVOI; - + %% Set axis labels and plot iso center matRad_plotAxisLabels(handles.axesFig,ct,this.plane,this.slice,defaultFontSize); set(get(handles.axesFig,'Title'),'Color',matRad_cfg.gui.textColor); - - if this.plotIsoCenter && this.typeOfPlot == 1 && ~isempty(pln) %get(handles.radioBtnIsoCenter,'Value') == 1 + + if this.plotIsoCenter && this.typeOfPlot == 1 && ~isempty(pln) %get(handles.radioBtnIsoCenter,'Value') == 1 hIsoCenterCross = matRad_plotIsoCenterMarker(handles.axesFig,pln,ct,this.plane,this.slice); end - + if this.plotPlan && ~isempty(pln) %get(handles.radiobtnPlan,'value') == 1 matRad_plotProjectedGantryAngles(handles.axesFig,pln,ct,this.plane); end - + %set axis ratio ratios = [1/ct.resolution.x 1/ct.resolution.y 1/ct.resolution.z]; set(handles.axesFig,'DataAspectRatioMode','manual'); @@ -706,8 +705,8 @@ function UpdatePlot(this) end axis(handles.axesFig,'tight'); - - + + %% profile plot if this.typeOfPlot == 2 && exist('result','var') % set SAD @@ -718,7 +717,7 @@ function UpdatePlot(this) catch this.showError(['Could not find the following machine file: ' fileName ]); end - + % clear view and initialize some values cla(handles.axesFig,'reset') set(handles.axesFig,'YDir','normal','Color',matRad_cfg.gui.elementColor,'XColor',matRad_cfg.gui.textColor); @@ -731,11 +730,11 @@ function UpdatePlot(this) end ylabel(['{\color{' tmpColor '}dose [Gy]}']); cColor={tmpColor,'green','magenta','cyan','yellow','red','blue'}; - + % Rotate the system into the beam. % passive rotation & row vector multiplication & inverted rotation requires triple matrix transpose rotMat_system_T = transpose(matRad_getRotationMatrix(pln.propStf.gantryAngles(this.selectedBeam),pln.propStf.couchAngles(this.selectedBeam))); - + if strcmp(this.ProfileType,'longitudinal') sourcePointBEV = [this.profileOffset -SAD 0]; targetPointBEV = [this.profileOffset SAD 0]; @@ -743,18 +742,20 @@ function UpdatePlot(this) sourcePointBEV = [-SAD this.profileOffset 0]; targetPointBEV = [ SAD this.profileOffset 0]; end - - rotSourcePointBEV = sourcePointBEV * rotMat_system_T; - rotTargetPointBEV = targetPointBEV * rotMat_system_T; - + + rotSourcePoint = sourcePointBEV * rotMat_system_T; + rotTargetPoint = targetPointBEV * rotMat_system_T; + % perform raytracing on the central axis of the selected beam, use unit % electron density for plotting against the geometrical depth - cubeIsoCenter = matRad_world2cubeCoords(pln.propStf.isoCenter(this.selectedBeam,:),ct); - [~,l,rho,~,ix] = matRad_siddonRayTracer(cubeIsoCenter,ct.resolution,rotSourcePointBEV,rotTargetPointBEV,{0*ct.cubeHU{1}+1}); - d = [0 l .* rho{1}]; + % cubeIsoCenter = matRad_world2cubeCoords(pln.propStf.isoCenter(this.selectedBeam,:),ct); + % [~,l,rho,~,ix] = matRad_siddonRayTracer(cubeIsoCenter,ct.resolution,rotSourcePointBEV,rotTargetPointBEV,{0*ct.cubeHU{1}+1}); + rayTracer = matRad_RayTracerSiddon(ct.cubeHU,ct); + [~,l,~,~,ix] = rayTracer.traceRay(pln.propStf.isoCenter(this.selectedBeam,:),rotSourcePoint,rotTargetPoint); + d = [0 l]; % Calculate accumulated d sum. vX = cumsum(d(1:end-1)); - + % plot physical dose %Content =this.SelectedDisplayOption; %get(this.popupDisplayOption,'String'); SelectedCube = this.SelectedDisplayOption; %Content{get(this.popupDisplayOption,'Value')}; @@ -764,25 +765,25 @@ function UpdatePlot(this) Idx = find(SelectedCube == '_'); Suffix = SelectedCube(Idx:end); end - + mPhysDose = result.(['physicalDose' Suffix]); PlotHandles{1} = plot(handles.axesFig,vX,mPhysDose(ix),'color',cColor{1,1},'LineWidth',3); hold(handles.axesFig,'on'); PlotHandles{1,2} ='physicalDose'; ylabel(handles.axesFig,'dose in [Gy]'); set(handles.axesFig,'FontSize',defaultFontSize); - + % plot counter Cnt=2; rightAx = []; - + if isfield(result,['RBE' Suffix]) - + %disbale specific plots %this.DispInfo{6,2}=0; %this.DispInfo{5,2}=0; %this.DispInfo{2,2}=0; - + % generate two lines for ylabel StringYLabel1 = ['\fontsize{8}{\color{red}RBE x dose [Gy(RBE)] \color{' tmpColor '}dose [Gy] ']; StringYLabel2 = ''; @@ -792,7 +793,7 @@ function UpdatePlot(this) if ~strcmp(this.DispInfo{i,1},['RBExDose' Suffix]) &&... ~strcmp(this.DispInfo{i,1},['RBE' Suffix]) && ... ~strcmp(this.DispInfo{i,1},['physicalDose' Suffix]) - + mCube = result.([this.DispInfo{i,1}]); PlotHandles{Cnt,1} = plot(handles.axesFig,vX,mCube(ix),'color',cColor{1,Cnt},'LineWidth',3); hold(handles.axesFig,'on'); PlotHandles{Cnt,2} = this.DispInfo{i,1}; @@ -807,13 +808,13 @@ function UpdatePlot(this) vBED = mRBExDose(ix); mRBE = result.(['RBE' Suffix]); vRBE = mRBE(ix); - + % plot biological dose against RBE - [ax, PlotHandles{Cnt,1}, PlotHandles{Cnt+1,1}]=plotyy(handles.axesFig,vX,vBED,vX,vRBE,'plot'); + [ax, PlotHandles{Cnt,1}, PlotHandles{Cnt+1,1}]=plotyy(handles.axesFig,vX,vBED,vX,vRBE,'plot'); hold(ax(2),'on'); PlotHandles{Cnt,2}='RBExDose'; - PlotHandles{Cnt+1,2}='RBE'; - + PlotHandles{Cnt+1,2}='RBE'; + % set plotyy properties set(get(ax(2),'Ylabel'),'String','RBE [1]','FontSize',defaultFontSize); @@ -826,7 +827,7 @@ function UpdatePlot(this) Cnt=Cnt+2; rightAx = ax(2); end - + % asses target coordinates tmpPrior = intmax; tmpSize = 0; @@ -838,13 +839,13 @@ function UpdatePlot(this) VOI = this.cst{i,2}; end end - + str = sprintf('profile plot - central axis of %d beam, gantry angle %d°, couch angle %d°',... this.selectedBeam ,pln.propStf.gantryAngles(this.selectedBeam),pln.propStf.couchAngles(this.selectedBeam)); h_title = title(handles.axesFig,str,'FontSize',defaultFontSize,'Color',matRad_cfg.gui.highlightColor); pos = get(h_title,'Position'); set(h_title,'Position',[pos(1)-40 pos(2) pos(3)]) - + % plot target boundaries mTargetCube = zeros(ct.cubeDim); mTargetCube(linIdxTarget) = 1; @@ -852,17 +853,17 @@ function UpdatePlot(this) WEPL_Target_Entry = vX(find(vProfile,1,'first')); WEPL_Target_Exit = vX(find(vProfile,1,'last')); PlotHandles{Cnt,2} =[VOI ' boundary']; - + if ~isempty(WEPL_Target_Entry) && ~isempty(WEPL_Target_Exit) hold(handles.axesFig,'on'); PlotHandles{Cnt,1} = ... - plot([WEPL_Target_Entry WEPL_Target_Entry],get(handles.axesFig,'YLim'),'--','Linewidth',3,'color',matRad_cfg.gui.highlightColor);hold(handles.axesFig,'on'); - plot([WEPL_Target_Exit WEPL_Target_Exit],get(handles.axesFig,'YLim'),'--','Linewidth',3,'color',matRad_cfg.gui.highlightColor);hold(handles.axesFig,'on'); - + plot(handles.axesFig,[WEPL_Target_Entry WEPL_Target_Entry],get(handles.axesFig,'YLim'),'--','Linewidth',3,'color',matRad_cfg.gui.highlightColor);hold(handles.axesFig,'on'); + plot(handles.axesFig,[WEPL_Target_Exit WEPL_Target_Exit],get(handles.axesFig,'YLim'),'--','Linewidth',3,'color',matRad_cfg.gui.highlightColor);hold(handles.axesFig,'on'); + else PlotHandles{Cnt,1} =[]; end - + Lines = PlotHandles(~cellfun(@isempty,PlotHandles(:,1)),1); Labels = PlotHandles(~cellfun(@isempty,PlotHandles(:,1)),2); l=legend(handles.axesFig,[Lines{:}],Labels{:},'TextColor',matRad_cfg.gui.textColor,'FontSize',defaultFontSize,'Color',matRad_cfg.gui.backgroundColor,'EdgeColor',matRad_cfg.gui.textColor); @@ -876,14 +877,14 @@ function UpdatePlot(this) if this.typeOfPlot==2 || ~this.plotContour || isempty(this.AxesHandlesVOI) || isempty([this.AxesHandlesVOI{:}]) %isempty(find(this.VOIPlotFlag, 1)) l=legend(handles.axesFig,'off'); else -% +% % in case of multiple lines per VOI, only display the legend once VOIlines=this.AxesHandlesVOI(~cellfun(@isempty,this.AxesHandlesVOI)); VOIlegendlines=cellfun(@(v)v(1),VOIlines); l=legend(handles.axesFig, VOIlegendlines, this.cst{ this.sliceContourLegend,2}); %, 'FontSize',8 hold(handles.axesFig,'on'); - + end end set(l,'FontSize',defaultFontSize); @@ -893,37 +894,37 @@ function UpdatePlot(this) set(l,'Visible','off') end this.legendHandle=l; - - + + % if this.rememberCurrAxes % axis(handles.axesFig);%currAxes); % end - + hold(handles.axesFig,'off'); - + %this.cBarChanged = false; - + % if this.typeOfPlot==1 % UpdateColormapOptions(handles); % end - + %this.legendHandle=legend(handles.axesFig); this.handles = handles; %profile off; %profile viewer; - + notifyPlotUpdated(this); end - + %Update IsodoseLines function this = updateIsoDoseLineCache(this) handles=this.handles; - + %Lock triggering an update during isoline caching currLock = this.updateLock; this.updateLock = true; - - if evalin('base','exist(''resultGUI'')') + + if evalin('base','exist(''resultGUI'')') resultGUI = evalin('base','resultGUI'); % select first cube if selected option does not exist if ~isfield(resultGUI,this.SelectedDisplayOption) @@ -932,68 +933,68 @@ function UpdatePlot(this) CubeNames = CubeNames(cubeIx); this.SelectedDisplayOption = CubeNames{1,1}; else - - + + end dose = resultGUI.(this.SelectedDisplayOption); - - %if function is called for the first time then set display parameters - if (isempty(this.dispWindow{2,2}) || ~this.checkUpdateNecessary({'viewer_options'},this.evt) ) && ~this.lockColorSettings + + %if function is called for the first time then set display parameters + if (isempty(this.dispWindow{2,2}) || ~this.checkUpdateNecessary({'viewer_options'},this.evt) ) && ~this.lockColorSettings this.dispWindow{2,1} = [min(dose(:)) max(dose(:))*1.001]; % set default dose range this.dispWindow{2,2} = [min(dose(:)) max(dose(:))*1.001]; % set min max values end - + minMaxRange = this.dispWindow{2,1}; % if upper colorrange is defined then use it otherwise 120% iso dose upperMargin = 1; if abs((max(dose(:)) - this.dispWindow{2,1}(1,2))) < 0.01 * max(dose(:)) upperMargin = 1.2; end - + %this creates a loop(needed the first time a dose cube is loaded) if isempty(this.IsoDose_Levels) || ~this.NewIsoDoseFlag || ~this.checkUpdateNecessary({'viewer_options'},this.evt) vLevels = [0.1:0.1:0.9 0.95:0.05:upperMargin]; referenceDose = (minMaxRange(1,2))/(upperMargin); - + this.IsoDose_Levels = minMaxRange(1,1) + (referenceDose-minMaxRange(1,1)) * vLevels; end - - + + this.IsoDose_Contours = matRad_computeIsoDoseContours(dose,this.IsoDose_Levels); end this.handles = handles; - + this.updateLock = currLock; end - + %% Data Cursors function cursorText = dataCursorUpdateFunction(this,hObject, eventdata)%event_obj) % Display the position of the data cursor % obj Currently not used (empty) % event_obj Handle to event object % output_txt Data cursor text string (string or cell array of strings). - + target = findall(0,'Name','matRadGUI'); - + % Get GUI data (maybe there is another way?) %handles = guidata(target); - + % position of the data point to label pos = get(eventdata,'Position'); - + %Different behavior for image and profile plot if this.typeOfPlot ==1 %get(handles.popupTypeOfPlot,'Value')==1 %Image view cursorText = cell(0,1); try - + if evalin('base','exist(''ct'')') %handles.State >= 1 % plane = get(handles.popupPlane,'Value'); % slice = round(get(handles.sliderSlice,'Value')); - + %Get the CT values ct = evalin('base','ct'); - + %We differentiate between pos and ix, since the user may put %the datatip on an isoline which returns a continous position cubePos = zeros(1,3); @@ -1006,23 +1007,23 @@ function UpdatePlot(this) %Space Coordinates coords = matRad_cubeIndex2worldCoords(cubeIx,ct); cursorText{end+1,1} = ['Space Coordinates: ' mat2str(coords,5) ' mm']; - + ctVal = ct.cubeHU{1}(cubeIx(1),cubeIx(2),cubeIx(3)); cursorText{end+1,1} = ['HU Value: ' num2str(ctVal,3)]; end catch cursorText{end+1,1} = 'Error while retreiving CT Data!'; end - - + + %Add dose information if available if evalin('base','exist(''resultGUI'')') %handles.State == 3 %get result structure result = evalin('base','resultGUI'); - - %get all cubes from the ResultGUI + + %get all cubes from the ResultGUI resultNames = fieldnames(result); %get(handles.popupDisplayOption,'String'); - + %Display all values of fields found in the resultGUI struct for runResult = 1:numel(resultNames) if ~isstruct(result.(resultNames{runResult,1})) && ~isvector(result.(resultNames{runResult,1})) @@ -1038,30 +1039,40 @@ function UpdatePlot(this) end end end - + else %Profile view cursorText = cell(2,1); cursorText{1} = ['Radiological Depth: ' num2str(pos(1),3) ' mm']; cursorText{2} = [get(target,'DisplayName') ': ' num2str(pos(2),3)]; end - + end - + %Scroll wheel update function matRadScrollWheelFcn(this,src,event) - % Check Position - cursorPos = get(src,'CurrentPoint'); - viewerPos = get(this.widgetHandle,'Position'); + matRad_cfg = MatRad_Config.instance(); + if matRad_cfg.isOctave + %Octave unfortunately does provide the CurrentPoint for the last + %click + inWindow = true; + else + % Check Position + cursorPos = get(src,'CurrentPoint'); + viewerPos = get(this.widgetHandle,'Position'); - %If the request came from within the widget change the slice - if cursorPos(1) > viewerPos(1) && cursorPos(1) < viewerPos(1) + viewerPos(3) ... - && cursorPos(2) > viewerPos(2) && cursorPos(2) < viewerPos(2) + viewerPos(4) + %If the request came from within the widget change the slice + inWindow = cursorPos(1) > viewerPos(1) && ... + cursorPos(1) < viewerPos(1) + viewerPos(3) && ... + cursorPos(2) > viewerPos(2) && ... + cursorPos(2) < viewerPos(2) + viewerPos(4); + end - % compute new slice - this.slice= this.slice - event.VerticalScrollCount; + % compute new slice + if inWindow + this.slice = this.slice - event.VerticalScrollCount; end end - + %Toggle Legend function legendToggleFunction(this,src,event) if isempty(this.legendHandle) || ~isobject(this.legendHandle) @@ -1073,14 +1084,14 @@ function legendToggleFunction(this,src,event) set(this.legendHandle,'Visible','off') end end - - %Toggle Colorbar + + %Toggle Colorbar function colorBarToggleFunction(this,src,event) if isempty(this.cBarHandle) || ~isobject(this.cBarHandle) || this.updateLock return; end if this.plotColorBar - + if evalin('base','exist(''resultGUI'')') this.colorData=2; else evalin('base','exist(''ct'')') @@ -1089,34 +1100,34 @@ function colorBarToggleFunction(this,src,event) set(this.cBarHandle,'Visible','on') else set(this.cBarHandle,'Visible','off'); - end + end % send a notification that the plot has changed (to update the options) %this.notifyPlotUpdated(); end - + % function initValues(this) lockState=this.updateLock; - - if lockState + + if lockState return; end - + this.updateLock=true; - + if isempty(this.plane) this.plane=3; end - - if evalin('base','exist(''ct'')') && evalin('base','exist(''cst'')') + + if evalin('base','exist(''ct'')') && evalin('base','exist(''cst'')') % update slice, beam and offset sliders parameters - + ct = evalin('base','ct'); cst = evalin('base','cst'); cst = matRad_computeVoiContoursWrapper(cst,ct); assignin('base','cst',cst); this.cst = cst; - + % define context menu for structures this.VOIPlotFlag=false(size(this.cst,1),1); for i = 1:size(this.cst,1) @@ -1144,17 +1155,17 @@ function initValues(this) this.numOfBeams = 1; visQuantity = this.tryVisQuantityFromPln(); - + if evalin('base','exist(''pln'')') pln = evalin('base','pln'); if isfield(pln,'propStf') && isfield(pln.propStf,'isoCenter') isoCoordinates = matRad_world2cubeIndex(pln.propStf.isoCenter(1,:), ct); planeCenters = ceil(isoCoordinates); - this.numOfBeams=pln.propStf.numOfBeams; + this.numOfBeams=numel(pln.propStf.gantryAngles); end end - - this.slice = planeCenters(this.plane); + + this.slice = planeCenters(this.plane); % set profile offset slider this.OffsetMinMax = [-100 100]; @@ -1167,27 +1178,27 @@ function initValues(this) end this.OffsetSliderStep=[1/this.OffsetSliderStep 1/this.OffsetSliderStep]; - + selectionIndex=1; this.plotColorBar=true; - - + + if evalin('base','exist(''resultGUI'')') this.colorData=2; this.plotColorBar=true; selectionIndex=1; - + Result = evalin('base','resultGUI'); - + this.DispInfo = fieldnames(Result); - + this.updateDisplaySelection(visQuantity); else this.colorData=1; this.SelectedDisplayAllOptions = 'no option available'; this.SelectedDisplayOption = ''; end - else %no data is loaded + else %no data is loaded this.slice=1; this.numOfBeams=1; this.OffsetMinMax = [1 1]; @@ -1200,29 +1211,29 @@ function initValues(this) this.SelectedDisplayAllOptions = 'no option available'; this.SelectedDisplayOption = ''; end - + this.dispWindow{selectionIndex,1} = minMax; this.dispWindow{selectionIndex,2} = minMax; - + this.updateLock=lockState; end - + %update the Viewer function updateValues(this) lockState=this.updateLock; - + if lockState return; end - + this.updateLock=true; - + if evalin('base','exist(''ct'')') && evalin('base','exist(''cst'')') % update slice, beam and offset sliders parameters ct = evalin('base','ct'); cst = evalin('base','cst'); this.cst = cst; - + % define context menu for structures this.VOIPlotFlag=false(size(this.cst,1),1); for i = 1:size(this.cst,1) @@ -1230,7 +1241,7 @@ function updateValues(this) this.VOIPlotFlag(i) = true; end end - % set isoCenter values + % set isoCenter values % Note: only defined for the first Isocenter if evalin('base','exist(''pln'')') pln = evalin('base','pln'); @@ -1246,7 +1257,7 @@ function updateValues(this) % set profile offset slider this.OffsetMinMax = [-100 100]; vRange = sum(abs(this.OffsetMinMax)); - + if strcmp(this.ProfileType,'lateral') this.OffsetSliderStep = vRange/ct.resolution.x; else @@ -1259,7 +1270,7 @@ function updateValues(this) this.updateLock=lockState; end - + function updateDisplaySelection(this,visSelection) %Lock triggering an update during isoline caching currLock = this.updateLock; @@ -1269,12 +1280,12 @@ function updateDisplaySelection(this,visSelection) visSelection = []; end - if evalin('base','exist(''resultGUI'')') + if evalin('base','exist(''resultGUI'')') result = evalin('base','resultGUI'); - + this.DispInfo = fieldnames(result); for i = 1:size(this.DispInfo,1) - + % delete weight vectors in Result struct for plotting if isstruct(result.(this.DispInfo{i,1})) || isvector(result.(this.DispInfo{i,1})) result = rmfield(result,this.DispInfo{i,1}); @@ -1286,37 +1297,37 @@ function updateDisplaySelection(this,visSelection) % right axis (fourth dimension, ignored so far if strfind(this.DispInfo{i,1},'physicalDose') this.DispInfo{i,3} = 'Gy'; - this.DispInfo{i,4} = 'left'; + this.DispInfo{i,4} = 'left'; elseif strfind(this.DispInfo{i,1},'alpha') this.DispInfo{i,3} = 'Gy^{-1}'; - this.DispInfo{i,4} = 'left'; + this.DispInfo{i,4} = 'left'; elseif strfind(this.DispInfo{i,1},'beta') this.DispInfo{i,3} = 'Gy^{-2}'; - this.DispInfo{i,4} = 'left'; + this.DispInfo{i,4} = 'left'; elseif strfind(this.DispInfo{i,1},'RBExDose') this.DispInfo{i,3} = 'Gy(RBE)'; - this.DispInfo{i,4} = 'left'; + this.DispInfo{i,4} = 'left'; elseif strfind(this.DispInfo{i,1},'LET') this.DispInfo{i,3} = 'keV/um'; - this.DispInfo{i,4} = 'left'; + this.DispInfo{i,4} = 'left'; elseif strfind(this.DispInfo{i,1},'effect') this.DispInfo{i,3} = '1'; - this.DispInfo{i,4} = 'right'; + this.DispInfo{i,4} = 'right'; elseif strfind(this.DispInfo{i,1},'RBE') this.DispInfo{i,3} = '1'; - this.DispInfo{i,4} = 'right'; + this.DispInfo{i,4} = 'right'; elseif strfind(this.DispInfo{i,1},'BED') this.DispInfo{i,3} = 'Gy'; - this.DispInfo{i,4} = 'left'; + this.DispInfo{i,4} = 'left'; else this.DispInfo{i,3} = 'a.u.'; this.DispInfo{i,4} = 'right'; end end end - - this.SelectedDisplayAllOptions=fieldnames(result); - + + this.SelectedDisplayAllOptions=fieldnames(result); + if ~isempty(visSelection) && isfield(result,visSelection) this.SelectedDisplayOption = visSelection; elseif ~isfield(result,this.SelectedDisplayOption) @@ -1324,7 +1335,7 @@ function updateDisplaySelection(this,visSelection) else %Keep option end - + if ~any(strcmp(this.SelectedDisplayOption,fieldnames(result))) this.SelectedDisplayOption = this.tryVisQuantityFromPln('physicalDose'); if ~any(strcmp(this.SelectedDisplayOption,fieldnames(result))) @@ -1332,14 +1343,14 @@ function updateDisplaySelection(this,visSelection) end this.updateIsoDoseLineCache(); - end + end else this.SelectedDisplayAllOptions = 'no option available'; this.SelectedDisplayOption = ''; end this.updateLock = currLock; - end + end function visQuantity = tryVisQuantityFromPln(~, default) if nargin < 2 @@ -1367,5 +1378,5 @@ function exportSlice(this,filename,varargin) end - -end \ No newline at end of file + +end diff --git a/matRad/gui/widgets/matRad_VisualizationWidget.m b/matRad/gui/widgets/matRad_VisualizationWidget.m index d9e296a2d..d4b69eecc 100644 --- a/matRad/gui/widgets/matRad_VisualizationWidget.m +++ b/matRad/gui/widgets/matRad_VisualizationWidget.m @@ -8,7 +8,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2020 the matRad development team. + % Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/gui/widgets/matRad_WorkflowWidget.m b/matRad/gui/widgets/matRad_WorkflowWidget.m index e66759b76..26c7a9020 100644 --- a/matRad/gui/widgets/matRad_WorkflowWidget.m +++ b/matRad/gui/widgets/matRad_WorkflowWidget.m @@ -8,7 +8,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2020 the matRad development team. + % Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -424,9 +424,11 @@ function btnCalcDose_Callback(this, hObject, eventdata) % prepare dij for 3d conformal if isfield(pln,'propOpt') && isfield(pln.propOpt,'conf3D') && pln.propOpt.conf3D dij = matRad_collapseDij(dij); + stf = matRad_collapseStf(stf); end % assign results to base worksapce assignin('base','dij',dij); + assignin('base','stf',stf); catch ME @@ -701,7 +703,7 @@ function btnSaveToGUI_Callback(this, hObject, eventdata) try pln = evalin('base','pln'); - numOfBeams = pln.propStf.numOfBeams; + numOfBeams = evalin('base','numel(stf)'); radMode = pln.radiationMode; fractions = pln.numOfFractions; @@ -886,7 +888,7 @@ function exportDicomButton_Callback(this, hObject, eventdata) function CheckOptimizerStatus(this, usedOptimizer,OptCase) - [statusmsg,statusflag] = usedOptimizer.GetStatus(); + [statusmsg,statusflag] = usedOptimizer.getStatus(); if statusflag == 0 || statusflag == 1 statusIcon = 'none'; diff --git a/matRad/gui/widgets/matRad_exportDicomWidget.m b/matRad/gui/widgets/matRad_exportDicomWidget.m index c4b89f998..6ad2db004 100644 --- a/matRad/gui/widgets/matRad_exportDicomWidget.m +++ b/matRad/gui/widgets/matRad_exportDicomWidget.m @@ -8,7 +8,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2020 the matRad development team. + % Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/gui/widgets/matRad_exportWidget.m b/matRad/gui/widgets/matRad_exportWidget.m index 0054f22ec..2c5b306df 100644 --- a/matRad/gui/widgets/matRad_exportWidget.m +++ b/matRad/gui/widgets/matRad_exportWidget.m @@ -9,7 +9,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2020 the matRad development team. + % Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/gui/widgets/matRad_importDicomWidget.m b/matRad/gui/widgets/matRad_importDicomWidget.m index 9c4deb25e..d4757dbd3 100644 --- a/matRad/gui/widgets/matRad_importDicomWidget.m +++ b/matRad/gui/widgets/matRad_importDicomWidget.m @@ -8,7 +8,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2020 the matRad development team. + % Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/gui/widgets/matRad_importWidget.m b/matRad/gui/widgets/matRad_importWidget.m index b0fb55d90..a4be4728a 100644 --- a/matRad/gui/widgets/matRad_importWidget.m +++ b/matRad/gui/widgets/matRad_importWidget.m @@ -8,7 +8,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2020 the matRad development team. + % Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/hluts/matRad_electronDensitiesToHU.m b/matRad/hluts/matRad_electronDensitiesToHU.m index acd8a3784..c47daaceb 100644 --- a/matRad/hluts/matRad_electronDensitiesToHU.m +++ b/matRad/hluts/matRad_electronDensitiesToHU.m @@ -5,13 +5,13 @@ % import process. HU values can only be calculated if the HLUT is % bijective. % -% call +% call: % ct = matRad_electronDensitiesToHU(ct) % -% input +% input: % ct: matRad ct struct containing cube and all additional information % -% output +% output: % ct: ct struct with HU and equivalent density cube % % References @@ -19,7 +19,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/matRad_calcDoseForward.m b/matRad/matRad_calcDoseForward.m index 436607e9e..98d8d7923 100644 --- a/matRad/matRad_calcDoseForward.m +++ b/matRad/matRad_calcDoseForward.m @@ -1,11 +1,11 @@ function resultGUI = matRad_calcDoseForward(ct,cst,stf,pln,w) % matRad forward dose calculation (no dij) % - % call + % call: % resultGUI = matRad_calcDoseForward(ct,cst,stf,pln) %If weights stored in stf % resultGUI = matRad_calcDoseForward(ct,cst,stf,pln,w) % - % input + % input: % ct: ct cube % cst: matRad cst cell array % stf: matRad steering information struct @@ -13,7 +13,7 @@ % w: optional (if no weights available in stf): bixel weight % vector % - % output + % output: % resultGUI: matRad result struct % % References @@ -21,7 +21,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2015 the matRad development team. + % Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -54,4 +54,4 @@ - \ No newline at end of file + diff --git a/matRad/matRad_calcDoseInfluence.m b/matRad/matRad_calcDoseInfluence.m index e69f53aba..044d9ad3e 100644 --- a/matRad/matRad_calcDoseInfluence.m +++ b/matRad/matRad_calcDoseInfluence.m @@ -2,17 +2,17 @@ % matRad dose calculation automaticly creating the appropriate dose engine % for the given pln struct and called the associated dose calculation funtion % -% call +% call: % dij = matRad_calcDoseInfluence(ct,cst,stf,pln) % -% input +% input: % ct: ct cube % cst: matRad cst cell array % stf: matRad steering information struct % pln: matRad plan meta information struct % % -% output +% output: % dij: matRad dij struct % % References @@ -20,7 +20,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/matRad_directApertureOptimization.m b/matRad/matRad_directApertureOptimization.m index 1e909924f..dc52bbd5c 100644 --- a/matRad/matRad_directApertureOptimization.m +++ b/matRad/matRad_directApertureOptimization.m @@ -1,10 +1,10 @@ function [optResult,optimizer] = matRad_directApertureOptimization(dij,cst,apertureInfo,optResult,pln) % matRad function to run direct aperture optimization % -% call +% call: % [optResult,optimizer] = matRad_directApertureOptimization(dij,cst,apertureInfo,optResult,pln) % -% input +% input: % dij: matRad dij struct % cst: matRad cst struct % apertureInfo: aperture shape info struct @@ -13,7 +13,7 @@ % % pln: matRad pln struct % -% output +% output: % optResult: struct containing optimized fluence vector, dose, and % shape info % optimizer: used optimizer object @@ -23,7 +23,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/matRad_fluenceOptimization.m b/matRad/matRad_fluenceOptimization.m index 640b43e7c..081f1ac20 100644 --- a/matRad/matRad_fluenceOptimization.m +++ b/matRad/matRad_fluenceOptimization.m @@ -1,17 +1,17 @@ function [resultGUI,optimizer] = matRad_fluenceOptimization(dij,cst,pln,wInit) % matRad inverse planning wrapper function % -% call +% call: % [resultGUI,optimizer] = matRad_fluenceOptimization(dij,cst,pln) % [resultGUI,optimizer] = matRad_fluenceOptimization(dij,cst,pln,wInit) % -% input +% input: % dij: matRad dij struct % cst: matRad cst struct % pln: matRad pln struct % wInit: (optional) custom weights to initialize problems % -% output +% output: % resultGUI: struct containing optimized fluence vector, dose, and (for % biological optimization) RBE-weighted dose etc. % optimizer: Used Optimizer Object @@ -21,7 +21,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2016 the matRad development team. +% Copyright 2016-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -34,6 +34,24 @@ matRad_cfg = MatRad_Config.instance(); +if isfield(pln, 'propOpt') && isfield(pln.propOpt, 'enableGPU') + enableGPU = pln.propOpt.enableGPU; +else + enableGPU = matRad_cfg.defaults.propOpt.enableGPU; +end + +if enableGPU + try + d = gpuDevice; + cst = matRad_moveCstToGPU(cst); + dij = matRad_moveDijToGPU(dij); + matRad_cfg.dispInfo('Running optimization on the GPU (as far as possible). Selected device: %s\n', d.Name); + catch ME + matRad_cfg.dispWarning('Failed to prepae GPU-based optimization, reverting to CPU. Reason: %s\n', ME.message); + enableGPU = false; + end +end + % consider VOI priorities cst = matRad_setOverlapPriorities(cst); @@ -378,14 +396,25 @@ if ~isfield(pln.propOpt,'optimizer') %While the default optimizer is IPOPT, we can try to fallback to %fmincon in case it does not work for some reason - if ~matRad_OptimizerIPOPT.IsAvailable() + if ~matRad_OptimizerIPOPT.isAvailable() pln.propOpt.optimizer = 'fmincon'; else pln.propOpt.optimizer = 'IPOPT'; end end -switch pln.propOpt.optimizer +if isstring(pln.propOpt.optimizer) || ischar(pln.propOpt.optimizer) + pln.propOpt.optimizer = struct('name',pln.propOpt.optimizer); +end + +if isstruct(pln.propOpt.optimizer) && isfield(pln.propOpt.optimizer,'name') + optimizerName = char(pln.propOpt.optimizer.name); + optimizerOptions = rmfield(pln.propOpt.optimizer,'name'); +else + matRad_cfg.dispError('Could not identify optimizer! Please provide a valid optimizer name or optimizer struct with field ''name''!'); +end + +switch optimizerName case 'IPOPT' optimizer = matRad_OptimizerIPOPT; case 'fmincon' @@ -393,12 +422,14 @@ case 'simulannealbnd' optimizer = matRad_OptimizerSimulannealbnd; otherwise - warning(['Optimizer ''' pln.propOpt.optimizer ''' not known! Fallback to IPOPT!']); + warning(['Optimizer ''' optimizerName ''' not known! Fallback to IPOPT!']); optimizer = matRad_OptimizerIPOPT; end -if ~optimizer.IsAvailable() - matRad_cfg.dispError(['Optimizer ''' pln.propOpt.optimizer ''' not available!']); +matRad_assignPropertiesFromStruct(optimizer,optimizerOptions); + +if ~optimizer.isAvailable() + matRad_cfg.dispError(['Optimizer ''' optimizerName ''' not available!']); end optimizer = optimizer.optimize(wInit,optiProb,dij,cst); diff --git a/matRad/matRad_generateStf.m b/matRad/matRad_generateStf.m index 70a20873a..e5e0d4df1 100644 --- a/matRad/matRad_generateStf.m +++ b/matRad/matRad_generateStf.m @@ -1,15 +1,15 @@ function stf = matRad_generateStf(ct,cst,pln,visMode) % matRad steering information generation % -% call +% call: % stf = matRad_generateStf(ct,cst,pln) % -% input +% input: % ct: ct cube % cst: matRad cst struct % pln: matRad plan meta information struct % -% output +% output: % stf: matRad steering information struct % % References @@ -17,7 +17,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/matRad_planAnalysis.m b/matRad/matRad_planAnalysis.m index 769411b11..0b709383f 100644 --- a/matRad/matRad_planAnalysis.m +++ b/matRad/matRad_planAnalysis.m @@ -3,7 +3,7 @@ % This function performs analysis on radiation therapy plans, including DVH (Dose-Volume Histogram) and quality indicators. % It optionally displays these analyses based on input parameters. % -% input +% input: % resultGUI: matRad resultGUI struct containing the analysis results % ct: matRad ct struct with computed tomography data % cst: matRad cst cell array with structure definitions @@ -13,7 +13,7 @@ % refGy: (optional) Dose values for V_XGy calculation (default: [40 50 60]) % refVol:(optional) Volume percentages for D_X calculation (default: [2 5 95 98]) % -% output +% output: % resultGUI: Updated resultGUI with analysis data % Initialize input parser for function arguments diff --git a/matRad/matRad_sequencing.m b/matRad/matRad_sequencing.m index 51207ada2..37f1274cc 100644 --- a/matRad/matRad_sequencing.m +++ b/matRad/matRad_sequencing.m @@ -1,17 +1,17 @@ function resultGUI = matRad_sequencing(resultGUI,stf,dij,pln,visBool) % matRad inverse planning wrapper function % -% call +% call: % resultGUI = matRad_sequencing(resultGUI,stf,dij,pln) % -% input +% input: % dij: matRad dij struct % stf: matRad stf struct % pln: matRad pln struct % resultGUI: struct containing optimized fluence vector, dose, and (for % biological optimization) RBE-weighted dose etc. % -% output +% output: % resultGUI: struct containing optimized fluence vector, dose, and (for % biological optimization) RBE-weighted dose etc. % @@ -20,7 +20,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2016 the matRad development team. +% Copyright 2016-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/matRad_version.m b/matRad/matRad_version.m index f5f2b7ce8..26825eb82 100644 --- a/matRad/matRad_version.m +++ b/matRad/matRad_version.m @@ -1,15 +1,15 @@ -function [versionString,matRadVer] = matRad_version(matRadRoot) -% matRad function to get the current matRad version +function [versionString, matRadVer] = matRad_version(matRadRoot) +% matRad function to get the current matRad version % (and git information when used from within a repository -% -% call +% +% call: % [versionString,matRadVer] = matRad_version() % [versionString,matRadVer] = matRad_version(matRadRoot) % -% input +% input: % matRadRoot: Optional Root Directory. This is for call in matRad % initialization when MatRad_Config is not yet available -% output +% output: % versionString: Readable string build from version information % matRadVer: struct with version information % @@ -18,85 +18,109 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2020 the matRad development team. -% -% This file is part of the matRad project. It is subject to the license -% terms in the LICENSE file found in the top-level directory of this -% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part -% of the matRad project, including this file, may be copied, modified, -% propagated, or distributed except according to the terms contained in the +% Copyright 2020-2026 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the % LICENSE file. % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%Hardcoded version name / numbers +% Hardcoded version name / numbers matRadVer.name = 'Cleve'; matRadVer.major = 3; matRadVer.minor = 2; -matRadVer.patch = 0; - -tagged = false; +matRadVer.patch = 1; +matRadVer.revision = 65534; % Fallback value for unknown revision (e.g., when not running from a git repository) +matRadVer.branch = []; +matRadVer.commitID = []; -%Retreive branch / commit information from current git repo if applicable -try - %read HEAD file to point to current ref / commit - if nargin == 1 - repoDir = matRadRoot; - else +% Determine repo root +if nargin == 1 + repoDir = matRadRoot; +else + try matRad_cfg = MatRad_Config.instance(); repoDir = matRad_cfg.matRadRoot; + catch + repoDir = fileparts(mfilename('fullpath')); end - repoGitDir = fullfile(repoDir,'.git'); - headText = fileread(fullfile(repoGitDir,'HEAD')); +end + +% Retrieve branch / commit / revision via system git (preferred) +gitAvailable = false; +gitRepo = exist(fullfile(repoDir, '.git'), 'dir') == 7; - %Test if detached head (HEAD contains 40 hex SHA1 commit ID) - i = regexp(headText,'[a-f0-9]{40}', 'once'); - if ~isempty(i) - matRadVer.branch = 'DETACHED'; - matRadVer.commitID = headText(1:40); - else %HEAD contains reference to branch - headParse = textscan(headText,'%s'); - refHead = headParse{1}{2}; - refParse = strsplit(refHead,'/'); - refType = refParse{2}; - matRadVer.branch = strjoin(refParse(3:end),'/'); +if gitRepo + try + git = @(cmd) system(sprintf('git -C "%s" %s', repoDir, cmd)); - %Read ID from ref path - refID = fileread([repoGitDir filesep strjoin(refParse,filesep)]); - matRadVer.commitID = refID(1:40); - - %Check if we are on a tagged commit (i.e., release) - %{ - tagRefs = dir([repoGitDir filesep 'refs' filesep 'tags']); - for t = 1:numel(tagRefs) - if ~any(strcmp(tagRefs(t).name, {'.', '..'})) - tagId = fileread([repoGitDir filesep 'refs' filesep 'tags' filesep tagRefs(t).name]); - if strcmp(tagId(1:40),matRadVer.commitID) - tagged=true; - break; - end + [st1, branch] = git('rev-parse --abbrev-ref HEAD'); + [st2, commitID] = git('rev-parse HEAD'); + branch = strtrim(branch); + commitID = strtrim(commitID); + + if st1 == 0 && st2 == 0 && numel(commitID) == 40 + gitAvailable = true; + matRadVer.commitID = commitID; + if strcmp(branch, 'HEAD') + matRadVer.branch = 'DETACHED'; + else + matRadVer.branch = branch; + end + + % Count commits since version tag -> revision number + tag = sprintf('v%d.%d.%d', matRadVer.major, matRadVer.minor, matRadVer.patch); + [stRev, revCount] = git(sprintf('rev-list --count %s..HEAD', tag)); + if stRev == 0 + matRadVer.revision = str2double(strtrim(revCount)); end end - %} + catch + % system git not available, will attempt file-based fallback below end - -catch - %Git repo information could not be read, set to empty - matRadVer.branch = []; - matRadVer.commitID = []; -end -%Create a readable string -%Git path first -gitString = ''; -if ~isempty(matRadVer.branch) && ~isempty(matRadVer.commitID) && ~tagged - gitString = sprintf('(%s-%s)',matRadVer.branch,matRadVer.commitID(1:8)); -end + % Fallback: read branch / commit from .git files directly (no revision) + if ~gitAvailable + try + repoGitDir = fullfile(repoDir, '.git'); + headText = fileread(fullfile(repoGitDir, 'HEAD')); + + % Test if detached head (HEAD contains 40 hex SHA1 commit ID) + i = regexp(headText, '[a-f0-9]{40}', 'once'); + if ~isempty(i) + matRadVer.branch = 'DETACHED'; + matRadVer.commitID = headText(1:40); + else % HEAD contains reference to branch + headParse = textscan(headText, '%s'); + refHead = headParse{1}{2}; + refParse = strsplit(refHead, '/'); + matRadVer.branch = strjoin(refParse(3:end), '/'); + + % Read commit ID from ref path + refID = fileread(fullfile(repoGitDir, strjoin(refParse, filesep))); + matRadVer.commitID = refID(1:40); + end + catch + % Git repo information could not be read - keep defaults + end + end -%Full string -versionString = sprintf('"%s" v%d.%d.%d%s',matRadVer.name,matRadVer.major,matRadVer.minor,matRadVer.patch,gitString); + % revision == 0 means we are sitting exactly on the version tag (release) + tagged = matRadVer.revision == 0; -%This checks if no explicit assigment is done in which case the version is printed. + % Create a readable string + if ~isempty(matRadVer.branch) && ~isempty(matRadVer.commitID) && ~tagged + gitString = sprintf('(%s-%s)', matRadVer.branch, matRadVer.commitID(1:8)); + end + versionString = sprintf('"%s" v%d.%d.%d.%d%s', matRadVer.name, matRadVer.major, matRadVer.minor, matRadVer.patch, matRadVer.revision, gitString); +else + versionString = sprintf('"%s" v%d.%d.%d', matRadVer.name, matRadVer.major, matRadVer.minor, matRadVer.patch); +end +% This checks if no explicit assignment is done in which case the version is printed. if nargout == 0 disp(['You are running matRad ' versionString]); end diff --git a/matRad/optimization/+DoseConstraints/matRad_DoseConstraint.m b/matRad/optimization/+DoseConstraints/matRad_DoseConstraint.m index df25e57b5..2a4a3474c 100644 --- a/matRad/optimization/+DoseConstraints/matRad_DoseConstraint.m +++ b/matRad/optimization/+DoseConstraints/matRad_DoseConstraint.m @@ -7,7 +7,7 @@ % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/optimization/+DoseConstraints/matRad_DoseConstraintFromObjective.m b/matRad/optimization/+DoseConstraints/matRad_DoseConstraintFromObjective.m index ff7f57447..017cd9b95 100644 --- a/matRad/optimization/+DoseConstraints/matRad_DoseConstraintFromObjective.m +++ b/matRad/optimization/+DoseConstraints/matRad_DoseConstraintFromObjective.m @@ -7,7 +7,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2020 the matRad development team. + % Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/optimization/+DoseConstraints/matRad_MinMaxDVH.m b/matRad/optimization/+DoseConstraints/matRad_MinMaxDVH.m index d54c00f4f..cad14b556 100644 --- a/matRad/optimization/+DoseConstraints/matRad_MinMaxDVH.m +++ b/matRad/optimization/+DoseConstraints/matRad_MinMaxDVH.m @@ -7,7 +7,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2020 the matRad development team. + % Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/optimization/+DoseConstraints/matRad_MinMaxDose.m b/matRad/optimization/+DoseConstraints/matRad_MinMaxDose.m index c1b598572..3de86e2b4 100644 --- a/matRad/optimization/+DoseConstraints/matRad_MinMaxDose.m +++ b/matRad/optimization/+DoseConstraints/matRad_MinMaxDose.m @@ -7,7 +7,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2020 the matRad development team. + % Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/optimization/+DoseConstraints/matRad_MinMaxEUD.m b/matRad/optimization/+DoseConstraints/matRad_MinMaxEUD.m index 36669d8a1..a61082edf 100644 --- a/matRad/optimization/+DoseConstraints/matRad_MinMaxEUD.m +++ b/matRad/optimization/+DoseConstraints/matRad_MinMaxEUD.m @@ -7,7 +7,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2020 the matRad development team. + % Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/optimization/+DoseConstraints/matRad_MinMaxMeanDose.m b/matRad/optimization/+DoseConstraints/matRad_MinMaxMeanDose.m index afd751b07..a62cd2605 100644 --- a/matRad/optimization/+DoseConstraints/matRad_MinMaxMeanDose.m +++ b/matRad/optimization/+DoseConstraints/matRad_MinMaxMeanDose.m @@ -7,7 +7,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2020 the matRad development team. + % Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/optimization/+DoseObjectives/matRad_DoseObjective.m b/matRad/optimization/+DoseObjectives/matRad_DoseObjective.m index 5270efd6f..56a954732 100644 --- a/matRad/optimization/+DoseObjectives/matRad_DoseObjective.m +++ b/matRad/optimization/+DoseObjectives/matRad_DoseObjective.m @@ -8,7 +8,7 @@ % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/optimization/+DoseObjectives/matRad_EUD.m b/matRad/optimization/+DoseObjectives/matRad_EUD.m index d7f8501d2..e195513fe 100644 --- a/matRad/optimization/+DoseObjectives/matRad_EUD.m +++ b/matRad/optimization/+DoseObjectives/matRad_EUD.m @@ -7,7 +7,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2020 the matRad development team. +% Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/optimization/+DoseObjectives/matRad_MaxDVH.m b/matRad/optimization/+DoseObjectives/matRad_MaxDVH.m index 3b90a4e96..c26c2b920 100644 --- a/matRad/optimization/+DoseObjectives/matRad_MaxDVH.m +++ b/matRad/optimization/+DoseObjectives/matRad_MaxDVH.m @@ -7,7 +7,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2020 the matRad development team. +% Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/optimization/+DoseObjectives/matRad_MeanDose.m b/matRad/optimization/+DoseObjectives/matRad_MeanDose.m index fa196c411..fddc78e42 100644 --- a/matRad/optimization/+DoseObjectives/matRad_MeanDose.m +++ b/matRad/optimization/+DoseObjectives/matRad_MeanDose.m @@ -7,7 +7,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2020 the matRad development team. +% Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/optimization/+DoseObjectives/matRad_MinDVH.m b/matRad/optimization/+DoseObjectives/matRad_MinDVH.m index 7935211c9..87f146c6b 100644 --- a/matRad/optimization/+DoseObjectives/matRad_MinDVH.m +++ b/matRad/optimization/+DoseObjectives/matRad_MinDVH.m @@ -7,7 +7,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2020 the matRad development team. +% Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -96,4 +96,4 @@ end end -end \ No newline at end of file +end diff --git a/matRad/optimization/+DoseObjectives/matRad_SquaredDeviation.m b/matRad/optimization/+DoseObjectives/matRad_SquaredDeviation.m index 8ddbe80a0..d38df8c77 100644 --- a/matRad/optimization/+DoseObjectives/matRad_SquaredDeviation.m +++ b/matRad/optimization/+DoseObjectives/matRad_SquaredDeviation.m @@ -7,9 +7,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/optimization/+DoseObjectives/matRad_SquaredOverdosing.m b/matRad/optimization/+DoseObjectives/matRad_SquaredOverdosing.m index 0616fc8e1..72ada4f1f 100644 --- a/matRad/optimization/+DoseObjectives/matRad_SquaredOverdosing.m +++ b/matRad/optimization/+DoseObjectives/matRad_SquaredOverdosing.m @@ -7,7 +7,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2020 the matRad development team. +% Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/optimization/+DoseObjectives/matRad_SquaredUnderdosing.m b/matRad/optimization/+DoseObjectives/matRad_SquaredUnderdosing.m index ec40612c7..5c81d4985 100644 --- a/matRad/optimization/+DoseObjectives/matRad_SquaredUnderdosing.m +++ b/matRad/optimization/+DoseObjectives/matRad_SquaredUnderdosing.m @@ -7,7 +7,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2020 the matRad development team. +% Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/optimization/@matRad_OptimizationProblem/matRad_OptimizationProblem.m b/matRad/optimization/@matRad_OptimizationProblem/matRad_OptimizationProblem.m index ca1b09db2..5b15e1be8 100644 --- a/matRad/optimization/@matRad_OptimizationProblem/matRad_OptimizationProblem.m +++ b/matRad/optimization/@matRad_OptimizationProblem/matRad_OptimizationProblem.m @@ -11,7 +11,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2020 the matRad development team. + % Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/optimization/@matRad_OptimizationProblem/matRad_constraintFunctions.m b/matRad/optimization/@matRad_OptimizationProblem/matRad_constraintFunctions.m index 7a1ab2956..e9c1eaf09 100644 --- a/matRad/optimization/@matRad_OptimizationProblem/matRad_constraintFunctions.m +++ b/matRad/optimization/@matRad_OptimizationProblem/matRad_constraintFunctions.m @@ -4,16 +4,16 @@ % max mean dose constraint, min EUD constraint, max EUD constraint, % max DVH constraint, min DVH constraint % -% call +% call: % c = matRad_constraintFunctions(optiProb,w,dij,cst) % -% input +% input: % optiProb: option struct defining the type of optimization % w: bixel weight vector % dij: dose influence matrix % cst: matRad cst struct % -% output +% output: % c: value of constraints % % References @@ -21,7 +21,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2016 the matRad development team. +% Copyright 2016-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/optimization/@matRad_OptimizationProblem/matRad_constraintJacobian.m b/matRad/optimization/@matRad_OptimizationProblem/matRad_constraintJacobian.m index 7013cd891..8a0b68d08 100644 --- a/matRad/optimization/@matRad_OptimizationProblem/matRad_constraintJacobian.m +++ b/matRad/optimization/@matRad_OptimizationProblem/matRad_constraintJacobian.m @@ -4,16 +4,16 @@ % max mean dose constraint, min EUD constraint, max EUD constraint, max DVH % constraint, min DVH constraint % -% call +% call: % jacob = matRad_jacobFunc(optiProb,w,dij,cst) % -% input +% input: % optiProb: option struct defining the type of optimization % w: bixel weight vector % dij: dose influence matrix % cst: matRad cst struct % -% output +% output: % jacob: jacobian of constraint function % % References @@ -22,7 +22,7 @@ % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2016 the matRad development team. +% Copyright 2016-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/optimization/@matRad_OptimizationProblem/matRad_getConstraintBounds.m b/matRad/optimization/@matRad_OptimizationProblem/matRad_getConstraintBounds.m index 9ad882bb0..5db51bb2d 100644 --- a/matRad/optimization/@matRad_OptimizationProblem/matRad_getConstraintBounds.m +++ b/matRad/optimization/@matRad_OptimizationProblem/matRad_getConstraintBounds.m @@ -1,13 +1,13 @@ function [cl,cu] = matRad_getConstraintBounds(optiProb,cst) % matRad IPOPT get constraint bounds wrapper function % -% call +% call: % [cl,cu] = matRad_getConstraintBounds(optiProb,cst) % -% input +% input: % cst: matRad cst struct % -% output +% output: % cl: lower bounds on constraints % cu: lower bounds on constraints % @@ -16,7 +16,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2016 the matRad development team. +% Copyright 2016-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/optimization/@matRad_OptimizationProblem/matRad_getJacobianStructure.m b/matRad/optimization/@matRad_OptimizationProblem/matRad_getJacobianStructure.m index ec5eabbff..5d7fb89b4 100644 --- a/matRad/optimization/@matRad_OptimizationProblem/matRad_getJacobianStructure.m +++ b/matRad/optimization/@matRad_OptimizationProblem/matRad_getJacobianStructure.m @@ -4,24 +4,23 @@ % max mean dose constraint, min EUD constraint, max EUD constraint, max DVH % constraint, min DVH constraint % -% call +% call: % jacobStruct = matRad_getJacobStruct(optiProb,w,dij,cst) % -% input +% input: % optiProb: matRad optimization problem % w: beamlet/ pencil beam weight vector % dij: dose influence matrix % cst: matRad cst struct % -% output +% output: % jacobStruct: jacobian of constraint function % % References % -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% -% Copyright 2016 the matRad development team. +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2016-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -30,8 +29,6 @@ % propagated, or distributed except according to the terms contained in the % LICENSE file. % -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - % Initializes constraints jacobStruct = sparse([]); tmp = false(size(dij.physicalDose{1},1),1); @@ -58,4 +55,4 @@ end end end - end \ No newline at end of file + end diff --git a/matRad/optimization/@matRad_OptimizationProblem/matRad_objectiveFunction.m b/matRad/optimization/@matRad_OptimizationProblem/matRad_objectiveFunction.m index b69e10bb5..7e1f4dbad 100644 --- a/matRad/optimization/@matRad_OptimizationProblem/matRad_objectiveFunction.m +++ b/matRad/optimization/@matRad_OptimizationProblem/matRad_objectiveFunction.m @@ -1,17 +1,17 @@ function f = matRad_objectiveFunction(optiProb,w,dij,cst) % matRad IPOPT objective function wrapper % -% call +% call: % f = matRad_objectiveFuncWrapper(optiProb,w,dij,cst) % -% input +% input: % optiProb: matRad optimization problem % w: beamlet/ pencil beam weight vector % dij: matRad dose influence struct % cst: matRad cst struct % scenario: index of dij scenario to consider (optional: default 1) % -% output +% output: % f: objective function value % % References @@ -19,7 +19,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2016 the matRad development team. +% Copyright 2016-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/optimization/@matRad_OptimizationProblem/matRad_objectiveGradient.m b/matRad/optimization/@matRad_OptimizationProblem/matRad_objectiveGradient.m index 15634cd78..fc99e43de 100644 --- a/matRad/optimization/@matRad_OptimizationProblem/matRad_objectiveGradient.m +++ b/matRad/optimization/@matRad_OptimizationProblem/matRad_objectiveGradient.m @@ -3,16 +3,16 @@ % supporting mean dose objectives, EUD objectives, squared overdosage, % squared underdosage, squared deviation and DVH objectives % -% call +% call: % g = matRad_gradFuncWrapper(optiProb,w,dij,cst) % -% input +% input: % optiProb: option struct defining the type of optimization % w: bixel weight vector % dij: dose influence matrix % cst: matRad cst struct % -% output +% output: % g: gradient of objective function % % References @@ -23,7 +23,7 @@ % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2016 the matRad development team. +% Copyright 2016-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_OptimizationProblemDAO.m b/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_OptimizationProblemDAO.m index 66f242ca5..b6efa8d96 100644 --- a/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_OptimizationProblemDAO.m +++ b/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_OptimizationProblemDAO.m @@ -8,7 +8,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2020 the matRad development team. +% Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_constraintFunctions.m b/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_constraintFunctions.m index 66a5bec0f..7384bfc52 100644 --- a/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_constraintFunctions.m +++ b/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_constraintFunctions.m @@ -1,16 +1,16 @@ function c = matRad_constraintFunctions(optiProb,apertureInfoVec,dij,cst) % matRad IPOPT callback: constraint function for direct aperture optimization % -% call +% call: % c = matRad_constraintFunctions(optiProb,apertureInfoVec,dij,cst) % -% input +% input: % optiProb: option struct defining the type of optimization % apertueInfoVec: aperture info vector % dij: dose influence matrix % cst: matRad cst struct % -% output +% output: % c: value of constraints % % Reference @@ -21,7 +21,7 @@ % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_constraintJacobian.m b/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_constraintJacobian.m index 30bed9248..a45090df5 100644 --- a/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_constraintJacobian.m +++ b/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_constraintJacobian.m @@ -1,16 +1,16 @@ function jacob = matRad_constraintJacobian(optiProb,apertureInfoVec,dij,cst) % matRad IPOPT callback: jacobian function for direct aperture optimization % -% call +% call: % jacob = matRad_daoJacobFunc(optiProb,apertureInfoVec,dij,cst) % -% input +% input: % optiProb: option struct defining the type of optimization % apertureInfoVec: aperture info vector % dij: dose influence matrix % cst: matRad cst struct % -% output +% output: % jacob: jacobian of constraint function % % References @@ -19,7 +19,7 @@ % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_daoApertureInfo2Vec.m b/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_daoApertureInfo2Vec.m index ee8fca784..7dd9de349 100644 --- a/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_daoApertureInfo2Vec.m +++ b/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_daoApertureInfo2Vec.m @@ -3,13 +3,13 @@ % A vector representation of the aperture weights and shapes and (optional) % some meta information needed during optimization is generated. % -% call +% call: % [apertureInfoVec, mappingMx, limMx] = matRad_daoApertureInfo2Vec(apertureInfo) % -% input +% input: % apertureInfo: aperture weight and shape info struct % -% output +% output: % apertureInfoVec: vector respresentation of the apertue weights and shapes % mappingMx: mapping of vector components to beams, shapes and leaves % limMx: bounds on vector components, i.e., minimum and maximum @@ -22,7 +22,7 @@ % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_daoVec2ApertureInfo.m b/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_daoVec2ApertureInfo.m index e7741fa73..dc2dd6729 100644 --- a/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_daoVec2ApertureInfo.m +++ b/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_daoVec2ApertureInfo.m @@ -5,14 +5,14 @@ % vector w is computed and a vector listing the correspondence between leaf % tips and bixel indices for gradient calculation % -% call +% call: % updatedInfo = matRad_daoVec2ApertureInfo(apertureInfo,apertureInfoVect) % -% input +% input: % apertureInfo: aperture shape info struct % apertureInfoVect: aperture weights and shapes parameterized as vector % -% output +% output: % updatedInfo: updated aperture shape info struct according to apertureInfoVect % % References @@ -22,7 +22,7 @@ % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_getConstraintBounds.m b/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_getConstraintBounds.m index f1be15e78..3f8250250 100644 --- a/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_getConstraintBounds.m +++ b/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_getConstraintBounds.m @@ -1,14 +1,14 @@ function [cl,cu] = matRad_getConstraintBounds(optiProb,cst) % matRad IPOPT get constraint bounds function for direct aperture optimization % -% call +% call: % [cl,cu] = matRad_daoGetConstBounds(optiProb,cst) % -% input +% input: % optiProb: option struct defining the type of optimization % cst: matRad cst struct % -% output +% output: % cl: lower bounds on constraints % cu: lower bounds on constraints % @@ -19,7 +19,7 @@ % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_getJacobianStructure.m b/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_getJacobianStructure.m index bf8b0309c..dfcbd3494 100644 --- a/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_getJacobianStructure.m +++ b/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_getJacobianStructure.m @@ -1,16 +1,16 @@ function jacobStruct = matRad_getJacobianStructure(optiProb,apertureInfoVec,dij,cst) % matRad IPOPT callback: get jacobian structure for direct aperture optimization % -% call +% call: % jacobStruct = matRad_daoGetJacobStruct(optiProb,apertureInfoVec,dij,cst) % -% input +% input: % optiProb: option struct defining the type of optimization % apertureInfoVect: aperture weights and shapes parameterized as vector % dij: dose influence matrix % cst: matRad cst struct % -% output +% output: % jacobStruct: jacobian of constraint function % % References @@ -19,7 +19,7 @@ % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_objectiveFunction.m b/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_objectiveFunction.m index 8d998c4ed..a32723e98 100644 --- a/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_objectiveFunction.m +++ b/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_objectiveFunction.m @@ -1,16 +1,16 @@ function f = matRad_objectiveFunction(optiProb,apertureInfoVec,dij,cst) % matRad IPOPT callback: objective function for direct aperture optimization % -% call +% call: % f = matRad_objectiveFunction(optiProb,apertureInfoVect,dij,cst) % -% input +% input: % optiProb: option struct defining the type of optimization % apertureInfoVec: aperture info in form of vector % dij: matRad dij struct as generated by bixel-based dose calculation % cst: matRad cst struct % -% output +% output: % f: objective function value % % References @@ -20,7 +20,7 @@ % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_objectiveGradient.m b/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_objectiveGradient.m index e7a9da081..5b8cf9d46 100644 --- a/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_objectiveGradient.m +++ b/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_objectiveGradient.m @@ -1,16 +1,16 @@ function g = matRad_objectiveGradient(optiProb,apertureInfoVec,dij,cst) % matRad IPOPT callback: gradient function for direct aperture optimization % -% call +% call: % g = matRad_objectiveGradient(optiProb,apertureInfoVec,dij,cst) % -% input +% input: % optiProb: option struct defining the type of optimization % apertureInfoVect: aperture info in form of vector % dij: matRad dij struct as generated by bixel-based dose calculation % cst: matRad cst struct % -% output +% output: % g: gradient % % References @@ -20,7 +20,7 @@ % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/optimization/matRad_DoseOptimizationFunction.m b/matRad/optimization/matRad_DoseOptimizationFunction.m index 44ff75f93..8f5cd7010 100644 --- a/matRad/optimization/matRad_DoseOptimizationFunction.m +++ b/matRad/optimization/matRad_DoseOptimizationFunction.m @@ -1,11 +1,12 @@ classdef (Abstract) matRad_DoseOptimizationFunction -% matRad_DoseOptimizationFunction. Superclass for objectives and constraints -% This is the superclass for all objectives and constraints to enable easy +% matRad_DoseOptimizationFunction. Superclass for objectives and +% constraints. +% This is the superclass for all objectives and constraints to enable easy % one-line identification. % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2019 the matRad development team. +% Copyright 2019-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -15,7 +16,7 @@ % LICENSE file. % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - + properties (Abstract, Constant) name %Display name of the Objective. Needs to be implemented in sub-classes. parameterNames %Cell array of Display names of the parameters. Needs to be implemented in sub-classes. @@ -42,6 +43,7 @@ function s = struct(obj) s.className = class(obj); s.parameters = obj.parameters; + s.robustness = obj.robustness; end function obj = set.robustness(obj,robustness) diff --git a/matRad/optimization/matRad_SFUDoptimization.m b/matRad/optimization/matRad_SFUDoptimization.m index 06d24e183..16f66a437 100644 --- a/matRad/optimization/matRad_SFUDoptimization.m +++ b/matRad/optimization/matRad_SFUDoptimization.m @@ -3,13 +3,13 @@ % If provided the dij matrix is used for optimisation, otherwise single % beam dijs are calculated (memory saving). % -% call +% call: % [resultGUI] = matRad_SFUDoptimization(pln, cst, dij) % or % [resultGUI] = matRad_SFUDoptimization(pln, cst, [], ct, stf) % % -% input +% input: % pln: matRad pln struct % cst: matRad cst struct % dij: matRad dij struct (optional) @@ -17,7 +17,7 @@ % ct: matRad ct struct (optional, only needed if no dij provided) % stf: matRad stf struct (optional, only if needed no dij provided) % -% output +% output: % resultGUI: struct containing optimized fluence vector, dose, and (for % biological optimization) RBE-weighted dose etc. % (info: struct containing information about optimization) @@ -27,13 +27,13 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this % distribution and at https://github.com/e0404/matRad/LICENSE.md. No part % of the matRad project, including this file, may be copied, modified, -% propagated, or distributed except according to the terms contained in the +% propagated, or distributed except according to the terms contained in the % LICENSE file. % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -42,6 +42,21 @@ sb_cst = cst; +if nargin < 3 || isempty(dij) + if nargin < 5 + matRad_cfg.dispError('If no dij provided, ct and stf are needed for calculation of single beam dijs!'); + end + useDij = false; +else + useDij = true; +end + +if useDij + numOfBeams = dij.numOfBeams; +else + numOfBeams = numel(stf); +end + % check & adjust objectives and constraints internally for fractionation % & adjust for single beams for i = 1:size(cst,1) @@ -73,14 +88,14 @@ % calculate dose per beam per fraction according to [1] ab = sb_cst{i,5}.alphaX / sb_cst{i,5}.betaX; - fx_dose = -0.5*ab + sqrt( 0.25*ab^2 + fx_dose./pln.propStf.numOfBeams .* (fx_dose + ab)); + fx_dose = -0.5*ab + sqrt( 0.25*ab^2 + fx_dose./numOfBeams .* (fx_dose + ab)); % calculate pseudo total Dose per Beam obj.setDoseParameters = fx_dose * pln.numOfFractions; % physical dose splitting else - obj = obj.setDoseParameters(obj.getDoseParameters()/pln.propStf.numOfBeams); + obj = obj.setDoseParameters(obj.getDoseParameters()/numOfbeams); end sb_cst{i,6}{j} = obj; @@ -93,7 +108,7 @@ % initialise total weight vector wTot = zeros(dij.totalNumOfBixels,1); - for i = 1:pln.propStf.numOfBeams + for i = 1:numOfBeams matRad_cfg.dispInfo('optimizing beam %d...\n',i); % columns in total dij for single beam @@ -125,7 +140,6 @@ sb_pln = pln; sb_pln.propStf.gantryAngles = pln.propStf.gantryAngles(i); sb_pln.propStf.couchAngles = pln.propStf.couchAngles(i); - sb_pln.propStf.numOfBeams = 1; sb_pln.propStf.isoCenter = pln.propStf.isoCenter(i,:); % optimize single beam @@ -144,7 +158,7 @@ % initialise total weight vector wTot = []; - for i = 1:pln.propStf.numOfBeams + for i = 1:numOfBeams matRad_cfg.dispInfo('optimizing beam %d...\n',i); % single beam stf sb_stf = stf(i); @@ -152,7 +166,6 @@ % adjust pln to one beam only sb_pln = pln; sb_pln.propStf.isoCenter = pln.propStf.isoCenter(i,:); - sb_pln.propStf.numOfBeams = 1; sb_pln.propStf.gantryAngles = pln.propStf.gantryAngles(i); sb_pln.propStf.couchAngles = pln.propStf.couchAngles(i); diff --git a/matRad/optimization/matRad_calcInversDVH.m b/matRad/optimization/matRad_calcInversDVH.m index 8e597d89f..b6d522fea 100644 --- a/matRad/optimization/matRad_calcInversDVH.m +++ b/matRad/optimization/matRad_calcInversDVH.m @@ -1,14 +1,14 @@ function dose = matRad_calcInversDVH(volume,doseVec) % matRad inverse DVH (Dose Volume Histogram) calculation % -% call +% call: % dose = matRad_calcInversDVH(volume,doseVec) % -% input +% input: % volume: rel volume of structure % doseVec: dose vector of specific structure % -% output +% output: % dose: dose that corresponds to rel volume % % References @@ -16,13 +16,13 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this % distribution and at https://github.com/e0404/matRad/LICENSE.md. No part % of the matRad project, including this file, may be copied, modified, -% propagated, or distributed except according to the terms contained in the +% propagated, or distributed except according to the terms contained in the % LICENSE file. % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/matRad/optimization/matRad_clearUnusedVoxelsFromDij.m b/matRad/optimization/matRad_clearUnusedVoxelsFromDij.m index c9cd0ecf5..49840ba58 100644 --- a/matRad/optimization/matRad_clearUnusedVoxelsFromDij.m +++ b/matRad/optimization/matRad_clearUnusedVoxelsFromDij.m @@ -2,26 +2,26 @@ % matRad function to set the voxels in dij that are not used for % optimization. % -% call +% call: % [dij] = matRad_clearUnusedVoxelsFromDij(cst, dij) % [dij] = matRad_clearUnusedVoxelsFromDij(cst, dij, scenarios) % [dij, mask] = matRad_clearUnusedVoxelsFromDij(cst, dij) % [dij, mask] = matRad_clearUnusedVoxelsFromDij(cst, dij, scenarios) % -% input +% input: % cstInDoseGrid: cst (on dose grid) % dij: dij struct % scenarios (optional): explicitly define the scenario indexes that need to be cleared % % -% output +% output: % % dij: cleared dij struct % mask (optional): cell array containig the mask that has been applied to every ct scenario % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2023 the matRad development team. +% Copyright 2023-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -32,7 +32,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - + matRad_cfg = MatRad_Config.instance(); matRad_cfg.dispInfo('Clearing unused voxels in dij... '); @@ -82,4 +82,4 @@ mask = includeMask; end matRad_cfg.dispInfo('done.\n'); -end \ No newline at end of file +end diff --git a/matRad/optimization/matRad_collapseDij.m b/matRad/optimization/matRad_collapseDij.m index 03c66895e..2485eebb5 100644 --- a/matRad/optimization/matRad_collapseDij.m +++ b/matRad/optimization/matRad_collapseDij.m @@ -1,15 +1,16 @@ -function dijNew = matRad_collapseDij(dij) +function dijNew = matRad_collapseDij(dij, mode) % matRad collapse dij function for simulation of 3D conformal treatments. -% Function to supress intensity-modulation for photons in order to simulate +% Function to supress intensity-modulation for photons in order to simulate % 3D conformal treatments. % -% call +% call: % dijNew = matRad_collapseDij(dij) % -% input +% input: % dij: dose influence matrix +% mode: collpase mode, beam or ray % -% output +% output: % dijNew: collapsed dose influence matrix % % References @@ -17,32 +18,97 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2018 the matRad development team. -% -% This file is part of the matRad project. It is subject to the license -% terms in the LICENSE file found in the top-level directory of this -% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part -% of the matRad project, including this file, may be copied, modified, -% propagated, or distributed except according to the terms contained in the +% Copyright 2018-2026 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the % LICENSE file. % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -dijNew.totalNumOfBixels = 1; -dijNew.totalNumOfRays = 1; -dijNew.numOfBeams = 1; -dijNew.numOfRaysPerBeam = 1; +if nargin < 2 + mode = 'beam'; % default +end +validatestring(mode, {'beam','ray'}); + +switch mode + case 'beam' + dijNew.totalNumOfBixels = dij.numOfBeams; + dijNew.totalNumOfRays = dij.numOfBeams; + dijNew.numOfBeams = dij.numOfBeams; + dijNew.numOfRaysPerBeam = ones(dij.numOfBeams,1); + + dijNew.beamNum = (1:dij.numOfBeams)'; + dijNew.bixelNum = ones(dij.numOfBeams, 1); + dijNew.rayNum = ones(dij.numOfBeams,1); + case 'ray' + dijNew.totalNumOfBixels = dij.totalNumOfRays; + dijNew.totalNumOfRays = dij.totalNumOfRays; + dijNew.numOfBeams = dij.numOfBeams; + dijNew.numOfRaysPerBeam = dij.numOfRaysPerBeam; + + dijNew.beamNum = repelem(1:dij.numOfBeams, dij.numOfRaysPerBeam(:))'; + dijNew.bixelNum = ones(dij.totalNumOfRays, 1); + dijNew.rayNum = cell2mat(arrayfun(@(n) 1:n, dij.numOfRaysPerBeam, 'UniformOutput', false))'; + + totNumRays = [0,cumsum(dij.numOfRaysPerBeam)]; +end -dijNew.beamNum = 1; -dijNew.bixelNum = 1; -dijNew.rayNum = 1; +if isfield(dij,'numParticlesPerMU') + switch mode + case 'beam' + dijNew.numParticlesPerMU = zeros(dij.numOfBeams,1); + for j = 1:dij.numOfBeams + dijNew.numParticlesPerMU(j) = sum(dij.numParticlesPerMU(dij.beamNum == j)); + end + case 'ray' + dijNew.numParticlesPerMU = zeros(dij.totalNumOfRays,1); + for j = 1:dij.numOfBeams + for k = 1:dij.numOfRaysPerBeam(j) + dijNew.numParticlesPerMU(totNumRays(j) + k) = sum(dij.numParticlesPerMU((dij.rayNum == k)&(dij.beamNum == j))); + end + end + end +end dijNew.doseGrid = dij.doseGrid; dijNew.ctGrid = dij.ctGrid; dijNew.numOfScenarios = dij.numOfScenarios; -for i = 1:dij.numOfScenarios - dijNew.physicalDose{i} = sum(dij.physicalDose{i},2); +collapsableQuantites = {'physicalDose', 'mLETDose', 'mAlphaDose', 'mSqrtBetaDose'}; + +% Identify quantities present in dij +quantitiesToCollapse = collapsableQuantites(ismember(collapsableQuantites, fieldnames(dij))); + +for q = 1:numel(quantitiesToCollapse) + quantityName = quantitiesToCollapse{q}; + + for i = 1:numel(dij.(quantityName)) + if isempty(dij.(quantityName){i}) + dijNew.(quantityName){i} = []; + continue; + end + switch mode + case 'beam' + tmp = sparse(dij.doseGrid.numOfVoxels,dij.numOfBeams); % initialize sparse matrix + for j = 1:dij.numOfBeams + % Sum only the columns corresponding to beam j + tmp(:, j) = sum(dij.(quantityName){i}(:, dij.beamNum == j), 2); + end + case 'ray' + tmp = sparse(dij.doseGrid.numOfVoxels,dij.totalNumOfRays); + for j = 1:dij.numOfBeams + for k = 1:dij.numOfRaysPerBeam(j) + tmp(:, totNumRays(j) + k) = sum(dij.(quantityName){i}(:, (dij.rayNum == k)&(dij.beamNum == j)),2); + end + end + end + dijNew.(quantityName){i} = tmp; + end + end diff --git a/matRad/optimization/matRad_collapseStf.m b/matRad/optimization/matRad_collapseStf.m new file mode 100644 index 000000000..002f96b45 --- /dev/null +++ b/matRad/optimization/matRad_collapseStf.m @@ -0,0 +1,49 @@ +function stf = matRad_collapseStf(stf,mode) +% matRad collapse stf function for simulation of 3D conformal treatments. +% Function to supress intensity-modulation for photons in order to simulate +% 3D conformal treatments. +% +% call: +% stf = matRad_collapseStd(stf) +% +% input: +% stf: steering information +% mode: collpase mode, beam or ray +% +% output: +% dijNew: collapsed dose influence matrix +% +% References +% - +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2018-2026 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% dummy collapse so that it works with sequencing + + if nargin < 2 + mode = 'beam'; % default + end + validatestring(mode, {'beam','ray'}); + +switch mode + case 'beam' + for i = 1:size(stf,2) + stf(i).numOfRays = 1; + stf(i).totalNumOfBixels = 1; + end + case 'ray' + for i = 1:size(stf,2) + stf(i).totalNumOfBixels = stf(i).numOfRays; + end +end diff --git a/matRad/optimization/matRad_getObjectivesAndConstraints.m b/matRad/optimization/matRad_getObjectivesAndConstraints.m index a8d07aed3..6b0326818 100644 --- a/matRad/optimization/matRad_getObjectivesAndConstraints.m +++ b/matRad/optimization/matRad_getObjectivesAndConstraints.m @@ -1,11 +1,11 @@ function classNames = matRad_getObjectivesAndConstraints() % matRad steering information generation % -% call +% call: % classNames = matRad_getObjectivesAndConstraints() % % -% output +% output: % classNames: contains class names (row 1) and display descriptions % (row 2) of all available objectives % @@ -14,13 +14,13 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this % distribution and at https://github.com/e0404/matRad/LICENSE.md. No part % of the matRad project, including this file, may be copied, modified, -% propagated, or distributed except according to the terms contained in the +% propagated, or distributed except according to the terms contained in the % LICENSE file. % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/matRad/optimization/optimizer/matRad_Optimizer.m b/matRad/optimization/optimizer/matRad_Optimizer.m index a9520321b..81944ecb7 100644 --- a/matRad/optimization/optimizer/matRad_Optimizer.m +++ b/matRad/optimization/optimizer/matRad_Optimizer.m @@ -1,50 +1,55 @@ classdef (Abstract) matRad_Optimizer < handle -% matRad_Optimizer. This is the superclass for all optimizer -% to be used within matRad -% -% References -% - -% -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% -% Copyright 2019 the matRad development team. -% -% This file is part of the matRad project. It is subject to the license -% terms in the LICENSE file found in the top-level directory of this -% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part -% of the matRad project, including this file, may be copied, modified, -% propagated, or distributed except according to the terms contained in the -% LICENSE file. -% -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - - + % matRad_Optimizer. This is the superclass for all optimizer + % to be used within matRad + % + % References + % - + % + % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + % + % Copyright 2019-2026 the matRad development team. + % + % This file is part of the matRad project. It is subject to the license + % terms in the LICENSE file found in the top-level directory of this + % distribution and at https://github.com/e0404/matRad/LICENSE.md. No part + % of the matRad project, including this file, may be copied, modified, + % propagated, or distributed except according to the terms contained in the + % LICENSE file. + % + % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + properties (Abstract) - options %options struct + options % options struct + end + + properties + showPlot = true end - properties (Abstract,SetAccess = protected) + properties (Abstract, SetAccess = protected) wResult resultInfo end - - - %These should be abstract methods, however Octave can't parse them. As soon - %as Octave is able to do this, they should be made abstract again - methods %(Abstract) - function obj = optimize(obj,w0,optiProb,dij,cst) - throw(MException('MATLAB:class:AbstractMember','Abstract function optimize needs to be implemented!')); + + % These should be abstract methods, however Octave can't parse them. As soon + % as Octave is able to do this, they should be made abstract again + methods % (Abstract) + + function obj = optimize(obj, w0, optiProb, dij, cst) + throw(MException('MATLAB:class:AbstractMember', 'Abstract function optimize needs to be implemented!')); end - - function [msg,statusflag] = GetStatus(obj) - throw(MException('MATLAB:class:AbstractMember','Abstract function GetStatus needs to be implemented!')); + + function [msg, statusflag] = getStatus(obj) + throw(MException('MATLAB:class:AbstractMember', 'Abstract function getStatus needs to be implemented!')); end + end - + methods (Static) - function available = IsAvailable(obj) - throw(MException('MATLAB:class:AbstractMember','Abstract function IsAvailable needs to be implemented!')); + + function available = isAvailable(obj) + throw(MException('MATLAB:class:AbstractMember', 'Abstract function isAvailable needs to be implemented!')); end + end end - diff --git a/matRad/optimization/optimizer/matRad_OptimizerFmincon.m b/matRad/optimization/optimizer/matRad_OptimizerFmincon.m index 361054939..55c891a5b 100644 --- a/matRad/optimization/optimizer/matRad_OptimizerFmincon.m +++ b/matRad/optimization/optimizer/matRad_OptimizerFmincon.m @@ -1,46 +1,46 @@ classdef matRad_OptimizerFmincon < matRad_Optimizer -% matRad_OptimizerFmincon implements the interface for the fmincon optimizer -% of the MATLAB Optiization toolbox -% -% References -% - -% -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% -% Copyright 2019 the matRad development team. -% -% This file is part of the matRad project. It is subject to the license -% terms in the LICENSE file found in the top-level directory of this -% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part -% of the matRad project, including this file, may be copied, modified, -% propagated, or distributed except according to the terms contained in the -% LICENSE file. -% -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - - + % matRad_OptimizerFmincon implements the interface for the fmincon optimizer + % of the MATLAB Optiization toolbox + % + % References + % - + % + % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + % + % Copyright 2019-2026 the matRad development team. + % + % This file is part of the matRad project. It is subject to the license + % terms in the LICENSE file found in the top-level directory of this + % distribution and at https://github.com/e0404/matRad/LICENSE.md. No part + % of the matRad project, including this file, may be copied, modified, + % propagated, or distributed except according to the terms contained in the + % LICENSE file. + % + % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + properties - options %the optimoptions for fmincon + options % the optimoptions for fmincon end properties (SetAccess = protected) - wResult %last optimization result - resultInfo %info struct about last results + wResult % last optimization result + resultInfo % info struct about last results end - + methods + function obj = matRad_OptimizerFmincon - %matRad_OptimizerFmincon - % Construct an instance of the fmincon optimizer from the Optimization Toolbox + % matRad_OptimizerFmincon + % Construct an instance of the fmincon optimizer from the Optimization Toolbox matRad_cfg = MatRad_Config.instance(); - if ~matRad_OptimizerFmincon.IsAvailable() + if ~matRad_OptimizerFmincon.isAvailable() matRad_cfg.dispError('matRad_OptimizerFmincon can not be constructed as fmincon is not available!'); end - + obj.wResult = []; obj.resultInfo = []; - + optDiag = 'off'; if matRad_cfg.logLevel >= 4 @@ -54,112 +54,120 @@ else optDisplay = 'off'; end - - %createDefaultOptimizerOptions Constructs a set of default - %options for the optimizer to use - obj.options = optimoptions('fmincon',... - 'Algorithm','interior-point',... - 'Display',optDisplay,... - 'SpecifyObjectiveGradient',true,... - 'SpecifyConstraintGradient',true,... - 'AlwaysHonorConstraints', 'bounds',... - 'MaxIterations',matRad_cfg.defaults.propOpt.maxIter,... - 'MaxFunctionEvaluations',3000,... - 'CheckGradients',false,... - 'HessianApproximation',{'lbfgs',50},... - 'UseParallel',true,... - 'Diagnostics',optDiag,... - 'ScaleProblem',true); - - if ~matRad_cfg.disableGUI - obj.options.PlotFcn = {@optimplotfval,@optimplotx,@optimplotfunccount,@optimplotconstrviolation,@optimplotstepsize,@optimplotfirstorderopt}; - end + + % createDefaultOptimizerOptions Constructs a set of default + % options for the optimizer to use + obj.options = optimoptions('fmincon', ... + 'Algorithm', 'interior-point', ... + 'Display', optDisplay, ... + 'SpecifyObjectiveGradient', true, ... + 'SpecifyConstraintGradient', true, ... + 'AlwaysHonorConstraints', 'bounds', ... + 'MaxIterations', matRad_cfg.defaults.propOpt.maxIter, ... + 'MaxFunctionEvaluations', 3000, ... + 'CheckGradients', false, ... + 'HessianApproximation', {'lbfgs', 50}, ... + 'UseParallel', true, ... + 'Diagnostics', optDiag, ... + 'ScaleProblem', true); end - - function obj = optimize(obj,w0,optiProb,dij,cst) - %optimize Carries Out the optimization - + + function obj = optimize(obj, w0, optiProb, dij, cst) + % optimize Carries Out the optimization + % obtain lower and upper variable bounds lb = optiProb.lowerBounds(w0); ub = optiProb.upperBounds(w0); - + % Informing user to press q to terminate optimization - %fprintf('\nOptimzation initiating...\n'); - %fprintf('Press q to terminate the optimization...\n'); + % fprintf('\nOptimzation initiating...\n'); + % fprintf('Press q to terminate the optimization...\n'); matRad_cfg = MatRad_Config.instance(); - if matRad_cfg.isMatlab && str2double(matRad_cfg.envVersion) <= 9.13 && strcmp(obj.options.Diagnostics,'on') - matRad_cfg.dispWarning('Diagnostics in fmincon will be turned off due to a bug when using lbfgs with specified number of histories!'); + if matRad_cfg.isMatlab && str2double(matRad_cfg.envVersion) <= 9.13 && strcmp(obj.options.Diagnostics, 'on') + matRad_cfg.dispWarning(['Diagnostics in fmincon will be turned off due to a bug when using lbfgs '... + 'with specified number of histories!']); obj.options.Diagnostics = 'off'; end - - + + if obj.showPlot && ~matRad_cfg.disableGUI + obj.options.PlotFcn = { ... + @optimplotfval, ... + @optimplotx, ... + @optimplotfunccount, ... + @optimplotconstrviolation, ... + @optimplotstepsize, ... + @optimplotfirstorderopt}; + else + obj.options.PlotFcn = {}; + end + % Run fmincon. - [obj.wResult,fVal,exitflag,info] = fmincon(@(x) obj.fmincon_objAndGradWrapper(x,optiProb,dij,cst),... - w0,... % Starting Point - [],[],... % Linear Constraints we do not explicitly use - [],[],... % Also no linear inequality constraints - lb,ub,... % Lower and upper bounds for optimization variable - @(x) obj.fmincon_nonlconWrapper(x,optiProb,dij,cst),... - obj.options); % Non linear constraint structure); - + [obj.wResult, fVal, exitflag, info] = fmincon(@(x) obj.fminconObjAndGradWrapper(x, optiProb, dij, cst), ... + double(matRad_gatherCompat(w0)), ... % Starting Point + [], [], ... % Linear Constraints we do not explicitly use + [], [], ... % Also no linear inequality constraints + lb, ub, ... % Lower and upper bounds for optimization variable + @(x) obj.fminconNonlconWrapper(x, optiProb, dij, cst), ... + obj.options); % Non linear constraint structure); + obj.resultInfo = info; obj.resultInfo.fVal = fVal; obj.resultInfo.exitflag = exitflag; end - - function [f, fGrad] = fmincon_objAndGradWrapper(obj,x,optiProb,dij,cst) - f = optiProb.matRad_objectiveFunction(x,dij,cst); - fGrad = optiProb.matRad_objectiveGradient(x,dij,cst); + + function [f, fGrad] = fminconObjAndGradWrapper(obj, x, optiProb, dij, cst) + f = double(matRad_gatherCompat(optiProb.matRad_objectiveFunction(x, dij, cst))); + fGrad = double(matRad_gatherCompat(optiProb.matRad_objectiveGradient(x, dij, cst))); end - - function [c,cEq,cJacob,cEqJacob] = fmincon_nonlconWrapper(obj,x,optiProb,dij,cst) - %Get the bounds of the constraint - [cl,cu] = optiProb.matRad_getConstraintBounds(cst); - - %Get finite bounds + + function [c, cEq, cJacob, cEqJacob] = fminconNonlconWrapper(obj, x, optiProb, dij, cst) + % Get the bounds of the constraint + [cl, cu] = optiProb.matRad_getConstraintBounds(cst); + + % Get finite bounds clFinIx = isfinite(cl); cuFinIx = isfinite(cu); - + % Some checks - assert(isequal(size(cl),size(cu))); + assert(isequal(size(cl), size(cu))); assert(all(cl <= cu)); - - %For fmincon we need to separate into equalty and inequality - %constraints + + % For fmincon we need to separate into equalty and inequality + % constraints isEqConstr = (cl == cu); eqIx = isEqConstr; ineqIx = ~isEqConstr; - - %Obtain all constraint functions and derivatives - cVals = optiProb.matRad_constraintFunctions(x,dij,cst); - cJacob = optiProb.matRad_constraintJacobian(x,dij,cst); - - %Subselection of equality constraints - cEq = cVals(eqIx & clFinIx); %We can only rely on cl indices here due to the equality index - cEqJacob = cJacob(eqIx & clFinIx,:)'; - - %Prepare inequality constraints: - %We need to separate upper and lower bound constraints for - %fmincon - cL = cl(ineqIx & clFinIx) - cVals(ineqIx & clFinIx); - cU = cVals(ineqIx & cuFinIx) - cu(ineqIx & cuFinIx); - cJacobL = -cJacob(ineqIx & clFinIx,:); - cJacobU = cJacob(ineqIx & cuFinIx,:); - - %build the inequality jacobian - c = [cL; cU]; - cJacob = transpose([cJacobL; cJacobU]); + + % Obtain all constraint functions and derivatives + cVals = optiProb.matRad_constraintFunctions(x, dij, cst); + cJacob = optiProb.matRad_constraintJacobian(x, dij, cst); + + % Subselection of equality constraints + cEq = cVals(eqIx & clFinIx); % We can only rely on cl indices here due to the equality index + cEqJacob = cJacob(eqIx & clFinIx, :)'; + + % Prepare inequality constraints: + % We need to separate upper and lower bound constraints for + % fmincon + cL = matRad_gatherCompat(cl(ineqIx & clFinIx) - cVals(ineqIx & clFinIx)); + cU = matRad_gatherCompat(cVals(ineqIx & cuFinIx) - cu(ineqIx & cuFinIx)); + cJacobL = matRad_gatherCompat(-cJacob(ineqIx & clFinIx, :)); + cJacobU = matRad_gatherCompat(cJacob(ineqIx & cuFinIx, :)); + + % build the inequality jacobian + c = double([cL; cU]); + cJacob = double(transpose([cJacobL; cJacobU])); end - - function [statusmsg,statusflag] = GetStatus(obj) - try + + function [statusmsg, statusflag] = getStatus(obj) + try statusmsg = obj.resultInfo.message; if obj.resultInfo.exitflag == 0 statusflag = 0; elseif obj.resultInfo.exitflag > 0 statusflag = 1; - else + else statusflag = -1; end catch @@ -167,12 +175,15 @@ statusflag = -1; end end + end - - methods (Static) - function available = IsAvailable() - %'fmincon' is a p-code file in the optimization toolbox + + methods (Static) + + function available = isAvailable() + % 'fmincon' is a p-code file in the optimization toolbox available = exist('fmincon') ~= 0; end + end -end \ No newline at end of file +end diff --git a/matRad/optimization/optimizer/matRad_OptimizerIPOPT.m b/matRad/optimization/optimizer/matRad_OptimizerIPOPT.m index 7395c06d4..b7b1f1652 100644 --- a/matRad/optimization/optimizer/matRad_OptimizerIPOPT.m +++ b/matRad/optimization/optimizer/matRad_OptimizerIPOPT.m @@ -1,28 +1,25 @@ classdef matRad_OptimizerIPOPT < matRad_Optimizer -% matRad_OptimizerIPOPT implements the interface for ipopt -% -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% -% Copyright 2019 the matRad development team. -% -% This file is part of the matRad project. It is subject to the license -% terms in the LICENSE file found in the top-level directory of this -% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part -% of the matRad project, including this file, may be copied, modified, -% propagated, or distributed except according to the terms contained in the -% LICENSE file. -% -% References -% - -% -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + % matRad_OptimizerIPOPT implements the interface for ipopt + % + % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + % + % Copyright 2019-2026 the matRad development team. + % + % This file is part of the matRad project. It is subject to the license + % terms in the LICENSE file found in the top-level directory of this + % distribution and at https://github.com/e0404/matRad/LICENSE.md. No part + % of the matRad project, including this file, may be copied, modified, + % propagated, or distributed except according to the terms contained in the + % LICENSE file. + % + % References + % - + % + % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% properties options env - - %Visualization - showPlot = true; end properties (SetAccess = protected) @@ -39,8 +36,9 @@ end methods + function obj = matRad_OptimizerIPOPT - %matRad_OptimizerIPOPT + % matRad_OptimizerIPOPT % Construct an instance of the IPOPT optimizer (mex % interface) @@ -52,7 +50,7 @@ obj.allObjectiveFunctionValues = []; obj.abortRequested = false; - %Set Default Options + % Set Default Options if matRad_cfg.logLevel <= 1 lvl = 0; elseif matRad_cfg.logLevel <= 2 @@ -60,8 +58,8 @@ elseif matRad_cfg.logLevel <= 3 lvl = 5; else - %There seems to be a problem with higher log levels in - %IPOPT! + % There seems to be a problem with higher log levels in + % IPOPT! lvl = 5; end @@ -73,14 +71,14 @@ obj.options.tol = 1e-10; % (Opt1) obj.options.dual_inf_tol = 1e-4; % (Opt2) obj.options.constr_viol_tol = 1e-4; % (Opt3) - obj.options.compl_inf_tol = 1e-4; % (Opt4), Optimal Solution Found if (Opt1),...,(Opt4) fullfiled + obj.options.compl_inf_tol = 1e-4; % (Opt4), Optimal Solution Found if (Opt1),...,(Opt4) fulfilled obj.options.acceptable_iter = 5; % (Acc1) obj.options.acceptable_tol = 1e10; % (Acc2) obj.options.acceptable_constr_viol_tol = 1e-2; % (Acc3) obj.options.acceptable_dual_inf_tol = 1e10; % (Acc4) obj.options.acceptable_compl_inf_tol = 1e10; % (Acc5) - obj.options.acceptable_obj_change_tol = 1e-4; % (Acc6), Solved To Acceptable Level if (Acc1),...,(Acc6) fullfiled + obj.options.acceptable_obj_change_tol = 1e-4; % (Acc6), Solved To Acceptable Level if (Acc1),...,(Acc6) fulfilled obj.options.max_iter = matRad_cfg.defaults.propOpt.maxIter; obj.options.max_cpu_time = 7200; @@ -89,12 +87,12 @@ obj.options.mu_strategy = 'adaptive'; % Line Sarch (C.8) - %obj.options.accept_every_trial_step = 'yes'; - %obj.options.line_search_method = 'cg-penalty'; + % obj.options.accept_every_trial_step = 'yes'; + % obj.options.line_search_method = 'cg-penalty'; % Restoration Phase (C.10) - %obj.options.soft_resto_pderror_reduction_factor = 100; - %obj.options.required_infeasibility_reduction = 0.9999; + % obj.options.soft_resto_pderror_reduction_factor = 100; + % obj.options.required_infeasibility_reduction = 0.9999; % Quasi-Newton (C.13) obj.options.hessian_approximation = 'limited-memory'; @@ -109,42 +107,42 @@ % obj.options.derivative_test_perturbation = 1e-6; % default 1e-8 % obj.options.derivative_test_tol = 1e-6; - if ~matRad_checkMexFileExists('ipopt') - matRad_cfg.dispError('IPOPT mex interface not available for %s!',obj.env); + if ~matRad_OptimizerIPOPT.isAvailable() + matRad_cfg.dispError('IPOPT mex interface not available for %s!', obj.env); end - if matRad_cfg.disableGUI || (matRad_cfg.isOctave && isequal(graphics_toolkit(),'gnuplot')) + if matRad_cfg.disableGUI || (matRad_cfg.isOctave && isequal(graphics_toolkit(), 'gnuplot')) obj.showPlot = false; end end - function obj = optimize(obj,w0,optiProb,dij,cst) + function obj = optimize(obj, w0, optiProb, dij, cst) matRad_cfg = MatRad_Config.instance(); % set optimization options - %Set up ipopt structure + % Set up ipopt structure ipoptStruct = struct; - %optimizer options + % optimizer options ipoptStruct.ipopt = obj.options; - %variable bounds + % variable bounds ipoptStruct.lb = optiProb.lowerBounds(w0); ipoptStruct.ub = optiProb.upperBounds(w0); - %constraint bounds; - [ipoptStruct.cl,ipoptStruct.cu] = optiProb.matRad_getConstraintBounds(cst); + % constraint bounds; + [ipoptStruct.cl, ipoptStruct.cu] = optiProb.matRad_getConstraintBounds(cst); % set callback functions. - funcs.objective = @(x) double(optiProb.matRad_objectiveFunction(x,dij,cst)); - funcs.constraints = @(x) double(optiProb.matRad_constraintFunctions(x,dij,cst)); - funcs.gradient = @(x) double(optiProb.matRad_objectiveGradient(x,dij,cst)); - funcs.jacobian = @(x) double(optiProb.matRad_constraintJacobian(x,dij,cst)); - funcs.jacobianstructure = @( ) optiProb.matRad_getJacobianStructure(w0,dij,cst); - funcs.iterfunc = @(iter,objective,paramter) obj.iterFunc(iter,objective,paramter,ipoptStruct.ipopt.max_iter); + funcs.objective = @(x) double(matRad_gatherCompat(optiProb.matRad_objectiveFunction(x, dij, cst))); + funcs.constraints = @(x) double(matRad_gatherCompat(optiProb.matRad_constraintFunctions(x, dij, cst))); + funcs.gradient = @(x) double(matRad_gatherCompat(optiProb.matRad_objectiveGradient(x, dij, cst))); + funcs.jacobian = @(x) double(matRad_gatherCompat(optiProb.matRad_constraintJacobian(x, dij, cst))); + funcs.jacobianstructure = @() optiProb.matRad_getJacobianStructure(w0, dij, cst); + funcs.iterfunc = @(iter, objective, parameter) obj.iterFunc(iter, objective, parameter, ipoptStruct.ipopt.max_iter); % Informing user to press q to terminate optimization matRad_cfg.dispInfo('\nOptimzation initiating...\n'); @@ -159,10 +157,10 @@ mde = com.mathworks.mde.desk.MLDesktop.getInstance; cw = mde.getClient('Command Window'); xCmdWndView = cw.getComponent(0).getViewport.getComponent(0); - h_cw = handle(xCmdWndView,'CallbackProperties'); + h_cw = handle(xCmdWndView, 'CallbackProperties'); % set Key Pressed Callback of Matlab command window - set(h_cw, 'KeyPressedCallback', @(h,event) obj.abortCallbackKey(h,event)); + set(h_cw, 'KeyPressedCallback', @(h, event) obj.abortCallbackKey(h, event)); matRad_cfg.dispInfo('Press q to terminate the optimization...\n'); qCallbackSet = true; catch @@ -171,21 +169,23 @@ end end - %ipoptStruct.options = obj.options; + % ipoptStruct.options = obj.options; obj.abortRequested = false; obj.plotFailed = false; % Run IPOPT. try - [obj.wResult, obj.resultInfo] = ipopt(double(w0),funcs,ipoptStruct); + [obj.wResult, obj.resultInfo] = ipopt(double(matRad_gatherCompat(w0)), funcs, ipoptStruct); catch ME - errorString = [ME.message '\nThis error was thrown by the MEX-interface of IPOPT.\nMex interfaces can raise compatability issues which may be resolved by compiling them by hand directly on your particular system.']; + errorString = [ME.message '\nThis error was thrown by the MEX-interface of IPOPT.\n' ... + 'Mex interfaces can raise compatibility issues which may be resolved by ' ... + 'compiling them by hand directly on your system.']; matRad_cfg.dispError(errorString); end % unset Key Pressed Callback of Matlab command window if qCallbackSet - set(h_cw, 'KeyPressedCallback',' '); + set(h_cw, 'KeyPressedCallback', ' '); end obj.abortRequested = false; @@ -193,7 +193,7 @@ obj.allObjectiveFunctionValues = []; end - function [statusmsg,statusflag] = GetStatus(obj) + function [statusmsg, statusflag] = getStatus(obj) try switch obj.resultInfo.status case 0 @@ -249,19 +249,29 @@ end end - function flag = iterFunc(obj,iter,objective,~,~) + function flag = iterFunc(obj, iter, objective, ~, ~) obj.allObjectiveFunctionValues(iter + 1) = objective; - %We don't want the optimization to crash because of drawing - %errors + % We don't want the optimization to crash because of drawing + % errors + + if ~isempty(obj.axesHandle) && ~isgraphics(obj.axesHandle) + obj.plotFailed = true; + end + if obj.showPlot && ~obj.plotFailed try obj.plotFunction(); catch ME matRad_cfg = MatRad_Config.instance(); - %Put a warning at iteration 1 that plotting failed - matRad_cfg.dispWarning('Objective Function plotting failed and thus disabled. Message:\n%s',ME.message); + % Put a warning at iteration 1 that plotting failed + matRad_cfg.dispWarning('Objective Function plotting failed and thus disabled. Message:\n%s', ME.message); obj.plotFailed = true; + if matRad_cfg.isOctave + fflush(stdout); + else + drawnow('update'); + end end end flag = ~obj.abortRequested; @@ -273,38 +283,49 @@ function plotFunction(obj) x = 1:numel(y); if isempty(obj.axesHandle) - %Create new Fiure and store axes handle + % Create new Figure and store axes handle matRad_cfg = MatRad_Config.instance(); - hFig = figure('Name','Progress of IPOPT Optimization','NumberTitle','off','Color',matRad_cfg.gui.backgroundColor); - hAx = axes(hFig,'Color',matRad_cfg.gui.elementColor,'XColor',matRad_cfg.gui.textColor,'YColor',matRad_cfg.gui.textColor,'GridColor',matRad_cfg.gui.textColor,'MinorGridColor',matRad_cfg.gui.backgroundColor); - - hold(hAx,'on'); - grid(hAx,'on'); - grid(hAx,'minor'); - set(hAx,'YScale','log'); - - %Add a Stop button with callback to change abort flag + hFig = figure('Name', 'Progress of IPOPT Optimization', 'NumberTitle', 'off', 'Color', matRad_cfg.gui.backgroundColor); + hAx = axes(hFig, ... + 'Color', matRad_cfg.gui.elementColor, ... + 'XColor', matRad_cfg.gui.textColor, ... + 'YColor', matRad_cfg.gui.textColor, ... + 'GridColor', matRad_cfg.gui.textColor, ... + 'MinorGridColor', matRad_cfg.gui.backgroundColor); + + hold(hAx, 'on'); + grid(hAx, 'on'); + grid(hAx, 'minor'); + set(hAx, 'YScale', 'log'); + + % Add a Stop button with callback to change abort flag c = uicontrol; - cPos = get(c,'Position'); + cPos = get(c, 'Position'); cPos(1) = 5; cPos(2) = 5; - set(c, 'String','Stop',... - 'Position',cPos,... - 'Callback',@(~,~) abortCallbackButton(obj)); + set(c, 'String', 'Stop', ... + 'Position', cPos, ... + 'Callback', @(~, ~) abortCallbackButton(obj)); - %Set up the axes scaling & labels + % Set up the axes scaling & labels defaultFontSize = 14; - set(hAx,'YScale','log'); - title(hAx,'Progress of Optimization','LineWidth',defaultFontSize,'Color',matRad_cfg.gui.highlightColor); - xlabel(hAx,'# iterations','Fontsize',defaultFontSize),ylabel(hAx,'objective function value','Fontsize',defaultFontSize); - - %Create plot handle and link to data for faster update - hPlot = plot(hAx,x,y,'x','MarkerEdgeColor',matRad_cfg.gui.highlightColor,'MarkerFaceColor',matRad_cfg.gui.elementColor,'LineWidth',1.5,'XDataSource','x','YDataSource','y'); + set(hAx, 'YScale', 'log'); + title(hAx, 'Progress of Optimization', 'LineWidth', defaultFontSize, 'Color', matRad_cfg.gui.highlightColor); + xlabel(hAx, '# iterations', 'Fontsize', defaultFontSize); + ylabel(hAx, 'objective function value', 'Fontsize', defaultFontSize); + + % Create plot handle and link to data for faster update + hPlot = plot(hAx, x, y, 'x', ... + 'MarkerEdgeColor', matRad_cfg.gui.highlightColor, ... + 'MarkerFaceColor', matRad_cfg.gui.elementColor, ... + 'LineWidth', 1.5, ... + 'XDataSource', 'x', ... + 'YDataSource', 'y'); obj.plotHandle = hPlot; obj.axesHandle = hAx; - else %Figure already exists, retreive from axes handle - hFig = get(obj.axesHandle,'Parent'); + else % Figure already exists, retrieve from axes handle + hFig = get(obj.axesHandle, 'Parent'); hAx = obj.axesHandle; hPlot = obj.plotHandle; end @@ -315,10 +336,10 @@ function plotFunction(obj) switch obj.env case 'OCTAVE' if ishghandle(hFig) - refreshdata(hFig,'caller'); + refreshdata(hFig, 'caller'); end otherwise - refreshdata(hPlot,'caller'); + refreshdata(hPlot, 'caller'); end drawnow; @@ -328,41 +349,44 @@ function plotFunction(obj) end end - function abortCallbackKey(obj,~,KeyEvent) + function abortCallbackKey(obj, ~, keyEvent) % check if user pressed q - if get(KeyEvent,'keyCode') == 81 + if get(keyEvent, 'keyCode') == 81 obj.abortRequested = true; end end - function abortCallbackButton(obj,~,~,~) + function abortCallbackButton(obj, ~, ~, ~) obj.abortRequested = true; end + end methods (Static) - function available = IsAvailable() + + function available = isAvailable() available = matRad_checkMexFileExists('ipopt'); - - %Let's run a tiny testproblem to see if it really works + + % Let's run a tiny testproblem to see if it really works if available funcs.objective = @(x) x.^2; - funcs.gradient = @(x) 2*x; - funcs.hessian = @(x,sigma,lambda) sigma*sparse(2); + funcs.gradient = @(x) 2 * x; + funcs.hessian = @(x, sigma, lambda) sigma * sparse(2); funcs.hessianstructure = @() sparse(true); - + s.ipopt.tol = 1e-5; % (Opt1) s.ipopt.print_level = 0; s.ipopt.print_user_options = 'no'; s.ipopt.print_options_documentation = 'no'; - + try - [x,~] = ipopt(1,funcs,s); + [x, ~] = ipopt(1, funcs, s); assert(abs(x) < 1e-5); catch ME available = false; end end end + end end diff --git a/matRad/optimization/optimizer/matRad_OptimizerSimulannealbnd.m b/matRad/optimization/optimizer/matRad_OptimizerSimulannealbnd.m index 77279cddc..865aefe4e 100644 --- a/matRad/optimization/optimizer/matRad_OptimizerSimulannealbnd.m +++ b/matRad/optimization/optimizer/matRad_OptimizerSimulannealbnd.m @@ -1,19 +1,19 @@ classdef matRad_OptimizerSimulannealbnd < matRad_Optimizer - % matRad_OptimizerSimulannealbnd implements the interface for the Simulated Annealing optimizer + % matRad_OptimizerSimulannealbnd implements the interface for the Simulated Annealing optimizer % of the MATLAB Global Optimization toolbox - % + % % References % - % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2024 the matRad development team. - % - % This file is part of the matRad project. It is subject to the license - % terms in the LICENSE file found in the top-level directory of this - % distribution and at https://github.com/e0404/matRad/LICENSE.md. No part - % of the matRad project, including this file, may be copied, modified, - % propagated, or distributed except according to the terms contained in the + % Copyright 2024-2026 the matRad development team. + % + % This file is part of the matRad project. It is subject to the license + % terms in the LICENSE file found in the top-level directory of this + % distribution and at https://github.com/e0404/matRad/LICENSE.md. No part + % of the matRad project, including this file, may be copied, modified, + % propagated, or distributed except according to the terms contained in the % LICENSE file. % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -26,16 +26,17 @@ wResult % last optimization result resultInfo % info struct about last results end - + methods + function obj = matRad_OptimizerSimulannealbnd % matRad_OptimizerSimulannealbnd Constructor matRad_cfg = MatRad_Config.instance(); - if ~matRad_OptimizerSimulannealbnd.IsAvailable() - matRad_cfg.dipsError('matRad_OptimizerSimulannealbnd cannot be constructed as simulannealbnd is not available!'); + if ~matRad_OptimizerSimulannealbnd.isAvailable() + matRad_cfg.dispError('matRad_OptimizerSimulannealbnd cannot be constructed as simulannealbnd is not available!'); end - + obj.wResult = []; obj.resultInfo = []; @@ -50,52 +51,58 @@ end if matRad_cfg.disableGUI - pltFcns = {[]}; + pltFcns = {[]}; else - pltFcns = {@saplotbestf,@saplotbestx, @saplotf,@saplotx,@saplotstopping,@saplottemperature}; + end - + % Create default optimizer options obj.options = optimoptions('simulannealbnd', ... - 'InitialTemperature', 0.7, ... - 'TemperatureFcn',@temperatureboltz, ... - 'Display', optDisplay, ... - 'MaxIterations', matRad_cfg.defaults.propOpt.maxIter, ... - 'MaxFunctionEvaluations', 120000, ... - 'PlotFcn',pltFcns); + 'InitialTemperature', 0.7, ... + 'TemperatureFcn', @temperatureboltz, ... + 'Display', optDisplay, ... + 'MaxIterations', matRad_cfg.defaults.propOpt.maxIter, ... + 'MaxFunctionEvaluations', 120000, ... + 'PlotFcn', pltFcns); end - + function obj = optimize(obj, w0, optiProb, dij, cst) matRad_cfg = MatRad_Config.instance(); % optimize Carries out the optimization - + % Obtain lower and upper variable bounds lb = optiProb.lowerBounds(w0); ub = optiProb.upperBounds(w0); - + % Informing user to press q to terminate optimization matRad_cfg.dispInfo('Optimization initiating...\n'); matRad_cfg.dispInfo('Press q to terminate the optimization...\n'); - + % Define the objective function objectiveFunction = @(x) optiProb.matRad_objectiveFunction(x, dij, cst); - + + if obj.showPlot && ~matRad_cfg.disableGUI + obj.options.PlotFcn = {@saplotbestf, @saplotbestx, @saplotf, @saplotx, @saplotstopping, @saplottemperature}; + else + obj.options.PlotFcn = {[]}; + end + % Run simulated annealing optimization [obj.wResult, fVal, exitflag, info] = simulannealbnd(objectiveFunction, w0, lb, ub, obj.options); - + obj.resultInfo = info; obj.resultInfo.fVal = fVal; obj.resultInfo.exitflag = exitflag; end - function [statusmsg, statusflag] = GetStatus(obj) - try + function [statusmsg, statusflag] = getStatus(obj) + try statusmsg = obj.resultInfo.message; if obj.resultInfo.exitflag == 0 statusflag = 0; elseif obj.resultInfo.exitflag > 0 statusflag = 1; - else + else statusflag = -1; end catch @@ -103,11 +110,14 @@ statusflag = -1; end end + end - - methods (Static) - function available = IsAvailable() + + methods (Static) + + function available = isAvailable() available = exist('simulannealbnd', 'file') ~= 0; end + end end diff --git a/matRad/optimization/projections/matRad_BEDProjection.m b/matRad/optimization/projections/matRad_BEDProjection.m index 11720459c..2e109789a 100644 --- a/matRad/optimization/projections/matRad_BEDProjection.m +++ b/matRad/optimization/projections/matRad_BEDProjection.m @@ -3,7 +3,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2019 the matRad development team. +% Copyright 2019-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/optimization/projections/matRad_BackProjection.m b/matRad/optimization/projections/matRad_BackProjection.m index f46140e51..dee6fa6df 100644 --- a/matRad/optimization/projections/matRad_BackProjection.m +++ b/matRad/optimization/projections/matRad_BackProjection.m @@ -4,7 +4,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2019 the matRad development team. +% Copyright 2019-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/optimization/projections/matRad_ConstantRBEProjection.m b/matRad/optimization/projections/matRad_ConstantRBEProjection.m index a7a8d3f5e..0fda75f03 100644 --- a/matRad/optimization/projections/matRad_ConstantRBEProjection.m +++ b/matRad/optimization/projections/matRad_ConstantRBEProjection.m @@ -3,7 +3,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2019 the matRad development team. +% Copyright 2019-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/optimization/projections/matRad_DoseProjection.m b/matRad/optimization/projections/matRad_DoseProjection.m index e3001973c..12c25da06 100644 --- a/matRad/optimization/projections/matRad_DoseProjection.m +++ b/matRad/optimization/projections/matRad_DoseProjection.m @@ -3,7 +3,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2019 the matRad development team. +% Copyright 2019-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/optimization/projections/matRad_EffectProjection.m b/matRad/optimization/projections/matRad_EffectProjection.m index ca3e0dc7b..a1e026d27 100644 --- a/matRad/optimization/projections/matRad_EffectProjection.m +++ b/matRad/optimization/projections/matRad_EffectProjection.m @@ -3,7 +3,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2019 the matRad development team. +% Copyright 2019-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/optimization/projections/matRad_VariableRBEProjection.m b/matRad/optimization/projections/matRad_VariableRBEProjection.m index e5b4a4d96..e6d79c99e 100644 --- a/matRad/optimization/projections/matRad_VariableRBEProjection.m +++ b/matRad/optimization/projections/matRad_VariableRBEProjection.m @@ -3,7 +3,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2019 the matRad development team. +% Copyright 2019-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/phantoms/builder/matRad_PhantomBuilder.m b/matRad/phantoms/builder/matRad_PhantomBuilder.m index 46a1ab0e7..45c3bad9d 100644 --- a/matRad/phantoms/builder/matRad_PhantomBuilder.m +++ b/matRad/phantoms/builder/matRad_PhantomBuilder.m @@ -1,42 +1,47 @@ classdef matRad_PhantomBuilder < handle % matRad_PhantomBuilder - % Class that helps to create radiotherapy phantoms + % Class that helps to create radiotherapy phantoms % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2023 the matRad development team. - % - % This file is part of the matRad project. It is subject to the license - % terms in the LICENSE file found in the top-level directory of this - % distribution and at https://github.com/e0404/matRad/LICENSE.md. No part - % of the matRad project, including this file, may be copied, modified, - % propagated, or distributed except according to the terms contained in the + % Copyright 2023-2026 the matRad development team. + % + % This file is part of the matRad project. It is subject to the license + % terms in the LICENSE file found in the top-level directory of this + % distribution and at https://github.com/e0404/matRad/LICENSE.md. No part + % of the matRad project, including this file, may be copied, modified, + % propagated, or distributed except according to the terms contained in the % LICENSE file. % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% properties (Access = public) - volumes = {}; + volumes = {} end - properties (Access = private) - ct; - cst = {}; + properties (Access = private) + ct + cst = {} end methods (Access = public) - function obj = matRad_PhantomBuilder(ctDim,ctResolution,numOfCtScen) + + function obj = matRad_PhantomBuilder(ctDim, ctResolution, numOfCtScen) obj.ct = struct(); - obj.ct.cubeDim = [ctDim(2),ctDim(1),ctDim(3)]; - obj.ct.resolution.x = ctResolution(1); - obj.ct.resolution.y = ctResolution(2); - obj.ct.resolution.z = ctResolution(3); + obj.ct.cubeDim = [ctDim(2), ctDim(1), ctDim(3)]; + if isstruct(ctResolution) + obj.ct.resolution = ctResolution; + else + obj.ct.resolution.x = ctResolution(1); + obj.ct.resolution.y = ctResolution(2); + obj.ct.resolution.z = ctResolution(3); + end obj.ct.numOfCtScen = numOfCtScen; obj.ct.cubeHU{1} = ones(obj.ct.cubeDim) * -1000; end - %functions to create Targets %TODO: Option to extend volumes - function addBoxTarget(obj,name,dimensions,varargin) + % functions to create Targets %TODO: Option to extend volumes + function addBoxTarget(obj, name, dimensions, varargin) % Adds a box target % % input: @@ -44,17 +49,15 @@ function addBoxTarget(obj,name,dimensions,varargin) % dimensions: Dimensions of the box as [x,y,z] array % % Name-Value pairs: - % 'offset': The offset of the VOI with respect to the - % center of the geometry as [x,y,z] array - % 'objectives': Either a single objective or a cell array - % of objectives + % 'offset': The offset of the VOI with respect to the center of the geometry as [x,y,z] array + % 'objectives': Either a single objective or a cell array of objectives % 'HU': Houndsfield unit of the volume - obj.volumes(end+1) = {matRad_PhantomVOIBox(name,'TARGET',dimensions,varargin{:})}; + obj.volumes(end + 1) = {matRad_PhantomVOIBox(name, 'TARGET', dimensions, varargin{:})}; obj.updatecst(); end - function addSphericalTarget(obj,name,radius,varargin) + function addSphericalTarget(obj, name, radius, varargin) % Adds a spherical target % % input: @@ -62,18 +65,15 @@ function addSphericalTarget(obj,name,radius,varargin) % radius: Radius of the sphere % % Name-Value pairs: - % 'offset': The offset of the VOI with respect to the - % center of the geometry as [x,y,z] array - % 'objectives': Either a single objective or a cell array - % of objectives + % 'offset': The offset of the VOI with respect to the center of the geometry as [x,y,z] array + % 'objectives': Either a single objective or a cell array of objectives % 'HU': Houndsfield unit of the volume - obj.volumes(end+1) = {matRad_PhantomVOISphere(name,'TARGET',radius,varargin{:})}; + obj.volumes(end + 1) = {matRad_PhantomVOISphere(name, 'TARGET', radius, varargin{:})}; obj.updatecst(); end - - function addBoxOAR(obj,name,dimensions,varargin) + function addBoxOAR(obj, name, dimensions, varargin) % Adds a box OAR % % input: @@ -81,17 +81,15 @@ function addBoxOAR(obj,name,dimensions,varargin) % dimensions: Dimensions of the box as [x,y,z] array % % Name-Value pairs: - % 'offset': The offset of the VOI with respect to the - % center of the geometry as [x,y,z] array - % 'objectives': Either a single objective or a cell array - % of objectives + % 'offset': The offset of the VOI with respect to the center of the geometry as [x,y,z] array + % 'objectives': Either a single objective or a cell array of objectives % 'HU': Houndsfield unit of the volume - obj.volumes(end+1) = {matRad_PhantomVOIBox(name,'OAR',dimensions,varargin{:})}; + obj.volumes(end + 1) = {matRad_PhantomVOIBox(name, 'OAR', dimensions, varargin{:})}; obj.updatecst(); end - function addSphericalOAR(obj,name,radius,varargin) + function addSphericalOAR(obj, name, radius, varargin) % Adds a spherical OAR % % input: @@ -99,45 +97,42 @@ function addSphericalOAR(obj,name,radius,varargin) % radius: Radius of the sphere % % Name-Value pairs: - % 'offset': The offset of the VOI with respect to the - % center of the geometry as [x,y,z] array - % 'objectives': Either a single objective or a cell array - % of objectives + % 'offset': The offset of the VOI with respect to the center of the geometry as [x,y,z] array + % 'objectives': Either a single objective or a cell array of objectives % 'HU': Houndsfield unit of the volume - obj.volumes(end+1) ={matRad_PhantomVOISphere(name,'OAR',radius,varargin{:})}; + obj.volumes(end + 1) = {matRad_PhantomVOISphere(name, 'OAR', radius, varargin{:})}; obj.updatecst(); end - - function [ct,cst] = getctcst(obj) + function [ct, cst] = getctcst(obj) % Returns the ct and struct. The function also initializes - % the HUs in reverse order of defintion + % the HUs in reverse order of definition % - % output + % output: % ct: matRad ct struct % cst: matRad cst struct - - %initialize the HU in reverse order of definition (objectives - %defined at the start will have the highest priority in case of - %overlaps) - - for i = 1:size(obj.cst,1) - vIxVOI = obj.cst{end-i+1,4}{1}; - obj.ct.cubeHU{1}(vIxVOI) = obj.volumes{1,end-i+1}.HU; % assign HU + + % initialize the HU in reverse order of definition (objectives + % defined at the start will have the highest priority in case of + % overlaps) + + for i = 1:size(obj.cst, 1) + vIxVOI = obj.cst{end - i + 1, 4}{1}; + obj.ct.cubeHU{1}(vIxVOI) = obj.volumes{1, end - i + 1}.HU; % assign HU end - + ct = obj.ct; cst = obj.cst; end - end + end methods (Access = private) - function updatecst(obj) - obj.cst = obj.volumes{end}.initializeParameters(obj.ct,obj.cst); + function updatecst(obj) + obj.cst = obj.volumes{end}.initializeParameters(obj.ct, obj.cst); end end -end \ No newline at end of file +end diff --git a/matRad/phantoms/builder/matRad_PhantomVOIBox.m b/matRad/phantoms/builder/matRad_PhantomVOIBox.m index e66c7b1fa..e469a5f0a 100644 --- a/matRad/phantoms/builder/matRad_PhantomVOIBox.m +++ b/matRad/phantoms/builder/matRad_PhantomVOIBox.m @@ -4,11 +4,8 @@ % References % - % - % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - % - % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2022 the matRad development team. + % Copyright 2022-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -18,56 +15,76 @@ % LICENSE file. % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - properties %additional property of cubic objects - boxDimensions; + properties % additional property of cubic objects + boxDimensions end methods (Access = public) - function obj = matRad_PhantomVOIBox(name,type,boxDimensions,varargin) + function obj = matRad_PhantomVOIBox(name, type, boxDimensions, varargin) p = inputParser; - addParameter(p,'objectives',{}); - addParameter(p,'offset',[0,0,0]); - addParameter(p,'HU',0); - parse(p,varargin{:}); + addParameter(p, 'objectives', {}); + addParameter(p, 'offset', [0, 0, 0]); + addParameter(p, 'HU', 0); + addParameter(p, 'coordType', 'voxel', @(x) numel(validatestring(x, {'voxel', 'mm'}))); + parse(p, varargin{:}); - obj@matRad_PhantomVOIVolume(name,type,p); %call superclass constructor + obj@matRad_PhantomVOIVolume(name, type, p); % call superclass constructor obj.boxDimensions = boxDimensions; end - function [cst] = initializeParameters(obj,ct,cst) - %add this objective to the phantomBuilders cst - - cst = initializeParameters@matRad_PhantomVOIVolume(obj,cst); - center = round(ct.cubeDim/2); - VOIHelper = zeros(ct.cubeDim); - offsets = obj.offset; - dims = obj.boxDimensions; - - xMinMax = center(2)+offsets(1) + round(dims(1)/2)*[-1,1]; - yMinMax = center(1)+offsets(2) + round(dims(2)/2)*[-1,1]; - zMinMax = center(3)+offsets(3) + round(dims(3)/2)*[-1,1]; - - %Correct if out of bounds - xMinMax(xMinMax < 1) = 1; - yMinMax(yMinMax < 1) = 1; - zMinMax(zMinMax < 1) = 1; - - xMinMax(xMinMax > ct.cubeDim(2)) = ct.cubeDim(2); - yMinMax(yMinMax > ct.cubeDim(1)) = ct.cubeDim(1); - zMinMax(zMinMax > ct.cubeDim(3)) = ct.cubeDim(3); - - for x = xMinMax(1):1:xMinMax(2) - for y = yMinMax(1):1:yMinMax(2) - for z = zMinMax(1):1:zMinMax(2) - VOIHelper(y,x,z) = 1; - end - end + function [cst] = initializeParameters(obj, ct, cst) + % add this objective to the phantomBuilders cst + ct = matRad_getWorldAxes(ct); + cst = initializeParameters@matRad_PhantomVOIVolume(obj, cst); + + % Swaps [i j k] (x-first) <-> [j i k] (y-first / MATLAB array order) + dimPerm = [0 1 0; 1 0 0; 0 0 1]; + + % Center in [j i k] (cubeDim is already in MATLAB array order) + centerPoint = (ct.cubeDim + 1) / 2; + + switch obj.coordType + case 'voxel' + ctMin = [1 1 1]; + ctMax = ct.cubeDim; % [j i k] + [y, x, z] = ndgrid(1:ct.cubeDim(1), 1:ct.cubeDim(2), 1:ct.cubeDim(3)); + + case 'mm' + % cubeIndex2worldCoords expects [i j k], outputs [x y z]; + % * dimPerm converts to [y x z] = [j i k] in world mm + centerPoint = matRad_cubeIndex2worldCoords(centerPoint, ct) * dimPerm; + halfRes = [ct.resolution.y ct.resolution.x ct.resolution.z] / 2; + ctMin = [min(ct.y) min(ct.x) min(ct.z)] - halfRes; + ctMax = [max(ct.y) max(ct.x) max(ct.z)] + halfRes; + % ct.y has nRows elements (dim1), ct.x has nCols elements (dim2) + [y, x, z] = ndgrid(ct.y, ct.x, ct.z); end - - cst{end,4}{1} = find(VOIHelper); - + % offset and boxDimensions are in [i j k]; convert to [j i k] + centerPoint = centerPoint + obj.offset * dimPerm; + dims = obj.boxDimensions * dimPerm; + + coords = [y(:) x(:) z(:)]; % [j i k] + + maxPoints = min(centerPoint + dims / 2, ctMax); + minPoints = max(centerPoint - dims / 2, ctMin); + + voiHelper = all(coords >= minPoints & coords <= maxPoints, 2); + voiHelper = reshape(voiHelper, ct.cubeDim); + + cst{end, 4}{1} = find(voiHelper); + end + + end + + % Set Methods + methods + + function set.boxDimensions(obj, dims) + validateattributes(dims, {'numeric'}, {'vector', 'numel', 3, 'positive'}); + obj.boxDimensions = dims; end + end -end \ No newline at end of file +end diff --git a/matRad/phantoms/builder/matRad_PhantomVOISphere.m b/matRad/phantoms/builder/matRad_PhantomVOISphere.m index c0e457f62..67b88edfe 100644 --- a/matRad/phantoms/builder/matRad_PhantomVOISphere.m +++ b/matRad/phantoms/builder/matRad_PhantomVOISphere.m @@ -4,11 +4,8 @@ % References % - % - % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - % - % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2022 the matRad development team. + % Copyright 2022-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -19,42 +16,67 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% properties - radius; + radius end methods (Access = public) - function obj = matRad_PhantomVOISphere(name,type,radius,varargin) + + function obj = matRad_PhantomVOISphere(name, type, radius, varargin) p = inputParser; - addParameter(p,'objectives',{}); - addParameter(p,'offset',[0,0,0]); - addParameter(p,'HU',0); - parse(p,varargin{:}); + addParameter(p, 'objectives', {}); + addParameter(p, 'offset', [0, 0, 0]); + addParameter(p, 'HU', 0); + addParameter(p, 'coordType', 'voxel', @(x) numel(validatestring(x, {'voxel', 'mm'}))); % numel trick to guarantee logical cast + parse(p, varargin{:}); - obj@matRad_PhantomVOIVolume(name,type,p); %call superclass constructor + obj@matRad_PhantomVOIVolume(name, type, p); % call superclass constructor obj.radius = radius; end - function [cst] = initializeParameters(obj,ct,cst) - %add this VOI to the phantomBuilders cst - - cst = initializeParameters@matRad_PhantomVOIVolume(obj,cst); - center = round([ct.cubeDim/2]); - VOIHelper = zeros(ct.cubeDim); - offsets = obj.offset; - - for x = 1:ct.cubeDim(2) - for y = 1:ct.cubeDim(1) - for z = 1:ct.cubeDim(3) - currPost = [y x z] + offsets - center; - if (sqrt(sum(currPost.^2)) < obj.radius) - VOIHelper(y,x,z) = 1; - end - end - end + function [cst] = initializeParameters(obj, ct, cst) + % add this VOI to the phantomBuilders cst + ct = matRad_getWorldAxes(ct); + cst = initializeParameters@matRad_PhantomVOIVolume(obj, cst); + + % Swaps [i j k] (x-first) <-> [j i k] (y-first / MATLAB array order) + dimPerm = [0 1 0; 1 0 0; 0 0 1]; + + % center as continuuos [j i k] + centerPoint = (ct.cubeDim + 1) / 2; + + switch obj.coordType + case 'voxel' + % Grid in [j i k]: y (rows) along dim1, x (cols) along dim2 + [y, x, z] = ndgrid(1:ct.cubeDim(1), 1:ct.cubeDim(2), 1:ct.cubeDim(3)); + + case 'mm' + % cubeIndex2worldCoords expects [j i k], outputs [x y z]; + % apply dimPerm to arrive at [y x z] = [j i k] in world mm + centerPoint = matRad_cubeIndex2worldCoords(centerPoint, ct) * dimPerm; + % ct.y has nRows elements (dim1), ct.x has nCols elements (dim2) + [y, x, z] = ndgrid(ct.y, ct.x, ct.z); end - - cst{end,4}{1} = find(VOIHelper); - + + % offset is always in [i j k]; convert to [j i k] before adding + centerPoint = centerPoint + obj.offset * dimPerm; + + % Both modes: grid and center are in [j i k] - no extra permutation needed + voiHelper = vecnorm([y(:) x(:) z(:)] - centerPoint, 2, 2) < obj.radius; + voiHelper = reshape(voiHelper, ct.cubeDim); + + cst{end, 4}{1} = find(voiHelper); + end + + end + + % Set Methods + methods + + function set.radius(obj, value) + validateattributes(value, {'numeric'}, {'scalar', 'positive'}); + obj.radius = value; + end + end -end \ No newline at end of file +end diff --git a/matRad/phantoms/builder/matRad_PhantomVOIVolume.m b/matRad/phantoms/builder/matRad_PhantomVOIVolume.m index 8f35db910..6ad0de19f 100644 --- a/matRad/phantoms/builder/matRad_PhantomVOIVolume.m +++ b/matRad/phantoms/builder/matRad_PhantomVOIVolume.m @@ -1,84 +1,93 @@ classdef (Abstract) matRad_PhantomVOIVolume < handle -% matRad_PhantomVOIVolume: Interface for VOI Volumes -% This abstract base class provides the structure of VOI Volumes. -% So far implemented: Box and spherical objectives -% -% -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% -% Copyright 2023 the matRad development team. -% -% This file is part of the matRad project. It is subject to the license -% terms in the LICENSE file found in the top-level directory of this -% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part -% of the matRad project, including this file, may be copied, modified, -% propagated, or distributed except according to the terms contained in the -% LICENSE file. -% -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + % matRad_PhantomVOIVolume: Interface for VOI Volumes + % This abstract base class provides the structure of VOI Volumes. + % So far implemented: Box and spherical objectives + % + % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + % + % Copyright 2023-2026 the matRad development team. + % + % This file is part of the matRad project. It is subject to the license + % terms in the LICENSE file found in the top-level directory of this + % distribution and at https://github.com/e0404/matRad/LICENSE.md. No part + % of the matRad project, including this file, may be copied, modified, + % propagated, or distributed except according to the terms contained in the + % LICENSE file. + % + % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% properties - name; - type; - TissueClass = 1; - alphaX = 0.1000; - betaX = 0.0500; - Priority = 1; - Visible = 1; - visibleColor = [0 0 0]; - HU = 0; - offset = [0,0,0]; %center of objective - objectives = {}; - colors = [[1,0,0];[0,1,0];[0,0,1];[1,1,0];[1,0,1];[0,1,1];[1,1,1]]; + name + type + TissueClass = 1 % mh:ignore_style + alphaX = 0.1000 + betaX = 0.0500 + Priority = 1 % mh:ignore_style + Visible = 1 % mh:ignore_style + visibleColor = [0 0 0] + HU = 0 + offset = [0, 0, 0] % center of objective + coordType = 'voxel' % 'voxel' or 'mm' - governs how dimensions and offset are interpreted + objectives = {} + colors = [[1, 0, 0]; [0, 1, 0]; [0, 0, 1]; [1, 1, 0]; [1, 0, 1]; [0, 1, 1]; [1, 1, 1]] end methods - function obj = matRad_PhantomVOIVolume(name,type,p) - %p is the input parser used in the child classes to check for additional variables - + + function obj = matRad_PhantomVOIVolume(name, type, p) + % p is the input parser used in the child classes to check for additional variables + obj.name = name; obj.type = type; obj.offset = p.Results.offset; obj.HU = p.Results.HU; + obj.coordType = p.Results.coordType; - - %idea is that DoseObjectiveFunction can be either a single objective or an - %array of objectives. If it is a single objective store it as a cell array + % idea is that DoseObjectiveFunction can be either a single objective or an + % array of objectives. If it is a single objective store it as a cell array if iscell(p.Results.objectives) obj.objectives = p.Results.objectives; - else + else obj.objectives = {p.Results.objectives}; end %} end - function cst = initializeParameters(obj,cst) - %initialize entry for this VOI in cst - nxIdx = size(cst,1)+1; - cst{nxIdx,1} = nxIdx-1; - cst{nxIdx,2} = obj.name; - cst{nxIdx,3} = obj.type; - cst{nxIdx,5}.TissueClass = obj.TissueClass; - cst{nxIdx,5}.alphaX = obj.alphaX; - cst{nxIdx,5}.betaX = obj.betaX; - cst{nxIdx,5}.Priority = nxIdx; - cst{nxIdx,5}.Visible = obj.Visible; + function cst = initializeParameters(obj, cst) + % initialize entry for this VOI in cst + nxIdx = size(cst, 1) + 1; + cst{nxIdx, 1} = nxIdx - 1; + cst{nxIdx, 2} = obj.name; + cst{nxIdx, 3} = obj.type; + cst{nxIdx, 5}.TissueClass = obj.TissueClass; + cst{nxIdx, 5}.alphaX = obj.alphaX; + cst{nxIdx, 5}.betaX = obj.betaX; + cst{nxIdx, 5}.Priority = nxIdx; + cst{nxIdx, 5}.Visible = obj.Visible; - if nxIdx <= size(obj.colors,1) - obj.visibleColor = obj.colors(nxIdx,:); + if nxIdx <= size(obj.colors, 1) + obj.visibleColor = obj.colors(nxIdx, :); end - cst{nxIdx,5}.visibleColor = obj.visibleColor; + cst{nxIdx, 5}.visibleColor = obj.visibleColor; - if ~iscell(obj.objectives) %should be redundant - DoseObjectives = {obj.objectives}; + if ~iscell(obj.objectives) % should be redundant + DoseObjectives = {obj.objectives}; else DoseObjectives = obj.objectives; end for i = 1:numel(DoseObjectives) - cst{nxIdx,6} {i}= DoseObjectives{i}; + cst{nxIdx, 6} {i} = DoseObjectives{i}; end end + + end + + % Set methods + methods + + function set.coordType(obj, cType) + obj.coordType = validatestring(cType, {'voxel', 'mm'}); + end + end -end \ No newline at end of file +end diff --git a/matRad/planAnalysis/matRad_EQD2accumulation.m b/matRad/planAnalysis/matRad_EQD2accumulation.m index 3090164d2..7603d8c3e 100644 --- a/matRad/planAnalysis/matRad_EQD2accumulation.m +++ b/matRad/planAnalysis/matRad_EQD2accumulation.m @@ -4,11 +4,11 @@ % matRad function to accumulate and compare dose and EQD2 for two treatment % plans % -% call +% call: % result = matRad_EQD2accumulation(pln1,ct1,cst1,dose1,prescribedDose1, ... % pln2,ct2,cst2,dose2,prescribedDose2) % -% input +% input: % pln1/2: matRad pln struct % ct1/2: matRad ct struct % cst1/2: matRad cst struct @@ -17,7 +17,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2019 the matRad development team. +% Copyright 2019-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -160,4 +160,4 @@ title('fixed vs moving'); figure,imshowpair( fixed(:,:,50),movedCT(:,:,50),'scaling','joint'); title('fixed vs moved'); -end \ No newline at end of file +end diff --git a/matRad/planAnalysis/matRad_calcDVH.m b/matRad/planAnalysis/matRad_calcDVH.m index 9b566dae4..6de611b70 100644 --- a/matRad/planAnalysis/matRad_calcDVH.m +++ b/matRad/planAnalysis/matRad_calcDVH.m @@ -1,13 +1,13 @@ function dvh = matRad_calcDVH(cst,doseCube,dvhType,doseGrid) % matRad dvh calculation % -% call +% call: % dvh = matRad_calcDVH(cst,doseCube) % dvh = matRad_calcDVH(cst,doseCube,dvhType) % dvh = matRad_calcDVH(cst,doseCube,doseGrid) % dvh = matRad_calcDVH(cst,doseCube,dvhType,doseGrid) % -% input +% input: % cst: matRad cst struct % doseCube: arbitrary doseCube (e.g. physicalDose) % dvhType: (optional) string, 'cum' for cumulative, 'diff' for differential @@ -15,7 +15,7 @@ % doseGrid: (optional) use predefined evaluation points. Useful when % comparing multiple realizations % -% output +% output: % dose volume histogram % % References @@ -24,7 +24,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2016 the matRad development team. +% Copyright 2016-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/planAnalysis/matRad_calcQualityIndicators.m b/matRad/planAnalysis/matRad_calcQualityIndicators.m index 8088b7ee3..ebc724618 100644 --- a/matRad/planAnalysis/matRad_calcQualityIndicators.m +++ b/matRad/planAnalysis/matRad_calcQualityIndicators.m @@ -1,11 +1,11 @@ function qi = matRad_calcQualityIndicators(cst,pln,doseCube,refGy,refVol) % matRad QI calculation % -% call +% call: % qi = matRad_calcQualityIndicators(cst,pln,doseCube) % qi = matRad_calcQualityIndicators(cst,pln,doseCube,refGy,refVol) % -% input +% input: % cst: matRad cst struct % pln: matRad pln struct % doseCube: arbitrary doseCube (e.g. physicalDose) @@ -15,7 +15,7 @@ % default is [2 5 95 98] % NOTE: Call either both or none! % -% output +% output: % qi various quality indicators like CI, HI (for % targets) and DX, VX within a structure set % @@ -25,7 +25,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2016 the matRad development team. +% Copyright 2016-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/planAnalysis/matRad_compareDose.m b/matRad/planAnalysis/matRad_compareDose.m index f79feb831..1465954b3 100644 --- a/matRad/planAnalysis/matRad_compareDose.m +++ b/matRad/planAnalysis/matRad_compareDose.m @@ -1,11 +1,11 @@ function [gammaCube,gammaPassRate,hfig] = matRad_compareDose(cube1, cube2, ct, cst,enable , contours, pln, criteria, n,localglobal) % Comparison of two dose cubes in terms of gamma index, absolute and visual difference % -% call +% call: % [gammaCube,gammaPassRate,hfig] = matRad_compareDose(cube1, cube2, ct, cst) % [gammaCube,gammaPassRate,hfig] = matRad_compareDose(cube1, cube2, ct, cst,enable , contours, pln, criteria, n,localglobal) % -% input +% input: % cube1: dose cube 1 as an M x N x O array % cube2: dose cube 2 as an M x N x O array % ct: ct struct with ct cube @@ -29,7 +29,7 @@ % normalization % % -% output +% output: % % gammaCube: result of gamma index calculation % gammaPassRate: rate of voxels passing the specified gamma criterion @@ -44,7 +44,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -148,6 +148,9 @@ % Calculate absolute difference cube and dose windows for plots differenceCube = cube1-cube2; doseDiffWindow = [-max(abs(differenceCube(:))) max(abs(differenceCube(:)))]; + if doseDiffWindow(1)==0 && doseDiffWindow(2)==0 + doseDiffWindow(2) = 1; + end %doseGammaWindow = [0 max(gammaCube(:))]; doseGammaWindow = [0 2]; %We choose 2 as maximum value since the gamma colormap has a sharp cut in the middle @@ -298,4 +301,4 @@ function profilePlot(hAx,x,y1,y2,titleTxt,nameProfile1,nameProfile2, xLabelTxt,y ylabel(yLabelTxt,'FontSize',guiSettings.fontSize); title(titleTxt,'Color',guiSettings.highlightColor); legend({nameProfile1,nameProfile2},'Location','best','TextColor',guiSettings.textColor,'Box','off'); -end \ No newline at end of file +end diff --git a/matRad/planAnalysis/matRad_gammaIndex.m b/matRad/planAnalysis/matRad_gammaIndex.m index dfcbdbdce..347817066 100644 --- a/matRad/planAnalysis/matRad_gammaIndex.m +++ b/matRad/planAnalysis/matRad_gammaIndex.m @@ -2,43 +2,36 @@ % gamma index calculation % according to http://www.ncbi.nlm.nih.gov/pubmed/9608475 % -% call +% call: % [gammaCube,gammaPassRateCell] = matRad_gammaIndex(cube1,cube2,resolution,criteria,cst) % [gammaCube,gammaPassRateCell] = matRad_gammaIndex(cube1,cube2,resolution,criteria,slice,cst) % [gammaCube,gammaPassRateCell] = matRad_gammaIndex(cube1,cube2,resolution,criteria,n,cst) % [gammaCube,gammaPassRateCell] = matRad_gammaIndex(cube1,cube2,resolution,criteria,localglobal,cst) % [gammaCube,gammaPassRateCell] = matRad_gammaIndex(cube1,cube2,resolution,criteria,slice,n,cst) -% ... +% ... % [gammaCube,gammaPassRateCell] = matRad_gammaIndex(cube1,cube2,resolution,criteria,slice,n,localglobal,cst) % -% input +% input: % cube1: dose cube as an M x N x O array % cube2: dose cube as an M x N x O array % resolution: resolution of the cubes [mm/voxel] -% criteria: [1x2] vector specifying the distance to agreement -% criterion; first element is percentage difference, -% second element is distance [mm] -% slice: (optional) slice in cube1/2 that will be visualized -% n: (optional) number of interpolations. there will be 2^n-1 -% interpolation points. The maximum suggested value is 3. -% localglobal: (optional) parameter to choose between 'global' and 'local' -% normalization +% criteria: [1x2] vector specifying the distance to agreement criterion; first element is percentage difference, second element is distance [mm] +% slice: (optional) slice in cube1/2 that will be visualized +% n: (optional) number of interpolations. there will be 2^n-1 interpolation points. The maximum suggested value is 3. +% localglobal: (optional) parameter to choose between 'global' and 'local' normalization % cst: list of interessing volumes inside the patient % % output % % gammaCube: result of gamma index calculation -% gammaPassRateCell: rate of voxels passing the specified gamma criterion -% evaluated for every structure listed in 'cst'. -% note that only voxels exceeding the dose threshold are -% considered. +% gammaPassRateCell: rate of voxels passing the specified gamma criterion, evaluated for every structure listed in 'cst'. Note that only voxels exceeding the dose threshold are considered. % % References % [1] http://www.ncbi.nlm.nih.gov/pubmed/9608475 % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/planAnalysis/matRad_indicatorWrapper.m b/matRad/planAnalysis/matRad_indicatorWrapper.m index 9e3bf18f2..d0627d689 100644 --- a/matRad/planAnalysis/matRad_indicatorWrapper.m +++ b/matRad/planAnalysis/matRad_indicatorWrapper.m @@ -1,11 +1,11 @@ function [dvh,qi] = matRad_indicatorWrapper(cst,pln,resultGUI,refGy,refVol) % matRad indictor wrapper % -% call +% call: % [dvh,qi] = matRad_indicatorWrapper(cst,pln,resultGUI) % [dvh,qi] = matRad_indicatorWrapper(cst,pln,resultGUI,refGy,refVol) % -% input +% input: % cst: matRad cst struct % pln: matRad pln struct % resultGUI: matRad resultGUI struct @@ -15,7 +15,7 @@ % default is [2 5 95 98] % NOTE: Call either both or none! % -% output +% output: % dvh: matRad dvh result struct % qi: matRad quality indicator result struct % graphical display of all results @@ -25,7 +25,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2017 the matRad development team. +% Copyright 2017-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/planAnalysis/matRad_sampling.m b/matRad/planAnalysis/matRad_sampling.m index a7d442604..3e45b0a4e 100644 --- a/matRad/planAnalysis/matRad_sampling.m +++ b/matRad/planAnalysis/matRad_sampling.m @@ -1,17 +1,17 @@ function [caSampRes, mSampDose, pln, resultGUInomScen] = matRad_sampling(ct,stf,cst,pln,w,structSel,multScen) % matRad_randomSampling enables sampling multiple treatment scenarios % -% call +% call: % [cst,pln] = matRad_setPlanUncertainties(ct,cst,pln) % -% input +% input: % ct: ct cube % stf: matRad steering information struct % pln: matRad plan meta information struct % cst: matRad cst struct % w: optional (if no weights available in stf): bixel weight % vector -% output +% output: % caSampRes: cell array of sampling results depicting plan parameter % mSampDose: matrix holding the sampled doses, each row corresponds to % one dose sample @@ -21,7 +21,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2017 the matRad development team. +% Copyright 2017-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/planAnalysis/matRad_showDVH.m b/matRad/planAnalysis/matRad_showDVH.m index b9ebde1cb..fc7760784 100644 --- a/matRad/planAnalysis/matRad_showDVH.m +++ b/matRad/planAnalysis/matRad_showDVH.m @@ -1,13 +1,13 @@ function matRad_showDVH(dvh,cst,varargin) % matRad dvh visualizaion % -% call +% call: % matRad_showDVH(dvh,cst) % matRad_showDVH(dvh,cst,pln) % matRad_showDVH(dvh,cst,Name,Value) % matRad_showDVH(dvh,cst,pln,Name,Value) % -% input +% input: % dvh: result struct from fluence optimization/sequencing % cst: matRad cst struct % pln: (now optional) matRad pln struct, @@ -16,7 +16,7 @@ function matRad_showDVH(dvh,cst,varargin) % (hint: use different lineStyles to overlay % different dvhs) % -% output +% output: % graphical display of DVH % % References @@ -24,7 +24,7 @@ function matRad_showDVH(dvh,cst,varargin) % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/planAnalysis/matRad_showQualityIndicators.m b/matRad/planAnalysis/matRad_showQualityIndicators.m index 6ccdf296f..3cd1efff9 100644 --- a/matRad/planAnalysis/matRad_showQualityIndicators.m +++ b/matRad/planAnalysis/matRad_showQualityIndicators.m @@ -1,14 +1,14 @@ function matRad_showQualityIndicators(figHandle,qi) % matRad display of quality indicators as table % -% call +% call: % matRad_showQualityIndicators(qi) % -% input +% input: % figHandle: handle to figure to display the Quality Indicators in % qi: result struct from matRad_calcQualityIndicators % -% output +% output: % graphical display of quality indicators in table form % % References @@ -16,7 +16,7 @@ function matRad_showQualityIndicators(figHandle,qi) % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -87,33 +87,6 @@ function matRad_showQualityIndicators(figHandle,qi) 'ForegroundColor',matRad_cfg.gui.textColor,... 'BackgroundColor',colorMatrix,... 'RowStriping','on'); - - %Try to adapt the position of the table - try - ext = get(table,'Extent'); - - pixPosTableBefore = getpixelposition(table); - - relScrollSize = 16./pixPosTableBefore([3 4]); - - posOld = pos; - - if ext(3) < pos(3) - pos(3) = ext(3) + relScrollSize(1); - pos(1) = posOld(3) - pos(3); - end - - if ext(4) < pos(4) - pos(4) = ext(4) + relScrollSize(2); - pos(2) = posOld(4) - pos(4); - end - - set(table,'Position',pos); - - - - catch - end catch ME matRad_cfg.dispWarning('The uitable function is not implemented in %s v%s.',env,vStr); end diff --git a/matRad/planAnalysis/samplingAnalysis/matRad_calcStudy.m b/matRad/planAnalysis/samplingAnalysis/matRad_calcStudy.m index ef8412d7c..7b645fdff 100644 --- a/matRad/planAnalysis/samplingAnalysis/matRad_calcStudy.m +++ b/matRad/planAnalysis/samplingAnalysis/matRad_calcStudy.m @@ -1,11 +1,10 @@ function matRad_calcStudy(multScen,varargin) -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % matRad uncertainty study wrapper % -% call +% call: % matRad_calcStudy(structSel,multScen,matPatientPath,param) % -% input +% input: % structSel: structures which should be examined (can be empty, % to examine all structures) cube % multScen: parameterset of uncertainty analysis @@ -13,14 +12,14 @@ function matRad_calcStudy(multScen,varargin) % empty mat file in current folder will be used % param: structure defining additional parameter % outputPath -% output +% output: % (binary) all results are saved; a pdf report will be generated % and saved % % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2017 the matRad development team. +% Copyright 2017-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/planAnalysis/samplingAnalysis/matRad_createAnimationForLatexReport.m b/matRad/planAnalysis/samplingAnalysis/matRad_createAnimationForLatexReport.m index 136ff19e0..a45969146 100644 --- a/matRad/planAnalysis/samplingAnalysis/matRad_createAnimationForLatexReport.m +++ b/matRad/planAnalysis/samplingAnalysis/matRad_createAnimationForLatexReport.m @@ -1,11 +1,11 @@ function matRad_createAnimationForLatexReport(confidenceValue, ct, cst, slice, meanCube, mRealizations, scenProb, subIx, outpath, legendColorbar,varargin) % matRad function to create figures for a GIF animation % -% call +% call: % matRad_createAnimationForLatexReport(confidenceValue, ct, cst, slice, ... % meanCube, mRealizations, scenProb, subIx, outpath, legendColorbar) % -% input +% input: % confidenceValue confidence used for visualization % ct matRad ct struct % cst matRad cst struct @@ -24,14 +24,14 @@ function matRad_createAnimationForLatexReport(confidenceValue, ct, cst, slice, m % Period total period [s] for the animation (default 5) % FilePrefix default 'anim' % -% output +% output: % % References % % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2017 the matRad development team. +% Copyright 2017-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/planAnalysis/samplingAnalysis/matRad_getGaussianOrbitSamples.m b/matRad/planAnalysis/samplingAnalysis/matRad_getGaussianOrbitSamples.m index f4284b523..f35f64872 100644 --- a/matRad/planAnalysis/samplingAnalysis/matRad_getGaussianOrbitSamples.m +++ b/matRad/planAnalysis/samplingAnalysis/matRad_getGaussianOrbitSamples.m @@ -1,12 +1,12 @@ function samples = matRad_getGaussianOrbitSamples(mu,SIGMA,nFrames,varargin) % matRad orbit sampling % -% call +% call: % samples = matRad_getGaussianOrbitSamples(mu,SIGMA,nFrames) % samples = matRad_getGaussianOrbitSamples(mu,SIGMA,nFrames,xr) % samples = matRad_getGaussianOrbitSamples(___,Name,Value) % -% input +% input: % mu mean vector % SIGMA covariance matrix % nFrames number of sample frames @@ -19,7 +19,7 @@ % cholesky decomposition. Default is 10 % % -% output +% output: % % References % [1] http://mlss.tuebingen.mpg.de/2013/Hennig_2013_Animating_Samples_from_Gaussian_Distributions.pdf @@ -27,7 +27,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2017 the matRad development team. +% Copyright 2017-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/planAnalysis/samplingAnalysis/matRad_latexReport.m b/matRad/planAnalysis/samplingAnalysis/matRad_latexReport.m index 21bde12ea..75d0e2cc1 100644 --- a/matRad/planAnalysis/samplingAnalysis/matRad_latexReport.m +++ b/matRad/planAnalysis/samplingAnalysis/matRad_latexReport.m @@ -1,10 +1,10 @@ function success = matRad_latexReport(outputPath, ct, cst, pln, nominalScenario, structureStat, doseStat, sampDose, listOfQI, varargin) % matRad uncertainty analysis report generaator function % -% call +% call: % latexReport(ct, cst, pln, nominalScenario, structureStat) % -% input +% input: % outputPath: where to generate the report % ct: ct cube % cst: matRad cst struct @@ -27,13 +27,13 @@ % the largest value found in target objectives % -% output +% output: % (binary) a pdf report will be generated and saved % % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2017 the matRad development team. +% Copyright 2017-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/planAnalysis/samplingAnalysis/matRad_samplingAnalysis.m b/matRad/planAnalysis/samplingAnalysis/matRad_samplingAnalysis.m index 368ee5d23..aece55f0e 100644 --- a/matRad/planAnalysis/samplingAnalysis/matRad_samplingAnalysis.m +++ b/matRad/planAnalysis/samplingAnalysis/matRad_samplingAnalysis.m @@ -1,11 +1,10 @@ function [cstStat, doseStat, meta] = matRad_samplingAnalysis(ct,cst,pln,caSampRes,mSampDose,resultGUInomScen,varargin) -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % matRad uncertainty sampling analysis function % -% call +% call: % [structureStat, doseStat] = samplingAnalysis(ct,cst,subIx,mSampDose,w) % -% input +% input: % ct: ct cube % cst: matRad cst struct % pln: matRad's pln struct @@ -18,10 +17,10 @@ % settings % - 'GammaCriterion': 1x2 vector [% mm] % - 'Percentiles': vector with desired percentiles -% between (0,1) +% between (0,1) % % -% output +% output: % cstStat structure-wise statistics (mean, max, percentiles, ...) % doseStat dose-wise statistics (mean, max, percentiles, ...) % meta contains additional information about sampling analysis @@ -29,7 +28,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2017 the matRad development team. +% Copyright 2017-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/planAnalysis/samplingAnalysis/matRad_setupStudyTemplate.m b/matRad/planAnalysis/samplingAnalysis/matRad_setupStudyTemplate.m index 807c72afe..f4557a019 100644 --- a/matRad/planAnalysis/samplingAnalysis/matRad_setupStudyTemplate.m +++ b/matRad/planAnalysis/samplingAnalysis/matRad_setupStudyTemplate.m @@ -1,7 +1,7 @@ % configuration script for uncertainty sampling % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2017 the matRad development team. +% Copyright 2017-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/plotting/colormaps/diffMap.m b/matRad/plotting/colormaps/diffMap.m index 4e51f549b..344ccc9ab 100644 --- a/matRad/plotting/colormaps/diffMap.m +++ b/matRad/plotting/colormaps/diffMap.m @@ -3,7 +3,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/plotting/colormaps/gammaIndex.m b/matRad/plotting/colormaps/gammaIndex.m index 7f3bc36a9..76bb68ed5 100644 --- a/matRad/plotting/colormaps/gammaIndex.m +++ b/matRad/plotting/colormaps/gammaIndex.m @@ -6,7 +6,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/plotting/matRad_computeAllVoiSurfaces.m b/matRad/plotting/matRad_computeAllVoiSurfaces.m index b41a1541f..2bfec0329 100644 --- a/matRad/plotting/matRad_computeAllVoiSurfaces.m +++ b/matRad/plotting/matRad_computeAllVoiSurfaces.m @@ -1,14 +1,14 @@ function cst = matRad_computeAllVoiSurfaces(ct,cst) % matRad function that computes all VOI surfaces % -% call +% call: % cst = matRad_computeAllVoiSurfaces(ct,cst) % -% input +% input: % ct matRad ct struct % cst matRad cst struct % -% output +% output: % cst the new cst with the column containing the precomputed surface % % References @@ -16,7 +16,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/plotting/matRad_computeIsoDoseContours.m b/matRad/plotting/matRad_computeIsoDoseContours.m index b37a152ad..dfec32392 100644 --- a/matRad/plotting/matRad_computeIsoDoseContours.m +++ b/matRad/plotting/matRad_computeIsoDoseContours.m @@ -2,14 +2,14 @@ % matRad function that computes all isodse contours % (along each dose cube dimension) % -% call +% call: % isoDoseContours = matRad_computeIsoDoseContours(doseCube,isoLevels) % -% input +% input: % doseCube 3D array containing the dose cube % isoLevels iso dose levels (same units as doseCube) % -% output +% output: % isoDoseContours cell array containing the isolines along each dim % % References @@ -17,7 +17,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/plotting/matRad_computeVoiContours.m b/matRad/plotting/matRad_computeVoiContours.m index d4679187e..7f00cecd0 100644 --- a/matRad/plotting/matRad_computeVoiContours.m +++ b/matRad/plotting/matRad_computeVoiContours.m @@ -1,14 +1,14 @@ function cst = matRad_computeVoiContours(ct,cst) % matRad function that computes all VOI contours % -% call +% call: % cst = matRad_computeVoiContours(ct,cst) % -% input +% input: % ct matRad ct struct % cst matRad cst struct % -% output +% output: % cst the new cst with the column containing the precomputed contours % % References @@ -16,7 +16,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -38,21 +38,21 @@ mask(cst{s,4}{ctScen}) = 1; for slice = 1:ct.cubeDim(1) if any(any(mask(slice,:,:) > 0)) - cst{s,7}{1,ctScen}{slice,1} = contourc(squeeze(mask(slice,:,:)),.5*[1 1]); + cst{s,7}{1,ctScen}{slice,1} = contourc(double(squeeze(mask(slice,:,:))),.5*[1 1]); end end for slice = 1:ct.cubeDim(2) if any(any(mask(:,slice,:) > 0)) - cst{s,7}{1,ctScen}{slice,2} = contourc(squeeze(mask(:,slice,:)),.5*[1 1]); + cst{s,7}{1,ctScen}{slice,2} = contourc(double(squeeze(mask(:,slice,:))),.5*[1 1]); end end for slice = 1:ct.cubeDim(3) if any(any(mask(:,:,slice) > 0)) - cst{s,7}{1,ctScen}{slice,3} = contourc(squeeze(mask(:,:,slice)),.5*[1 1]); + cst{s,7}{1,ctScen}{slice,3} = contourc(double(squeeze(mask(:,:,slice))),.5*[1 1]); end end if matRad_cfg.logLevel > 2 matRad_progress(s + (ctScen-1)*s,size(cst,1)*ct.numOfCtScen); end end -end \ No newline at end of file +end diff --git a/matRad/plotting/matRad_computeVoiContoursWrapper.m b/matRad/plotting/matRad_computeVoiContoursWrapper.m index cc690e25a..f08a4de1b 100644 --- a/matRad/plotting/matRad_computeVoiContoursWrapper.m +++ b/matRad/plotting/matRad_computeVoiContoursWrapper.m @@ -1,7 +1,7 @@ function cst = matRad_computeVoiContoursWrapper(cst,ct) % matRad computation of VOI contours if not precomputed % -% call +% call: % cst = matRad_computeVoiContoursWrapper(ct,cst) % % input: @@ -16,7 +16,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2017 the matRad development team. +% Copyright 2017-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/plotting/matRad_getColormap.m b/matRad/plotting/matRad_getColormap.m index e587f41fa..894042976 100644 --- a/matRad/plotting/matRad_getColormap.m +++ b/matRad/plotting/matRad_getColormap.m @@ -3,23 +3,23 @@ % We use this wrapper to manually handle the supported colormaps enabling % the definition of custom colormaps. % -% call +% call: % cMap = matRad_getColormap(name,size) % list = matRad_getColormap() -% input +% input: % name name of the colorbar % size optional argument for the size / resolution of the colorbar % % if no argument is passed, a list (cell array) of the names of all % supported colormaps will be returned % -% output +% output: % This is either the requested colormap, or a list of all available % colormaps (see above) % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/plotting/matRad_plotAxisLabels.m b/matRad/plotting/matRad_plotAxisLabels.m index 7a9db6bff..a614f2b37 100644 --- a/matRad/plotting/matRad_plotAxisLabels.m +++ b/matRad/plotting/matRad_plotAxisLabels.m @@ -2,19 +2,19 @@ function matRad_plotAxisLabels(axesHandle,ct,plane,slice,defaultFontSize,tickdi % matRad function to plot x and y labels denoting the ct dimensions % according to the selected plane % -% call +% call: % matRad_plotAxisLabels(axesHandle,ct,plane,slice,defaultFontSize) % matRad_plotAxisLabels(axesHandle,ct,plane,slice,defaultFontSize, % tickdist) % -% input +% input: % axesHandle handle to axes the slice should be displayed in % ct matRad ct structure % plane plane view (coronal=1,sagittal=2,axial=3) % slice slice in the selected plane of the 3D cube % defaultFontSize default font size as double value % -% output +% output: % - % % References @@ -22,7 +22,7 @@ function matRad_plotAxisLabels(axesHandle,ct,plane,slice,defaultFontSize,tickdi % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2017 the matRad development team. +% Copyright 2017-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/plotting/matRad_plotColorbar.m b/matRad/plotting/matRad_plotColorbar.m index a912870c1..025cfc78f 100644 --- a/matRad/plotting/matRad_plotColorbar.m +++ b/matRad/plotting/matRad_plotColorbar.m @@ -3,17 +3,17 @@ % This is necessary since the rgb colors are manually mapped within the ct % and the dose plotting, and MATLAB attaches colorbars to axes. % -% call +% call: % cBarHandle = matRad_plotColorbar(axesHandle,cMap,window,varargin) % -% input +% input: % axesHandle handle to axes the colorbar will be attached to % cMap corresponding colormap % window colormap window (corresponds to clim) % varargin additional key-value pairs that will be forwarded to the % MATLAB colorbar(__) call % -% output +% output: % cBarHandle handle of the colorbar object % % References @@ -21,7 +21,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/plotting/matRad_plotCtSlice.m b/matRad/plotting/matRad_plotCtSlice.m index 708a5431e..d70de1ab9 100644 --- a/matRad/plotting/matRad_plotCtSlice.m +++ b/matRad/plotting/matRad_plotCtSlice.m @@ -3,13 +3,13 @@ % The function can also be used in personal matlab figures by passing the % corresponding axes handle % -% call +% call: % [ctHandle,cMap,window] = matRad_plotCtSlice(axesHandle,ctCube,cubeIdx,plane,slice) % [ctHandle,cMap,window] = matRad_plotCtSlice(axesHandle,ctCube,cubeIdx,plane,slice,cMap) % [ctHandle,cMap,window] = matRad_plotCtSlice(axesHandle,ctCube,cubeIdx,plane,slice,window) % [ctHandle,cMap,window] = matRad_plotCtSlice(axesHandle,ctCube,cubeIdx,plane,slice,cMap,window) % -% input +% input: % axesHandle handle to axes the slice should be displayed in % ctCube the cell of ct cubes % cubeIdx Index of the desired cube in the ct struct @@ -21,7 +21,7 @@ % window optional argument defining the displayed range. default is % [min(ctCube(:)) max(ctCube(:))] % -% output +% output: % ctHandle handle of the plotted CT axes % cMap used colormap (same as input if set) % window used window (same as input if set) @@ -31,7 +31,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/plotting/matRad_plotCtSlice3D.m b/matRad/plotting/matRad_plotCtSlice3D.m index 59124a60e..f56f8b85a 100644 --- a/matRad/plotting/matRad_plotCtSlice3D.m +++ b/matRad/plotting/matRad_plotCtSlice3D.m @@ -3,10 +3,10 @@ % The function can also be used in personal matlab figures by passing the % corresponding axes handle % -% call +% call: % [ctHandle,cMap,window] = matRad_plotCtSlice3D(axesHandle,ct,cubeIdx,plane,ctSlice,cMap,window) % -% input +% input: % axesHandle handle to 3D axes the slice should be displayed in % ct the ct struct used in matRad % cubeIdx Index of the desired cube in the ct struct @@ -18,7 +18,7 @@ % window optional argument defining the displayed range. default is % [min(ctCube(:)) max(ctCube(:))] % -% output +% output: % ctHandle handle of the plotted CT axes % cMap used colormap (same as input if set) % window used window (same as input if set) @@ -28,7 +28,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/plotting/matRad_plotDVHBand.m b/matRad/plotting/matRad_plotDVHBand.m index fdb4f35d9..3aa1f1d71 100644 --- a/matRad/plotting/matRad_plotDVHBand.m +++ b/matRad/plotting/matRad_plotDVHBand.m @@ -1,20 +1,20 @@ function matRad_plotDVHBand(nominalDVH, structureStat, doseLabel) % matRad_plotDVHBand to plot dose volume bands % -% call +% call: % matRad_plotDVHBand(nominalDVH, structureStat, doseLabel) % -% input +% input: % nominalDVH: x axis values % structureStat: lower bound (start of shadowing) % doseLabel: upper bound (end of shadowing) % -% output +% output: % - % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2017 the matRad development team. +% Copyright 2017-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/plotting/matRad_plotDoseSlice.m b/matRad/plotting/matRad_plotDoseSlice.m index 1c49dd991..1b6261921 100644 --- a/matRad/plotting/matRad_plotDoseSlice.m +++ b/matRad/plotting/matRad_plotDoseSlice.m @@ -3,7 +3,7 @@ % The function can also be used in personal matlab figures by passing the % corresponding axes handle. % -% call +% call: % [doseHandle,cMap,window] = matRad_plotDoseSlice(axesHandle, doseCube,plane,slice,threshold) % [doseHandle,cMap,window] = matRad_plotDoseSlice(axesHandle, doseCube,plane,slice,threshold,alpha) % [doseHandle,cMap,window] = matRad_plotDoseSlice(axesHandle, doseCube,plane,slice,threshold,cMap) @@ -13,7 +13,7 @@ % [doseHandle,cMap,window] = matRad_plotDoseSlice(axesHandle, doseCube,plane,slice,threshold,cMap,window) % [doseHandle,cMap,window] = matRad_plotDoseSlice(axesHandle, doseCube,plane,slice,threshold,alpha,cMap,window) % -% input +% input: % axesHandle handle to axes the slice should be displayed in % doseCube 3D array of the dose to select the slice from % plane plane view (coronal=1,sagittal=2,axial=3) @@ -31,7 +31,7 @@ % window optional argument defining the displayed range. default is % [min(doseCube(:)) max(doseCube(:))] % -% output +% output: % doseHandle: handle of the plotted dose axes % cMap used colormap (same as input if set) % window used window (same as input if set) @@ -41,7 +41,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/plotting/matRad_plotDoseSlice3D.m b/matRad/plotting/matRad_plotDoseSlice3D.m index 83ad41c42..6a67bfebc 100644 --- a/matRad/plotting/matRad_plotDoseSlice3D.m +++ b/matRad/plotting/matRad_plotDoseSlice3D.m @@ -1,7 +1,7 @@ function [doseHandle,cMap,window] = matRad_plotDoseSlice3D(axesHandle,ct,doseCube,plane,slice,threshold,alpha,cMap,window) % matRad function that generates a dose plot of a selected slice in 3D view % -% call +% call: % [doseHandle,cMap,window] = matRad_plotDose3DSlice(axesHandle, doseCube,plane,slice,threshold) % [doseHandle,cMap,window] = matRad_plotDose3DSlice(axesHandle, doseCube,plane,slice,threshold,alpha) % [doseHandle,cMap,window] = matRad_plotDose3DSlice(axesHandle, doseCube,plane,slice,threshold,cMap) @@ -11,7 +11,7 @@ % [doseHandle,cMap,window] = matRad_plotDose3DSlice(axesHandle, doseCube,plane,slice,threshold,cMap,window) % [doseHandle,cMap,window] = matRad_plotDose3DSlice(axesHandle, doseCube,plane,slice,threshold,alpha,cMap,window) % -% input +% input: % axesHandle handle to axes the slice should be displayed in % ct matRad CT struct which contains resolution % doseCube 3D array of the dose to select the slice from @@ -30,7 +30,7 @@ % window optional argument defining the displayed range. default is % [min(doseCube(:)) max(doseCube(:))] % -% output +% output: % doseHandle: handle of the plotted dose axes % cMap used colormap (same as input if set) % window used window (same as input if set) @@ -40,7 +40,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/plotting/matRad_plotIsoCenterMarker.m b/matRad/plotting/matRad_plotIsoCenterMarker.m index e857151d8..04d91b64b 100644 --- a/matRad/plotting/matRad_plotIsoCenterMarker.m +++ b/matRad/plotting/matRad_plotIsoCenterMarker.m @@ -1,11 +1,11 @@ function markerHandle = matRad_plotIsoCenterMarker(axesHandle,pln,ct,plane,slice,style) % matRad function that plots an isocenter marker % -% call +% call: % markerHandle = matRad_plotIsoCenterMarker(axesHandle,pln,ct,plane,slice) % markerHandle = matRad_plotIsoCenterMarker(axesHandle,pln,ct,plane,slice,style) % -% input +% input: % axesHandle handle to axes the marker should be displayed in % pln matRad plan structure % ct matRad ct structure @@ -16,7 +16,7 @@ % with export to tikz for example % % -% output +% output: % markerHandle: handle to the isocenter marker % % References @@ -25,7 +25,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/plotting/matRad_plotIsoDose3D.m b/matRad/plotting/matRad_plotIsoDose3D.m index c36d65fe9..c3ffbe397 100644 --- a/matRad/plotting/matRad_plotIsoDose3D.m +++ b/matRad/plotting/matRad_plotIsoDose3D.m @@ -1,10 +1,10 @@ function hpatch = matRad_plotIsoDose3D(axesHandle,xMesh,yMesh,zMesh,doseCube,isoLevels,cMap,window,alpha) % matRad function that plots isolines in 3d % -% call +% call: % hpatch = matRad_plotIsoDose3D(axesHandle,xMesh,yMesh,zMesh,doseCube,isoLevels,cMap,window,alpha) % -% input +% input: % axesHandle handle to axes the slice should be displayed in % x/y/zMesh meshs % doseCube dose cube @@ -13,7 +13,7 @@ % window window for dose display % alpha transparency % -% output +% output: % hpatch: handle to the patch object % % References @@ -21,7 +21,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/plotting/matRad_plotIsoDoseLines.m b/matRad/plotting/matRad_plotIsoDoseLines.m index bbeb32278..9518b2b9e 100644 --- a/matRad/plotting/matRad_plotIsoDoseLines.m +++ b/matRad/plotting/matRad_plotIsoDoseLines.m @@ -1,13 +1,13 @@ -function isoLineHandles = matRad_plotIsoDoseLines(axesHandle,doseCube,isoContours,isoLevels,plotLabels,plane,slice,cMap,window,varargin) -% matRad function that plots isolines, by precomputed contourc data +function isoLineHandles = matRad_plotIsoDoseLines(axesHandle, doseCube, isoContours, isoLevels, plotLabels, plane, slice, cMap, window, varargin) +% matRad function that plots isolines, by precomputed contourc data % computed by matRad_computeIsoDoseContours or manually by calling contourc % itself % -% call +% call: % isoLineHandles = % matRad_plotIsoDoseLines(axesHandle,doseCube,isoContours,isoLevels,plotLabels,plane,slice,...) % -% input +% input: % axesHandle handle to axes the slice should be displayed in % doseCube 3D array of the corresponding dose cube % isoContours precomputed isodose contours in a cell array {maxDim,3} @@ -24,7 +24,7 @@ % [min(doseCube(:)) max(doseCube(:))] % varargin Additional MATLAB Line-Property/Value-Pairs etc. % -% output +% output: % isoLineHandles: handle to the plotted isolines % % References @@ -32,13 +32,13 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. -% -% This file is part of the matRad project. It is subject to the license -% terms in the LICENSE file found in the top-level directory of this -% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part -% of the matRad project, including this file, may be copied, modified, -% propagated, or distributed except according to the terms contained in the +% Copyright 2015-2026 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the % LICENSE file. % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -46,7 +46,7 @@ matRad_cfg = MatRad_Config.instance(); %% manage optional arguments -%Use default colormap? +% Use default colormap? if nargin < 8 || isempty(cMap) cMap = jet(64); end @@ -54,63 +54,65 @@ window = [min(doseCube(:)) max(doseCube(:))]; end -%Check if precomputed contours where passed, if not, calculate it on the -%fly +isoLevels = double(matRad_gatherCompat(isoLevels)); + +% Check if precomputed contours where passed, if not, calculate it on the +% fly if isempty(isoContours) if plane == 1 - C = contourc(squeeze(doseCube(slice,:,:)),isoLevels); + C = contourc(double(squeeze(doseCube(slice, :, :))), isoLevels); elseif plane == 2 - C = contourc(squeeze(doseCube(:,slice,:)),isoLevels); + C = contourc(double(squeeze(doseCube(:, slice, :))), isoLevels); elseif plane == 3 - C = contourc(squeeze(doseCube(:,:,slice)),isoLevels); - end - isoContours{slice,plane} = C; + C = contourc(double(squeeze(doseCube(:, :, slice))), isoLevels); + end + isoContours{slice, plane} = C; end %% Plotting -cMapScale = size(cMap,1) - 1; +cMapScale = size(cMap, 1) - 1; -isoColorLevel = uint8(cMapScale*(isoLevels - window(1))./(window(2)-window(1))); +isoColorLevel = uint8(cMapScale * (isoLevels - window(1)) ./ (window(2) - window(1))); -%This circumenvents a bug in Octave when the index in the image hase the maximum value of uint8 +% This circumenvents a bug in Octave when the index in the image hase the maximum value of uint8 if matRad_cfg.isOctave - isoColorLevel(isoColorLevel == 255) = 254; + isoColorLevel(isoColorLevel == 255) = 254; isoLineHandles = []; - + elseif matRad_cfg.isMatlab isoLineHandles = gobjects(0); end -colors = squeeze(ind2rgb(isoColorLevel,cMap)); +colors = squeeze(ind2rgb(isoColorLevel, cMap)); -axes(axesHandle); -hold(axesHandle,'on'); +% axes(axesHandle); +hold(axesHandle, 'on'); -%Check if there is a contour in the plane -if any(isoContours{slice,plane}(:)) +% Check if there is a contour in the plane +if any(isoContours{slice, plane}(:)) % plot precalculated contourc data - + lower = 1; % lower marks the beginning of a section - while lower-1 ~= size(isoContours{slice,plane},2) - steps = isoContours{slice,plane}(2,lower); % number of elements of current line section + while lower - 1 ~= size(isoContours{slice, plane}, 2) + steps = isoContours{slice, plane}(2, lower); % number of elements of current line section if numel(unique(isoLevels)) > 1 - color = colors(isoLevels(:) == isoContours{slice,plane}(1,lower),:); + color = colors(isoLevels(:) == isoContours{slice, plane}(1, lower), :); else - color = unique(colors,'rows'); + color = unique(colors, 'rows'); end - isoLineHandles{end+1} = line(isoContours{slice,plane}(1,lower+1:lower+steps),... - isoContours{slice,plane}(2,lower+1:lower+steps),... - 'Color',color,'Parent',axesHandle,varargin{:}); + isoLineHandles{end + 1} = line(isoContours{slice, plane}(1, lower + 1:lower + steps), ... + isoContours{slice, plane}(2, lower + 1:lower + steps), ... + 'Color', color, 'Parent', axesHandle, varargin{:}); if plotLabels - text(isoContours{slice,plane}(1,lower+1),... - isoContours{slice,plane}(2,lower+1),... - num2str(isoContours{slice,plane}(1,lower)),'Parent',axesHandle) + text(isoContours{slice, plane}(1, lower + 1), ... + isoContours{slice, plane}(2, lower + 1), ... + num2str(isoContours{slice, plane}(1, lower)), 'Parent', axesHandle); end - lower = lower+steps+1; - + lower = lower + steps + 1; + end end -hold(axesHandle,'off'); +hold(axesHandle, 'off'); end diff --git a/matRad/plotting/matRad_plotIsoDoseLines3D.m b/matRad/plotting/matRad_plotIsoDoseLines3D.m index 5eb67edc0..3e5d47fe3 100644 --- a/matRad/plotting/matRad_plotIsoDoseLines3D.m +++ b/matRad/plotting/matRad_plotIsoDoseLines3D.m @@ -3,14 +3,14 @@ % computed by matRad_computeIsoDoseContours or manually by calling % contourslice itself % -% call +% call: % isoLineHandles = matRad_plotIsoDoseLines3D(axesHandle,ct,doseCube,isoContours,isoLevels,plane,slice) % isoLineHandles = matRad_plotIsoDoseLines3D(axesHandle,ct,doseCube,isoContours,isoLevels,plane,slice,cMap) % isoLineHandles = matRad_plotIsoDoseLines3D(axesHandle,ct,doseCube,isoContours,isoLevels,plane,slice,window) % isoLineHandles = matRad_plotIsoDoseLines3D(axesHandle,ct,doseCube,isoContours,isoLevels,plane,slice,cMap,window) % isoLineHandles = matRad_plotIsoDoseLines3D(axesHandle,ct,doseCube,isoContours,isoLevels,plane,slice,cMap,window, ...) % -% input +% input: % axesHandle handle to axes the slice should be displayed in % ct matRad ct struct which contains resolution % doseCube 3D array of the corresponding dose cube @@ -27,7 +27,7 @@ % [min(doseCube(:)) max(doseCube(:))] % varargin Additional Matlab Line-Property/value pairs % -% output +% output: % isoLineHandles: handle to the plotted isolines % % References @@ -35,7 +35,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/plotting/matRad_plotPlan3D.m b/matRad/plotting/matRad_plotPlan3D.m index ce5be996f..35a5aacbd 100644 --- a/matRad/plotting/matRad_plotPlan3D.m +++ b/matRad/plotting/matRad_plotPlan3D.m @@ -3,11 +3,11 @@ function matRad_plotPlan3D(axesHandle,pln,stf) % Stf is optional for plotting more detailed field contours in % visualization of the impinging beams. % -% call +% call: % rotMat = matRad_plotPlan3D(axesHandle,pln) % rotMat = matRad_plotPlan3D(axesHandle,pln,stf) % -% input +% input: % axesHandle: handle to the axes the plan should be visualized in. % pln: matRad plan meta information struct % stf: optional steering information struct. if stf is passed and @@ -15,7 +15,7 @@ function matRad_plotPlan3D(axesHandle,pln,stf) % information to plot more detailed field contours than with % pln only % -% output +% output: % - % % References @@ -23,7 +23,7 @@ function matRad_plotPlan3D(axesHandle,pln,stf) % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -41,6 +41,12 @@ function matRad_plotPlan3D(axesHandle,pln,stf) hold(axesHandle,'on'); end +if nargin < 3 + numOfBeams = numel(pln.propStf.gantryAngles); +else + numOfBeams = numel(stf); +end + %nice pink ;) beamColor = [255 20 147]/255; @@ -73,7 +79,7 @@ function matRad_plotPlan3D(axesHandle,pln,stf) beamVector = [0 SAD 0]; - for beamIx = 1:pln.propStf.numOfBeams + for beamIx = 1:numOfBeams rotMat = matRad_getRotationMatrix(pln.propStf.gantryAngles(beamIx),pln.propStf.couchAngles(beamIx)); beamIsoCenter = pln.propStf.isoCenter(beamIx,:); currBeamVector = rotMat*beamVector'; @@ -141,7 +147,7 @@ function matRad_plotPlan3D(axesHandle,pln,stf) rayMat(el2DIx(1),el2DIx(2)) = 1; end %Create contour of the field - fieldContour2D = contourc(rayMat,1); + fieldContour2D = contourc(double(rayMat),1); %Column in the contour matrix cColumn = 1; diff --git a/matRad/plotting/matRad_plotProjectedGantryAngles.m b/matRad/plotting/matRad_plotProjectedGantryAngles.m index a72adb033..9e787d4df 100644 --- a/matRad/plotting/matRad_plotProjectedGantryAngles.m +++ b/matRad/plotting/matRad_plotProjectedGantryAngles.m @@ -2,16 +2,16 @@ function matRad_plotProjectedGantryAngles(axesHandle,pln,ct,plane) % matRad function that plots all gantry angles % projected to the coplanar plane if current view is axial view % -% call +% call: % matRad_plotProjectedGantryAngles(axesHandle,pln,ct,plane) % -% input +% input: % axesHandle: handle to the axis where the plot shouldd appear % pln: matRad pln struct % ct: matRad ct struct % plane: current view plane % -% output +% output: % - % % References @@ -19,7 +19,7 @@ function matRad_plotProjectedGantryAngles(axesHandle,pln,ct,plane) % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2019 the matRad development team. +% Copyright 2019-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -64,4 +64,4 @@ function matRad_plotProjectedGantryAngles(axesHandle,pln,ct,plane) plot(axesHandle,[0 r*sind(180-pln.propStf.gantryAngles(i))]+cubeIso(2),[0 r*cosd(180-pln.propStf.gantryAngles(i))]+cubeIso(1),'LineWidth',1,'Color',gantryAngleVisColor) end -end \ No newline at end of file +end diff --git a/matRad/plotting/matRad_plotVoiContourSlice.m b/matRad/plotting/matRad_plotVoiContourSlice.m index 7f483c6e6..02dc7b10f 100644 --- a/matRad/plotting/matRad_plotVoiContourSlice.m +++ b/matRad/plotting/matRad_plotVoiContourSlice.m @@ -1,12 +1,12 @@ function [voiContourHandles, visibleOnSlice] = matRad_plotVoiContourSlice(axesHandle,cst,ct,ctIndex,selection,plane,slice,cMap,varargin) % matRad function that plots the contours of the segmentations given in cst % -% call +% call: % voiContourHandles = matRad_plotVoiContourSlice(axesHandle,cst,ct,ctIndex,selection,plane,slice) % voiContourHandles = matRad_plotVoiContourSlice(axesHandle,cst,ct,ctIndex,selection,plane,slice,cMap) % voiContourHandles = matRad_plotVoiContourSlice(axesHandle,cst,ct,ctIndex,selection,plane,slice,cMap,...) % -% input +% input: % axesHandle handle to axes the slice should be displayed in % cst matRad cst cell array % ct matRad ct structure @@ -20,7 +20,7 @@ % colorcube % varargin Additional Matlab Line-Property/value pairs % -% output +% output: % voiContourHandles: handles of the plotted contours % visibleOnSlice: logicals defining if the contour is actually % visible on the current slice @@ -30,7 +30,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -91,11 +91,11 @@ mask(cst{s,4}{ctIndex}) = 1; if plane == 1 && any(any(mask(slice,:,:) > 0)) - C = contourc(squeeze(mask(slice,:,:)),.5*[1 1]); + C = contourc(double(squeeze(mask(slice,:,:))),.5*[1 1]); elseif plane == 2 && any(any(mask(:,slice,:) > 0)) - C = contourc(squeeze(mask(:,slice,:)),.5*[1 1]); + C = contourc(double(squeeze(mask(:,slice,:))),.5*[1 1]); elseif plane == 3 && any(any(mask(:,:,slice) > 0)) - C = contourc(squeeze(mask(:,:,slice)),.5*[1 1]); + C = contourc(double(squeeze(mask(:,:,slice))),.5*[1 1]); end end diff --git a/matRad/plotting/matRad_plotVois3D.m b/matRad/plotting/matRad_plotVois3D.m index 1bf2204c8..7f70dcc5c 100644 --- a/matRad/plotting/matRad_plotVois3D.m +++ b/matRad/plotting/matRad_plotVois3D.m @@ -2,11 +2,11 @@ % matRad function that plots 3D structures of the volumes of interest % If the 3D-data is not stored in the CT, it will be commputed on the fly. % -% call +% call: % patches = matRad_plotVois3D(axesHandle,ct,cst,selection) % patches = matRad_plotVois3D(axesHandle,ct,cst,selection,cMap) % -% input +% input: % axesHandle handle to axes the structures should be displayed in % ct matRad ct struct which contains resolution % cst matRad cst struct @@ -16,7 +16,7 @@ % cMap optional argument defining the colormap, default are the % colors stored in the cst % -% output +% output: % patches patch objects created by the matlab 3D visualization % % References @@ -24,7 +24,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/plotting/matRad_shadowPlot.m b/matRad/plotting/matRad_shadowPlot.m index fed2e8e7d..326f3a05e 100644 --- a/matRad/plotting/matRad_shadowPlot.m +++ b/matRad/plotting/matRad_shadowPlot.m @@ -1,10 +1,10 @@ function s = matRad_shadowPlot(x, yLow, yUp, color, legendName, alphaTrans) % shadowPlot to plot confidence bands mainly % -% call +% call: % shadowPlot(x, yLow, yUp, color, legendName, alphaTrans) % -% input +% input: % x: x axis values % yLow: lower bound (start of shadowing) % yUp: upper bound (end of shadowing) @@ -12,13 +12,13 @@ % legendName: legendname to be shown in the plot % alphaTrans: transparency % -% output +% output: % axesHandle % % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2017 the matRad development team. +% Copyright 2017-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/rayTracing/matRad_RayTracer.m b/matRad/rayTracing/matRad_RayTracer.m new file mode 100644 index 000000000..63b9d1d61 --- /dev/null +++ b/matRad/rayTracing/matRad_RayTracer.m @@ -0,0 +1,309 @@ +classdef matRad_RayTracer < handle + % matRad_RayTracer Base class for ray tracing through a patient CT volume + % This class provides the infrastructure for ray tracing in beam's-eye-view + % (BEV) coordinates to compute radiological depths along rays from a + % radiation source through a patient CT grid. + % + % Subclasses must implement the abstract method: + % traceRay() - trace a single ray and return path segment lengths, + % density values, and intersected voxel indices + % Subclasses can optionally override: + % traceRays() - trace multiple rays in a vectorized or otherwise accelerated manner + % (default: loop over traceRay) + % + % Usage example: + % tracer = matRad_SomeRayTracerSubclass(cubes, grid); + % [radDepthsV, radDepthCube] = tracer.traceCube(stfElement, V); + % + % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + % + % Copyright 2026 the matRad development team. + % + % This file is part of the matRad project. It is subject to the license + % terms in the LICENSE file found in the top-level directory of this + % distribution and at https://github.com/e0404/matRad/LICENSE.md. No part + % of the matRad project, including this file, may be copied, modified, + % propagated, or distributed except according to the terms contained in the + % LICENSE file. + % + % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + properties + lateralCutOff + forcePrecision = [] % override precision (double or single), otherwise inherited + enableGPU = false % whether to use GPU arrays for ray tracing (if supported by subclass implementation) + end + + properties (Access = protected) + cubes + planes + grid + numPlanes + end + + properties (Hidden) + useOldCandidateRayMatrixCalculation = false + end + + methods + + function this = matRad_RayTracer(cubes, grid) + % matRad_RayTracer Construct a ray tracer instance + % cubes - cell array of density/material cubes to trace through + % grid - CT grid struct with fields x, y, z and resolution + + this.cubes = cubes; + this.grid = grid; + + this.initializeGeometry(); + + matRad_cfg = MatRad_Config.instance(); + this.lateralCutOff = matRad_cfg.defaults.propDoseCalc.geometricLateralCutOff; + end + + function [alphas, l, rho, d12, ix] = traceRays(this, ... + isocenter, ... + sourcePoints, ... + targetPoints) + + if ~isempty(this.forcePrecision) + isocenter = cast(isocenter, this.forcePrecision); + sourcePoints = cast(sourcePoints, this.forcePrecision); + targetPoints = cast(targetPoints, this.forcePrecision); + end + + % Default trivial implementation based on traceRay + nRays = size(targetPoints, 1); + nSources = size(sourcePoints, 1); + + if nSources ~= nRays && nSources ~= 1 + matRad_cfg = MatRad_Config.instance(); + matRad_cfg.dispError('Number of source points (%d) needs to be one or equal to number of target points (%d)!', nSources, nRays); + elseif nSources == 1 + sourcePoints = repmat(sourcePoints, nRays, 1); + nSources = nRays; + end + + nCubes = numel(this.cubes); + + rhoTmp = cell(nRays, nCubes); + alphas = cell(nRays, 1); + l = cell(nRays, 1); + ix = cell(nRays, 1); + d12 = NaN(nRays, 1, matRad_underlyingTypeCompat(isocenter)); + for r = 1:nRays + [alphas{r}, l{r}, rhoTmp(r, :), d12(r), ix{r}] = this.traceRay(isocenter, sourcePoints(r, :), targetPoints(r, :)); + end + + % pad with NaN values + maxnumalphas = max(cellfun(@numel, alphas)); + maxnumix = max(cellfun(@numel, ix)); + + nanpad = @(x, maxval) [x(1:end), NaN(1, maxnumalphas - length(x))]; + alphas = cellfun(nanpad, alphas, 'UniformOutput', false); + nanpad = @(x) [x(1:end), NaN(1, maxnumix - length(x))]; + + l = cellfun(nanpad, l, 'UniformOutput', false); + ix = cellfun(nanpad, ix, 'UniformOutput', false); + rhoTmp = cellfun(nanpad, rhoTmp, 'UniformOutput', false); + + % now make matrices + alphas = cell2mat(alphas); + l = cell2mat(l); + ix = cell2mat(ix); + + rho = cell(1, nCubes); + for c = flip(1:nCubes) + rho{c} = cell2mat(rhoTmp(:, c)); + rhoTmp(:, c) = []; + end + + end + + function [alphas, l, rho, d12, ix] = traceRay(this, ... + isocenter, ... + sourcePoint, ... + targetPoint) + error('Needs to be implemented!'); + end + + function [radDepthsV, radDepthCube] = traceCube(this, stfElement, voxelIndices, rotCoordsV) + matRad_cfg = MatRad_Config.instance(); + + if ~isstruct(stfElement) || numel(stfElement) ~= 1 + matRad_cfg.dispError('The RayTracer does not accept stf struct arrays and only operates on a single field!'); + end + + cubeDim = size(this.cubes{1}); + nCubeVox = numel(this.cubes{1}); + + % If no subset of voxels is specified, take all of them + if nargin < 4 + voxelIndices = transpose(1:nCubeVox); + if this.enableGPU + voxelIndices = gpuArray(int32(voxelIndices)); + end + end + + % if we don't provide rotated patient coordinates we can compute + % them here on our own + if nargin < 5 + coordsV = single(matRad_cubeIndex2worldCoords(voxelIndices, this.grid)); + + % Get Rotation Matrix + % Do not transpose matrix since we usage of row vectors & + % transformation of the coordinate system need double transpose + + rotMat_system_T = matRad_getRotationMatrix(stfElement.gantryAngle, stfElement.couchAngle); + + % Rotate coordinates (1st couch around Y axis, 2nd gantry movement) + rotCoordsV = (coordsV - stfElement.isoCenter) * rotMat_system_T; + + % translate relative to source + rotCoordsV = rotCoordsV - stfElement.sourcePoint_bev; + end + + % set up ray matrix direct behind last voxel + rayMx_bev_y = max(rotCoordsV(:, 2)) + max([this.grid.resolution.x this.grid.resolution.y this.grid.resolution.z]); + rayMx_bev_y = rayMx_bev_y + stfElement.sourcePoint_bev(2); + rayMatrixScale = 1 + rayMx_bev_y / stfElement.SAD; + + % set up list with bev coordinates for calculation of radiological depth + if matRad_cfg.isMatlab && isgpuarray(rotCoordsV) + coords = zeros(nCubeVox, 3, matRad_underlyingTypeCompat(rotCoordsV),'gpuArray'); + else + coords = zeros(nCubeVox, 3, matRad_underlyingTypeCompat(rotCoordsV)); + end + coords(voxelIndices, :) = rotCoordsV; + + referencePositionsBEV = rayMatrixScale * vertcat(stfElement.ray.rayPos_bev); + + % calculate spacing of rays on ray matrix + rayMxSpacing = cast(1 / sqrt(2) * ... + min([this.grid.resolution.x this.grid.resolution.y this.grid.resolution.z]), matRad_underlyingTypeCompat(coords)); + spacingRange = rayMxSpacing * (floor(-500 / rayMxSpacing):ceil(500 / rayMxSpacing)); + + t_candidate = tic; + candidateRayMx = this.getCandidateRayMatrix(spacingRange, referencePositionsBEV); + t_candidate = toc(t_candidate); + + [rayIdxZ, rayIdxX] = find(candidateRayMx); + rayMx_bev = [spacingRange(rayIdxX)' rayMx_bev_y * ones(sum(candidateRayMx(:)), 1) spacingRange(rayIdxZ)']; + + % Rotation matrix. Transposed because of row vectors + rotMat_vectors_T = transpose(matRad_getRotationMatrix(stfElement.gantryAngle, stfElement.couchAngle)); + + % rotate ray matrix from bev to world coordinates + rayMx_world = rayMx_bev * rotMat_vectors_T; + + % criterium for ray selection + raySelection = rayMxSpacing / 2; + + % Trace all selected rays + [~, l, rho, ~, ixHitVoxel] = this.traceRays(stfElement.isoCenter, stfElement.sourcePoint, rayMx_world); + + % find voxels for which we should remember this tracing because this is + % the closest ray by projecting the voxel coordinates to the + % intersection points with the ray matrix and checking if the distance + % in x and z direction is smaller than the resolution of the ray matrix + valid_ix = ~isnan(ixHitVoxel); + % scale_factor has same shape as ixHitVoxel (nRays s maxHits); + % coords(...,2) is the BEV y-coordinate of each hit voxel + validCoords = coords(ixHitVoxel(valid_ix), :); + scale_factor = (rayMx_bev_y - stfElement.sourcePoint_bev(2)) ./ ... + validCoords(:, 2); + + validCoords = validCoords .* scale_factor; + + % rayMx_bev(:,1/3) broadcasts across hit columns via implicit expansion + x_dist = NaN(size(ixHitVoxel)); + z_dist = NaN(size(ixHitVoxel)); + x_dist(valid_ix) = validCoords(:, 1); + z_dist(valid_ix) = validCoords(:, 3); + x_dist = x_dist - rayMx_bev(:, 1); + z_dist = z_dist - rayMx_bev(:, 3); + + % Find indices + ixRememberFromCurrTracing = x_dist > -raySelection & x_dist <= raySelection & ... + z_dist > -raySelection & z_dist <= raySelection; + + % set up rad depth cube for results + if matRad_cfg.isMatlab && isgpuarray(rotCoordsV) + radDepthCubeTemplate = NaN(cubeDim, matRad_underlyingTypeCompat(l), 'gpuArray'); + else + radDepthCubeTemplate = NaN(cubeDim, matRad_underlyingTypeCompat(l)); + end + radDepthCube = repmat({radDepthCubeTemplate}, numel(this.cubes)); + radDepthsV = cell(size(radDepthCube)); + + for j = 1:numel(this.cubes) + rayDistances = l .* rho{j}; + rayWepl = cumsum(rayDistances, 2) - rayDistances / 2; + + radDepthCube{j}(ixHitVoxel(ixRememberFromCurrTracing)) = rayWepl(ixRememberFromCurrTracing); + + radDepthsV{j} = radDepthCube{j}(voxelIndices); + end + end + + end + + methods (Access = protected) + + function candidateRayMx = getCandidateRayMatrix(this, spacingRange, refPosBEV) + % define candidate ray matrix covering 1000x1000mm^2 + numOfCandidateRays = numel(spacingRange); + candidateRayMx = zeros(numOfCandidateRays, 'logical'); + + rSq = this.lateralCutOff^2; + + if this.useOldCandidateRayMatrixCalculation + % old implementation (based on meshgrid), which is slightly + % faster for GPUs + [candidateRayCoordsX, candidateRayCoordsZ] = meshgrid(spacingRange); + + % check which rays should be used + for i = 1:size(refPosBEV, 1) + ixCandidates = (candidateRayCoordsX(:) - refPosBEV(i, 1)).^2 + ... + (candidateRayCoordsZ(:) - refPosBEV(i, 3)).^2 <= rSq; + + candidateRayMx(ixCandidates) = 1; + end + end + + % check which rays should be used + for i = 1:size(refPosBEV, 1) + % Old implementation (based on meshgrid) + % xCandidateCoords = (candidateRayCoords_X(:) - refPosBEV(i,1)).^2; + % zCandidateCoords = (candidateRayCoords_Z(:) - refPosBEV(i,3)).^2; + + % ixCandidates = (xCandidateCoords + zCandidateCoords) <= rSq; + % candidateRayMx(ixCandidates) = true; + + % simple boolean update + xCandidateCoords = (spacingRange - refPosBEV(i, 1)).^2; + zCandidateCoords = (spacingRange - refPosBEV(i, 3)).^2; + + zOk = zCandidateCoords <= rSq; + if ~any(zOk) + continue + end + + z0 = find(zOk, 1, 'first'); + z1 = find(zOk, 1, 'last'); + candidateRayMx(z0:z1, :) = candidateRayMx(z0:z1, :) | zCandidateCoords(z0:z1)' + xCandidateCoords <= rSq; + end + end + + function initializeGeometry(this) + this.grid = matRad_getWorldAxes(this.grid); + + this.planes.x = [this.grid.x - this.grid.resolution.x / 2, this.grid.x(end) + this.grid.resolution.x / 2]; + this.planes.y = [this.grid.y - this.grid.resolution.y / 2, this.grid.y(end) + this.grid.resolution.y / 2]; + this.planes.z = [this.grid.z - this.grid.resolution.z / 2, this.grid.z(end) + this.grid.resolution.z / 2]; + + this.numPlanes = arrayfun(@(x) numel(this.planes.(x)), 'xyz'); + end + + end +end diff --git a/matRad/rayTracing/matRad_RayTracerSiddon.m b/matRad/rayTracing/matRad_RayTracerSiddon.m new file mode 100644 index 000000000..cb1f920d4 --- /dev/null +++ b/matRad/rayTracing/matRad_RayTracerSiddon.m @@ -0,0 +1,250 @@ +classdef matRad_RayTracerSiddon < matRad_RayTracer + % matRad_RayTracerSiddon Ray tracer using Siddon's algorithm + % Implements ray tracing through a CT volume using Siddon's algorithm, + % which analytically computes the exact intersection lengths of a ray + % with each voxel by tracking parametric alpha values at which the ray + % crosses each set of parallel CT planes (x, y, z). + % + % This class overrides traceRays() from matRad_RayTracer with a + % vectorized implementation that handles multiple rays simultaneously, + % significantly improving performance over the default loop. + % + % References: + % Siddon RL (1985), "Fast calculation of the exact radiological + % path for a three-dimensional CT array", Med. Phys. 12(2):252-5. + % + % Usage example: + % tracer = matRad_RayTracerSiddon(cubes, grid); + % [alphas, l, rho, d12, ix] = tracer.traceRays(isocenter, sourcePoints, targetPoints); + % + % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + % + % Copyright 2026 the matRad development team. + % + % This file is part of the matRad project. It is subject to the license + % terms in the LICENSE file found in the top-level directory of this + % distribution and at https://github.com/e0404/matRad/LICENSE.md. No part + % of the matRad project, including this file, may be copied, modified, + % propagated, or distributed except according to the terms contained in the + % LICENSE file. + % + % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + properties + vectorized = true % Uses vectorization instead of looping through rays + end + + properties (Access = private, Hidden) + + end + + methods + + function this = matRad_RayTracerSiddon(cubes, grid) + this = this@matRad_RayTracer(cubes, grid); + end + + function [alphas, l, rho, d12, ix] = traceRay(this, ... + isocenter, ... + sourcePoint, ... + targetPoint) + + nRays = size(targetPoint, 1); + nSources = size(sourcePoint, 1); + + if nRays ~= 1 || nSources ~= 1 + matRad_cfg = MatRad_Config.instance(); + matRad_cfg.dispError(['Number of target Points and source points needs to be equal to one! ' ... + 'If you want to trace multiple rays at once, use traceRays instead!']); + end + + [alphas, l, rho, d12, ix] = this.traceRays(isocenter, sourcePoint, targetPoint); + end + + function [alphas, l, rho, d12, ix] = traceRays(this, ... + isocenter, ... + sourcePoints, ... + targetPoints) + + if ~isempty(this.forcePrecision) + isocenter = cast(isocenter, this.forcePrecision); + sourcePoints = cast(sourcePoints, this.forcePrecision); + targetPoints = cast(targetPoints, this.forcePrecision); + end + + nRays = size(targetPoints, 1); + nSources = size(sourcePoints, 1); + + if nSources ~= nRays && nSources ~= 1 + matRad_cfg = MatRad_Config.instance(); + matRad_cfg.dispError('Number of source points (%d) needs to be one or equal to number of target points (%d)!', nSources, nRays); + elseif nSources == 1 + sourcePoints = repmat(sourcePoints, nRays, 1); + % nSources = nRays; + end + + if ~this.vectorized && nRays > 1 + [alphas, l, rho, d12, ix] = this.traceRays@matRad_RayTracer(isocenter, sourcePoints, targetPoints); + return + end + + rayVec = targetPoints - sourcePoints; + sourcePoints = sourcePoints + isocenter; + + % eq 7 & 8 + % Calculate relative distances (alphas) at which intersections + % occur + alphas = this.computeAllAlphas(sourcePoints, rayVec); + + % eq 11 + % Calculate the distance from source to target point. + % TODO: for some reason this cast needs to be explicit for some octave versions on linux + d12 = cast(vecnorm(rayVec, 2, 2), matRad_underlyingTypeCompat(alphas)); + + % eq 10 + % Calculate the voxel intersection length. + tmpDiff = diff(alphas, 1, 2); + + l = d12 .* tmpDiff; + alphas_mid = alphas(:, 1:end - 1) + 0.5 * tmpDiff; + + % eq 12 + % Calculate the voxel indices: first convert to physical coords + % and convert to voxel indices + sourcePoints = matRad_world2cubeCoords(sourcePoints, this.grid, true); + i = double(round((sourcePoints(:, 1) + alphas_mid .* rayVec(:, 1)) ./ this.grid.resolution.x)); + j = double(round((sourcePoints(:, 2) + alphas_mid .* rayVec(:, 2)) ./ this.grid.resolution.y)); + k = double(round((sourcePoints(:, 3) + alphas_mid .* rayVec(:, 3)) ./ this.grid.resolution.z)); + + % Handle numerical instabilities at the borders. + i(i < 1) = 1; + i(i > this.numPlanes(1) - 1) = this.numPlanes(1) - 1; + j(j < 1) = 1; + j(j > this.numPlanes(2) - 1) = this.numPlanes(2) - 1; + k(k < 1) = 1; + k(k > this.numPlanes(3) - 1) = this.numPlanes(3) - 1; + + valIx = ~isnan(alphas_mid); + + % Compute linear indices via direct arithmetic (equivalent to sub2ind + % but NaN propagates naturally, avoiding the need to mask before calling) + dims = [this.numPlanes(2), this.numPlanes(1), this.numPlanes(3)] - 1; + ix = j + (i - 1) * dims(1) + (k - 1) * (dims(1) * dims(2)); + + validLinIx = ix(valIx); % extract once, reuse across cubes + for i = 1:numel(this.cubes) + rho{i} = NaN(size(valIx), class(alphas)); + rho{i}(valIx) = this.cubes{i}(validLinIx); + end + + end + + end + + methods (Access = protected) + + function alphas = computeAllAlphas(this, sourcePoint, rayVec) + + % Here we setup grids to enable logical indexing when computing + % the alphas along each dimension. All alphas between the + % minimum and maximum index will be computed, with additional + % exclusion of singular plane occurrences (max == min) + % All values out of scope will be set to NaN. + + nRays = size(rayVec, 1); + + % eq 4 + % Calculate parametrics values of \alpha_{min} and \alpha_{max} for every + % axis, intersecting the ray with the sides of the CT. + aX_1 = (this.planes.x(1) - sourcePoint(:, 1)) ./ rayVec(:, 1); + aX_end = (this.planes.x(end) - sourcePoint(:, 1)) ./ rayVec(:, 1); + + tmpIx = rayVec(:, 1) == 0; + aX_1(tmpIx) = NaN; + aX_end(tmpIx) = NaN; + + aY_1 = (this.planes.y(1) - sourcePoint(:, 2)) ./ rayVec(:, 2); + aY_end = (this.planes.y(end) - sourcePoint(:, 2)) ./ rayVec(:, 2); + + tmpIx = rayVec(:, 2) == 0; + aY_1(tmpIx) = NaN; + aY_end(tmpIx) = NaN; + + aZ_1 = (this.planes.z(1) - sourcePoint(:, 3)) ./ rayVec(:, 3); + aZ_end = (this.planes.z(end) - sourcePoint(:, 3)) ./ rayVec(:, 3); + + tmpIx = rayVec(:, 3) == 0; + aZ_1(tmpIx) = NaN; + aZ_end(tmpIx) = NaN; + + % eq 5 + % Compute the \alpha_{min} and \alpha_{max} in terms of parametric values + % given by equation 4. + alphaLimits(:, 1) = max([zeros(nRays, 1) min(aX_1, aX_end) min(aY_1, aY_end) min(aZ_1, aZ_end)], [], 2); + alphaLimits(:, 2) = min([ones(nRays, 1) max(aX_1, aX_end) max(aY_1, aY_end) max(aZ_1, aZ_end)], [], 2); + + % Rays that miss the geometry: alphaMin >= alphaMax before the sort destroys this information + alphaLimits(alphaLimits(:, 1) > alphaLimits(:, 2), :) = NaN; + + % eq 6 + % Calculate the range of indices who gives parametric values for + % intersected planes. + [dimMin, dimMax] = this.computeEntryAndExit(sourcePoint, rayVec, alphaLimits); + + % eq 7 + % For the given range of indices, calculate the paremetrics values who + % represents intersections of the ray with the plane. + alphas = this.computePlaneAlphas(sourcePoint, rayVec, dimMin, dimMax); + + % eq 8 + % Merge parametrics sets. + % The following might look slow but is quite close to Matlab's + % "unique" implementation + alphas = sort(horzcat(alphaLimits, alphas{:}), 2); % NaN's are placed at the end when sorting in ascending order + alphas(diff(alphas, 1, 2) == 0) = NaN; % Remove duplicates + alphas = sort(alphas, 2); % Again place NaN's at the end + + % Size Reduction (reduce NaN padding) for further computations + maxNumColumns = max(sum(~isnan(alphas), 2)); + alphas = alphas(:, 1:maxNumColumns); + end + + function [dimMin, dimMax] = computeEntryAndExit(this, sourcePoint, rayVec, alphaLimits) + % eq 6 + % Calculate the range of indices who gives parametric values for + % intersected planes. + + rayDirectionPositive = rayVec > 0; + alphaLimitsReverse = flip(alphaLimits, 2); + + alphaLimitsRep = repmat(reshape(alphaLimits, [size(alphaLimits, 1) 1 2]), 1, 3, 1); + alphaLimitsReverseRep = repmat(reshape(alphaLimitsReverse, [size(alphaLimits, 1) 1 2]), 1, 3, 1); + rayDirPosRep = repmat(rayDirectionPositive, 1, 1, 2); + alphaAxis = alphaLimitsReverseRep; + alphaAxis(rayDirPosRep) = alphaLimitsRep(rayDirPosRep); + + tmp = 'xyz'; + [lowerPlanes, upperPlanes] = arrayfun(@(x) deal(this.planes.(x)(1), this.planes.(x)(end)), tmp); + resArray = arrayfun(@(x) this.grid.resolution.(x), tmp); + + dimMin = this.numPlanes - (upperPlanes - alphaAxis(:, :, 1) .* rayVec - sourcePoint) ./ resArray; + dimMax = 1 + (sourcePoint + alphaAxis(:, :, 2) .* rayVec - lowerPlanes) ./ resArray; + + dimMin = ceil(round(1e3 * dimMin) / 1e3); + dimMax = floor(round(1e3 * dimMax) / 1e3); + end + + function alphas = computePlaneAlphas(this, sourcePoint, rayVec, dimMin, dimMax) + tmp = 'xyz'; + for i = 1:3 + planeIx = 1:length(this.planes.(tmp(i))); + planeGrid = repmat(this.planes.(tmp(i)), size(rayVec, 1), 1); + planeGrid(planeIx < dimMin(:, i) | planeIx > dimMax(:, i) | ... + (planeIx == dimMin(:, i) & planeIx == dimMax(:, i)) | ... + isnan(dimMin(:, i)) | isnan(dimMax(:, i))) = NaN; + alphas{i} = (planeGrid - sourcePoint(:, i)) ./ (rayVec(:, i)); + end + end + + end +end diff --git a/matRad/rayTracing/matRad_rayTracing.m b/matRad/rayTracing/matRad_rayTracing.m index 87cd3df98..a186533d6 100644 --- a/matRad/rayTracing/matRad_rayTracing.m +++ b/matRad/rayTracing/matRad_rayTracing.m @@ -1,19 +1,19 @@ -function [radDepthV, radDepthCube] = matRad_rayTracing(stf,ct,V,rot_coordsV,lateralCutoff) +function [radDepthV, radDepthCube] = matRad_rayTracing(stfElement, ct, V, rot_coordsV, lateralCutoff) % matRad visualization of two-dimensional dose distributions on ct including % segmentation -% -% call +% +% call: % [radDepthV, radDepthCube] = matRad_rayTracing(stf,ct,V,rot_coordsV,lateralCutoff) % -% input -% stf: matRad steering information struct of one beam +% input: +% stfElement: matRad steering information struct of single(!) beam % ct: ct cube % V: linear voxel indices e.g. of voxels inside patient. % rot_coordsV coordinates in beams eye view inside the patient -% lateralCutoff: lateral cut off used for ray tracing +% lateralCutoff: lateral cut off used for ray tracing (optional) % -% output +% output: % radDepthV: radiological depth inside the patient % radDepthCube: radiological depth in whole ct % @@ -22,115 +22,31 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. -% -% This file is part of the matRad project. It is subject to the license -% terms in the LICENSE file found in the top-level directory of this -% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part -% of the matRad project, including this file, may be copied, modified, -% propagated, or distributed except according to the terms contained in the +% Copyright 2015-2026 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the % LICENSE file. % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% set up rad depth cube for results -radDepthCube = repmat({NaN*ones(ct.cubeDim)},ct.numOfCtScen); - -% set up ray matrix direct behind last voxel -rayMx_bev_y = max(rot_coordsV(:,2)) + max([ct.resolution.x ct.resolution.y ct.resolution.z]); -rayMx_bev_y = rayMx_bev_y + stf.sourcePoint_bev(2); - -% set up list with bev coordinates for calculation of radiological depth -coords = zeros(prod(ct.cubeDim),3); -coords(V,:) = rot_coordsV; +matRad_cfg = MatRad_Config.instance(); +matRad_cfg.dispDeprecationWarning('Calls to matRad_rayTracing will be replaced by usage of the matRad_RayTracer class in the future!'); -% calculate spacing of rays on ray matrix -rayMxSpacing = 1/sqrt(2) * min([ct.resolution.x ct.resolution.y ct.resolution.z]); +% At the moment we only implement the Siddon ray-tracer in [1] +hTracer = matRad_RayTracerSiddon(ct.cube, ct); -% define candidate ray matrix covering 1000x1000mm^2 -numOfCandidateRays = 2 * ceil(500/rayMxSpacing) + 1; -candidateRayMx = zeros(numOfCandidateRays); - -% define coordinates -[candidateRaysCoords_X,candidateRaysCoords_Z] = meshgrid(rayMxSpacing*[floor(-500/rayMxSpacing):ceil(500/rayMxSpacing)]); - -% check which rays should be used -for i = 1:stf.numOfRays - - ix = (candidateRaysCoords_X(:) - (1+rayMx_bev_y/stf.SAD)*stf.ray(i).rayPos_bev(1)).^2 + ... - (candidateRaysCoords_Z(:) - (1+rayMx_bev_y/stf.SAD)*stf.ray(i).rayPos_bev(3)).^2 ... - <= lateralCutoff^2; - - candidateRayMx(ix) = 1; - +if nargin >= 5 + hTracer.lateralCutOff = lateralCutoff; end -% set up ray matrix -rayMx_bev = [candidateRaysCoords_X(logical(candidateRayMx(:))) ... - rayMx_bev_y*ones(sum(candidateRayMx(:)),1) ... - candidateRaysCoords_Z(logical(candidateRayMx(:)))]; - -% figure, -% for jj = 1:length(rayMx_bev) -% plot(rayMx_bev(jj,1),rayMx_bev(jj,3),'rx'),hold on -% end - -% Rotation matrix. Transposed because of row vectors -rotMat_vectors_T = transpose(matRad_getRotationMatrix(stf.gantryAngle,stf.couchAngle)); - -% rotate ray matrix from bev to world coordinates -rayMx_world = rayMx_bev * rotMat_vectors_T; - -% criterium for ray selection -raySelection = rayMxSpacing/2; - -% perform ray tracing over all rays -for i = 1:size(rayMx_world,1) - - cubeIsoCenter = matRad_world2cubeCoords(stf.isoCenter,ct); - - % run siddon ray tracing algorithm - [~,l,rho,~,ixHitVoxel] = matRad_siddonRayTracer(cubeIsoCenter, ... - ct.resolution, ... - stf.sourcePoint, ... - rayMx_world(i,:), ... - ct.cube); - - % find voxels for which we should remember this tracing because this is - % the closest ray by projecting the voxel coordinates to the - % intersection points with the ray matrix and checking if the distance - % in x and z direction is smaller than the resolution of the ray matrix - scale_factor = (rayMx_bev_y - stf.sourcePoint_bev(2)) ./ ... - coords(ixHitVoxel,2); - - x_dist = coords(ixHitVoxel,1).*scale_factor - rayMx_bev(i,1); - z_dist = coords(ixHitVoxel,3).*scale_factor - rayMx_bev(i,3); - - ixRememberFromCurrTracing = x_dist > -raySelection & x_dist <= raySelection ... - & z_dist > -raySelection & z_dist <= raySelection; - - if any(ixRememberFromCurrTracing) > 0 - - for j = 1:ct.numOfCtScen - % calc radiological depths - - % eq 14 - % It multiply voxel intersections with \rho values. - d = l .* rho{j}; %Note. It is not a number "one"; it is the letter "l" - - % Calculate accumulated d sum. - dCum = cumsum(d)-d/2; - - % write radiological depth for voxel which we want to remember - radDepthCube{j}(ixHitVoxel(ixRememberFromCurrTracing)) = dCum(ixRememberFromCurrTracing); - end - end - -end - -% only take voxel inside the patient -for i = 1:ct.numOfCtScen - radDepthV{i} = radDepthCube{i}(V); +if nargin >= 4 + [radDepthV, radDepthCube] = hTracer.traceCube(stfElement, V, rot_coordsV); +elseif nargin >= 3 + [radDepthV, radDepthCube] = hTracer.traceCube(stfElement, V); +else + [radDepthV, radDepthCube] = hTracer.traceCube(stfElement); end - - diff --git a/matRad/rayTracing/matRad_siddonRayTracer.m b/matRad/rayTracing/matRad_siddonRayTracer.m index c1cc9313e..f1d626af8 100644 --- a/matRad/rayTracing/matRad_siddonRayTracer.m +++ b/matRad/rayTracing/matRad_siddonRayTracer.m @@ -1,20 +1,20 @@ -function [alphas,l,rho,d12,ix] = matRad_siddonRayTracer(isocenterCube, ... - resolution, ... - sourcePoint, ... - targetPoint, ... - cubes) -% siddon ray tracing through 3D cube to calculate the radiological depth +function [alphas, l, rho, d12, ix] = matRad_siddonRayTracer(isocenterCube, ... + resolution, ... + sourcePoint, ... + targetPoint, ... + cubes) +% siddon ray tracing through 3D cube to calculate the radiological depth % according to Siddon 1985 Medical Physics. The raytracer expects the % isocenter in cube coordinates! -% -% call +% +% call: % [alphas,l,rho,d12,vis] = matRad_siddonRayTracer(isocenter, ... % resolution, ... % sourcePoint, ... % targetPoint, ... % cubes) % -% input +% input: % isocenterCube: isocenter in cube coordinates [mm] % resolution: resolution of the cubes [mm/voxel] % sourcePoint: source point of ray tracing @@ -22,226 +22,37 @@ % cubes: cell array of cubes for ray tracing (it is possible to pass % multiple cubes for ray tracing to save computation time) % -% output (see Siddon 1985 Medical Physics for a detailed description of the -% variales) -% alphas relative distance between start and endpoint for the -% intersections with the cube -% l lengths of intersestions with cubes -% rho densities extracted from cubes -% d12 distance between start and endpoint of ray tracing -% ix indices of hit voxels +% output: +% alphas: relative distance between start and endpoint for the intersections with the cube +% l: lengths of intersections with cubes +% rho: densities extracted from cubes +% d12: distance between start and endpoint of ray tracing +% ix: indices of hit voxels % % References % [1] http://www.ncbi.nlm.nih.gov/pubmed/4000088 % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. -% -% This file is part of the matRad project. It is subject to the license -% terms in the LICENSE file found in the top-level directory of this -% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part -% of the matRad project, including this file, may be copied, modified, -% propagated, or distributed except according to the terms contained in the +% Copyright 2015-2026 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the % LICENSE file. % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% Add isocenter to source and target point. Because the algorithm does not -% works with negatives values. This put (resolution.x,resolution.y,resolution.z) -% in the center of first voxel - -sourcePoint = sourcePoint + isocenterCube; -targetPoint = targetPoint + isocenterCube; - -% Save the numbers of planes. -[yNumPlanes, xNumPlanes, zNumPlanes] = size(cubes{1}); -xNumPlanes = xNumPlanes + 1; -yNumPlanes = yNumPlanes + 1; -zNumPlanes = zNumPlanes + 1; - -% eq 11 -% Calculate the distance from source to target point. -d12 = norm(sourcePoint-targetPoint); - -% eq 3 -% Position of first planes in millimeter. 0.5 because the central position -% of the first voxel is at [resolution.x resolution.y resolution.z] -xPlane_1 = .5*resolution.x; -yPlane_1 = .5*resolution.y; -zPlane_1 = .5*resolution.z; - -% Position of last planes in milimiter -xPlane_end = (xNumPlanes - .5)*resolution.x; -yPlane_end = (yNumPlanes - .5)*resolution.y; -zPlane_end = (zNumPlanes - .5)*resolution.z; - -% Calc insersection point with CT cube planes -tvalues = ([xPlane_1,yPlane_1,zPlane_1] - sourcePoint)./ (targetPoint - sourcePoint); -tvalues = [tvalues,([xPlane_end,yPlane_end,zPlane_end] - sourcePoint)./ (targetPoint - sourcePoint)]; - -%check if intersection point within CT -doesHit = false; -for t = tvalues - p = sourcePoint + t*(targetPoint - sourcePoint); - lowerPlanes = [xPlane_1,yPlane_1,zPlane_1] - sqrt(eps); - upperPlanes = [xPlane_end,yPlane_end,zPlane_end] + sqrt(eps); - if all(p > lowerPlanes & p < upperPlanes) - doesHit = true; - continue; - end -end - -%If it does not hit, write empty values -if ~doesHit - alphas = []; - l = []; - for i = 1:numel(cubes) - rho{i} = []; - end - ix = []; - return; -end - -% eq 4 -% Calculate parametrics values of \alpha_{min} and \alpha_{max} for every -% axis, intersecting the ray with the sides of the CT. -if targetPoint(1) ~= sourcePoint(1) - aX_1 = (xPlane_1 - sourcePoint(1)) / (targetPoint(1) - sourcePoint(1)); - aX_end = (xPlane_end - sourcePoint(1)) / (targetPoint(1) - sourcePoint(1)); -else - aX_1 = []; - aX_end = []; -end -if targetPoint(2) ~= sourcePoint(2) - aY_1 = (yPlane_1 - sourcePoint(2)) / (targetPoint(2) - sourcePoint(2)); - aY_end = (yPlane_end - sourcePoint(2)) / (targetPoint(2) - sourcePoint(2)); -else - aY_1 = []; - aY_end = []; -end -if targetPoint(3) ~= sourcePoint(3) - aZ_1 = (zPlane_1 - sourcePoint(3)) / (targetPoint(3) - sourcePoint(3)); - aZ_end = (zPlane_end - sourcePoint(3)) / (targetPoint(3) - sourcePoint(3)); -else - aZ_1 = []; - aZ_end = []; -end - -% eq 5 -% Compute the \alpha_{min} and \alpha_{max} in terms of parametric values -% given by equation 4. -alpha_min = max([0 min(aX_1,aX_end) min(aY_1,aY_end) min(aZ_1,aZ_end)]); -alpha_max = min([1 max(aX_1,aX_end) max(aY_1,aY_end) max(aZ_1,aZ_end)]); - -% eq 6 -% Calculate the range of indeces who gives parametric values for -% intersected planes. -if targetPoint(1) == sourcePoint(1) - i_min = []; i_max = []; -elseif targetPoint(1) > sourcePoint(1) - i_min = xNumPlanes - (xPlane_end - alpha_min * (targetPoint(1) - sourcePoint(1)) - sourcePoint(1))/resolution.x; - i_max = 1 + (sourcePoint(1) + alpha_max * (targetPoint(1) - sourcePoint(1)) - xPlane_1)/resolution.x; - % rounding - i_min = ceil(1/1000*(round(1000*i_min))); - i_max = floor(1/1000*(round(1000*i_max))); -else - i_min = xNumPlanes - (xPlane_end - alpha_max * (targetPoint(1) - sourcePoint(1)) - sourcePoint(1))/resolution.x; - i_max = 1 + (sourcePoint(1) + alpha_min * (targetPoint(1) - sourcePoint(1)) - xPlane_1)/resolution.x; - i_min = ceil(1/1000*(round(1000*i_min))); - i_max = floor(1/1000*(round(1000*i_max))); -end -if targetPoint(2) == sourcePoint(2) - j_min = []; j_max = []; -elseif targetPoint(2) > sourcePoint(2) - j_min = yNumPlanes - (yPlane_end - alpha_min * (targetPoint(2) - sourcePoint(2)) - sourcePoint(2))/resolution.y; - j_max = 1 + (sourcePoint(2) + alpha_max * (targetPoint(2) - sourcePoint(2)) - yPlane_1)/resolution.y; - j_min = ceil(1/1000*(round(1000*j_min))); - j_max = floor(1/1000*(round(1000*j_max))); -else - j_min = yNumPlanes - (yPlane_end - alpha_max * (targetPoint(2) - sourcePoint(2)) - sourcePoint(2))/resolution.y; - j_max = 1 + (sourcePoint(2) + alpha_min * (targetPoint(2) - sourcePoint(2)) - yPlane_1)/resolution.y; - j_min = ceil(1/1000*(round(1000*j_min))); - j_max = floor(1/1000*(round(1000*j_max))); -end -if targetPoint(3) == sourcePoint(3) - k_min = []; k_max = []; -elseif targetPoint(3) >= sourcePoint(3) - k_min = zNumPlanes - (zPlane_end - alpha_min * (targetPoint(3) - sourcePoint(3)) - sourcePoint(3))/resolution.z; - k_max = 1 + (sourcePoint(3) + alpha_max * (targetPoint(3) - sourcePoint(3)) - zPlane_1)/resolution.z; - k_min = ceil(1/1000*(round(1000*k_min))); - k_max = floor(1/1000*(round(1000*k_max))); -else - k_min = zNumPlanes - (zPlane_end - alpha_max * (targetPoint(3) - sourcePoint(3)) - sourcePoint(3))/resolution.z; - k_max = 1 + (sourcePoint(3) + alpha_min * (targetPoint(3) - sourcePoint(3)) - zPlane_1)/resolution.z; - k_min = ceil(1/1000*(round(1000*k_min))); - k_max = floor(1/1000*(round(1000*k_max))); -end - -% eq 7 -% For the given range of indices, calculate the paremetrics values who -% represents intersections of the ray with the plane. -if i_min ~= i_max - if targetPoint(1) > sourcePoint(1) - alpha_x = (resolution.x*(i_min:1:i_max)-sourcePoint(1)-.5*resolution.x)/(targetPoint(1)-sourcePoint(1)); - else - alpha_x = (resolution.x*(i_max:-1:i_min)-sourcePoint(1)-.5*resolution.x)/(targetPoint(1)-sourcePoint(1)); - end -else - alpha_x = []; -end -if j_min ~= j_max - if targetPoint(2) > sourcePoint(2) - alpha_y = (resolution.y*(j_min:1:j_max)-sourcePoint(2)-.5*resolution.y)/(targetPoint(2)-sourcePoint(2)); - else - alpha_y = (resolution.y*(j_max:-1:j_min)-sourcePoint(2)-.5*resolution.y)/(targetPoint(2)-sourcePoint(2)); - end -else - alpha_y = []; -end -if k_min ~= k_max - if targetPoint(3) > sourcePoint(3) - alpha_z = (resolution.z*(k_min:1:k_max)-sourcePoint(3)-.5*resolution.z)/(targetPoint(3)-sourcePoint(3)); - else - alpha_z = (resolution.z*(k_max:-1:k_min)-sourcePoint(3)-.5*resolution.z)/(targetPoint(3)-sourcePoint(3)); - end -else - alpha_z = []; -end - -% eq 8 -% Merge parametrics sets. -alphas = unique([alpha_min alpha_x alpha_y alpha_z alpha_max]); - -% eq 10 -% Calculate the voxel intersection length. -l = d12*diff(alphas); - -% eq 13 -% Calculate \alpha_{middle} -alphas_mid = .5*(alphas(1:end-1)+alphas(2:end)); - -% eq 12 -% Calculate the voxel indices: first convert to physical coords -i_mm = sourcePoint(1) + alphas_mid*(targetPoint(1) - sourcePoint(1)); -j_mm = sourcePoint(2) + alphas_mid*(targetPoint(2) - sourcePoint(2)); -k_mm = sourcePoint(3) + alphas_mid*(targetPoint(3) - sourcePoint(3)); -% then convert to voxel index -i = round(i_mm/resolution.x); -j = round(j_mm/resolution.y); -k = round(k_mm/resolution.z); +matRad_cfg = MatRad_Config.instance(); +matRad_cfg.dispDeprecationWarning('Calls to matRad_siddonRayTracer will be replaced by usage of the matRad_RayTracerSiddon class in the future!'); -% Handle numerical instabilities at the borders. -i(i<=0) = 1; j(j<=0) = 1; k(k<=0) = 1; -i(i>xNumPlanes-1) = xNumPlanes-1; -j(j>yNumPlanes-1) = yNumPlanes-1; -k(k>zNumPlanes-1) = zNumPlanes-1; +grid.resolution = resolution; +grid.dimensions = size(cubes{1}); -% Convert to linear indices -ix = j + (i-1)*size(cubes{1},1) + (k-1)*size(cubes{1},1)*size(cubes{1},2); +% At the moment we only implement the Siddon ray-tracer in [1] +hTracer = matRad_RayTracerSiddon(cubes, grid); -% obtains the values from cubes -rho = cell(numel(cubes),1); -for i = 1:numel(cubes) - rho{i} = cubes{i}(ix); -end +isocenterCube = matRad_cubeCoords2worldCoords(isocenterCube, grid); +[alphas, l, rho, d12, ix] = hTracer.traceRay(isocenterCube, sourcePoint, targetPoint); diff --git a/matRad/scenarios/matRad_GriddedScenariosAbstract.m b/matRad/scenarios/matRad_GriddedScenariosAbstract.m index fd88abf51..8f34f3f9d 100644 --- a/matRad/scenarios/matRad_GriddedScenariosAbstract.m +++ b/matRad/scenarios/matRad_GriddedScenariosAbstract.m @@ -35,6 +35,7 @@ %this.updateScenarios(); end + %% set methods function set.combineRange(this,combineRange_) valid = isscalar(combineRange_) && (isnumeric(combineRange_) || islogical(combineRange_)); if ~valid @@ -45,19 +46,6 @@ this.updateScenarios(); end - %% set methods - %{ - function set.includeNominalScenario(this,includeNomScen) - valid = isscalar(includeNomScen) && (isnumeric(includeNomScen) || islogical(includeNomScen)); - if ~valid - matRad_cfg = MatRad_Config.instance(); - matRad_cfg.dispError('Invalid value for includeNominalScenario! Needs to be a boolean / logical value!'); - end - this.includeNominalScenario = includeNomScen; - this.updateScenarios(); - end - %} - function set.combinations(this,combinations_) valid = any(strcmp(combinations_,this.validCombinationTypes)); if ~valid diff --git a/matRad/scenarios/matRad_ImportanceScenarios.m b/matRad/scenarios/matRad_ImportanceScenarios.m index a8f015a90..d6b116970 100644 --- a/matRad/scenarios/matRad_ImportanceScenarios.m +++ b/matRad/scenarios/matRad_ImportanceScenarios.m @@ -11,12 +11,12 @@ % matRad_ImportanceScenarios() % matRad_ImportanceScenarios(ct) % -% input +% input: % ct: ct cube % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2022 the matRad development team. +% Copyright 2022-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/scenarios/matRad_NominalScenario.m b/matRad/scenarios/matRad_NominalScenario.m index 23912443f..3d15dda71 100644 --- a/matRad/scenarios/matRad_NominalScenario.m +++ b/matRad/scenarios/matRad_NominalScenario.m @@ -6,12 +6,12 @@ % matRad_NominalScenario() % matRad_NominalScenario(ct) % -% input +% input: % ct: ct cube % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2022 the matRad development team. +% Copyright 2022-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/scenarios/matRad_RandomScenarios.m b/matRad/scenarios/matRad_RandomScenarios.m index 91a309d8e..a0eaad8c8 100644 --- a/matRad/scenarios/matRad_RandomScenarios.m +++ b/matRad/scenarios/matRad_RandomScenarios.m @@ -6,12 +6,12 @@ % matRad_RandomScenarios() % matRad_RandomScenarios(ct) % -% input +% input: % ct: ct cube % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2022 the matRad development team. +% Copyright 2022-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/scenarios/matRad_ScenarioModel.m b/matRad/scenarios/matRad_ScenarioModel.m index f84a3dfc3..c51f204d8 100644 --- a/matRad/scenarios/matRad_ScenarioModel.m +++ b/matRad/scenarios/matRad_ScenarioModel.m @@ -9,12 +9,12 @@ % matRad_ScenarioModel() % matRad_ScenarioModel(ct) % -% input +% input: % ct: ct cube % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2022 the matRad development team. +% Copyright 2022-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -91,7 +91,7 @@ function listAllScenarios(this) matRad_cfg.dispInfo('Listing all scenarios...\n'); matRad_cfg.dispInfo('\t#\tctScen\txShift\tyShift\tzShift\tabsRng\trelRng\tprob.\n'); for s = 1:size(this.scenForProb,1) - str = [num2str(this.scenForProb(s,1),'\t%d'),sprintf('\t\t'), num2str(this.scenForProb(s,2:end),'\t%.3f')]; + str = [num2str(this.scenForProb(s,1),'%d\t'),sprintf('\t'), num2str(this.scenForProb(s,2:end),'\t%.3f')]; matRad_cfg.dispInfo('\t%d\t%s\t%.3f\n',s,str,this.scenProb(s)); end end diff --git a/matRad/scenarios/matRad_WorstCaseScenarios.m b/matRad/scenarios/matRad_WorstCaseScenarios.m index 76f5730d0..434cdf6c4 100644 --- a/matRad/scenarios/matRad_WorstCaseScenarios.m +++ b/matRad/scenarios/matRad_WorstCaseScenarios.m @@ -6,12 +6,12 @@ % matRad_WorstCaseScenarios() % matRad_WorstCaseScenarios(ct) % -% input +% input: % ct: ct cube % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2022 the matRad development team. +% Copyright 2022-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/scenarios/matRad_calcScenProb.m b/matRad/scenarios/matRad_calcScenProb.m index 1c266c064..4d6922d03 100644 --- a/matRad/scenarios/matRad_calcScenProb.m +++ b/matRad/scenarios/matRad_calcScenProb.m @@ -2,10 +2,10 @@ % matRad_calcScenProb provides different ways of calculating the probability % of occurance of individual scenarios % -% call +% call: % scenProb = matRad_calcScenProb(mu,sigma,samplePos,calcType,probDist) % -% input +% input: % mu: mean of the distrubtion % sigma: standard deviation of the distribution % calcType: can be set to @@ -14,13 +14,13 @@ % probDist: identifier for the underlying probability distribution % (i) normDist % -% output +% output: % scenProb: occurance probability of the specified scenario % % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2017 the matRad development team. +% Copyright 2017-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/scenarios/matRad_multScen.m b/matRad/scenarios/matRad_multScen.m index 156d8ff23..326effabb 100644 --- a/matRad/scenarios/matRad_multScen.m +++ b/matRad/scenarios/matRad_multScen.m @@ -4,12 +4,12 @@ % the respective matRad_ScenarioModel instance for the specific scenario % model chosen with standard parameters. % -% call +% call: % pln.multScen = matRad_multScen(ct,scenarioModel); % % e.g. pln.multScen = matRad_multScen(ct,'nomScen'); % -% input +% input: % ct: ct cube % scenarioModel: string to denote a scenario creation method % 'nomScen' create only the nominal scenario @@ -19,7 +19,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2022 the matRad development team. +% Copyright 2022-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/sequencing/matRad_aperture2collimation.m b/matRad/sequencing/matRad_aperture2collimation.m index 934daf8e4..64ef91cc6 100644 --- a/matRad/sequencing/matRad_aperture2collimation.m +++ b/matRad/sequencing/matRad_aperture2collimation.m @@ -3,16 +3,16 @@ % into collimation information in pln and stf for field-based dose % calculation % -% call +% call: % [pln,stf] = matRad_aperture2collimation(pln,stf,sequencing,apertureInfo) % -% input +% input: % pln: pln file used to generate the sequenced plan % stf: stf file used to generate the sequenced plan % sequencing: sequencing information (from resultGUI) % apertureInfo: apertureInfo (from resultGUI) % -% output +% output: % pln: matRad pln struct with collimation information % stf: matRad stf struct with shapes instead of beamlets % @@ -21,7 +21,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2022 the matRad development team. +% Copyright 2022-2026 the matRad development team. % Author: wahln % % This file is part of the matRad project. It is subject to the license diff --git a/matRad/sequencing/matRad_engelLeafSequencing.m b/matRad/sequencing/matRad_engelLeafSequencing.m index a1d944374..528b72d15 100644 --- a/matRad/sequencing/matRad_engelLeafSequencing.m +++ b/matRad/sequencing/matRad_engelLeafSequencing.m @@ -3,10 +3,10 @@ % for intensity modulated beams with multiple static segments accroding % to Engel et al. 2005 Discrete Applied Mathematics % -% call +% call: % resultGUI = matRad_engelLeafSequencing(resultGUI,stf,dij,numOfLevels,visBool) % -% input +% input: % resultGUI: resultGUI struct to which the output data will be added, if % this field is empty resultGUI struct will be created % stf: matRad steering information struct @@ -14,7 +14,7 @@ % numOfLevels: number of stratification levels % visBool: toggle on/off visualization (optional) % -% output +% output: % resultGUI: matRad result struct containing the new dose cube % as well as the corresponding weights % @@ -23,7 +23,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -57,12 +57,12 @@ numOfRaysPerBeam = stf(i).numOfRays; % get relevant weights for current beam - wOfCurrBeams = resultGUI.w(1+offset:numOfRaysPerBeam+offset); + wOfCurrBeams = resultGUI.w(1+offset:numOfRaysPerBeam+offset).* ones(size(stf(i).ray,2),1); - X = ones(numOfRaysPerBeam,1)*NaN; - Z = ones(numOfRaysPerBeam,1)*NaN; + X = ones(size(stf(i).ray,2),1)*NaN; %this way it also works with3dconformal + Z = ones(size(stf(i).ray,2),1)*NaN; - for j=1:stf(i).numOfRays + for j=1:size(stf(i).ray,2) X(j) = stf(i).ray(j).rayPos_bev(:,1); Z(j) = stf(i).ray(j).rayPos_bev(:,3); end @@ -352,15 +352,24 @@ clear rightIntLimit; end - - sequencing.beam(i).numOfShapes = k; - sequencing.beam(i).shapes = shapes(:,:,1:k); - sequencing.beam(i).shapesWeight = shapesWeight(1:k)/numOfLevels*calFac; - sequencing.beam(i).bixelIx = 1+offset:numOfRaysPerBeam+offset; - sequencing.beam(i).fluence = D_0; + if sum(wOfCurrBeams)>0 + + sequencing.beam(i).numOfShapes = k; + sequencing.beam(i).shapes = shapes(:,:,1:k); + sequencing.beam(i).shapesWeight = shapesWeight(1:k)/numOfLevels*calFac; + sequencing.beam(i).bixelIx = 1+offset:numOfRaysPerBeam+offset; + sequencing.beam(i).fluence = D_0; + + else + sequencing.beam(i).numOfShapes = 1; + sequencing.beam(i).shapes = zeros(dimOfFluenceMxZ,dimOfFluenceMxX); + sequencing.beam(i).shapesWeight = zeros(dimOfFluenceMxZ,dimOfFluenceMxX); + sequencing.beam(i).bixelIx = 1+offset:numOfRaysPerBeam+offset; + sequencing.beam(i).fluence = zeros(dimOfFluenceMxZ,dimOfFluenceMxX); + end sequencing.w(1+offset:numOfRaysPerBeam+offset,1) = D_0(indInFluenceMx)/numOfLevels*calFac; - + offset = offset + numOfRaysPerBeam; end diff --git a/matRad/sequencing/matRad_sequencing2ApertureInfo.m b/matRad/sequencing/matRad_sequencing2ApertureInfo.m index 867cabd43..cbbd6aa38 100644 --- a/matRad/sequencing/matRad_sequencing2ApertureInfo.m +++ b/matRad/sequencing/matRad_sequencing2ApertureInfo.m @@ -2,22 +2,21 @@ % matRad function to generate a shape info struct % based on the result of multileaf collimator sequencing % -% call +% call: % apertureInfo = matRad_sequencing2ApertureInfo(Sequencing,stf) % -% input +% input: % Sequencing: matRad sequencing result struct % stf: matRad steering information struct % -% output +% output: % apertureInfo: matRad aperture weight and shape info struct % % References % % - -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/sequencing/matRad_siochiLeafSequencing.m b/matRad/sequencing/matRad_siochiLeafSequencing.m index 4240a6c9a..96a0444b8 100644 --- a/matRad/sequencing/matRad_siochiLeafSequencing.m +++ b/matRad/sequencing/matRad_siochiLeafSequencing.m @@ -6,13 +6,13 @@ % % Implemented in matRad by Eric Christiansen, Emily Heath, and Tong Xu % -% call +% call: % resultGUI = % matRad_siochiLeafSequencing(resultGUI,stf,dij,numOfLevels) % resultGUI = % matRad_siochiLeafSequencing(resultGUI,stf,dij,numOfLevels,visBool) % -% input +% input: % resultGUI: resultGUI struct to which the output data will be % added, if this field is empty resultGUI struct will % be created @@ -21,7 +21,7 @@ % numOfLevels: number of stratification levels % visBool: toggle on/off visualization (optional) % -% output +% output: % resultGUI: matRad result struct containing the new dose cube % as well as the corresponding weights % @@ -30,7 +30,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -70,12 +70,12 @@ numOfRaysPerBeam = stf(i).numOfRays; % get relevant weights for current beam - wOfCurrBeams = wUnsequenced(1+offset:numOfRaysPerBeam+offset);%REVIEW OFFSET + wOfCurrBeams = wUnsequenced(1+offset:numOfRaysPerBeam+offset).* ones(size(stf(i).ray,2),1);%REVIEW OFFSET - X = ones(numOfRaysPerBeam,1)*NaN; - Z = ones(numOfRaysPerBeam,1)*NaN; + X = ones(size(stf(i).ray,2),1)*NaN; %this way it also works with3dconformal + Z = ones(size(stf(i).ray,2),1)*NaN; - for j = 1:stf(i).numOfRays + for j = 1:size(stf(i).ray,2) X(j) = stf(i).ray(j).rayPos_bev(:,1); Z(j) = stf(i).ray(j).rayPos_bev(:,3); end @@ -137,27 +137,41 @@ D_k_MinX = min(D_k_X); D_k_MaxX = max(D_k_X); - %Decompose the port, do rod pushing - [tops, bases] = matRad_siochiDecomposePort(D_k,dimOfFluenceMxZ,dimOfFluenceMxX,D_k_MinZ,D_k_MaxZ,D_k_MinX,D_k_MaxX); - %Form segments with and without visualization - if visBool - [shapes,shapesWeight,k,D_k]=matRad_siochiConvertToSegments(shapes,shapesWeight,k,tops,bases,visBool,i,D_k,numOfLevels,seqFig,seqSubPlots); + if sum(wOfCurrBeams)>0 + %Decompose the port, do rod pushing + [tops, bases] = matRad_siochiDecomposePort(D_k,dimOfFluenceMxZ,dimOfFluenceMxX,D_k_MinZ,D_k_MaxZ,D_k_MinX,D_k_MaxX); + %Form segments with and without visualization + if visBool + [shapes,shapesWeight,k,D_k]=matRad_siochiConvertToSegments(shapes,shapesWeight,k,tops,bases,visBool,i,D_k,numOfLevels,seqFig,seqSubPlots); + else + [shapes,shapesWeight,k]=matRad_siochiConvertToSegments(shapes,shapesWeight,k,tops,bases); + end + + sequencing.beam(i).numOfShapes = k; + sequencing.beam(i).shapes = shapes(:,:,1:k); + sequencing.beam(i).shapesWeight = shapesWeight(1:k)/numOfLevels*calFac; + sequencing.beam(i).bixelIx = 1+offset:numOfRaysPerBeam+offset; + sequencing.beam(i).fluence = D_0; + sequencing.beam(i).sum = zeros(dimOfFluenceMxZ,dimOfFluenceMxX); + + for j = 1:k + sequencing.beam(i).sum = sequencing.beam(i).sum+sequencing.beam(i).shapes(:,:,j)*sequencing.beam(i).shapesWeight(j); + end + else - [shapes,shapesWeight,k]=matRad_siochiConvertToSegments(shapes,shapesWeight,k,tops,bases); + sequencing.beam(i).numOfShapes = 1; + sequencing.beam(i).shapes = zeros(dimOfFluenceMxZ,dimOfFluenceMxX); + sequencing.beam(i).shapesWeight = zeros(dimOfFluenceMxZ,dimOfFluenceMxX); + sequencing.beam(i).bixelIx = 1+offset:numOfRaysPerBeam+offset; + sequencing.beam(i).fluence = zeros(dimOfFluenceMxZ,dimOfFluenceMxX); + sequencing.beam(i).sum = zeros(dimOfFluenceMxZ,dimOfFluenceMxX); end - sequencing.beam(i).numOfShapes = k; - sequencing.beam(i).shapes = shapes(:,:,1:k); - sequencing.beam(i).shapesWeight = shapesWeight(1:k)/numOfLevels*calFac; - sequencing.beam(i).bixelIx = 1+offset:numOfRaysPerBeam+offset; - sequencing.beam(i).fluence = D_0; - sequencing.beam(i).sum = zeros(dimOfFluenceMxZ,dimOfFluenceMxX); - - for j = 1:k - sequencing.beam(i).sum = sequencing.beam(i).sum+sequencing.beam(i).shapes(:,:,j)*sequencing.beam(i).shapesWeight(j); + if numOfRaysPerBeam >1 + sequencing.w(1+offset:numOfRaysPerBeam+offset,1) = sequencing.beam(i).sum(indInFluenceMx); + else + sequencing.w(1+offset:numOfRaysPerBeam+offset,1) = wOfCurrBeams(1); end - sequencing.w(1+offset:numOfRaysPerBeam+offset,1) = sequencing.beam(i).sum(indInFluenceMx); - offset = offset + numOfRaysPerBeam; end diff --git a/matRad/sequencing/matRad_xiaLeafSequencing.m b/matRad/sequencing/matRad_xiaLeafSequencing.m index aaa893324..e8f3a3ade 100644 --- a/matRad/sequencing/matRad_xiaLeafSequencing.m +++ b/matRad/sequencing/matRad_xiaLeafSequencing.m @@ -3,11 +3,11 @@ % for intensity modulated beams with multiple static segments according to % Xia et al. (1998) Medical Physics % -% call +% call: % resultGUI = matRad_xiaLeafSequencing(resultGUI,stf,dij,numOfLevels) % resultGUI = matRad_xiaLeafSequencing(resultGUI,stf,dij,numOfLevels,visBool) % -% input +% input: % resultGUI: resultGUI struct to which the output data will be added, if % this field is empty resultGUI struct will be created % stf: matRad steering information struct @@ -15,7 +15,7 @@ % numOfLevels: number of stratification levels % visBool: toggle on/off visualization (optional) % -% output +% output: % resultGUI: matRad result struct containing the new dose cube as well as % the corresponding weights % @@ -24,7 +24,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -60,12 +60,12 @@ numOfRaysPerBeam = stf(i).numOfRays; % get relevant weights for current beam - wOfCurrBeams = resultGUI.w(1+offset:numOfRaysPerBeam+offset);%REVIEW OFFSET + wOfCurrBeams = resultGUI.w(1+offset:numOfRaysPerBeam+offset).* ones(size(stf(i).ray,2),1);%;%REVIEW OFFSET - X = ones(numOfRaysPerBeam,1)*NaN; - Z = ones(numOfRaysPerBeam,1)*NaN; - - for j=1:stf(i).numOfRays + X = ones(size(stf(i).ray,2),1)*NaN; %this way it also works with3dconformal + Z = ones(size(stf(i).ray,2),1)*NaN; + + for j=1:size(stf(i).ray,2) X(j) = stf(i).ray(j).rayPos_bev(:,1); Z(j) = stf(i).ray(j).rayPos_bev(:,3); end @@ -239,15 +239,26 @@ L_k = max(D_k(:)); % eq 5 end + + if sum(wOfCurrBeams)>0 + + sequencing.beam(i).numOfShapes = k; + sequencing.beam(i).shapes = shapes(:,:,1:k); + sequencing.beam(i).shapesWeight = shapesWeight(1:k)/numOfLevels*calFac; + sequencing.beam(i).bixelIx = 1+offset:numOfRaysPerBeam+offset; + sequencing.beam(i).fluence = D_0; + + else + sequencing.beam(i).numOfShapes = 1; + sequencing.beam(i).shapes = zeros(dimOfFluenceMxZ,dimOfFluenceMxX); + sequencing.beam(i).shapesWeight = zeros(dimOfFluenceMxZ,dimOfFluenceMxX); + sequencing.beam(i).bixelIx = 1+offset:numOfRaysPerBeam+offset; + sequencing.beam(i).fluence = zeros(dimOfFluenceMxZ,dimOfFluenceMxX); + end - sequencing.beam(i).numOfShapes = k; - sequencing.beam(i).shapes = shapes(:,:,1:k); - sequencing.beam(i).shapesWeight = shapesWeight(1:k)/numOfLevels*calFac; - sequencing.beam(i).bixelIx = 1+offset:numOfRaysPerBeam+offset; - sequencing.beam(i).fluence = D_0; sequencing.w(1+offset:numOfRaysPerBeam+offset,1) = D_0(indInFluenceMx)/numOfLevels*calFac; - + offset = offset + numOfRaysPerBeam; end diff --git a/matRad/steering/matRad_StfGeneratorBase.m b/matRad/steering/matRad_StfGeneratorBase.m index 31c5a11ee..e269d7671 100644 --- a/matRad/steering/matRad_StfGeneratorBase.m +++ b/matRad/steering/matRad_StfGeneratorBase.m @@ -4,7 +4,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2024 the matRad development team. +% Copyright 2024-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -32,6 +32,7 @@ bioModel; %Biological Model radiationMode; %Radiation Mode machine; %Machine + enableGPU = false; %Enable computation on the GPU (experimenta, default false) end properties (Access = protected) @@ -155,8 +156,6 @@ function assignPropertiesFromPln(this,pln,warnWhenPropertyChanged) plnStruct = struct(); end - fields = fieldnames(plnStruct); - %Set up warning message if warnWhenPropertyChanged warningMsg = 'Property in stf generator overwritten from pln.propStf'; @@ -164,41 +163,8 @@ function assignPropertiesFromPln(this,pln,warnWhenPropertyChanged) warningMsg = ''; end - % iterate over all fieldnames and try to set the - % corresponding properties inside the stf generator - if matRad_cfg.isOctave - c2sWarningState = warning('off','Octave:classdef-to-struct'); - end - - for i = 1:length(fields) - try - field = fields{i}; - if matRad_ispropCompat(this,field) - this.(field) = matRad_recursiveFieldAssignment(this.(field),plnStruct.(field),true,warningMsg); - else - matRad_cfg.dispWarning('Not able to assign property ''%s'' from pln.propStf to stf generator!',field); - end - catch ME - % catch exceptions when the stf generator has no - % properties which are defined in the struct. - % When defining an engine with custom setter and getter - % methods, custom exceptions can be caught here. Be - % careful with Octave exceptions! - if ~isempty(warningMsg) - matRad_cfg = MatRad_Config.instance(); - switch ME.identifier - case 'MATLAB:noPublicFieldForClass' - matRad_cfg.dispWarning('Not able to assign property from pln.propStf to stf generator: %s',ME.message); - otherwise - matRad_cfg.dispWarning('Problem while setting up stf generator from struct:%s %s',field,ME.message); - end - end - end - end + matRad_assignPropertiesFromStruct(this,plnStruct,true,warningMsg); - if matRad_cfg.isOctave - warning(c2sWarningState.state,'Octave:classdef-to-struct'); - end end end @@ -214,9 +180,14 @@ function assignPropertiesFromPln(this,pln,warnWhenPropertyChanged) % Instance of MatRad_Config class matRad_cfg = MatRad_Config.instance(); matRad_cfg.dispInfo('matRad: Generating stf struct with generator ''%s''... ',this.name); - - this.ct = ct; - this.cst = cst; + + if this.enableGPU + this.ct = matRad_moveCtToGPU(ct); + this.cst = matRad_moveCstToGPU(cst); + else + this.ct = ct; + this.cst = cst; + end this.initialize(); this.createPatientGeometry(); @@ -409,7 +380,7 @@ function createPatientGeometry(this) function classList = getAvailableGenerators(pln,optionalPaths) % Returns a list of names and coresponding handle for stf % generators. Returns all stf generators when no arg is - % given. If no generators are found return gonna be empty. + % given. If no generators are found return gonna be empty. % % call: % classList = matRad_StfGeneratorBase.getAvailableGenerators(pln,optional_path) @@ -510,14 +481,16 @@ function createPatientGeometry(this) function [available,msg] = isAvailable(pln,machine) % return a boolean if the generator is is available for the given pln % struct. Needs to be implemented in non abstract subclasses + % % input: - % - pln: matRad pln struct - % - machine: optional machine to avoid loading the machine from + % pln: matRad pln struct + % machine: optional machine to avoid loading the machine from % disk (makes sense to use if machine already loaded) + % % output: - % - available: boolean value to check if the dose engine is + % available: boolean value to check if the dose engine is % available for the given pln/machine - % - msg: msg to elaborate on availability. If not available, + % msg: msg to elaborate on availability. If not available, % a msg string indicates an error during the check % if available, indicates a warning that not all % information was present in the machine file and diff --git a/matRad/steering/matRad_StfGeneratorExternalRayBixelAbstract.m b/matRad/steering/matRad_StfGeneratorExternalRayBixelAbstract.m index 524cf6490..a47dd0203 100644 --- a/matRad/steering/matRad_StfGeneratorExternalRayBixelAbstract.m +++ b/matRad/steering/matRad_StfGeneratorExternalRayBixelAbstract.m @@ -4,7 +4,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2024 the matRad development team. +% Copyright 2024-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -130,7 +130,7 @@ function initialize(this) this.isoCenter = matRad_getIsoCenter(this.cst,this.ct,visBool); end - if ~isequal(size(this.isoCenter),[this.numOfBeams,3]) && ~size(this.isoCenter,1) ~= 1 + if ~isequal(size(this.isoCenter),[this.numOfBeams,3]) && size(this.isoCenter,1) ~= 1 matRad_cfg = MatRad_Config.instance(); matRad_cfg.dispWarning('IsoCenter invalid, creating new one automatically!'); this.isoCenter = matRad_getIsoCenter(this.cst,this.ct,visBool); diff --git a/matRad/steering/matRad_StfGeneratorParticleIMPT.m b/matRad/steering/matRad_StfGeneratorParticleIMPT.m index 7e8e2845b..cb73e60f9 100644 --- a/matRad/steering/matRad_StfGeneratorParticleIMPT.m +++ b/matRad/steering/matRad_StfGeneratorParticleIMPT.m @@ -6,7 +6,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2024 the matRad development team. +% Copyright 2024-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -49,8 +49,6 @@ %Assigns the max particle machine energy layers to all rays matRad_cfg = MatRad_Config.instance(); - isoCenterInCubeCoords = matRad_world2cubeCoords(beam.isoCenter,this.ct); - if isfield(this.machine.meta,'LUT_bxWidthminFWHM') LUTspotSize = this.machine.meta.LUT_bxWidthminFWHM; else @@ -77,11 +75,10 @@ for shiftScen = 1:this.multScen.totNumShiftScen % ray tracing necessary to determine depth of the target - [alphas,l{shiftScen},rho{shiftScen},d12,~] = matRad_siddonRayTracer(isoCenterInCubeCoords + this.multScen.isoShift(shiftScen,:), ... - this.ct.resolution, ... + [alphas,l{shiftScen},rho{shiftScen},d12,~] = this.rayTracer.traceRay(... + beam.isoCenter + this.multScen.isoShift(shiftScen,:), ... beam.sourcePoint, ... - beam.ray(j).targetPoint, ... - [this.ct.cube {this.voiTarget}]); + beam.ray(j).targetPoint); %Used for generic range-shifter placement ctEntryPoint(shiftScen) = alphas(1) * d12; @@ -125,9 +122,21 @@ end % find target entry & exit - diff_voi = [diff([rho{shiftScen}{end}])]; - entryIx = find(diff_voi == 1); - exitIx = find(diff_voi == -1); + if rho{shiftScen}{end}(1)~=0 + matRad_cfg.dispWarning('Target entry on the first voxel'); + entryIx = 1; + else + diffVoi = [diff([rho{shiftScen}{end}])]; + entryIx = find(diffVoi == 1); + end + + if rho{shiftScen}{end}(end)~=0 + matRad_cfg.dispWarning('Target exit on the last voxel'); + exitIx = numel(rho{shiftScen}{end}); + else + diffVoi = [diff([rho{shiftScen}{end}])]; + exitIx = find(diffVoi == -1); + end %We approximate the interface using the rad depth between the last voxel before and the first voxel after the interface % This captures the case that the first relevant voxel is a target voxel @@ -176,9 +185,13 @@ %non-reachable low-range spots raShiEnergies = this.availableEnergies(this.availablePeakPosRaShi >= targetEntry(k) & min(this.availablePeakPos) > this.availablePeakPosRaShi); + if isempty(raShiEnergies) + matRad_cfg.dispWarning('No energies available for range shifting, please change the range shifter thickness'); + end + raShi.ID = 1; - raShi.eqThickness = rangeShifterEqD; - raShi.sourceRashiDistance = round(min(ctEntryPoint) - 2*rangeShifterEqD,-1); %place a little away from entry, round to cms to reduce number of unique settings + raShi.eqThickness = this.rangeShifterEqD; + raShi.sourceRashiDistance = 10 * round((min(ctEntryPoint) - 2*this.rangeShifterEqD) / 10); %place a little away from entry, round to cms to reduce number of unique settings beam.ray(j).energy = [beam.ray(j).energy raShiEnergies]; beam.ray(j).rangeShifter = [beam.ray(j).rangeShifter repmat(raShi,1,length(raShiEnergies))]; diff --git a/matRad/steering/matRad_StfGeneratorParticleRayBixelAbstract.m b/matRad/steering/matRad_StfGeneratorParticleRayBixelAbstract.m index 7862cfecb..bd8592b1c 100644 --- a/matRad/steering/matRad_StfGeneratorParticleRayBixelAbstract.m +++ b/matRad/steering/matRad_StfGeneratorParticleRayBixelAbstract.m @@ -4,7 +4,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2024 the matRad development team. +% Copyright 2024-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -18,6 +18,7 @@ properties useRangeShifter = false; + rangeShifterEqD end properties (Access = protected) @@ -26,6 +27,7 @@ availablePeakPosRaShi maxPBwidth pbMargin + rayTracer end methods @@ -66,19 +68,35 @@ function initialize(this) if this.useRangeShifter %For now only a generic range shifter is used whose thickness is %determined by the minimum peak width to play with - rangeShifterEqD = round(min(this.availablePeakPos)* 1.25); - this.availablePeakPosRaShi = this.availablePeakPos - rangeShifterEqD; - - matRad_cfg.dispWarning('Use of range shifter enabled. matRad will generate a generic range shifter with WEPL %f to enable ranges below the shortest base data entry.',rangeShifterEqD); + + matRad_cfg.dispInfo('\nUse of range shifter active'); + + if ~isempty(this.rangeShifterEqD) + matRad_cfg.dispInfo('\nUsing provided range shifter thickness of %f mm\n', this.rangeShifterEqD); + else + this.rangeShifterEqD = round(min(this.availablePeakPos)* 1.25); + matRad_cfg.dispInfo('\nUsing generic range shifter thickness of %f mm determined to allow ranges below the shortest base data entry\n', this.rangeShifterEqD); + end + + this.availablePeakPosRaShi = this.availablePeakPos - this.rangeShifterEqD; + + % Available PeakPositionRaShi has to have same size() as + % availablePeakPos for indexing + this.availablePeakPosRaShi(this.availablePeakPosRaShi<0) = 0; end if sum(this.availablePeakPos<0)>0 - matRad_cfg.dispError('at least one available peak position is negative - inconsistent machine file') + matRad_cfg.dispError('At least one available peak position is negative - inconsistent machine file') end %Create Water equivalent cube in ct this.ct = matRad_calcWaterEqD(this.ct,this.radiationMode); end + + function createPatientGeometry(this) + this.createPatientGeometry@matRad_StfGeneratorExternalRayBixelAbstract(); + this.rayTracer = matRad_RayTracerSiddon([this.ct.cube {this.voiTarget}],this.ct); + end end methods (Static) diff --git a/matRad/steering/matRad_StfGeneratorParticleSingleBeamlet.m b/matRad/steering/matRad_StfGeneratorParticleSingleBeamlet.m index 775617a6f..f921aa5f1 100644 --- a/matRad/steering/matRad_StfGeneratorParticleSingleBeamlet.m +++ b/matRad/steering/matRad_StfGeneratorParticleSingleBeamlet.m @@ -4,7 +4,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2024 the matRad development team. +% Copyright 2024-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -18,6 +18,7 @@ properties energy; raShiThickness = 50; %Range shifter to be used if useRangeShifter = true; + visualize = false; end properties (Constant) @@ -50,12 +51,12 @@ function createPatientGeometry(this) matRad_cfg = MatRad_Config.instance(); if isempty(this.isoCenter) - this.isoCenter = matRad_getIsoCenter(this.cst,this.ct,visBool); + this.isoCenter = matRad_getIsoCenter(this.cst,this.ct,this.visualize); end - if ~isequal(size(this.isoCenter),[this.numOfBeams,3]) && ~size(this.isoCenter,1) ~= 1 + if ~isequal(size(this.isoCenter),[this.numOfBeams,3]) && size(this.isoCenter,1) ~= 1 matRad_cfg.dispWarning('IsoCenter invalid, creating new one automatically!'); - this.isoCenter = matRad_getIsoCenter(this.cst,this.ct,visBool); + this.isoCenter = matRad_getIsoCenter(this.cst,this.ct,this.visualize); end if size(this.isoCenter,1) == 1 @@ -113,14 +114,19 @@ function createPatientGeometry(this) end function beam = setBeamletEnergies(this,beam) - isoCenterCubeSystem = matRad_world2cubeCoords(beam.isoCenter,this.ct); + matRad_cfg = MatRad_Config.instance(); + %isoCenterCubeSystem = matRad_world2cubeCoords(beam.isoCenter,this.ct); + % ray tracing necessary to determine depth of the target - [alphas,l,rho,d12,~] = matRad_siddonRayTracer(isoCenterCubeSystem, ... - this.ct.resolution, ... - beam.sourcePoint, ... - beam.ray.targetPoint, ... - [{this.ct.cube{1}} {this.voiTarget}]); + % [alphas,l,rho,d12,~] = matRad_siddonRayTracer(isoCenterCubeSystem, ... + % this.ct.resolution, ... + % beam.sourcePoint, ... + % beam.ray.targetPoint, ... + % [{this.ct.cube{1}} {this.voiTarget}]); + + rayTracer = matRad_RayTracerSiddon({this.ct.cube{1} this.voiTarget},this.ct); + [alphas,l,rho,d12] = rayTracer.traceRay(beam.isoCenter,beam.sourcePoint,beam.ray.targetPoint); if isempty(alphas) matRad_cfg.dispError('Beam seems to not hit the CT! Check Isocenter placement!'); @@ -197,7 +203,10 @@ function createPatientGeometry(this) %Place range shifter 2 times the range away from isocenter, but %at least 10 cm - sourceRaShi = round(ctEntryPoint - 2*this.raShiThickness,-1); %place a little away from entry, round to cms to reduce number of unique settings; + + roundToDigit = @(x,n) round(x * 10^n)/10^n; + + sourceRaShi = roundToDigit((ctEntryPoint - 2*this.raShiThickness),-1); %place a little away from entry, round to cms to reduce number of unique settings; beam.ray.rangeShifter.sourceRashiDistance = sourceRaShi; else beam.ray.rangeShifter.ID = 0; diff --git a/matRad/steering/matRad_StfGeneratorParticleVHEE.m b/matRad/steering/matRad_StfGeneratorParticleVHEE.m index fb097e639..926d91672 100644 --- a/matRad/steering/matRad_StfGeneratorParticleVHEE.m +++ b/matRad/steering/matRad_StfGeneratorParticleVHEE.m @@ -6,7 +6,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2025 the matRad development team. +% Copyright 2025-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/steering/matRad_StfGeneratorPhotonRayBixelAbstract.m b/matRad/steering/matRad_StfGeneratorPhotonRayBixelAbstract.m index 44a4d4b6a..b1ff73e8c 100644 --- a/matRad/steering/matRad_StfGeneratorPhotonRayBixelAbstract.m +++ b/matRad/steering/matRad_StfGeneratorPhotonRayBixelAbstract.m @@ -4,7 +4,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2024 the matRad development team. +% Copyright 2024-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/steering/matRad_StfGeneratorPhotonSingleBeamlet.m b/matRad/steering/matRad_StfGeneratorPhotonSingleBeamlet.m index 8d158ce60..544123856 100644 --- a/matRad/steering/matRad_StfGeneratorPhotonSingleBeamlet.m +++ b/matRad/steering/matRad_StfGeneratorPhotonSingleBeamlet.m @@ -4,7 +4,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2024 the matRad development team. +% Copyright 2024-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/util/matRad_SpotRemovalDij.m b/matRad/util/matRad_SpotRemovalDij.m index 7870b29e0..def0760e7 100644 --- a/matRad/util/matRad_SpotRemovalDij.m +++ b/matRad/util/matRad_SpotRemovalDij.m @@ -6,7 +6,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2019 the matRad development team. + % Copyright 2019-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/util/matRad_addMUdataFromMachine.m b/matRad/util/matRad_addMUdataFromMachine.m index 0e6851bf5..f47ee2c69 100644 --- a/matRad/util/matRad_addMUdataFromMachine.m +++ b/matRad/util/matRad_addMUdataFromMachine.m @@ -2,16 +2,16 @@ % helper function to add MU data included in machine file to dij and stf % computed with earlier versions of matRad % -% call +% call: % [stf, dij] = matRad_addMUdataFromMachine(machine, stf, dij) % stf = matRad_addMUdataFromMachine(machine, stf) % -% input +% input: % machine: machine struct as stored in basedata file % stf: matRad steering information struct % dij: matRad dose influence matrix struct % -% output +% output: % stf: matRad steering information struct with MUdata % dij: matRad dose influence matrix struct with MUdata % @@ -19,7 +19,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2012 the matRad development team. +% Copyright 2012-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -70,7 +70,7 @@ numParticlesPerMUspot = stf(beamNum).ray(rayNum).numParticlesPerMU(spotNum); dij.minMU(iBixel,1) = minMUSpot; - dij.mxaMU(iBixel,1) = maxMUSpot; + dij.maxMU(iBixel,1) = maxMUSpot; dij.numParticlesPerMU(iBixel) = numParticlesPerMUspot; end end diff --git a/matRad/util/matRad_appendResultGUI.m b/matRad/util/matRad_appendResultGUI.m index fb042c692..ffef82090 100644 --- a/matRad/util/matRad_appendResultGUI.m +++ b/matRad/util/matRad_appendResultGUI.m @@ -2,21 +2,21 @@ % function to merge two seperate resultGUI structs into one for % visualisation % -% call +% call: % resultGUI = matRad_mergeResultGUIs(resultGUI,resultGUIrob) % -% input +% input: % resultGUI: matRads resultGUI struct % resultGUItoAppend: resultGUI struct which will be appendet % boolOverwrite: if true existing fields be overwritten in case % they already exist % -% output +% output: % resultGUI: matRads resultGUI struct % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2018 the matRad development team. +% Copyright 2018-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/util/matRad_assignPropertiesFromStruct.m b/matRad/util/matRad_assignPropertiesFromStruct.m new file mode 100644 index 000000000..141b3061c --- /dev/null +++ b/matRad/util/matRad_assignPropertiesFromStruct.m @@ -0,0 +1,78 @@ +function matRad_assignPropertiesFromStruct(obj,propertiesStruct,overwrite,fieldChangedWarningMessage) +% matRad helper function to configure object from structure +% +% call: +% obj = matRad_assignPropertiesFromStruct(obj,propertiesStruct,overwrite,fieldChangedWarningMessage) +% obj = matRad_assignPropertiesFromStruct(obj,propertiesStruct,overwrite) +% obj = matRad_assignPropertiesFromStruct(obj,propertiesStruct) +% +% input: +% obj: Object to be configured +% propertiesStruct: Structure containing properties to be +% assigned +% overwrite: (optional) Boolean flag to overwrite +% existing properties (default: true) +% fieldChangedWarningMessage: (optional) Custom warning message for +% changed fields +% +% output: +% resultGUI: struct containing optimized fluence vector, dose, and (for +% biological optimization) RBE-weighted dose etc. +% optimizer: Used Optimizer Object +% +% References +% - +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2016-2026 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +matRad_cfg = MatRad_Config.instance(); + +if nargin < 4 + fieldChangedWarningMessage = ''; +end + +if nargin < 3 + overwrite = true; +end + +if matRad_cfg.isOctave + c2sWarningState = warning('off','Octave:classdef-to-struct'); +end + +fields = fieldnames(propertiesStruct); + +for i = 1:numel(fields) + field = fields{i}; + try + if matRad_ispropCompat(obj,field) + obj.(field) = matRad_recursiveFieldAssignment(obj.(field),propertiesStruct.(field),overwrite,fieldChangedWarningMessage,field); + else + matRad_cfg.dispWarning('Tried to assign nonexisting property ''%s'' from property struct to Object of class %s!',field,class(obj)); + end + catch ME + % catch exceptions when the class has no properties defined in the + % struct. + switch ME.identifier + case {'MATLAB:class:noPublicField','MATLAB:class:noSetMethod'} + matRad_cfg = MatRad_Config.instance(); + matRad_cfg.dispWarning('Could not assign field %s to object of class %s: %s', field, class(obj), ME.message); + otherwise + matRad_cfg.dispWarning('Problem while setting up object of class %s from struct:%s %s',class(obj),field,ME.message); + end + end +end + +if matRad_cfg.isOctave + warning(c2sWarningState.state,'Octave:classdef-to-struct'); +end diff --git a/matRad/util/matRad_calcCubes.m b/matRad/util/matRad_calcCubes.m index e0cff357b..e2713f4ab 100644 --- a/matRad/util/matRad_calcCubes.m +++ b/matRad/util/matRad_calcCubes.m @@ -2,16 +2,16 @@ % matRad computation of all cubes for the resultGUI struct % which is used as result container and for visualization in matRad's GUI % -% call +% call: % resultGUI = matRad_calcCubes(w,dij) % resultGUI = matRad_calcCubes(w,dij,scenNum) % -% input +% input: % w: bixel weight vector % dij: dose influence matrix % scenNum: optional: number of scenario to calculated (default 1) % -% output +% output: % resultGUI: matRad result struct % % References @@ -19,7 +19,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2024 the matRad development team. +% Copyright 2024-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -55,7 +55,7 @@ %% Physical Dose doseFields = {'physicalDose','doseToWater'}; -doseQuantities = {'','_std','_batchStd'}; +doseQuantities = {'','_std','_batchStd', '_MCvar'}; % compute physical dose for all beams individually and together for j = 1:length(doseFields) for k = 1:length(doseQuantities) @@ -68,6 +68,11 @@ resultGUI.([doseFields{j}, doseQuantities{k}, beamInfo(i).suffix])(isnan(resultGUI.([doseFields{j}, doseQuantities{k}, beamInfo(i).suffix]))) = 0; end % Handle normal fields as usual + elseif ~isempty(strfind(lower(doseQuantities{1}),'var')) + for i = 1:length(beamInfo) + resultGUI.([doseFields{j}, doseQuantities{k}, beamInfo(i).suffix]) = reshape(full(dij.([doseFields{j} doseQuantities{k}]){scenNum} * (resultGUI.w .* beamInfo(i).logIx)),dij.doseGrid.dimensions); + resultGUI.([doseFields{j}, doseQuantities{k}, beamInfo(i).suffix])(isnan(resultGUI.([doseFields{j}, doseQuantities{k}, beamInfo(i).suffix]))) = 0; + end else for i = 1:length(beamInfo) resultGUI.([doseFields{j}, doseQuantities{k}, beamInfo(i).suffix]) = reshape(full(dij.([doseFields{j} doseQuantities{k}]){scenNum} * (resultGUI.w .* beamInfo(i).logIx)),dij.doseGrid.dimensions); diff --git a/matRad/util/matRad_calcIntEnergy.m b/matRad/util/matRad_calcIntEnergy.m index ae8dd28ad..71f605a66 100644 --- a/matRad/util/matRad_calcIntEnergy.m +++ b/matRad/util/matRad_calcIntEnergy.m @@ -1,15 +1,15 @@ function intDose = matRad_calcIntEnergy(dose,ct,pln) % matRad function to compute the integral energy in MeV for a dose cube % -% call +% call: % intDose = matRad_calcIntEnergy(dose,ct,pln) % -% input +% input: % dose: 3D matlab array with dose e.g. resultGUI.physicalDose % ct: matRad ct struct % pln: matRad pln struct % -% output +% output: % intDose: integral dose in MeV % % References @@ -17,7 +17,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2019 the matRad development team. +% Copyright 2019-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/util/matRad_checkMexFileExists.m b/matRad/util/matRad_checkMexFileExists.m index 87fbb340d..9271e43c5 100644 --- a/matRad/util/matRad_checkMexFileExists.m +++ b/matRad/util/matRad_checkMexFileExists.m @@ -2,7 +2,7 @@ % Checks if a matching mex file exists, and can create a link % if a matching custom, system specific precompiled octave mex file is found % -% call +% call: % matRad_checkMexFileExists(filename) % matRad_checkMexFileExists(filename,linkOctave) % @@ -23,7 +23,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2020 the matRad development team. +% Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/util/matRad_compareDijStf.m b/matRad/util/matRad_compareDijStf.m index ec27c5091..6550b016f 100644 --- a/matRad/util/matRad_compareDijStf.m +++ b/matRad/util/matRad_compareDijStf.m @@ -1,14 +1,15 @@ -function [allMatch, msg] = matRad_compareDijStf(dij,stf) -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +function [allMatch, msg] = matRad_compareDijStf(dij, stf) +% matRad_compareDijStf compares the matRad dij struct with the matRad +% stf struct to check for consistency. % -% call -% matching = matRad_comparePlnDijStf(pln,stf,dij) +% call: +% matching = matRad_compareDijStf(stf,dij) % -% input +% input: % dij: matRad dij struct % stf: matRad steering information struct % -% output +% output: % % allMatch: flag is true if they all match % matching: message to display @@ -16,7 +17,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2019 the matRad development team. +% Copyright 2019-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -25,23 +26,23 @@ % propagated, or distributed except according to the terms contained in the % LICENSE file. % -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -allMatch=true; +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +allMatch = true; msg = []; - %% compare number of rays per beam in dij and stf - stf_RaysPerBeam=[stf.numOfRays]; - if numel(stf_RaysPerBeam) ~= numel(dij.numOfRaysPerBeam) ... % different size - || ~isempty(find(stf_RaysPerBeam-dij.numOfRaysPerBeam,1)) % different values - msg= 'Number of rays do not match'; - allMatch=false; - return - end - stf_gantryAngles=[stf.gantryAngle]; - if dij.numOfBeams ~= numel(stf_gantryAngles) - msg= 'Number of beams do not match'; - allMatch=false; - return - end +%% compare number of rays per beam in dij and stf +stf_RaysPerBeam = [stf.numOfRays]; +if numel(stf_RaysPerBeam) ~= numel(dij.numOfRaysPerBeam) || ... % different size + ~isempty(find(stf_RaysPerBeam - dij.numOfRaysPerBeam, 1)) % different values + msg = 'Number of rays do not match'; + allMatch = false; + return +end +stf_gantryAngles = [stf.gantryAngle]; +if dij.numOfBeams ~= numel(stf_gantryAngles) + msg = 'Number of beams do not match'; + allMatch = false; + return +end -end \ No newline at end of file +end diff --git a/matRad/util/matRad_comparePlnStf.m b/matRad/util/matRad_comparePlnStf.m index 7ac29ec60..035ee9f83 100644 --- a/matRad/util/matRad_comparePlnStf.m +++ b/matRad/util/matRad_comparePlnStf.m @@ -1,15 +1,16 @@ -function [allMatch, msg] = matRad_comparePlnStf(pln,stf) -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +function [allMatch, msg] = matRad_comparePlnStf(pln, stf) +% matRad_comparePlnStf compares the matRad pln struct with the matRad +% stf struct for matching parameterization. % -% call -% matching = matRad_comparePlnDijStf(pln,stf,dij) +% call: +% matching = matRad_comparePlnStf(pln,stf) % -% input -% dij: matRad dij struct +% input: +% pln: matRad plan meta information struct % stf: matRad steering information struct % pln: matRad plan meta information struct % -% output +% output: % % allMatch: flag is true if they all match % matching: message to display @@ -17,7 +18,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2019 the matRad development team. +% Copyright 2019-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -26,78 +27,71 @@ % propagated, or distributed except according to the terms contained in the % LICENSE file. % -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -allMatch=true; +allMatch = true; msg = []; -%% check if steering information is available in plan from the begining -if ~isfield(pln,'propStf') - allMatch=false; - msg= 'No steering information in plan'; +%% check if steering information is available in plan from the beginning +if ~isfield(pln, 'propStf') + allMatch = false; + msg = 'No steering information in plan'; return end -%% compare number of gantry angles, but ignore if numOfBeams not set -if isfield(pln.propStf,'numOfBeams') && pln.propStf.numOfBeams ~= numel(stf) - msg= 'Number of beams do not match'; - allMatch=false; - return -end - %% compare gantry angles in stf and pln -stfGantryAngles=[stf.gantryAngle]; -if ~isfield(pln.propStf,'gantryAngles') || numel(stfGantryAngles) ~= numel(pln.propStf.gantryAngles) ... % different size - || ~isempty(find(stfGantryAngles-pln.propStf.gantryAngles, 1)) % values in stf and pln do not match % values in stf and pln do not match - allMatch=false; - msg= 'Gantry angles do not match'; +stfGantryAngles = [stf.gantryAngle]; +if ~isfield(pln.propStf, 'gantryAngles') || numel(stfGantryAngles) ~= numel(pln.propStf.gantryAngles) || ... % different size + ~isempty(find(stfGantryAngles - pln.propStf.gantryAngles, 1)) % values in stf and pln do not match % values in stf and pln do not match + allMatch = false; + msg = 'Gantry angles do not match'; return end %% compare couch angles in stf and pln -stfCouchAngles=[stf.couchAngle]; -if ~isfield(pln.propStf,'couchAngles') || numel(stfCouchAngles) ~= numel(pln.propStf.couchAngles) ... % different size - || ~isempty(find(stfCouchAngles-pln.propStf.couchAngles, 1)) % values in stf and pln do not match - allMatch=false; - msg= 'Couch angles do not match'; +stfCouchAngles = [stf.couchAngle]; +if ~isfield(pln.propStf, 'couchAngles') || numel(stfCouchAngles) ~= numel(pln.propStf.couchAngles) || ... % different size + ~isempty(find(stfCouchAngles - pln.propStf.couchAngles, 1)) % values in stf and pln do not match + allMatch = false; + msg = 'Couch angles do not match'; return end %% compare Bixel width in stf and pln bixelMatch = false; -if isfield(pln.propStf,'bixelWidth') && isfield(stf(1),'bixelWidth') - if isnumeric(stf(1).bixelWidth) && isequal(stf(1).bixelWidth,pln.propStf.bixelWidth) +if isfield(pln.propStf, 'bixelWidth') && isfield(stf(1), 'bixelWidth') + if isnumeric(stf(1).bixelWidth) && isequal(stf(1).bixelWidth, pln.propStf.bixelWidth) bixelMatch = true; - elseif ischar(stf(1).bixelWidth) && strcmp(stf(1).bixelWidth,'field') + elseif ischar(stf(1).bixelWidth) && strcmp(stf(1).bixelWidth, 'field') bixelMatch = true; end end if ~bixelMatch - allMatch=false; - msg= 'Bixel width does not match'; + allMatch = false; + msg = 'Bixel width does not match'; return end %% compare radiation mode in stf and pln -if ~isfield(pln,'radiationMode') || ~strcmp(stf(1).radiationMode, pln.radiationMode) - allMatch=false; - msg= 'Radiation mode does not match'; +if ~isfield(pln, 'radiationMode') || ~strcmp(stf(1).radiationMode, pln.radiationMode) + allMatch = false; + msg = 'Radiation mode does not match'; return end %% compare isocenter in stf and pln for each gantry angle for i = 1:numel(pln.propStf.gantryAngles) - if size(pln.propStf.isoCenter,1) == 1 - isoCenter = repmat(pln.propStf.isoCenter,numel(stf),1); + if size(pln.propStf.isoCenter, 1) == 1 + isoCenter = repmat(pln.propStf.isoCenter, numel(stf), 1); else isoCenter = pln.propStf.isoCenter; end - if size(isoCenter,1) ~= numel(stf) || any(stf(i).isoCenter - isoCenter(i,:) ~= 0) - allMatch=false; - msg= 'Isocenters do not match'; + if size(isoCenter, 1) ~= numel(stf) || any(stf(i).isoCenter - isoCenter(i, :) ~= 0) + allMatch = false; + msg = 'Isocenters do not match'; return end end -end \ No newline at end of file +end diff --git a/matRad/util/matRad_convertOldCstToNewCstObjectives.m b/matRad/util/matRad_convertOldCstToNewCstObjectives.m index 1322ea247..c990b3dec 100644 --- a/matRad/util/matRad_convertOldCstToNewCstObjectives.m +++ b/matRad/util/matRad_convertOldCstToNewCstObjectives.m @@ -3,10 +3,10 @@ % Converts a cst with struct array objectives / constraints to the new cst % format using a cell array of objects. % -% call +% call: % newCst = matRad_convertOldCstToNewCstObjectives(cst) % -% input +% input: % cst a cst cell array that contains the old obectives as struct % array % @@ -22,7 +22,7 @@ % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2019 the matRad development team. +% Copyright 2019-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -47,4 +47,4 @@ newCst{m,6}{n} = s; end end -end \ No newline at end of file +end diff --git a/matRad/util/matRad_findSubclasses.m b/matRad/util/matRad_findSubclasses.m index 0bd571a10..9dd732a3f 100644 --- a/matRad/util/matRad_findSubclasses.m +++ b/matRad/util/matRad_findSubclasses.m @@ -6,16 +6,13 @@ % % call: % classList = matRad_findSubclasses(superClass); -% classList = -% matRad_findSubclasses(superClass,'package',{packageNames}) -% classList = -% matRad_findSubclasses(superClass,'folders',{folderNames}) -% classList = -% matRad_findSubclasses(superClass,'includeAbstract',true/false) +% classList = matRad_findSubclasses(superClass,'package',{packageNames}) +% classList = matRad_findSubclasses(superClass,'folders',{folderNames}) +% classList = matRad_findSubclasses(superClass,'includeAbstract',true/false) % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2022 the matRad development team. +% Copyright 2022-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/util/matRad_fitBaseData.m b/matRad/util/matRad_fitBaseData.m index 1fc204b2d..b5aa17f8d 100644 --- a/matRad/util/matRad_fitBaseData.m +++ b/matRad/util/matRad_fitBaseData.m @@ -3,10 +3,10 @@ % pencil beam in positive y direction, target per default in center of % plane % -% call +% call: % fitData = matRad_fitBaseData(doseCube, resolution, energy, initSigma0) % -% input +% input: % doseCube: dose cube as an M x N x O array % resolution: resolution of the cubes [mm/voxel] % energy: energy of ray @@ -19,7 +19,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2022 the matRad development team. +% Copyright 2022-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/util/matRad_generateBodyContour.m b/matRad/util/matRad_generateBodyContour.m index e0953751c..7c96c1bfc 100644 --- a/matRad/util/matRad_generateBodyContour.m +++ b/matRad/util/matRad_generateBodyContour.m @@ -1,21 +1,21 @@ function cst = matRad_generateBodyContour(ct,cst,thresholdHU) % function to create a BODY contour for imported patient cases that do not have one % -% call +% call: % cst = matRad_generateBodyContour(ct,cst,thresholdHU) % -% input +% input: % ct: matrad ct structure % cst: matrad cst structure % thresholdHU: HU thresholding value (optional) default = -500 HU % % -% output +% output: % cst: matRads cst struct with inserted BODY contour % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2024 the matRad development team. +% Copyright 2024-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -64,4 +64,4 @@ cst{pos+1,5}.visibleColor = [0.7,0.3,0.1]; cst{pos+1,6} = []; -end \ No newline at end of file +end diff --git a/matRad/util/matRad_generateSingleBixelStf.m b/matRad/util/matRad_generateSingleBixelStf.m index dcd74040e..5ddda3672 100644 --- a/matRad/util/matRad_generateSingleBixelStf.m +++ b/matRad/util/matRad_generateSingleBixelStf.m @@ -1,15 +1,15 @@ function stf = matRad_generateSingleBixelStf(ct,cst,pln) % -% call +% call: % stf = matRad_generateSingleBixelStf(ct,cst,pln,visMode) % -% input +% input: % ct: ct cube % cst: matRad cst struct % pln: matRad plan meta information struct % visMode: toggle on/off different visualizations by setting this value to 1,2,3 (optional) % -% output +% output: % stf: matRad steering information struct % % References @@ -17,7 +17,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/util/matRad_getAlphaBetaCurves.m b/matRad/util/matRad_getAlphaBetaCurves.m index 27a0547d2..cbda82093 100644 --- a/matRad/util/matRad_getAlphaBetaCurves.m +++ b/matRad/util/matRad_getAlphaBetaCurves.m @@ -1,26 +1,26 @@ function [machine] = matRad_getAlphaBetaCurves(machine,varargin) % matRad alpha beta curve calculation tool % -% call +% call: % machine = matRad_getAlphaBetaCurves(machine) % machine = matRad_getAlphaBetaCurves(machine,cst,modelName,overrideAB) -% Example full call for protons +% +% Example full call for protons: % machine = matRad_getAlphaBetaCurves(machine,pln,cst,'MCN','override') -% input -% machine: matRad machine file to change -% varargin (optional): cst: matRad cst struct (for custom alpha/beta, -% otherwise default is alpha=0.1, beta=0.05; -% modelName: specify RBE modelName -% overrideAB: calculate new alpha beta even if available -% and override % -% output +% input: +% machine: matRad machine file to change +% varargin (optional): cst: matRad cst struct (for custom alpha/beta, otherwise default is alpha=0.1, beta=0.05) +% modelName: specify RBE modelName +% overrideAB: calculate new alpha beta even if available and override +% +% output: % machine: updated machine file with alpha/beta curves % % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2021 the matRad development team. +% Copyright 2021-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -107,4 +107,4 @@ matRad_cfg.dispWarning('Basedata already contains alpha/beta curves. Use "overrideAB"-flag to override.') end -end \ No newline at end of file +end diff --git a/matRad/util/matRad_getEnvironment.m b/matRad/util/matRad_getEnvironment.m index db87f4727..e59ef277d 100644 --- a/matRad/util/matRad_getEnvironment.m +++ b/matRad/util/matRad_getEnvironment.m @@ -1,13 +1,13 @@ function [env, versionString] = matRad_getEnvironment() % matRad function to get the software environment matRad is running on % -% call +% call: % [env, versionString] = matRad_getEnvironment() % -% input +% input: % - % -% output +% output: % env: outputs either 'MATLAB' or 'OCTAVE' as string % versionString: returns the version number as string % @@ -16,7 +16,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2017 the matRad development team. +% Copyright 2017-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/util/matRad_identifyClassesByConstantProperties.m b/matRad/util/matRad_identifyClassesByConstantProperties.m index 5457edb4e..372ba9745 100644 --- a/matRad/util/matRad_identifyClassesByConstantProperties.m +++ b/matRad/util/matRad_identifyClassesByConstantProperties.m @@ -1,41 +1,31 @@ function [classList] = matRad_identifyClassesByConstantProperties(metaClasses,primaryPropertyName,varargin) -% matRad_identifyClassesByProperty: Helper function to identify classes by -% property +% matRad_identifyClassesByProperty: Helper function to identify classes by property % This method identifies classes based on a primary property and optional % additional properties. % % call: -% classList = matRad_identifyClassesByProperty(metaClasses, -% primaryPropertyName) classList = -% matRad_identifyClassesByProperty(metaClasses, primaryPropertyName, -% 'defaults', {defaultClasses}) -% classList = -% matRad_identifyClassesByProperty(metaClasses, primaryPropertyName, -% 'additionalPropertyNames', {additionalProperties}) +% classList = matRad_identifyClassesByProperty(metaClasses, primaryPropertyName) +% classList = matRad_identifyClassesByProperty(metaClasses, primaryPropertyName, 'defaults', {defaultClasses}) +% classList = matRad_identifyClassesByProperty(metaClasses, primaryPropertyName, 'additionalPropertyNames', {additionalProperties}) % -% inputs: -% - metaClasses: A cell array of meta.class objects representing the -% classes to be identified. -% - primaryPropertyName: The name of the primary property used for -% identification. +% input: +% metaClasses: A cell array of meta.class objects representing the classes to be identified. +% primaryPropertyName: The name of the primary property used for identification. % % optional Parameter Inputs: -% - defaults: A cell array of default classes that should be listed -% first. -% - additionalPropertyNames: A cell array of additional property names -% used for identification. +% defaults: A cell array of default classes that should be listed first. +% additionalPropertyNames: A cell array of additional property names used for identification. % % outputs: -% - classList: A structure array containing the identified classes. -% - primaryPropertyName: The values of the primary property for each -% class. - additionalPropertyNames: The values of the additional -% properties for each class. - className: The names of the identified -% classes. - handle: The constructor handles of the identified -% classes. +% classList: A structure array containing the identified classes with fields: +% - primaryPropertyName: The values of the primary property for each class. +% - additionalPropertyNames: The values of the additional properties for each class. +% - className: The names of the identified classes. +% - handle: The constructor handles of the identified classes. % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2024 the matRad development team. +% Copyright 2024-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/util/matRad_info.m b/matRad/util/matRad_info.m index d4c5fb60d..8039fd983 100644 --- a/matRad/util/matRad_info.m +++ b/matRad/util/matRad_info.m @@ -1,12 +1,12 @@ function message = matRad_info() % matRad function to get information message % -% call +% call: % message = matRad_info() % -% input +% input: % -% output +% output: % message: An Information message about matRad % % References @@ -14,7 +14,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2020 the matRad development team. +% Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -40,4 +40,4 @@ message = [message newline newline ... 'Check www.matRad.org and github.com/e0404/matRad for more information.']; -end \ No newline at end of file +end diff --git a/matRad/util/matRad_interp1.m b/matRad/util/matRad_interp1.m index 89ac8bc67..eef03dd3a 100644 --- a/matRad/util/matRad_interp1.m +++ b/matRad/util/matRad_interp1.m @@ -2,18 +2,18 @@ % interpolates 1-D data (table lookup) and utilizes griddedInterpolant % if availabe in the used MATLAB version % -% call +% call: % y = matRad_interp1(xi,yi,x) % y = matRad_interp1(xi,yi,x,extrapolation) % -% input +% input: % xi: sample points % yi: corresponding data to sample points % x: query points for interpolation % extrapolation: (optional) strategy for extrapolation. Similar to % interp1. NaN is the default extrapolation value % -% output +% output: % y: interpolated data % % Note that all input data has to be given as column vectors for a correct @@ -25,7 +25,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2020 the matRad development team. +% Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -96,14 +96,19 @@ if size(yi,2) > 1 % interpolation for multiple 1-D datasets - samplePoints = {xi, 1:size(yi,2)}; - queryPoints = {x, 1:size(yi,2)}; + samplePoints = {xi, cast(1:size(yi,2),class(xi))}; + queryPoints = {x, cast(1:size(yi,2),class(x))}; else % interpolation for a single 1-D dataset samplePoints = {xi}; queryPoints = {x}; end + if isgpuarray(x) + samplePoints = cellfun(@gpuArray,samplePoints,'UniformOutput',false); + yi = gpuArray(yi); + end + F = griddedInterpolant(samplePoints,yi,'linear',extrapmethod); y = F(queryPoints); diff --git a/matRad/util/matRad_interp3.m b/matRad/util/matRad_interp3.m index 6b240b553..57e7da202 100644 --- a/matRad/util/matRad_interp3.m +++ b/matRad/util/matRad_interp3.m @@ -1,19 +1,19 @@ function y = matRad_interp3(xi,yi,zi,x,xq,yq,zq,mode,extrapVal) % interpolates 3-D data (table lookup) % -% call +% call: % y = matRad_interp3(xi,yi,zi,x,xq,yq,zy) % y = matRad_interp3(xi,yi,zi,x,xq,yq,zy,mode) % y = matRad_interp3(xi,yi,zi,x,xq,yq,zy,mode,extrapVal) % -% input +% input: % xi,yi,zi: grid vectors % x: data % xq,yq,zq: coordinates of quer points as a grid % mode: optional interpolation mode (default linear) % extrapVal: (optional) value for extrapolation % -% output +% output: % y: interpolated data % % References @@ -21,7 +21,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2019 the matRad development team. +% Copyright 2019-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/util/matRad_plotSlice.m b/matRad/util/matRad_plotSlice.m index 0ff7e4a56..daa9ce1f8 100644 --- a/matRad/util/matRad_plotSlice.m +++ b/matRad/util/matRad_plotSlice.m @@ -2,7 +2,7 @@ % matRad tool function to directly plot a complete slice of a ct with dose % optionally including contours and isolines % -% call +% call: % [] = matRad_plotSlice(ct, dose, varargin) % % input (required) @@ -21,9 +21,7 @@ % doseColorMap colormap for the dose % doseWindow dose value window % doseIsoLevels levels defining the isodose contours -% voiSelection logicals defining the current selection of contours -% that should be plotted. Can be set to [] to plot -% all non-ignored contours. +% voiSelection logicals defining the current selection of contours that should be plotted. Can be set to [] to plot all non-ignored contours. % colorBarLabel string defining the yLabel of the colorBar % boolPlotLegend boolean if legend should be plottet or not % showCt boolean if CT slice should be displayed or not @@ -34,7 +32,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2025 the matRad development team. +% Copyright 2025-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -51,10 +49,12 @@ defaultAxesHandle = []; defaultCubeIdx = 1; defaultPlane = 1; +defaultCtWindow = []; defaultDoseWindow = []; defaultThresh = []; defaultAlpha = []; -defaultDoseColorMap = jet; +defaultCtColorMap = bone(128); +defaultDoseColorMap = jet(128); defaultDoseIsoLevels = []; defaultVOIselection = []; defaultContourColorMap = []; @@ -68,10 +68,10 @@ isAxes = @(x) strcmp(get(x, 'type'), 'axes') || isempty(x); isCubeIdx = @(x) isscalar(x); isPlane = @(x) isscalar(x) && (sum(x==[1, 2, 3])==1); -isDoseWindow = @(x) (length(x) == 2 && isvector(x) && diff(x) > 0); +isImageWindow = @(x) (length(x) == 2 && isvector(x) && diff(x) > 0); isThresh = @(x) (isscalar(x) && (x>=0) && (x<=1)) || isempty(x); isAlpha = @(x) isscalar(x) && (x>=0) && (x<=1) || isempty(x); -isDoseColorMap = @(x) (isnumeric(x) && (size(x, 2)==3) && all(x(:) >= 0) && all(x(:) <= 1)) || isempty(x); +isColorMap = @(x) (isnumeric(x) && (size(x, 2)==3) && all(x(:) >= 0) && all(x(:) <= 1)) || isempty(x); isDoseIsoLevels = @(x) isnumeric(x) && isvector(x)|| isempty(x); isVOIselection = @(x) isnumeric(x) || isempty(x); %all(x(:)==1 | x(:)==0) || isempty(x); isContourColorMap = @(x) (isnumeric(x) && (size(x, 2)==3) && size(x, 1)>=2 && all(x(:) >= 0) && all(x(:) <= 1)) || isempty(x); @@ -91,10 +91,12 @@ addParameter(p, 'axesHandle', defaultAxesHandle, isAxes); addParameter(p, 'cubeIdx', defaultCubeIdx, isCubeIdx); addParameter(p, 'plane', defaultPlane, isPlane); -addParameter(p, 'doseWindow', defaultDoseWindow, isDoseWindow); +addParameter(p, 'ctWindow', defaultCtWindow, isImageWindow); +addParameter(p, 'ctColorMap', defaultCtColorMap, isColorMap) +addParameter(p, 'doseWindow', defaultDoseWindow, isImageWindow); addParameter(p, 'thresh', defaultThresh, isThresh); addParameter(p, 'alpha', defaultAlpha, isAlpha); -addParameter(p, 'doseColorMap', defaultDoseColorMap, isDoseColorMap); +addParameter(p, 'doseColorMap', defaultDoseColorMap, isColorMap); addParameter(p, 'doseIsoLevels', defaultDoseIsoLevels, isDoseIsoLevels); addParameter(p, 'voiSelection', defaultVOIselection, isVOIselection); addParameter(p, 'contourColorMap', defaultContourColorMap, isContourColorMap); @@ -150,7 +152,7 @@ set(axesHandle,'YDir','Reverse'); % plot ct slice if p.Results.showCt - hCt = matRad_plotCtSlice(axesHandle,p.Results.ct.cubeHU,p.Results.cubeIdx,p.Results.plane,p.Results.slice, [], []); + hCt = matRad_plotCtSlice(axesHandle,p.Results.ct.cubeHU,p.Results.cubeIdx,p.Results.plane,p.Results.slice, p.Results.ctColorMap, p.Results.ctWindow); end axis(axesHandle, 'off'); diff --git a/matRad/util/matRad_plotSliceWrapper.m b/matRad/util/matRad_plotSliceWrapper.m index 4e73d3796..87a9596e9 100644 --- a/matRad/util/matRad_plotSliceWrapper.m +++ b/matRad/util/matRad_plotSliceWrapper.m @@ -3,7 +3,7 @@ % matRad tool function to directly plot a complete slice of a ct with dose % including contours and isolines. % -% call +% call: % [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSliceWrapper(axesHandle,ct,cst,cubeIdx,dose,plane,slice) % [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSliceWrapper(axesHandle,ct,cst,cubeIdx,dose,plane,slice,thresh) % [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSliceWrapper(axesHandle,ct,cst,cubeIdx,dose,plane,slice,alpha) @@ -11,9 +11,8 @@ % [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSliceWrapper(axesHandle,ct,cst,cubeIdx,dose,plane,slice,doseColorMap) % [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSliceWrapper(axesHandle,ct,cst,cubeIdx,dose,plane,slice,doseWindow) % [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSliceWrapper(axesHandle,ct,cst,cubeIdx,dose,plane,slice,doseIsoLevels) -% ... -% [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSliceWrapper(axesHandle,ct,cst,cubeIdx,dose,plane,slice,thresh,alpha,contourColorMap,... -% doseColorMap,doseWindow,doseIsoLevels,voiSelection,colorBarLabel,boolPlotLegend,...) +% ... +% [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSliceWrapper(axesHandle,ct,cst,cubeIdx,dose,plane,slice,thresh,alpha,contourColorMap,doseColorMap,doseWindow,doseIsoLevels,voiSelection,colorBarLabel,boolPlotLegend,...) % % input (required) % axesHandle handle to axes the slice should be displayed in @@ -31,16 +30,13 @@ % doseColorMap colormap for the dose % doseWindow dose value window % doseIsoLevels levels defining the isodose contours -% voiSelection logicals defining the current selection of contours -% that should be plotted. Can be set to [] to plot -% all non-ignored contours. +% voiSelection logicals defining the current selection of contours that should be plotted. Can be set to [] to plot all non-ignored contours. % colorBarLabel string defining the yLabel of the colorBar % boolPlotLegend boolean if legend should be plottet or not -% varargin additional input parameters that are passed on to -% individual plotting functions (e.g. 'LineWidth',1.5) +% varargin additional input parameters that are passed on to individual plotting functions (e.g. 'LineWidth',1.5) % % -% output +% output: % hCMap handle to the colormap % hDose handle to the dose plot % hCt handle to the ct plot @@ -52,7 +48,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/util/matRad_progress.m b/matRad/util/matRad_progress.m index 9cc357a3b..6d16e5772 100644 --- a/matRad/util/matRad_progress.m +++ b/matRad/util/matRad_progress.m @@ -1,14 +1,14 @@ function matRad_progress(currentIndex, totalNumberOfEvaluations) % matRad progress bar % -% call +% call: % matRad_progress(currentIndex, totalNumberOfEvaluations) % -% input +% input: % currentIndex: current iteration index % totalNumberOfEvaluations: maximum iteration index % -% output +% output: % graphical display of progess. make sure there is no other output % written during the loop to prevent confusion % @@ -17,7 +17,7 @@ function matRad_progress(currentIndex, totalNumberOfEvaluations) % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/util/matRad_readCsvData.m b/matRad/util/matRad_readCsvData.m index 30c2b5908..0a4c7d9e4 100644 --- a/matRad/util/matRad_readCsvData.m +++ b/matRad/util/matRad_readCsvData.m @@ -1,14 +1,14 @@ function dataOut = matRad_readCsvData(csvFile,cubeDim) % matRad read TOPAS csv data % -% call +% call: % dataOut = matRad_readCsvData(csvFile,cubeDim) % -% input +% input: % csvFile: TOPAS csv scoring file % cubeDim: size of cube % -% output +% output: % dataOut: cube of size cubeDim containing scored values % % References @@ -16,7 +16,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2024 the matRad development team. +% Copyright 2024-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -47,4 +47,4 @@ dataOut{i}(ix) = dataTable.(['Var' num2str(i+3)]); end -end \ No newline at end of file +end diff --git a/matRad/util/matRad_recursiveFieldAssignment.m b/matRad/util/matRad_recursiveFieldAssignment.m index eefed3ab6..7e87585db 100644 --- a/matRad/util/matRad_recursiveFieldAssignment.m +++ b/matRad/util/matRad_recursiveFieldAssignment.m @@ -1,26 +1,26 @@ function assigned = matRad_recursiveFieldAssignment(assignTo,reference,overwrite,fieldChangedWarningMessage,fieldname) % matRad recursive field assignment tool -% This function recursively assigns fields from one structure to another. If both 'assignTo' and 'reference' are structures, +% This function recursively assigns fields from one structure to another. If both 'assignTo' and 'reference' are structures, % it will recurse into their fields. If a field in 'assignTo' is a structure and its corresponding field in 'reference' is not, % or vice versa, a warning message is displayed. The function also handles the case where 'assignTo' or 'reference' are not structures, % directly assigning the values. Custom warning messages can be specified for overwriting fields. % -% call +% call: % assigned = matRad_recursiveFieldAssignment(assignTo,reference,fieldChangedWarningMessage,fieldname) % -% input +% input: % assignTo: The initial structure to which the fields are to be assigned. % reference: The structure containing the fields and values to be assigned to 'assignTo'. % overwrite: Boolean flag that determines if the field value should be overwritten ( by defaults ) or preserved % fieldChangedWarningMessage: Optional. A message to display if a field is overwritten. If not provided, no message is displayed. % fieldname: Optional. The name of the current field being processed. Used for generating specific warning messages. % -% output +% output: % assigned: The structure 'assignTo' after assigning the fields from 'reference'. % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2024 the matRad development team. +% Copyright 2024-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -99,4 +99,4 @@ end -end \ No newline at end of file +end diff --git a/matRad/util/matRad_resampleCTtoGrid.m b/matRad/util/matRad_resampleCTtoGrid.m index b9ff932bc..e08deba4c 100644 --- a/matRad/util/matRad_resampleCTtoGrid.m +++ b/matRad/util/matRad_resampleCTtoGrid.m @@ -1,20 +1,20 @@ function [ctR] = matRad_resampleCTtoGrid(ct,dij) % function to resample the ct grid for example for faster MC computation % -% call +% call: % [ctR] = matRad_resampleGrid(ct) % -% input +% input: % ct: Path to folder where TOPAS files are in (as string) % cst: matRad segmentation struct % -% output +% output: % ctR: resampled CT % cst: updated ct struct (due to calcDoseInit) % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2022 the matRad development team. +% Copyright 2022-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/util/matRad_visApertureInfo.m b/matRad/util/matRad_visApertureInfo.m index 2647d728a..9f5973c6d 100644 --- a/matRad/util/matRad_visApertureInfo.m +++ b/matRad/util/matRad_visApertureInfo.m @@ -1,23 +1,23 @@ function matRad_visApertureInfo(apertureInfo,mode) % matRad function to visualize aperture shapes stored as struct % -% call +% call: % matRad_visApertureInfo(apertureInfo,mode) % -% input +% input: % apertureInfo: aperture weight and shape info struct % mode: switch to display leaf numbers ('leafNum') or physical % coordinates of the leaves ('physical') % -% output +% output: % - % % References -% % - +% % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/util/matRad_visPhotonFieldShapes.m b/matRad/util/matRad_visPhotonFieldShapes.m index 669f09bca..acfdcb8a4 100644 --- a/matRad/util/matRad_visPhotonFieldShapes.m +++ b/matRad/util/matRad_visPhotonFieldShapes.m @@ -4,13 +4,13 @@ function matRad_visPhotonFieldShapes(pln) % with matRad's dicom import tool and feature the appropriate field shape % information % -% call +% call: % matRad_visPhotonFieldShapes(pln) % -% input +% input: % pln: matRad plan struct % -% output +% output: % - % % References @@ -18,7 +18,7 @@ function matRad_visPhotonFieldShapes(pln) % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2020 the matRad development team. +% Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -58,4 +58,4 @@ function matRad_visPhotonFieldShapes(pln) drawnow pause(.1) -end \ No newline at end of file +end diff --git a/matRad/util/matRad_visSpotWeights.m b/matRad/util/matRad_visSpotWeights.m index 0a8b12e83..9b9c1318e 100644 --- a/matRad/util/matRad_visSpotWeights.m +++ b/matRad/util/matRad_visSpotWeights.m @@ -2,10 +2,10 @@ function matRad_visSpotWeights(stf,weights) % visualise spot weights per energy slice (or fluence map for photons resp.) % for single beams % -% call +% call: % matRad_visSpotWeights(stf,weights) % -% input +% input: % stf: matRad stf struct % weights: spot weights for bixels (resultGUI.w) @@ -17,7 +17,7 @@ function matRad_visSpotWeights(stf,weights) % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/util/matRad_weightedQuantile.m b/matRad/util/matRad_weightedQuantile.m index 5dcb7ad6d..67e8684fa 100644 --- a/matRad/util/matRad_weightedQuantile.m +++ b/matRad/util/matRad_weightedQuantile.m @@ -1,10 +1,10 @@ function wQ = matRad_weightedQuantile(values, percentiles, weight, isSorted, extraPolMethod) % matRad uncertainty analysis report generaator function % -% call +% call: % matRad_weightedQuantile(values, percentiles, weight, isSorted, extraPol) % -% input +% input: % values: random variable vector % percentiles: percentiles to be calculated % weight: (optional) weight vector (same length as values) @@ -13,7 +13,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2017 the matRad development team. +% Copyright 2017-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this diff --git a/matRad/util/octaveCompat/matRad_gatherCompat.m b/matRad/util/octaveCompat/matRad_gatherCompat.m new file mode 100644 index 000000000..eac28608b --- /dev/null +++ b/matRad/util/octaveCompat/matRad_gatherCompat.m @@ -0,0 +1,38 @@ +function x = matRad_gatherCompat(x) +% matRad wrapper around Matlab's gather function for Octave compatibility. +% In Matlab, gather transfers a gpuArray from device to host memory. +% Octave has no GPU array support, so this function returns the input +% unchanged. +% +% call: +% x = matRad_gatherCompat(x) +% +% input: +% x array (gpuArray on Matlab, regular array on Octave) +% +% output: +% x array in host memory +% +% References +% - +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2026 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +matRad_cfg = MatRad_Config.instance(); + +if matRad_cfg.isMatlab + x = gather(x); +end + +end diff --git a/matRad/util/octaveCompat/matRad_getPropsCompat.m b/matRad/util/octaveCompat/matRad_getPropsCompat.m index 7e18e6279..024f45f3e 100644 --- a/matRad/util/octaveCompat/matRad_getPropsCompat.m +++ b/matRad/util/octaveCompat/matRad_getPropsCompat.m @@ -2,13 +2,13 @@ % matRad function mimicking Matlab's properties for compatibility with % Octave 6 in classdef files (avoids a parse error in the file) % -% call +% call: % p = matRad_getPropsCompat(obj) % -% input +% input: % obj object (classdef) to get properties from % -% output +% output: % p properties of the object % % References @@ -16,7 +16,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2024 the matRad development team. +% Copyright 2024-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -31,4 +31,4 @@ catch ME p = []; end -end \ No newline at end of file +end diff --git a/matRad/util/octaveCompat/matRad_ispropCompat.m b/matRad/util/octaveCompat/matRad_ispropCompat.m index 4d52dccc8..8c00a7740 100644 --- a/matRad/util/octaveCompat/matRad_ispropCompat.m +++ b/matRad/util/octaveCompat/matRad_ispropCompat.m @@ -2,14 +2,14 @@ % matRad function mimicking Matlab's properties for compatibility with % Octave 6 in classdef files (avoids a parse error in the file) % -% call +% call: % result = matRad_ispropCompat(obj) % -% input +% input: % obj object (classdef) to check for property % prop property to check for % -% output +% output: % result true if property exists, false otherwise % % References @@ -17,7 +17,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2024 the matRad development team. +% Copyright 2024-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -36,4 +36,4 @@ result = isprop(obj,prop); end -end \ No newline at end of file +end diff --git a/matRad/util/octaveCompat/matRad_underlyingTypeCompat.m b/matRad/util/octaveCompat/matRad_underlyingTypeCompat.m new file mode 100644 index 000000000..ae344f9ce --- /dev/null +++ b/matRad/util/octaveCompat/matRad_underlyingTypeCompat.m @@ -0,0 +1,41 @@ +function utype = matRad_underlyingTypeCompat(x) +% matRad function to obtain the type of a numeric datatype. +% Matlab has the function underlyingType to robustly determine the type +% of numerical data, since class might return, for example, "gpuArray" in +% case of data stored on the GPU compared to "single", or "double" for +% standard arrays. This wraps the function for Octave compatibility. +% +% call: +% utype = matRad_underlyingTypeCompat(x) +% +% input: +% x object to check for datatype +% +% output: +% utype datatype +% +% References +% - +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2026 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +matRad_cfg = MatRad_Config.instance(); + +if matRad_cfg.isMatlab + utype = underlyingType(x); +else + utype = class(x); +end + +end diff --git a/matRadGUI.m b/matRadGUI.m index 7d5e1a8a7..b0ee79975 100644 --- a/matRadGUI.m +++ b/matRadGUI.m @@ -12,7 +12,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -62,7 +62,9 @@ if matRad_cfg.disableGUI matRad_cfg.dispInfo('matRad GUI disabled in matRad_cfg!\n'); - hGUI = []; + if nargout > 0 + hGUI = []; + end return; end diff --git a/matRad_buildStandalone.m b/matRad_buildStandalone.m index 55de5a97e..0df6a67d0 100644 --- a/matRad_buildStandalone.m +++ b/matRad_buildStandalone.m @@ -1,5 +1,5 @@ function buildResult = matRad_buildStandalone(varargin) -% Compiles the standalone exectuable & packages installer using Matlab's +% Compiles the standalone executable & packages installer using Matlab's % Compiler Toolbox % % References @@ -7,7 +7,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2020 the matRad development team. +% Copyright 2020-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -21,20 +21,20 @@ matRad_cfg = matRad_rc; matRadRoot = matRad_cfg.matRadRoot; -standaloneFolder = fullfile(matRadRoot,'standalone'); +standaloneFolder = fullfile(matRadRoot, 'standalone'); -%Setup Input Parsing +% Setup Input Parsing p = inputParser; -p.addParameter('isRelease',false,@islogical); %By default we compile a snapshot of the current branch -p.addParameter('compileWithRT',false,@islogical); %By default we don't package installers with runtime -p.addParameter('buildDir',fullfile(matRadRoot,'build'),@(x) ischar(x) || isstring(x)); %Build directory -p.addParameter('verbose',false,@islogical); -p.addParameter('docker',false,@islogical); -p.addParameter('java',false,@islogical); -p.addParameter('python',false,@islogical); -p.addParameter('json',[],@(x) isempty(x) || ischar(x) || isstring(x)); - -%Parse and manage inputs +p.addParameter('isRelease', false, @islogical); % By default we compile a snapshot of the current branch +p.addParameter('compileWithRT', false, @islogical); % By default we don't package installers with runtime +p.addParameter('buildDir', fullfile(matRadRoot, 'build'), @(x) ischar(x) || isstring(x)); % Build directory +p.addParameter('verbose', false, @islogical); +p.addParameter('docker', false, @islogical); +p.addParameter('java', false, @islogical); +p.addParameter('python', false, @islogical); +p.addParameter('json', [], @(x) isempty(x) || ischar(x) || isstring(x)); + +% Parse and manage inputs p.parse(varargin{:}); isRelease = p.Results.isRelease; compileWithRT = p.Results.compileWithRT; @@ -56,61 +56,59 @@ rtOption = 'web'; end -%Display OS for debugging and information +% Display OS for debugging and information arch = computer('arch'); -fprintf('Build Architecture: %s\n',arch); -archcheck = string([ispc,isunix,ismac]).cellstr(); -fprintf('pc\t\tunix\tmac\n%s\t%s\t%s\n',archcheck{:}); +fprintf('Build Architecture: %s\n', arch); +archcheck = string([ispc, isunix, ismac]).cellstr(); +fprintf('pc\t\tunix\tmac\n%s\t%s\t%s\n', archcheck{:}); -%Check build directory +% Check build directory try mkdir(buildDir); catch ME - error(ME.identifier,'Could not create build directory %s\n Error:',buildDir,ME.message); + error(ME.identifier, 'Could not create build directory %s\n Error:', buildDir, ME.message); end -%Docker build? +% Docker build? if buildDocker && isunix && ~ismac warning('Can''t build docker container. Only works on linux!'); buildDocker = false; end - -[~,versionFull] = matRad_version(); +[~, versionFull] = matRad_version(); if isRelease - vernumApp = sprintf('%d.%d.%d',versionFull.major,versionFull.minor,versionFull.patch); + vernumApp = sprintf('%d.%d.%d', versionFull.major, versionFull.minor, versionFull.patch); vernumInstall = vernumApp; - %vernumInstall = sprintf('%d.%d',versionFull.major,versionFull.minor); + % vernumInstall = sprintf('%d.%d',versionFull.major,versionFull.minor); else - vernumApp = sprintf('%d.%d.%d.65534',versionFull.major,versionFull.minor,versionFull.patch); + vernumApp = sprintf('%d.%d.%d.%d', versionFull.major, versionFull.minor, versionFull.patch, versionFull.revision); vernumInstall = vernumApp; - %vernumInstall = sprintf('%d.%d',versionFull.major,versionFull.minor); + % vernumInstall = sprintf('%d.%d',versionFull.major,versionFull.minor); end - -%elseif isempty(versionFull.commitID) +% elseif isempty(versionFull.commitID) % vernum = sprintf('%d.%d.%d.dev',versionFull.major,versionFull.minor,versionFull.patch); -%else +% else % vernum = sprintf('%d.%d.%d.%s/%s',versionFull.major,versionFull.minor,versionFull.patch,versionFull.branch,versionFull.commitID(1:8)); -%end +% end buildResult.version = vernumInstall; buildResult.versionDetail = versionFull; %% Set Options and Compile try - buildOpts = compiler.build.StandaloneApplicationOptions('matRadGUI.m',... - 'OutputDir',buildDir,... - 'ExecutableIcon',fullfile(standaloneFolder,'matRad_icon.png'),... - 'AutodetectDataFiles','on',... - 'AdditionalFiles',{'matRad','thirdParty','matRad_rc.m'},... - 'EmbedArchive','on',... - 'ExecutableName','matRad',... - 'ExecutableSplashScreen',fullfile(standaloneFolder,'matRad_splashscreen.png'),... - 'ExecutableVersion',vernumApp,... - 'TreatInputsAsNumeric','off',... - 'Verbose',verbose); + buildOpts = compiler.build.StandaloneApplicationOptions('matRadGUI.m', ... + 'OutputDir', buildDir, ... + 'ExecutableIcon', fullfile(standaloneFolder, 'matRad_icon.png'), ... + 'AutodetectDataFiles', 'on', ... + 'AdditionalFiles', {'matRad', 'thirdParty', 'matRad_rc.m'}, ... + 'EmbedArchive', 'on', ... + 'ExecutableName', 'matRad', ... + 'ExecutableSplashScreen', fullfile(standaloneFolder, 'matRad_splashscreen.png'), ... + 'ExecutableVersion', vernumApp, ... + 'TreatInputsAsNumeric', 'off', ... + 'Verbose', verbose); if ispc resultsStandalone = compiler.build.standaloneWindowsApplication(buildOpts); @@ -121,7 +119,7 @@ buildResult.standalone.compiledFiles = resultsStandalone.Files; catch ME - warning(ME.identifier,'Failed to compile standalone due to %s',ME.message); + warning(ME.identifier, 'Failed to compile standalone due to %s', ME.message); end @@ -135,73 +133,83 @@ end installerId = arch; +additionalFiles = {fullfile(matRadRoot, 'AUTHORS.txt'), fullfile(matRadRoot, 'LICENSE.md'), fullfile(standaloneFolder, readmeFile)}; +description = ['matRad is an open source software for radiation treatment planning of intensity-modulated photon, ' ... + 'proton, and carbon ion therapy started in 2015 in the research group "Radiotherapy Optimization" ', ... + 'within the Department of Medical Physics in Radiation Oncology at the German Cancer Research Center - DKFZ.']; +installationNotes = ['matRad contains precompiled libraries and third party software. In some cases, ' ... + 'those precompiled libraries may not run out of the box on your system. ' ... + 'Please contact us should this be the case. ' ... + 'For the third party licenses, check the subfolder in the matRad installation directory.']; +summary = 'matRad is an open source treatment planning system for radiation therapy written in Matlab.'; + try - packageOpts = compiler.package.InstallerOptions(resultsStandalone,... - 'AdditionalFiles',{fullfile(matRadRoot,'AUTHORS.txt'),fullfile(matRadRoot,'LICENSE.md'),fullfile(standaloneFolder,readmeFile)},... - 'ApplicationName','matRad',... - 'AuthorCompany','German Cancer Research Center (DKFZ)',... - 'AuthorEmail','contact@matRad.org',... - 'AuthorName','matRad development team @ DKFZ',... - 'Description','matRad is an open source software for radiation treatment planning of intensity-modulated photon, proton, and carbon ion therapy started in 2015 in the research group "Radiotherapy Optimization" within the Department of Medical Physics in Radiation Oncology at the German Cancer Research Center - DKFZ.', ... %\n\nmatRad targets education and research in radiotherapy treatment planning, where the software landscape is dominated by proprietary medical software. As of August 2022, matRad had more than 130 forks on GitHub and its development paper was cited more than 160 times (according to Google Scholar). matRad is entirely written in MATLAB and mostly compatible to GNU Octave.',... - 'InstallationNotes','matRad contains precompiled libraries and thirdParty software. In some cases, those precompiled libraries may not run out of the box on your system. Please contact us should this be the case. For the Third-Party licenses, check the thirdParty subfolder in the matRad installation directory.',... - 'InstallerIcon',fullfile(standaloneFolder,'matRad_icon.png'),... - 'InstallerLogo',fullfile(standaloneFolder,'matRad_installscreen.png'),... - 'InstallerSplash',fullfile(standaloneFolder,'matRad_splashscreen.png'),... - 'InstallerName',sprintf('matRad_installer_%s_v%s',installerId,vernumApp),... - 'OutputDir',fullfile(buildDir,'installer'),... - 'RuntimeDelivery',rtOption,... - 'Summary','matRad is an open source treatment planning system for radiation therapy written in Matlab.',... - 'Version',vernumInstall,... - 'Verbose',verbose); - compiler.package.installer(resultsStandalone,'Options',packageOpts); + packageOpts = compiler.package.InstallerOptions(resultsStandalone, ... + 'AdditionalFiles', additionalFiles, ... + 'ApplicationName', 'matRad', ... + 'AuthorCompany', 'German Cancer Research Center (DKFZ)', ... + 'AuthorEmail', 'contact@matRad.org', ... + 'AuthorName', 'matRad development team @ DKFZ', ... + 'Description', description, ... + 'InstallationNotes', installationNotes, ... + 'InstallerIcon', fullfile(standaloneFolder, 'matRad_icon.png'), ... + 'InstallerLogo', fullfile(standaloneFolder, 'matRad_installscreen.png'), ... + 'InstallerSplash', fullfile(standaloneFolder, 'matRad_splashscreen.png'), ... + 'InstallerName', sprintf('matRad_installer_%s_v%s', installerId, vernumApp), ... + 'OutputDir', fullfile(buildDir, 'installer'), ... + 'RuntimeDelivery', rtOption, ... + 'Summary', summary, ... + 'Version', vernumInstall, ... + 'Verbose', verbose); + compiler.package.installer(resultsStandalone, 'Options', packageOpts); outFiles = dir([packageOpts.OutputDir filesep packageOpts.InstallerName '.*']); - outFiles = arrayfun(@(f) fullfile(f.folder,f.name),outFiles,'UniformOutput',false); + outFiles = arrayfun(@(f) fullfile(f.folder, f.name), outFiles, 'UniformOutput', false); buildResult.standalone.installerFiles = outFiles; catch ME - warning(ME.identifier,'Failed to package standalone installer due to %s!',ME.message); + warning(ME.identifier, 'Failed to package standalone installer due to %s!', ME.message); end %% Python if buildPython - functionFiles = {'matRadGUI.m',... - 'matRad/matRad_generateStf.m',... - 'matRad/matRad_calcDoseForward.m',... - 'matRad/matRad_calcDoseInfluence.m',... - 'matRad/matRad_fluenceOptimization.m',... - 'matRad/matRad_directApertureOptimization.m',... - 'matRad/matRad_sequencing.m',... - 'matRad/matRad_planAnalysis.m'}; + functionFiles = {'matRadGUI.m', ... + 'matRad/matRad_generateStf.m', ... + 'matRad/matRad_calcDoseForward.m', ... + 'matRad/matRad_calcDoseInfluence.m', ... + 'matRad/matRad_fluenceOptimization.m', ... + 'matRad/matRad_directApertureOptimization.m', ... + 'matRad/matRad_sequencing.m', ... + 'matRad/matRad_planAnalysis.m'}; sampleGenerationFiles = {'matRad.m'}; try - pythonOpts = compiler.build.PythonPackageOptions(functionFiles,... - 'AdditionalFiles',{'matRad','thirdParty','matRad_rc.m'},... - 'SampleGenerationFiles',sampleGenerationFiles,... - 'Verbose',verbose, ... - 'PackageName','pyMatRad',... - 'OutputDir',fullfile(buildDir,'python')); + pythonOpts = compiler.build.PythonPackageOptions(functionFiles, ... + 'AdditionalFiles', {'matRad', 'thirdParty', 'matRad_rc.m'}, ... + 'SampleGenerationFiles', sampleGenerationFiles, ... + 'Verbose', verbose, ... + 'PackageName', 'pyMatRad', ... + 'OutputDir', fullfile(buildDir, 'python')); resultsPython = compiler.build.pythonPackage(pythonOpts); buildResult.python.packageFiles = resultsPython.Files; catch ME - warning(ME.identifier,'Java build failed due to %s!',ME.message); + warning(ME.identifier, 'Java build failed due to %s!', ME.message); end end %% Java if buildJava - javaOpts = compiler.build.JavaPackageOptions('matRadGUI.m',... - 'AdditionalFiles',{'matRad','thirdParty','matRad_rc.m'},... - 'Verbose',verbose); + javaOpts = compiler.build.JavaPackageOptions('matRadGUI.m', ... + 'AdditionalFiles', {'matRad', 'thirdParty', 'matRad_rc.m'}, ... + 'Verbose', verbose); try resultsJava = compiler.build.javaPackage(javaOpts); buildResult.java.packageFiles = resultsJava.Files; catch ME - warning(ME.identifier,'Java build failed due to %s!',ME.message); + warning(ME.identifier, 'Java build failed due to %s!', ME.message); end end @@ -213,28 +221,28 @@ else imageName = 'matRad:develop'; end - dockerOpts = compiler.package.DockerOptions(results,... - 'AdditionalInstructions','',... - 'AdditionalPackages','',... - 'ContainerUser','appuser',... - 'DockerContext',fullfile(buildDir,'docker'),... - 'ExecuteDockerBuild','on',... - 'ImageName',imageName); - - compiler.package.docker(results,'Options',dockerOpts); + dockerOpts = compiler.package.DockerOptions(results, ... + 'AdditionalInstructions', '', ... + 'AdditionalPackages', '', ... + 'ContainerUser', 'appuser', ... + 'DockerContext', fullfile(buildDir, 'docker'), ... + 'ExecuteDockerBuild', 'on', ... + 'ImageName', imageName); + + compiler.package.docker(results, 'Options', dockerOpts); buildResult.docker.image = dockerOpts.ImageName; catch ME - warning(ME.identifier,'Java build failed due to %s!',ME.message); + warning(ME.identifier, 'Java build failed due to %s!', ME.message); end end if ~isempty(json) try - fH = fopen(json,'w'); - jsonStr = jsonencode(buildResult,"PrettyPrint",true); - fwrite(fH,jsonStr); + fH = fopen(json, 'w'); + jsonStr = jsonencode(buildResult, "PrettyPrint", true); + fwrite(fH, jsonStr); fclose(fH); catch ME - warning(ME.identifier,'Could not open JSON file for writing: %s',ME.message); + warning(ME.identifier, 'Could not open JSON file for writing: %s', ME.message); end end diff --git a/matRad_rc.m b/matRad_rc.m index 8a5b98106..cb092c0bd 100644 --- a/matRad_rc.m +++ b/matRad_rc.m @@ -3,7 +3,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2015-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -69,4 +69,4 @@ matRad_cfg = tmp_cfg; else assignin('base','matRad_cfg',tmp_cfg); -end \ No newline at end of file +end diff --git a/matRad_runTests.m b/matRad_runTests.m index efcd8bc46..44068985d 100644 --- a/matRad_runTests.m +++ b/matRad_runTests.m @@ -10,7 +10,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2017 the matRad development team. +% Copyright 2017-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -84,4 +84,4 @@ matRad_cfg.logLevel = 3; cd(back); clear back; -matRad_cfg.dispInfo('Restored default properties and returned to original directory!\n'); \ No newline at end of file +matRad_cfg.dispInfo('Restored default properties and returned to original directory!\n'); diff --git a/submodules/MOcov b/submodules/MOcov index 995abc8ef..2bcb1741f 160000 --- a/submodules/MOcov +++ b/submodules/MOcov @@ -1 +1 @@ -Subproject commit 995abc8ef4766dcbc6318804bd01573901cad718 +Subproject commit 2bcb1741fefa79718f32d04b7694042a6c60255e diff --git a/submodules/MOxUnit b/submodules/MOxUnit index db6c87fe7..92bcf4eec 160000 --- a/submodules/MOxUnit +++ b/submodules/MOxUnit @@ -1 +1 @@ -Subproject commit db6c87fe7379d5a5d727feed760c38f3c533b339 +Subproject commit 92bcf4eec200c29024ce8d1145ec66bd76c1d623 diff --git a/submodules/matlab2tikz b/submodules/matlab2tikz index 816f87548..7c8f50a93 160000 --- a/submodules/matlab2tikz +++ b/submodules/matlab2tikz @@ -1 +1 @@ -Subproject commit 816f8754804cd45d8b41b3adf3ff9709a29cf173 +Subproject commit 7c8f50a93b630fedd6f08602d2d27d9a481798fd diff --git a/test/bioModel/test_biologicalModel.m b/test/bioModel/test_biologicalModel.m index 51af189d8..987b79501 100644 --- a/test/bioModel/test_biologicalModel.m +++ b/test/bioModel/test_biologicalModel.m @@ -160,9 +160,8 @@ % pln.propStf.gantryAngles = 0; % pln.propStf.couchAngles = 0; % pln.propStf.bixelWidth = 5; -% pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); % -% pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0); +% pln.propStf.isoCenter = matRad_getIsoCenter(cst,ct,0); % pln.propOpt.runDAO = 0; % pln.propSeq.runSequencing = 0; % diff --git a/test/doseCalc/test_FREDEngine.m b/test/doseCalc/test_FREDEngine.m index b455c9f2b..18ab898a4 100644 --- a/test/doseCalc/test_FREDEngine.m +++ b/test/doseCalc/test_FREDEngine.m @@ -1,141 +1,188 @@ function test_suite = test_FREDEngine -test_functions=localfunctions(); +test_functions = localfunctions(); initTestSuite; function test_constructFREDEngine - radModes = DoseEngines.matRad_ParticleFREDEngine.possibleRadiationModes; - for i = 1:numel(radModes) - plnDummy = struct('radiationMode',radModes{i},'machine','Generic','propDoseCalc',struct('engine','FRED')); - engine = DoseEngines.matRad_ParticleFREDEngine(plnDummy); - assertTrue(isa(engine,'DoseEngines.matRad_ParticleFREDEngine')); - end +radModes = DoseEngines.matRad_ParticleFREDEngine.possibleRadiationModes; +for i = 1:numel(radModes) + plnDummy = struct('radiationMode', radModes{i}, 'machine', 'Generic', 'propDoseCalc', struct('engine', 'FRED')); + engine = DoseEngines.matRad_ParticleFREDEngine(plnDummy); + assertTrue(isa(engine, 'DoseEngines.matRad_ParticleFREDEngine')); +end function test_constructFailOnWrongRadMode - plnDummy = struct('radiationMode','brachy','machine','HDR','propDoseCalc',struct('engine','FRED')); - assertExceptionThrown(@()DoseEngines.matRad_ParticleFREDEngine(plnDummy)); +plnDummy = struct('radiationMode', 'brachy', 'machine', 'HDR', 'propDoseCalc', struct('engine', 'FRED')); +assertExceptionThrown(@()DoseEngines.matRad_ParticleFREDEngine(plnDummy)); function test_propertyAssignmentFromPln - radModes = DoseEngines.matRad_ParticleFREDEngine.possibleRadiationModes; - - for i = 1:numel(radModes) - pln = struct('radiationMode',radModes{i},'machine','Generic','propDoseCalc',struct('engine','FRED')); - - pln.propDoseCalc.HUclamping = false; - pln.propDoseCalc.HUtable = 'matRad_default_FredMaterialConverter'; - pln.propDoseCalc.externalCalculation = 'write'; - pln.propDoseCalc.sourceModel = 'gaussian'; - pln.propDoseCalc.useGPU = false; - pln.propDoseCalc.roomMaterial = 'Vacuum'; - pln.propDoseCalc.printOutput = false; - pln.propDoseCalc.numHistoriesDirect = 42; - pln.propDoseCalc.numHistoriesPerBeamlet = 42; - - engine = DoseEngines.matRad_ParticleFREDEngine(pln); - - assertTrue(isa(engine,'DoseEngines.matRad_ParticleFREDEngine')); - - plnFields = fieldnames(pln.propDoseCalc); - plnFields(strcmp([plnFields(:)], 'engine')) = []; - - for fieldIdx=1:numel(plnFields) - assertTrue(isequal(engine.(plnFields{fieldIdx}), pln.propDoseCalc.(plnFields{fieldIdx}))); - end +radModes = DoseEngines.matRad_ParticleFREDEngine.possibleRadiationModes; + +for i = 1:numel(radModes) + pln = struct('radiationMode', radModes{i}, 'machine', 'Generic', 'propDoseCalc', struct('engine', 'FRED')); + + pln.propDoseCalc.HUclamping = false; + pln.propDoseCalc.HUtable = 'matRad_default_FredMaterialConverter'; + pln.propDoseCalc.externalCalculation = 'write'; + pln.propDoseCalc.sourceModel = 'gaussian'; + pln.propDoseCalc.useGPU = false; + pln.propDoseCalc.roomMaterial = 'Vacuum'; + pln.propDoseCalc.printOutput = false; + pln.propDoseCalc.numHistoriesDirect = 42; + pln.propDoseCalc.numHistoriesPerBeamlet = 42; + + engine = DoseEngines.matRad_ParticleFREDEngine(pln); + + assertTrue(isa(engine, 'DoseEngines.matRad_ParticleFREDEngine')); + + plnFields = fieldnames(pln.propDoseCalc); + plnFields(strcmp([plnFields(:)], 'engine')) = []; + + for fieldIdx = 1:numel(plnFields) + assertEqual(engine.(plnFields{fieldIdx}), pln.propDoseCalc.(plnFields{fieldIdx})); end +end function test_writeFiles - matRad_cfg = MatRad_Config.instance(); - radModes = DoseEngines.matRad_ParticleFREDEngine.possibleRadiationModes; +radModes = DoseEngines.matRad_ParticleFREDEngine.possibleRadiationModes; - load([radModes{1} '_testData.mat']); - pln.radiationMode = radModes{1}; - pln.machine = 'Generic'; - pln.propDoseCalc.engine = 'FRED'; - pln.propDoseCalc.externalCalculation = 'write'; +load([radModes{1} '_testData.mat']); +pln.radiationMode = radModes{1}; +pln.machine = 'Generic'; +pln.propDoseCalc.engine = 'FRED'; +pln.propDoseCalc.externalCalculation = 'write'; +pln.propDoseCalc.workingDir = helper_temporaryFolder('testFRED', true); - w = ones(sum([stf(:).totalNumOfBixels]),1); +w = ones(sum([stf(:).totalNumOfBixels]), 1); - resultGUI = matRad_calcDoseForward(ct,cst,stf,pln,w); +resultGUI = matRad_calcDoseForward(ct, cst, stf, pln, w); - fredMainDir = fullfile(matRad_cfg.primaryUserFolder, 'FRED'); - runFolder = fullfile(fredMainDir, 'MCrun'); - inputFolder = fullfile(runFolder, 'inp'); - planFolder = fullfile(inputFolder, 'plan'); - regionsFolder = fullfile(inputFolder, 'regions'); +fredMainDir = pln.propDoseCalc.workingDir; +runFolder = fullfile(fredMainDir, 'MCrun'); +inputFolder = fullfile(runFolder, 'inp'); +planFolder = fullfile(inputFolder, 'plan'); +regionsFolder = fullfile(inputFolder, 'regions'); - - assertTrue(all(cellfun(@isfolder, {fredMainDir,runFolder,inputFolder,planFolder,regionsFolder}))); - assertTrue(all(cellfun(@isfile, {fullfile(planFolder, 'plan.inp'),... - fullfile(planFolder, 'planDelivery.inp'),... - fullfile(regionsFolder, 'CTpatient.raw'),... - fullfile(regionsFolder, 'CTpatient.mhd'),... - fullfile(regionsFolder, 'regions.inp'),... - fullfile(runFolder, 'fred.inp'),... - }))); +assertTrue(all(cellfun(@isfolder, {fredMainDir, runFolder, inputFolder, planFolder, regionsFolder}))); +assertTrue(all(cellfun(@isfile, {fullfile(planFolder, 'plan.inp'), ... + fullfile(planFolder, 'planDelivery.inp'), ... + fullfile(regionsFolder, 'CTpatient.raw'), ... + fullfile(regionsFolder, 'CTpatient.mhd'), ... + fullfile(regionsFolder, 'regions.inp'), ... + fullfile(runFolder, 'fred.inp') ... + }))); function test_loadDij - matRad_cfg = MatRad_Config.instance(); - load(['protons_testData.mat']); - pln.machine = 'Generic'; - pln.propDoseCalc.engine = 'FRED'; - pln.propDoseCalc.useGPU = true; - pln.propDoseCalc.externalCalculation = fullfile(matRad_cfg.matRadRoot, 'test', 'testData', 'FRED_data'); - - % Test dij-load - dijFredLoad = matRad_calcDoseInfluence(ct,cst,stf,pln); - - % Test forward calculation cube load - w = ones(sum([stf(:).totalNumOfBixels]),1); - forwardDoseFredLoad = matRad_calcDoseForward(ct,cst,stf,pln,w); +matRad_cfg = MatRad_Config.instance(); +load(['protons_testData.mat']); +pln.machine = 'Generic'; +pln.propDoseCalc.engine = 'FRED'; +pln.propDoseCalc.useGPU = true; +pln.propDoseCalc.calcLET = true; +pln.propDoseCalc.externalCalculation = fullfile(matRad_cfg.matRadRoot, 'test', 'testData', 'FRED_data'); + +% Test dij-load +dijFredLoad = matRad_calcDoseInfluence(ct, cst, stf, pln); - resultGUI = matRad_calcCubes(w, dijFredLoad, 1); +% Test forward calculation cube load +w = ones(sum([stf(:).totalNumOfBixels]), 1); +forwardDoseFredLoad = matRad_calcDoseForward(ct, cst, stf, pln, w); - nBixels = sum([stf(:).totalNumOfBixels]); - nVoxles = prod(ct.cubeDim); +resultGUI = matRad_calcCubes(w, dijFredLoad, 1); - % Assert basic parameters - assertTrue(isequal(dijFredLoad.externalCalculationLodPath, fullfile(pln.propDoseCalc.externalCalculation, 'MCrun', 'out', 'scoreij', 'Phantom.Dose.bin'))); - assertTrue(isequal(size(dijFredLoad.physicalDose{1}),[nVoxles, nBixels])); - assertTrue(isequal(size(forwardDoseFredLoad.physicalDose), size(resultGUI.physicalDose))); +nBixels = sum([stf(:).totalNumOfBixels]); +nVoxles = prod(ct.cubeDim); +% Assert basic parameters +assertEqual(dijFredLoad.externalCalculationLodPath{1}, fullfile(pln.propDoseCalc.externalCalculation, 'MCrun', 'out', 'scoreij', 'Phantom.Dose.bin')); +assertEqual(size(dijFredLoad.physicalDose{1}), [nVoxles, nBixels]); +assertEqual(size(forwardDoseFredLoad.physicalDose), size(resultGUI.physicalDose)); +assertEqual(size(dijFredLoad.mLETDose{1}), [nVoxles, nBixels]); +assertEqual(size(forwardDoseFredLoad.LET), size(resultGUI.LET)); function test_bioCalculation - matRad_cfg = MatRad_Config.instance(); - load(['protons_testData.mat']); - pln.machine = 'Generic'; - pln.propDoseCalc.bioModel = matRad_bioModel('protons', 'MCN'); - - pln.propDoseCalc.engine = 'FRED'; - pln.propDoseCalc.useGPU = true; - pln.propDoseCalc.externalCalculation = fullfile(matRad_cfg.matRadRoot, 'test', 'testData', 'FRED_data'); - - % Test dij-load - dijFredLoad = matRad_calcDoseInfluence(ct,cst,stf,pln); - - % Test forward calculation cube load - w = ones(sum([stf(:).totalNumOfBixels]),1); - forwardDoseFredLoad = matRad_calcDoseForward(ct,cst,stf,pln,w); - - % Assert basic parameters - assertTrue(all(cellfun(@(x) isfield(dijFredLoad, x), {'physicalDose', 'mLETd', 'mAlphaDose', 'mSqrtBetaDose'}))); - assertTrue(all(cellfun(@(x) isfield(forwardDoseFredLoad, x), {'physicalDose', 'LET', 'alpha', 'beta', 'effect'}))); +matRad_cfg = MatRad_Config.instance(); +load(['protons_testData.mat']); +pln.machine = 'Generic'; +pln.bioModel = matRad_bioModel('protons', 'MCN'); +pln.propDoseCalc.engine = 'FRED'; +pln.propDoseCalc.useGPU = true; +pln.propDoseCalc.externalCalculation = fullfile(matRad_cfg.matRadRoot, 'test', 'testData', 'FRED_data'); -function test_additionalParameters - - matRad_cfg = MatRad_Config.instance(); - load(['protons_testData.mat']); - pln.machine = 'Generic'; +% Test dij-load +dijFredLoad = matRad_calcDoseInfluence(ct, cst, stf, pln); - pln.propDoseCalc.HUtable = 'internal'; - pln.propDoseCalc.sourceModel = 'emittance'; - pln.propDoseCalc.HUclamping = true; +% Test forward calculation cube load +w = ones(sum([stf(:).totalNumOfBixels]), 1); +forwardDoseFredLoad = matRad_calcDoseForward(ct, cst, stf, pln, w); - engine = DoseEngines.matRad_ParticleFREDEngine(pln); +% Assert basic parameters +assertTrue(all(cellfun(@(x) isfield(dijFredLoad, x), {'physicalDose', 'mLETd', 'mAlphaDose', 'mSqrtBetaDose'}))); +assertTrue(all(cellfun(@(x) isfield(forwardDoseFredLoad, x), {'physicalDose', 'LET', 'alpha', 'beta', 'effect'}))); + +function test_additionalParameters - assertTrue(all(cellfun(@(x,y) isequal(engine.(x), y), {'sourceModel','HUtable', 'HUclamping'}, {'emittance','internal', true}))); \ No newline at end of file +matRad_cfg = MatRad_Config.instance(); +load(['protons_testData.mat']); +pln.machine = 'Generic'; + +pln.propDoseCalc.HUtable = 'internal'; +pln.propDoseCalc.sourceModel = 'emittance'; +pln.propDoseCalc.HUclamping = true; + +engine = DoseEngines.matRad_ParticleFREDEngine(pln); + +assertTrue(all(cellfun(@(x, y) isequal(engine.(x), y), {'sourceModel', 'HUtable', 'HUclamping'}, {'emittance', 'internal', true}))); + +function test_errorScenarios +radModes = DoseEngines.matRad_ParticleFREDEngine.possibleRadiationModes; + +load([radModes{1} '_testData.mat']); +pln.radiationMode = radModes{1}; +pln.machine = 'Generic'; +pln.propDoseCalc.engine = 'FRED'; +pln.propDoseCalc.externalCalculation = 'write'; +pln.propDoseCalc.workingDir = helper_temporaryFolder('testFRED', true); + +pln.multScen = matRad_RandomScenarios(ct); +pln.multScen.nSamples = 2; + +w = ones(sum([stf(:).totalNumOfBixels]), 1); + +resultGUI = matRad_calcDoseForward(ct, cst, stf, pln, w); + +fredMainDir = pln.propDoseCalc.workingDir; +runFolder = fullfile(fredMainDir, 'MCrun_1'); +inputFolder = fullfile(runFolder, 'inp'); +planFolder = fullfile(inputFolder, 'plan'); +regionsFolder = fullfile(inputFolder, 'regions'); + +assertTrue(all(cellfun(@isfolder, {fredMainDir, runFolder, inputFolder, planFolder, regionsFolder}))); +assertTrue(all(cellfun(@isfile, {fullfile(planFolder, 'plan.inp'), ... + fullfile(planFolder, 'planDelivery.inp'), ... + fullfile(regionsFolder, 'CTpatient.raw'), ... + fullfile(regionsFolder, 'CTpatient.mhd'), ... + fullfile(regionsFolder, 'regions.inp'), ... + fullfile(runFolder, 'fred.inp') ... + }))); + +% Check second folder +runFolder = fullfile(fredMainDir, 'MCrun_2'); +inputFolder = fullfile(runFolder, 'inp'); +planFolder = fullfile(inputFolder, 'plan'); +regionsFolder = fullfile(inputFolder, 'regions'); + +assertTrue(all(cellfun(@isfolder, {fredMainDir, runFolder, inputFolder, planFolder, regionsFolder}))); +assertTrue(all(cellfun(@isfile, {fullfile(planFolder, 'plan.inp'), ... + fullfile(planFolder, 'planDelivery.inp'), ... + fullfile(regionsFolder, 'CTpatient.raw'), ... + fullfile(regionsFolder, 'CTpatient.mhd'), ... + fullfile(regionsFolder, 'regions.inp'), ... + fullfile(runFolder, 'fred.inp') ... + }))); diff --git a/test/doseCalc/test_HongPB.m b/test/doseCalc/test_HongPB.m index 40f5300b4..74382fbb3 100644 --- a/test/doseCalc/test_HongPB.m +++ b/test/doseCalc/test_HongPB.m @@ -1,102 +1,121 @@ function test_suite = test_HongPB - test_functions=localfunctions(); - - initTestSuite; - - function test_getHongPBEngineFromPln - % Single gaussian lateral model - testData.pln = struct('radiationMode','protons','machine','Generic'); - testData.pln.propDoseCalc.engine = 'HongPB'; - engine = DoseEngines.matRad_ParticleHongPencilBeamEngine.getEngineFromPln(testData.pln); - assertTrue(isa(engine,'DoseEngines.matRad_ParticleHongPencilBeamEngine')); - - function test_loadMachineForHongPB - possibleRadModes = DoseEngines.matRad_ParticleHongPencilBeamEngine.possibleRadiationModes; - for i = 1:numel(possibleRadModes) - machine = DoseEngines.matRad_ParticleHongPencilBeamEngine.loadMachine(possibleRadModes{i},'Generic'); - assertTrue(isstruct(machine)); - assertTrue(isfield(machine, 'meta')); - assertTrue(isfield(machine.meta, 'radiationMode')); - assertTrue(strcmp(machine.meta.radiationMode, possibleRadModes{i})); - end - - function test_calcDoseHongPBprotons - testData = load('protons_testData.mat'); - - assertTrue(DoseEngines.matRad_ParticleHongPencilBeamEngine.isAvailable(testData.pln)); - - testData.pln.propDoseCalc.engine = 'HongPB'; - testData.pln.propDoseCalc.dosimetricLateralCutOff = 0.995; - testData.pln.propDoseCalc.geometricLateralCutOff = 50; - resultGUI = matRad_calcDoseForward(testData.ct, testData.cst, testData.stf, testData.pln, ones(sum([testData.stf(:).totalNumOfBixels]),1)); - - assertTrue(isequal(fieldnames(resultGUI),fieldnames(testData.resultGUI))); - assertTrue(isequal(testData.ct.cubeDim, size(resultGUI.physicalDose))); - assertElementsAlmostEqual(resultGUI.physicalDose,testData.resultGUI.physicalDose,'relative',1e-2,1e-2); - - function test_calcDoseHongPBhelium - testData = load('helium_testData.mat'); - assertTrue(DoseEngines.matRad_ParticleHongPencilBeamEngine.isAvailable(testData.pln)); - - testData.pln.propDoseCalc.engine = 'HongPB'; - testData.pln.propDoseCalc.dosimetricLateralCutOff = 0.995; - testData.pln.propDoseCalc.geometricLateralCutOff = 50; - resultGUI = matRad_calcDoseForward(testData.ct, testData.cst, testData.stf, testData.pln, ones(sum([testData.stf(:).totalNumOfBixels]),1)); - - assertTrue(isequal(fieldnames(resultGUI),fieldnames(testData.resultGUI))); - assertTrue(isequal(testData.ct.cubeDim, size(resultGUI.physicalDose))); - assertElementsAlmostEqual(resultGUI.physicalDose,testData.resultGUI.physicalDose,'relative',1e-2,1e-2); - - function test_calcDoseHongPBcarbon - testData = load('carbon_testData.mat'); - assertTrue(DoseEngines.matRad_ParticleHongPencilBeamEngine.isAvailable(testData.pln)); - - testData.pln.propDoseCalc.engine = 'HongPB'; - testData.pln.propDoseCalc.dosimetricLateralCutOff = 0.995; - testData.pln.propDoseCalc.geometricLateralCutOff = 50; - resultGUI = matRad_calcDoseForward(testData.ct, testData.cst, testData.stf, testData.pln, ones(sum([testData.stf(:).totalNumOfBixels]),1)); - - assertTrue(isequal(fieldnames(resultGUI),fieldnames(testData.resultGUI))); - assertTrue(isequal(testData.ct.cubeDim, size(resultGUI.physicalDose))); - assertElementsAlmostEqual(resultGUI.physicalDose,testData.resultGUI.physicalDose,'relative',1e-2,1e-2); - - function test_calcDoseHongPBVHEE - testData = load('VHEE_testData.mat'); - assertTrue(DoseEngines.matRad_ParticleHongPencilBeamEngine.isAvailable(testData.pln)); - testData.pln.propDoseCalc.engine = 'HongPB'; - testData.pln.propDoseCalc.dosimetricLateralCutOff = 0.995; - testData.pln.propDoseCalc.geometricLateralCutOff = 50; - resultGUI = matRad_calcDoseForward(testData.ct, testData.cst, testData.stf, testData.pln, ones(sum([testData.stf(:).totalNumOfBixels]),1)); - - assertTrue(isequal(fieldnames(resultGUI),fieldnames(testData.resultGUI))); - assertTrue(isequal(testData.ct.cubeDim, size(resultGUI.physicalDose))); - assertElementsAlmostEqual(resultGUI.physicalDose,testData.resultGUI.physicalDose,'relative',1e-2,1e-2); - - function test_calcDoseHongPBVHEE_Focused - testData = load('VHEE_testData_Focused.mat'); - assertTrue(DoseEngines.matRad_ParticleHongPencilBeamEngine.isAvailable(testData.pln)); - testData.pln.propDoseCalc.engine = 'HongPB'; - testData.pln.propDoseCalc.dosimetricLateralCutOff = 0.995; - testData.pln.propDoseCalc.geometricLateralCutOff = 50; - resultGUI = matRad_calcDoseForward(testData.ct, testData.cst, testData.stf, testData.pln, ones(sum([testData.stf(:).totalNumOfBixels]),1)); - - assertTrue(isequal(fieldnames(resultGUI),fieldnames(testData.resultGUI))); - assertTrue(isequal(testData.ct.cubeDim, size(resultGUI.physicalDose))); - assertElementsAlmostEqual(resultGUI.physicalDose,testData.resultGUI.physicalDose,'relative',1e-2,1e-2); - - function test_nonSupportedSettings - % Radiation mode other than protons not implemented - testData = load('photons_testData.mat'); - testData.pln.propDoseCalc.engine = 'HongPB'; - assertFalse(DoseEngines.matRad_ParticleHongPencilBeamEngine.isAvailable(testData.pln)); - - % Invalid machine without radiation mode field - testData.pln.machine = 'Empty'; - testData.pln.propDoseCalc.engine = 'HongPB'; - assertExceptionThrown(@() DoseEngines.matRad_ParticleHongPencilBeamEngine.isAvailable(testData.pln)); - assertFalse(DoseEngines.matRad_ParticleHongPencilBeamEngine.isAvailable(testData.pln,[])); - - - - \ No newline at end of file +test_functions = localfunctions(); + +initTestSuite; + +function test_getHongPBEngineFromPln +% Single gaussian lateral model +testData.pln = struct('radiationMode', 'protons', 'machine', 'Generic'); +testData.pln.propDoseCalc.engine = 'HongPB'; +engine = DoseEngines.matRad_ParticleHongPencilBeamEngine.getEngineFromPln(testData.pln); +assertTrue(isa(engine, 'DoseEngines.matRad_ParticleHongPencilBeamEngine')); + +function test_loadMachineForHongPB +possibleRadModes = DoseEngines.matRad_ParticleHongPencilBeamEngine.possibleRadiationModes; +for i = 1:numel(possibleRadModes) + machine = DoseEngines.matRad_ParticleHongPencilBeamEngine.loadMachine(possibleRadModes{i}, 'Generic'); + assertTrue(isstruct(machine)); + assertTrue(isfield(machine, 'meta')); + assertTrue(isfield(machine.meta, 'radiationMode')); + assertTrue(strcmp(machine.meta.radiationMode, possibleRadModes{i})); +end + +function test_calcDoseHongPBprotons +testData = load('protons_testData.mat'); + +assertTrue(DoseEngines.matRad_ParticleHongPencilBeamEngine.isAvailable(testData.pln)); + +testData.pln.propDoseCalc.engine = 'HongPB'; +resultGUI = matRad_calcDoseForward(testData.ct, testData.cst, testData.stf, testData.pln, ones(sum([testData.stf(:).totalNumOfBixels]), 1)); + +assertTrue(isequal(fieldnames(resultGUI), fieldnames(testData.resultGUI))); +assertTrue(isequal(testData.ct.cubeDim, size(resultGUI.physicalDose))); +assertElementsAlmostEqual(resultGUI.physicalDose, testData.resultGUI.physicalDose, 'relative', 1e-2, 1e-2); + +function test_calcDoseHongPBhelium +testData = load('helium_testData.mat'); +assertTrue(DoseEngines.matRad_ParticleHongPencilBeamEngine.isAvailable(testData.pln)); + +testData.pln.propDoseCalc.engine = 'HongPB'; +resultGUI = matRad_calcDoseForward(testData.ct, testData.cst, testData.stf, testData.pln, ones(sum([testData.stf(:).totalNumOfBixels]), 1)); + +assertTrue(isequal(fieldnames(resultGUI), fieldnames(testData.resultGUI))); +assertTrue(isequal(testData.ct.cubeDim, size(resultGUI.physicalDose))); +assertElementsAlmostEqual(resultGUI.physicalDose, testData.resultGUI.physicalDose, 'relative', 1e-2, 1e-2); + +function test_calcDoseHongPBcarbon +testData = load('carbon_testData.mat'); +assertTrue(DoseEngines.matRad_ParticleHongPencilBeamEngine.isAvailable(testData.pln)); + +resultGUI = matRad_calcDoseForward(testData.ct, testData.cst, testData.stf, testData.pln, ones(sum([testData.stf(:).totalNumOfBixels]), 1)); + +assertTrue(isequal(fieldnames(resultGUI), fieldnames(testData.resultGUI))); +assertTrue(isequal(testData.ct.cubeDim, size(resultGUI.physicalDose))); +assertElementsAlmostEqual(resultGUI.physicalDose, testData.resultGUI.physicalDose, 'relative', 1e-2, 1e-2); + +function test_calcDoseHongPBVHEE +testData = load('VHEE_testData.mat'); +assertTrue(DoseEngines.matRad_ParticleHongPencilBeamEngine.isAvailable(testData.pln)); + +testData.pln.propDoseCalc.engine = 'HongPB'; +resultGUI = matRad_calcDoseForward(testData.ct, testData.cst, testData.stf, testData.pln, ones(sum([testData.stf(:).totalNumOfBixels]), 1)); + +assertTrue(isequal(fieldnames(resultGUI), fieldnames(testData.resultGUI))); +assertTrue(isequal(testData.ct.cubeDim, size(resultGUI.physicalDose))); +assertElementsAlmostEqual(resultGUI.physicalDose, testData.resultGUI.physicalDose, 'relative', 1e-2, 1e-2); + +function test_calcDoseHongPBVHEEfocused +testData = load('VHEE_testData_Focused.mat'); +assertTrue(DoseEngines.matRad_ParticleHongPencilBeamEngine.isAvailable(testData.pln)); +testData.pln.propDoseCalc.engine = 'HongPB'; +testData.pln.propDoseCalc.dosimetricLateralCutOff = 0.995; +testData.pln.propDoseCalc.geometricLateralCutOff = 50; +resultGUI = matRad_calcDoseForward(testData.ct, testData.cst, testData.stf, testData.pln, ones(sum([testData.stf(:).totalNumOfBixels]), 1)); + +assertTrue(isequal(fieldnames(resultGUI), fieldnames(testData.resultGUI))); +assertTrue(isequal(testData.ct.cubeDim, size(resultGUI.physicalDose))); +assertElementsAlmostEqual(resultGUI.physicalDose, testData.resultGUI.physicalDose, 'relative', 1e-2, 1e-2); + +function test_nonSupportedSettings +% Radiation mode other than protons not implemented +testData = load('photons_testData.mat'); +testData.pln.propDoseCalc.engine = 'HongPB'; +assertFalse(DoseEngines.matRad_ParticleHongPencilBeamEngine.isAvailable(testData.pln)); + +% Invalid machine without radiation mode field +testData.pln.machine = 'Empty'; +testData.pln.propDoseCalc.engine = 'HongPB'; +assertExceptionThrown(@() DoseEngines.matRad_ParticleHongPencilBeamEngine.isAvailable(testData.pln)); +assertFalse(DoseEngines.matRad_ParticleHongPencilBeamEngine.isAvailable(testData.pln, [])); + +function test_doseCalcWithRashi +testData = load('protons_testData.mat'); +engine = DoseEngines.matRad_ParticleHongPencilBeamEngine(testData.pln); + +stf = testData.stf; + +% Add rangeShifter +stf(1).ray(1).rangeShifter.ID = 1; +stf(1).ray(1).rangeShifter.eqThickness = 1; +stf(1).ray(1).rangeShifter.sourceRashiDistance = -(stf(1).sourcePoint(2) + 100); + +% Add rangeShifter +stf(2).ray(2).rangeShifter.ID = 1; +stf(2).ray(2).rangeShifter.eqThickness = 1; +stf(2).ray(2).rangeShifter.sourceRashiDistance = -(stf(2).sourcePoint(2) + 100); + +resultGUI = engine.calcDoseForward(testData.ct, testData.cst, stf, ones(sum([stf.totalNumOfBixels]), 1)); +assertTrue(isequal(fieldnames(resultGUI), fieldnames(testData.resultGUI))); + +function test_traceDoseGrid +testData = load('protons_testData.mat'); +testData.pln.propDoseCalc.engine = 'HongPB'; +testData.pln.propDoseCalc.doseGrid.resolution = testData.ct.resolution; + +testData.pln.propDoseCalc.traceOnDoseGrid = false; +resultGUI = matRad_calcDoseForward(testData.ct, testData.cst, testData.stf, testData.pln, ones(sum([testData.stf(:).totalNumOfBixels]), 1)); + +testData.pln.propDoseCalc.traceOnDoseGrid = true; +resultGUI2 = matRad_calcDoseForward(testData.ct, testData.cst, testData.stf, testData.pln, ones(sum([testData.stf(:).totalNumOfBixels]), 1)); +assertElementsAlmostEqual(resultGUI.physicalDose, resultGUI2.physicalDose, 'relative', 1e-2, 1e-2); diff --git a/test/doseCalc/test_MCsquareEngine.m b/test/doseCalc/test_MCsquareEngine.m new file mode 100644 index 000000000..cb632db11 --- /dev/null +++ b/test/doseCalc/test_MCsquareEngine.m @@ -0,0 +1,121 @@ +function test_suite = test_MCsquareEngine + +test_functions=localfunctions(); + +initTestSuite; + + +function test_MCsquareDoseCalcBasic + + matRad_cfg = MatRad_Config.instance(); + radModes = DoseEngines.matRad_ParticleMCsquareEngine.possibleRadiationModes; + + for i = 1:numel(radModes) + load([radModes{i} '_testData.mat']); + pln.bioModel = matRad_bioModel(radModes{i},'none'); + + w = ones(1,sum([stf(:).totalNumOfBixels])); + + engineMC = DoseEngines.matRad_ParticleMCsquareEngine(pln); + engineMC.numHistoriesDirect = 42; + engineMC.externalCalculation = 'write'; + engineMC.workingDir = helper_temporaryFolder('testMCsquare', true); + + resultMC = engineMC.calcDoseForward(ct,cst,stf, w); + + %assertTrue(exist(fullfile(matRad_cfg.primaryUserFolder, 'MCsquare'), 'dir')==7); % Check it exists and its a folder + assertTrue(exist(fullfile(engineMC.workingDir, 'MCsquareConfig.txt'), 'file')==2); + assertTrue(exist(fullfile(engineMC.workingDir, 'currBixels.txt'), 'file')==2); + + + % Check parameters + % Read config file + fid = fopen(fullfile(engineMC.workingDir, 'MCsquareConfig.txt'),'r'); + linesConfigFile = {}; + while ~feof(fid) + linesConfigFile{end+1,1} = fgetl(fid); + end + fclose(fid); + + assertTrue(any(strcmp(linesConfigFile, "Num_Primaries 42"))); + + % Read currBixel file + fid = fopen(fullfile(engineMC.workingDir, 'currBixels.txt'),'r'); + linesBixelFile = {}; + while ~feof(fid) + linesBixelFile{end+1,1} = fgetl(fid); + end + fclose(fid); + + assertTrue(any(strcmp(linesBixelFile, "##NumberOfFields"))); + assertTrue(str2double(linesBixelFile(find(strcmp(linesBixelFile, "##NumberOfFields"))+1)) == numel(stf)); + + end + +function test_MCsquareRaShi + + matRad_cfg = MatRad_Config.instance(); + radModes = DoseEngines.matRad_ParticleMCsquareEngine.possibleRadiationModes; + + for i = 1:numel(radModes) + load([radModes{i} '_testData.mat']); + pln.bioModel = matRad_bioModel(radModes{i},'none'); + + % pln.propStf.gantryAngles = 0; + % pln.propStf.couchAngles = 0; + stfGenerator = matRad_StfGeneratorParticleSingleBeamlet(pln); + stfGenerator.useRangeShifter = true; + stfGenerator.gantryAngles = 0; + stfGenerator.couchAngles = 0; + stf = stfGenerator.generate(ct,cst); + + w = 1; + + engineMC = DoseEngines.matRad_ParticleMCsquareEngine(pln); + engineMC.numHistoriesDirect = 42; + engineMC.externalCalculation = 'write'; + engineMC.workingDir = helper_temporaryFolder('testMCsquare', true); + + resultGUI = engineMC.calcDoseForward(ct,cst,stf,w); + + assertTrue(exist(fullfile(engineMC.workingDir, 'MCsquareConfig.txt'), 'file')==2); + assertTrue(exist(fullfile(engineMC.workingDir, 'currBixels.txt'), 'file')==2); + + fid = fopen(fullfile(engineMC.workingDir, 'currBixels.txt'),'r'); + linesBixelFile = {}; + while ~feof(fid) + linesBixelFile{end+1,1} = fgetl(fid); + end + fclose(fid); + + assertTrue(any(strcmp(linesBixelFile, "####RangeShifterWaterEquivalentThickness"))); + assertTrue(str2double(linesBixelFile(find(strcmp(linesBixelFile, "####RangeShifterWaterEquivalentThickness"))+1)) == stf.ray.rangeShifter.eqThickness); + end + +function test_readOutput + + matRad_cfg = MatRad_Config.instance(); + radModes = DoseEngines.matRad_ParticleMCsquareEngine.possibleRadiationModes; + + for i = 1:numel(radModes) + load([radModes{i} '_testData.mat']); + pln.bioModel = matRad_bioModel(radModes{i},'none'); + + w = ones(1,sum([stf(:).totalNumOfBixels])); + + engineMC = DoseEngines.matRad_ParticleMCsquareEngine(pln); + engineMC.numHistoriesDirect = 42; + engineMC.externalCalculation = fullfile(matRad_cfg.matRadRoot, 'test', 'testData', 'MCsquare_data'); + % engineMC.externalCalculation = 'write'; + + resultMC = engineMC.calcDoseForward(ct,cst,stf,w); + + assertTrue(sum(resultMC.physicalDose(:))>0); + assertTrue(sum(resultMC.LET(:))>0); + + dij = engineMC.calcDoseInfluence(ct,cst,stf); + + + assertTrue(all(size(dij.physicalDose{1})==[prod(ct.cubeDim),sum([stf.totalNumOfBixels])])); + + end \ No newline at end of file diff --git a/test/doseCalc/test_SVDPB.m b/test/doseCalc/test_SVDPB.m index c06d1eed9..eac7ce694 100644 --- a/test/doseCalc/test_SVDPB.m +++ b/test/doseCalc/test_SVDPB.m @@ -27,9 +27,6 @@ assertTrue(DoseEngines.matRad_PhotonPencilBeamSVDEngine.isAvailable(testData.pln)); testData.pln.propDoseCalc.engine = 'SVDPB'; - testData.pln.propDoseCalc.dosimetricLateralCutOff = 0.995; - testData.pln.propDoseCalc.geometricLateralCutOff = 50; - testData.pln.propDoseCalc.kernelCutOff = Inf; if moxunit_util_platform_is_octave() %The random number generator is not consistent between octave and matlab testData.pln.propDoseCalc.enableDijSampling = false; diff --git a/test/doseCalc/test_TopasMCEngine.m b/test/doseCalc/test_TopasMCEngine.m index 361463771..a19ce6660 100644 --- a/test/doseCalc/test_TopasMCEngine.m +++ b/test/doseCalc/test_TopasMCEngine.m @@ -1,55 +1,55 @@ function test_suite = test_TopasMCEngine -test_functions=localfunctions(); +test_functions = localfunctions(); initTestSuite; function test_loadMachine - radModes = DoseEngines.matRad_TopasMCEngine.possibleRadiationModes; - for i = 1:numel(radModes) - machineName = 'Generic'; - machine = DoseEngines.matRad_TopasMCEngine.loadMachine(radModes{i},machineName); - assertTrue(isstruct(machine)); - end - assertExceptionThrown(@() DoseEngines.matRad_TopasMCEngine.loadMachine('grbl','grbl'),'matRad:Error') +radModes = DoseEngines.matRad_TopasMCEngine.possibleRadiationModes; +for i = 1:numel(radModes) + machineName = 'Generic'; + machine = DoseEngines.matRad_TopasMCEngine.loadMachine(radModes{i}, machineName); + assertTrue(isstruct(machine)); +end +assertExceptionThrown(@() DoseEngines.matRad_TopasMCEngine.loadMachine('grbl', 'grbl'), 'matRad:Error'); function test_getEngineFromPlnByName - radModes = DoseEngines.matRad_TopasMCEngine.possibleRadiationModes; - for i = 1:numel(radModes) - machineName = 'Generic'; - plnDummy = struct('radiationMode',radModes{i},'machine',machineName,'propDoseCalc',struct('engine','TOPAS')); - engine = DoseEngines.matRad_TopasMCEngine.getEngineFromPln(plnDummy); - assertTrue(isa(engine,'DoseEngines.matRad_TopasMCEngine')); - end +radModes = DoseEngines.matRad_TopasMCEngine.possibleRadiationModes; +for i = 1:numel(radModes) + machineName = 'Generic'; + plnDummy = struct('radiationMode', radModes{i}, 'machine', machineName, 'propDoseCalc', struct('engine', 'TOPAS')); + engine = DoseEngines.matRad_TopasMCEngine.getEngineFromPln(plnDummy); + assertTrue(isa(engine, 'DoseEngines.matRad_TopasMCEngine')); +end function test_TopasMCdoseCalcBasic -% test if all the necessary output files are written vor a couple of cases. -% i am not using the default number of histories for testing her, insted 1e6. -% Because the files are just writen and not simulated so we dont care about simulation time. -% To few histories may result in wierd behavior in the topas interface, i.e if a beam -% recieves no histories because there are not enough to be distributed accros the spots, +% test if all the necessary output files are written for a couple of cases. +% i am not using the default number of histories for testing her, instead 1e6. +% Because the files are just written and not simulated so we dont care about simulation time. +% To few histories may result in weird behavior in the topas interface, i.e if a beam +% receives no histories because there are not enough to be distributed across the spots, % it causes an error radModes = DoseEngines.matRad_TopasMCEngine.possibleRadiationModes; matRad_cfg = MatRad_Config.instance(); if moxunit_util_platform_is_octave - confirm_recursive_rmdir(false,'local'); + confirm_recursive_rmdir(false, 'local'); end for i = 1:numel(radModes) load([radModes{i} '_testData.mat']); - pln.bioModel = matRad_bioModel(radModes{i},'none'); - - w = ones(1,sum([stf(:).totalNumOfBixels])); - - if strcmp(radModes{i},'photons') + pln.bioModel = matRad_bioModel(radModes{i}, 'none'); + + w = ones(1, sum([stf(:).totalNumOfBixels])); + + if strcmp(radModes{i}, 'photons') pln.propOpt.runSequencing = 1; pln.propOpt.runDAO = 1; - dij = matRad_calcDoseInfluence(ct,cst,stf,pln); - resultGUI = matRad_calcCubes(ones(dij.totalNumOfBixels,1),dij); - resultGUI.wUnsequenced = ones(dij.totalNumOfBixels,1); - resultGUI = matRad_siochiLeafSequencing(resultGUI,stf,dij,5); - [pln,stf] = matRad_aperture2collimation(pln,stf,resultGUI.sequencing,resultGUI.apertureInfo); + dij = matRad_calcDoseInfluence(ct, cst, stf, pln); + resultGUI = matRad_calcCubes(ones(dij.totalNumOfBixels, 1), dij); + resultGUI.wUnsequenced = ones(dij.totalNumOfBixels, 1); + resultGUI = matRad_siochiLeafSequencing(resultGUI, stf, dij, 5); + [pln, stf] = matRad_aperture2collimation(pln, stf, resultGUI.sequencing, resultGUI.apertureInfo); w = resultGUI.w; pln.propDoseCalc.beamProfile = 'phasespace'; end @@ -57,71 +57,70 @@ pln.propDoseCalc.engine = 'TOPAS'; pln.propDoseCalc.externalCalculation = 'write'; pln.propDoseCalc.numHistoriesDirect = 1e6; - resultGUI = matRad_calcDoseForward(ct,cst,stf,pln, w); - + resultGUI = matRad_calcDoseForward(ct, cst, stf, pln, w); + folderName = [matRad_cfg.primaryUserFolder filesep 'TOPAS' filesep]; - folderName = [folderName stf(1).radiationMode,'_',stf(1).machine,'_',datestr(now, 'dd-mm-yy')]; - %check of outputfolder exists + folderName = [folderName stf(1).radiationMode, '_', stf(1).machine, '_', datestr(now, 'dd-mm-yy')]; + % check of outputfolder exists assertTrue(isfolder(folderName)); - %check if file in folder existi + % check if file in folder existi assertTrue(isfile([folderName filesep 'matRad_cube.dat'])); assertTrue(isfile([folderName filesep 'matRad_cube.txt'])); assertTrue(isfile([folderName filesep 'MCparam.mat'])); - for j = 1:pln.propStf.numOfBeams + for j = 1:numel(stf) assertTrue(isfile([folderName filesep 'beamSetup_matRad_plan_field' num2str(j) '.txt'])); assertTrue(isfile([folderName filesep 'matRad_plan_field' num2str(j) '_run1.txt'])); end - rmdir(folderName,'s'); %clean up + rmdir(folderName, 's'); % clean up end - function test_TopasMCdoseCalcBasicRBE -% test if all the necessary output files are written vor a couple of cases. -% i am not using the default number of histories for testing her, insted 1e6. -% Because the files are just writen and not simulated so we dont care about simulation time. -% To few histories may result in wierd behavior in the topas interface, i.e if a beam -% recieves no histories because there are not enough to be distributed accros the spots, +% test if all the necessary output files are written for a couple of cases. +% i am not using the default number of histories for testing her, instead 1e6. +% Because the files are just written and not simulated so we dont care about simulation time. +% To few histories may result in weird behavior in the topas interface, i.e if a beam +% receives no histories because there are not enough to be distributed across the spots, % it causes an error radModes = DoseEngines.matRad_TopasMCEngine.possibleRadiationModes; matRad_cfg = MatRad_Config.instance(); if moxunit_util_platform_is_octave - confirm_recursive_rmdir(false,'local'); + confirm_recursive_rmdir(false, 'local'); end for i = 1:numel(radModes) switch radModes{i} case 'protons' RBEmodel = {'mcn', 'wed'}; - case {'helium', 'carbon'} - RBEmodel ={'libamtrack','lem'}; + case {'helium', 'carbon'} + RBEmodel = {'libamtrack', 'lem'}; otherwise - continue; + continue end matRad_cfg = MatRad_Config.instance(); load([radModes{i} '_testData.mat']); - + pln.propDoseCalc.engine = 'TOPAS'; pln.propDoseCalc.externalCalculation = 'write'; pln.propDoseCalc.numHistoriesDirect = 1e6; pln.propDoseCalc.scorer.RBE = true; pln.propDoseCalc.scorer.RBE_model = RBEmodel; - pln.bioModel = matRad_bioModel(radModes{i},'none'); - resultGUI = matRad_calcDoseForward(ct,cst,stf,pln, ones(1,sum([stf(:).totalNumOfBixels]))); + pln.bioModel = matRad_bioModel(radModes{i}, 'none'); + resultGUI = matRad_calcDoseForward(ct, cst, stf, pln, ones(1, sum([stf(:).totalNumOfBixels]))); folderName = [matRad_cfg.primaryUserFolder filesep 'TOPAS' filesep]; - folderName = [folderName stf(1).radiationMode,'_',stf(1).machine,'_',datestr(now, 'dd-mm-yy')]; - %check of outputfolder exists + folderName = [folderName stf(1).radiationMode, '_', stf(1).machine, '_', datestr(now, 'dd-mm-yy')]; + % check of outputfolder exists assertTrue(isfolder(folderName)); - %check if file in folder existi + % check if file in folder existi assertTrue(isfile([folderName filesep 'matRad_cube.dat'])); assertTrue(isfile([folderName filesep 'matRad_cube.txt'])); assertTrue(isfile([folderName filesep 'MCparam.mat'])); - for j = 1:pln.propStf.numOfBeams + for j = 1:numel(stf) assertTrue(isfile([folderName filesep 'beamSetup_matRad_plan_field' num2str(j) '.txt'])); assertTrue(isfile([folderName filesep 'matRad_plan_field' num2str(j) '_run1.txt'])); end - rmdir(folderName,'s'); %clean up + rmdir(folderName, 's'); % clean up end function test_TopasMCdoseCalcMultRuns @@ -130,68 +129,67 @@ matRad_cfg = MatRad_Config.instance(); if moxunit_util_platform_is_octave - confirm_recursive_rmdir(false,'local'); + confirm_recursive_rmdir(false, 'local'); end for i = 1:numel(radModes) load([radModes{i} '_testData.mat']); - pln.bioModel = matRad_bioModel(radModes{i},'none'); - w = ones(1,sum([stf(:).totalNumOfBixels])); - - if strcmp(radModes{i},'photons') + pln.bioModel = matRad_bioModel(radModes{i}, 'none'); + w = ones(1, sum([stf(:).totalNumOfBixels])); + + if strcmp(radModes{i}, 'photons') pln.propOpt.runSequencing = 1; pln.propOpt.runDAO = 1; - dij = matRad_calcDoseInfluence(ct,cst,stf,pln); - resultGUI = matRad_calcCubes(ones(dij.totalNumOfBixels,1),dij); - resultGUI.wUnsequenced = ones(dij.totalNumOfBixels,1); - resultGUI = matRad_siochiLeafSequencing(resultGUI,stf,dij,5); - [pln,stf] = matRad_aperture2collimation(pln,stf,resultGUI.sequencing,resultGUI.apertureInfo); + dij = matRad_calcDoseInfluence(ct, cst, stf, pln); + resultGUI = matRad_calcCubes(ones(dij.totalNumOfBixels, 1), dij); + resultGUI.wUnsequenced = ones(dij.totalNumOfBixels, 1); + resultGUI = matRad_siochiLeafSequencing(resultGUI, stf, dij, 5); + [pln, stf] = matRad_aperture2collimation(pln, stf, resultGUI.sequencing, resultGUI.apertureInfo); pln.propDoseCalc.beamProfile = 'phasespace'; w = resultGUI.w; end - + pln.propDoseCalc.engine = 'TOPAS'; pln.propDoseCalc.externalCalculation = 'write'; pln.propDoseCalc.numHistoriesDirect = 1e6; pln.propDoseCalc.numOfRuns = numOfRuns; - resultGUI = matRad_calcDoseForward(ct,cst,stf,pln, w); + resultGUI = matRad_calcDoseForward(ct, cst, stf, pln, w); folderName = [matRad_cfg.primaryUserFolder filesep 'TOPAS' filesep]; - folderName = [folderName stf(1).radiationMode,'_',stf(1).machine,'_',datestr(now, 'dd-mm-yy')]; - %check of outputfolder exists + folderName = [folderName stf(1).radiationMode, '_', stf(1).machine, '_', datestr(now, 'dd-mm-yy')]; + % check of outputfolder exists assertTrue(isfolder(folderName)); - %check if file in folder existi + % check if file in folder existi assertTrue(isfile([folderName filesep 'matRad_cube.dat'])); assertTrue(isfile([folderName filesep 'matRad_cube.txt'])); assertTrue(isfile([folderName filesep 'MCparam.mat'])); - for j = 1:pln.propStf.numOfBeams + for j = 1:numel(stf) assertTrue(isfile([folderName filesep 'beamSetup_matRad_plan_field' num2str(j) '.txt'])); for k = 1:numOfRuns assertTrue(isfile([folderName filesep 'matRad_plan_field' num2str(j) '_run' num2str(k) '.txt'])); end end - rmdir(folderName,'s'); %clean up + rmdir(folderName, 's'); % clean up end - function test_TopasMCdoseCalc4D numOfPhases = 5; radModes = DoseEngines.matRad_TopasMCEngine.possibleRadiationModes; matRad_cfg = MatRad_Config.instance(); if moxunit_util_platform_is_octave - confirm_recursive_rmdir(false,'local'); + confirm_recursive_rmdir(false, 'local'); end % physical Dose for i = 1:numel(radModes) - if ~strcmp(radModes{i},'photons') + if ~strcmp(radModes{i}, 'photons') load([radModes{i} '_testData.mat']); - [ct,cst] = matRad_addMovement(ct, cst,5, numOfPhases,[0 3 0],'dvfType','pull'); - pln.bioModel = matRad_bioModel(radModes{i},'none'); - resultGUI.w = ones(1,sum([stf(:).totalNumOfBixels]))'; + [ct, cst] = matRad_addMovement(ct, cst, 5, numOfPhases, [0 3 0], 'dvfType', 'pull'); + pln.bioModel = matRad_bioModel(radModes{i}, 'none'); + resultGUI.w = ones(1, sum([stf(:).totalNumOfBixels]))'; timeSequence = matRad_makeBixelTimeSeq(stf, resultGUI); timeSequence = matRad_makePhaseMatrix(timeSequence, ct.numOfCtScen, ct.motionPeriod, 'linear'); pln.propDoseCalc.engine = 'TOPAS'; @@ -199,15 +197,15 @@ pln.propDoseCalc.calc4DInterplay = true; pln.propDoseCalc.calcTimeSequence = timeSequence; pln.propDoseCalc.numHistoriesDirect = 1e6; - resultGUI = matRad_calcDoseForward(ct,cst,stf,pln, resultGUI.w); + resultGUI = matRad_calcDoseForward(ct, cst, stf, pln, resultGUI.w); folderName = [matRad_cfg.primaryUserFolder filesep 'TOPAS' filesep]; - folderName = [folderName stf(1).radiationMode,'_',stf(1).machine,'_',datestr(now, 'dd-mm-yy')]; - %check of outputfolder exists + folderName = [folderName stf(1).radiationMode, '_', stf(1).machine, '_', datestr(now, 'dd-mm-yy')]; + % check of outputfolder exists assertTrue(isfolder(folderName)); - %check if file in folder existi + % check if file in folder existi assertTrue(isfile([folderName filesep 'MCparam.mat'])); - for j = 1:pln.propStf.numOfBeams + for j = 1:numel(stf) assertTrue(isfile([folderName filesep 'beamSetup_matRad_plan_field' num2str(j) '.txt'])); assertTrue(isfile([folderName filesep 'matRad_plan_field' num2str(j) '_run1.txt'])); assertTrue(isfile([folderName filesep 'matRad_cube_field' num2str(j) '.txt'])); @@ -215,24 +213,24 @@ assertTrue(isfile([folderName filesep 'matRad_cube' num2str(k) '.dat'])); end end - rmdir(folderName,'s'); %clean up + rmdir(folderName, 's'); % clean up end end -%RBExDose +% RBExDose for i = 1:numel(radModes) switch radModes{i} case 'protons' RBEmodel = {'mcn', 'wed'}; - case {'helium', 'carbon'} - RBEmodel ={'libamtrack','lem'}; + case {'helium', 'carbon'} + RBEmodel = {'libamtrack', 'lem'}; otherwise - continue; + continue end load([radModes{i} '_testData.mat']); - [ct,cst] = matRad_addMovement(ct, cst,5, numOfPhases,[0 3 0],'dvfType','pull'); - pln.bioModel = matRad_bioModel(radModes{i},'none'); - resultGUI.w = ones(1,sum([stf(:).totalNumOfBixels]))'; + [ct, cst] = matRad_addMovement(ct, cst, 5, numOfPhases, [0 3 0], 'dvfType', 'pull'); + pln.bioModel = matRad_bioModel(radModes{i}, 'none'); + resultGUI.w = ones(1, sum([stf(:).totalNumOfBixels]))'; timeSequence = matRad_makeBixelTimeSeq(stf, resultGUI); timeSequence = matRad_makePhaseMatrix(timeSequence, ct.numOfCtScen, ct.motionPeriod, 'linear'); pln.propDoseCalc.engine = 'TOPAS'; @@ -242,15 +240,15 @@ pln.propDoseCalc.numHistoriesDirect = 1e6; pln.propDoseCalc.scorer.RBE = true; pln.propDoseCalc.scorer.RBE_model = RBEmodel; - resultGUI = matRad_calcDoseForward(ct,cst,stf,pln, resultGUI.w); + resultGUI = matRad_calcDoseForward(ct, cst, stf, pln, resultGUI.w); folderName = [matRad_cfg.primaryUserFolder filesep 'TOPAS' filesep]; - folderName = [folderName stf(1).radiationMode,'_',stf(1).machine,'_',datestr(now, 'dd-mm-yy')]; - %check of outputfolder exists + folderName = [folderName stf(1).radiationMode, '_', stf(1).machine, '_', datestr(now, 'dd-mm-yy')]; + % check of outputfolder exists assertTrue(isfolder(folderName)); - %check if file in folder existi + % check if file in folder existi assertTrue(isfile([folderName filesep 'MCparam.mat'])); - for j = 1:pln.propStf.numOfBeams + for j = 1:numel(stf) assertTrue(isfile([folderName filesep 'beamSetup_matRad_plan_field' num2str(j) '.txt'])); assertTrue(isfile([folderName filesep 'matRad_plan_field' num2str(j) '_run1.txt'])); assertTrue(isfile([folderName filesep 'matRad_cube_field' num2str(j) '.txt'])); @@ -258,8 +256,101 @@ assertTrue(isfile([folderName filesep 'matRad_cube' num2str(k) '.dat'])); end end - rmdir(folderName,'s'); %clean up + rmdir(folderName, 's'); % clean up +end + +function test_TopasMCdoseCalc_multiAlphaBeta +radModes = {'protons'}; +matRad_cfg = MatRad_Config.instance(); + +if moxunit_util_platform_is_octave + confirm_recursive_rmdir(false, 'local'); end +for i = 1:numel(radModes) + switch radModes{i} + case 'protons' + RBEmodel = {'mcn', 'wed'}; + case {'helium', 'carbon'} + RBEmodel = {'libamtrack', 'lem'}; + otherwise + continue + end + matRad_cfg = MatRad_Config.instance(); + load([radModes{i} '_testData.mat']); + + % Extract purely random alpha/beta couples + for idx = 1:size(cst, 1) + alphaList(1, idx) = 0.1 + (0.8 - 0.1) .* rand; + betaList(1, idx) = 0.001 + (0.1 - 0.001) .* rand; + end + % alpha1 = 0.1 + (0.8-0.1).*rand; + % alpha2 = 0.1 + (0.8-0.1).*rand; + % beta1 = 0.001 + (0.1-0.001).*rand; + % beta2 = 0.001 + (0.1-0.001).*rand; + rbeIdx = 1 + (length(RBEmodel) - 1) * round(rand); + + for idx = 1:size(cst, 1) + cst{idx, 5}.alphaX = alphaList(idx); + cst{idx, 5}.betaX = betaList(idx); + % cst{1,5}.alphaX = alpha1; + % cst{1,5}.betaX = beta1; + % cst{2,5}.alphaX = alpha2; + % cst{2,5}.betaX = beta2; + end + + pln.propDoseCalc.bioParameters.AlphaX = alphaList; % [alpha1, alpha2]; + pln.propDoseCalc.bioParameters.BetaX = betaList; % [beta1, beta2]; + + pln.propDoseCalc.engine = 'TOPAS'; + pln.propDoseCalc.externalCalculation = 'write'; + pln.propDoseCalc.numHistoriesDirect = 1e6; + pln.propDoseCalc.numOfRuns = 1; + pln.propDoseCalc.scorer.RBE = true; + pln.propDoseCalc.scorer.RBE_model = {RBEmodel{rbeIdx}}; + % pln.bioModel = matRad_bioModel(radModes{i},'none'); + pln.bioModel = upper(RBEmodel{rbeIdx}); + pln.propDoseCalc.scorer.reportQuantity = {'Sum'}; + resultGUI = matRad_calcDoseForward(ct, cst, stf, pln, ones(1, sum([stf(:).totalNumOfBixels]))); + % matRad_calcDoseForward(ct,cst,stf,pln,resultGUI.w); + + folderName = [matRad_cfg.primaryUserFolder filesep 'TOPAS' filesep]; + folderName = [folderName stf(1).radiationMode, '_', stf(1).machine, '_', datestr(now, 'dd-mm-yy')]; + % check of outputfolder exists + assertTrue(isfolder(folderName)); + % check if file in folder existi + assertTrue(isfile([folderName filesep 'matRad_cube.dat'])); + assertTrue(isfile([folderName filesep 'matRad_cube.txt'])); + assertTrue(isfile([folderName filesep 'MCparam.mat'])); + for j = 1:pln.propStf.numOfBeams + assertTrue(isfile([folderName filesep 'beamSetup_matRad_plan_field' num2str(j) '.txt'])); + assertTrue(isfile([folderName filesep 'matRad_plan_field' num2str(j) '_run1.txt'])); + + for runIdx = 1:pln.propDoseCalc.numOfRuns + pathFile = [folderName filesep 'matRad_plan_field' num2str(j) '_run' num2str(runIdx) '.txt']; + fileText = fileread(pathFile); + matches = unique(regexp(fileText, 'CellType_\d+/', 'match')); + assertTrue(length(matches) == length(alphaList) && length(matches) == length(betaList)); + lines = strsplit(fileText, '\n'); + idx = strncmp(lines, 'd:Sc/AlphaX', length('d:Sc/AlphaX')); + matchingLines = lines(idx); + for cellIdx = 1:length(matchingLines) + tmp = strsplit(matchingLines{cellIdx}, '_'); + tmp = strsplit(tmp{end}, ' '); + assertTrue(abs(alphaList(str2double(tmp{1})) - str2double(tmp{3})) <= 1e-4); + end + + lines = strsplit(fileText, '\n'); + idx = strncmp(lines, 'd:Sc/BetaX', length('d:Sc/BetaX')); + matchingLines = lines(idx); + for cellIdx = 1:length(matchingLines) + tmp = strsplit(matchingLines{cellIdx}, '_'); + tmp = strsplit(tmp{end}, ' '); + assertTrue(abs(betaList(str2double(tmp{1})) - str2double(tmp{3})) < 1e-4); + end + end + + end +end diff --git a/test/doseCalc/test_sigmaRashi.m b/test/doseCalc/test_sigmaRashi.m new file mode 100644 index 000000000..4b00db312 --- /dev/null +++ b/test/doseCalc/test_sigmaRashi.m @@ -0,0 +1,19 @@ +function test_suite= test_sigmaRashi + +test_functions=localfunctions(); + +initTestSuite; + +function test_calcSigmaRashi + + baseDataEntry.range = 100; + + rangeShifter.ID = 1; + rangeShifter.eqThickness = 1; + rangeShifter.sourceRashiDistance = 9000; + + SSD = 10000; + + sigma = matRad_calcSigmaRashi(baseDataEntry, rangeShifter, SSD); + + assertElementsAlmostEqual(sigma,1.1,'relative',1e-2,1e-2); diff --git a/test/helper_createTestCt.m b/test/helper_createTestCt.m new file mode 100644 index 000000000..4dd14dae0 --- /dev/null +++ b/test/helper_createTestCt.m @@ -0,0 +1,64 @@ +function [ct] = helper_createTestCt(cubeDim, resolution, varargin) +% helper_createTestCt Creates a minimal matRad ct struct for testing. +% +% call +% [ct] = helper_createTestCt() +% [ct] = helper_createTestCt(cubeDim) +% [ct] = helper_createTestCt(cubeDim, resolution) +% +% inputs +% cubeDim [nRows nCols nSlices], default [10 12 8] +% (unequal dimensions help catch x/y coordinate-swap bugs) +% resolution scalar (isotropic mm), 1x3 vector [x y z mm], or struct +% with fields .x .y .z; default struct(x=1, y=2, z=3) +% (unequal resolutions help catch axis-scaling bugs) +% Options (name-value pairs) +% 'createCoordinateArrays' (logical) if true, adds ct.x ct.y ct +% ct.z arrays in world mm coordinates; default false +% 'datatype' (string) numeric class for ct.cubeHU; default 'double +% 'HUvalue' (numeric scalar) value to fill ct.cubeHU with; default 0 +% +% outputs +% ct matRad ct struct with cubeDim and resolution populated + +p = inputParser; + +p.addOptional('cubeDim', [10 12 8], @(x) isnumeric(x) && isvector(x) && numel(x) == 3 && all(x > 0) && all(mod(x, 1) == 0)); +p.addOptional('resolution', [1 2 3], @(x) (isstruct(x) && all(isfield(x, {'x', 'y', 'z'}))) || ... + (isnumeric(x) && (isscalar(x) || (isvector(x) && numel(x) == 3)))); +p.addParameter('createCoordinateArrays', false, @(x) islogical(x) && isscalar(x)); +p.addParameter('datatype', 'double', @(x) ischar(x) || (isstring(x) && isscalar(x))); +p.addParameter('HUvalue', 0, @(x) isnumeric(x) && isscalar(x)); + +args = varargin; +if nargin >= 2 + args = [{resolution}, args]; +end + +if nargin >= 1 + args = [{cubeDim}, args]; +end + +p.parse(args{:}); + +ct.cubeDim = p.Results.cubeDim; + +if isstruct(p.Results.resolution) + ct.resolution = p.Results.resolution; +elseif isscalar(p.Results.resolution) + ct.resolution.x = p.Results.resolution; + ct.resolution.y = p.Results.resolution; + ct.resolution.z = p.Results.resolution; +else + ct.resolution.x = p.Results.resolution(1); + ct.resolution.y = p.Results.resolution(2); + ct.resolution.z = p.Results.resolution(3); +end + +if p.Results.createCoordinateArrays + ct = matRad_getWorldAxes(ct); +end + +ct.cubeHU{1} = p.Results.HUvalue * ones(ct.cubeDim, p.Results.datatype); + +end diff --git a/test/matRad_unitTestTextManipulation.m b/test/matRad_unitTestTextManipulation.m index 444943b30..46c464321 100644 --- a/test/matRad_unitTestTextManipulation.m +++ b/test/matRad_unitTestTextManipulation.m @@ -23,7 +23,7 @@ function matRad_unitTestTextManipulation(filename, string1, string2, path) % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2018 the matRad development team. +% Copyright 2018-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -70,4 +70,4 @@ function matRad_unitTestTextManipulation(filename, string1, string2, path) movefile(tempFile, currFile, 'f'); end -end \ No newline at end of file +end diff --git a/test/optimization/test_collapseDij.m b/test/optimization/test_collapseDij.m new file mode 100644 index 000000000..07d4b175e --- /dev/null +++ b/test/optimization/test_collapseDij.m @@ -0,0 +1,193 @@ +function test_suite = test_collapseDij + + test_functions=localfunctions(); + + initTestSuite; + +function test_collapse_dij_numeric_photon + + testData = load('photons_testData.mat'); + % Beam Wise Test + dijNew = matRad_collapseDij(testData.dij); + assertEqual(dijNew.totalNumOfBixels, testData.dij.numOfBeams); + assertEqual(dijNew.totalNumOfRays, testData.dij.numOfBeams); + assertEqual(dijNew.numOfBeams, testData.dij.numOfBeams); + assertEqual(dijNew.numOfRaysPerBeam, ones(testData.dij.numOfBeams,1)); + assertEqual(dijNew.beamNum, transpose(1:testData.dij.numOfBeams)); + assertEqual(dijNew.bixelNum, ones(testData.dij.numOfBeams, 1)); + assertEqual(dijNew.rayNum, ones(testData.dij.numOfBeams, 1)); + assertEqual(dijNew.doseGrid, testData.dij.doseGrid); + assertEqual(dijNew.ctGrid, testData.dij.ctGrid); + + assertEqual(size(dijNew.physicalDose), size(testData.dij.physicalDose)); + assertEqual(cellfun(@isempty, dijNew.physicalDose), cellfun(@isempty, testData.dij.physicalDose)); + + for i = 1:numel(dijNew.physicalDose) + if ~isempty(dijNew.physicalDose{i}) + assertEqual(size(dijNew.physicalDose{i},1), size(testData.dij.physicalDose{i},1)); + assertEqual(size(dijNew.physicalDose{i},2), testData.dij.numOfBeams); + assertElementsAlmostEqual(sum(dijNew.physicalDose{i}(:)), sum(testData.dij.physicalDose{i}(:)), 'relative', 1e-5, 1e-5); + end + end + +function test_collapse_dij_numeric_photon_ray + + testData = load('photons_testData.mat'); + % RayWise Test , raywise for photons should not do anything + dijNew = matRad_collapseDij(testData.dij,'ray'); + assertEqual(dijNew.totalNumOfBixels, testData.dij.totalNumOfBixels); + assertEqual(dijNew.totalNumOfRays, testData.dij.totalNumOfRays); + assertEqual(dijNew.numOfBeams, testData.dij.numOfBeams); + assertEqual(dijNew.numOfRaysPerBeam, testData.dij.numOfRaysPerBeam); + assertEqual(dijNew.beamNum, testData.dij.beamNum); + assertEqual(dijNew.bixelNum, testData.dij.bixelNum); + assertEqual(dijNew.rayNum, testData.dij.rayNum); + assertEqual(dijNew.doseGrid, testData.dij.doseGrid); + assertEqual(dijNew.ctGrid, testData.dij.ctGrid); + + assertEqual(size(dijNew.physicalDose), size(testData.dij.physicalDose)); + assertEqual(cellfun(@isempty, dijNew.physicalDose), cellfun(@isempty, testData.dij.physicalDose)); + + for i = 1:numel(dijNew.physicalDose) + if ~isempty(dijNew.physicalDose{i}) + assertEqual(size(dijNew.physicalDose{i},1), size(testData.dij.physicalDose{i},1)); + assertEqual(size(dijNew.physicalDose{i},2), testData.dij.totalNumOfRays); + assertElementsAlmostEqual(sum(dijNew.physicalDose{i}(:)), sum(testData.dij.physicalDose{i}(:)), 'relative', 1e-5, 1e-5); + end + end + +function test_collapse_dij_numeric_proton + + testData = load('protons_testData.mat'); + % Beam Wise Test + dijNew = matRad_collapseDij(testData.dij); + assertEqual(dijNew.totalNumOfBixels, testData.dij.numOfBeams); + assertEqual(dijNew.totalNumOfRays, testData.dij.numOfBeams); + assertEqual(dijNew.numOfBeams, testData.dij.numOfBeams); + assertEqual(dijNew.numOfRaysPerBeam, ones(testData.dij.numOfBeams,1)); + assertEqual(dijNew.beamNum, transpose(1:testData.dij.numOfBeams)); + assertEqual(dijNew.bixelNum, ones(testData.dij.numOfBeams, 1)); + assertEqual(dijNew.rayNum, ones(testData.dij.numOfBeams, 1)); + assertEqual(numel(dijNew.numParticlesPerMU),testData.dij.numOfBeams); + assertEqual(sum(dijNew.numParticlesPerMU),sum(testData.dij.numParticlesPerMU)); + assertEqual(dijNew.doseGrid, testData.dij.doseGrid); + assertEqual(dijNew.ctGrid, testData.dij.ctGrid); + + assertEqual(size(dijNew.physicalDose), size(testData.dij.physicalDose)); + assertEqual(cellfun(@isempty, dijNew.physicalDose), cellfun(@isempty, testData.dij.physicalDose)); + + quantities = {'physicalDose','mLETDose'}; + + for q = 1:numel(quantities) + for i = 1:numel(dijNew.(quantities{q})) + if ~isempty(dijNew.(quantities{q})) + assertEqual(size(dijNew.(quantities{q}){i},1), size(testData.dij.(quantities{q}){i},1)); + assertEqual(size(dijNew.(quantities{q}){i},2), testData.dij.numOfBeams); + assertElementsAlmostEqual(sum(dijNew.(quantities{q}){i}(:)), sum(testData.dij.(quantities{q}){i}(:)), 'relative', 1e-5, 1e-5); + end + end + end + % wrong mode test + assertExceptionThrown(@() matRad_collapseDij(testData.dij,2)); + assertExceptionThrown(@() matRad_collapseDij(testData.dij,'rab')); + +function test_collapse_dij_numeric_proton_ray + testData = load('protons_testData.mat'); + % RayWiseTest + dijNew = matRad_collapseDij(testData.dij,'ray'); + assertEqual(dijNew.totalNumOfBixels, testData.dij.totalNumOfRays); + assertEqual(dijNew.totalNumOfRays, testData.dij.totalNumOfRays); + assertEqual(dijNew.numOfBeams, testData.dij.numOfBeams); + assertEqual(dijNew.numOfRaysPerBeam, testData.dij.numOfRaysPerBeam); + assertEqual(dijNew.beamNum, reshape(cell2mat(arrayfun(@(x) repelem(x,dijNew.numOfRaysPerBeam(x)),1:dijNew.numOfBeams,'UniformOutput',false)),[],1)); + assertEqual(dijNew.bixelNum, ones(testData.dij.totalNumOfRays, 1)); + assertEqual(dijNew.rayNum, reshape(cell2mat(arrayfun(@(x) 1:x,dijNew.numOfRaysPerBeam,'UniformOutput',false)),[],1)); + assertEqual(numel(dijNew.numParticlesPerMU),testData.dij.totalNumOfRays); + assertEqual(sum(dijNew.numParticlesPerMU),sum(testData.dij.numParticlesPerMU)); + assertEqual(dijNew.doseGrid, testData.dij.doseGrid) + assertEqual(dijNew.ctGrid, testData.dij.ctGrid); + + assertEqual(size(dijNew.physicalDose), size(testData.dij.physicalDose)); + assertEqual(cellfun(@isempty, dijNew.physicalDose), cellfun(@isempty, testData.dij.physicalDose)); + + quantities = {'physicalDose','mLETDose'}; + + for q = 1:numel(quantities) + for i = 1:numel(dijNew.(quantities{q})) + if ~isempty(dijNew.(quantities{q})) + assertEqual(size(dijNew.(quantities{q}){i},1), size(testData.dij.(quantities{q}){i},1)); + assertEqual(size(dijNew.(quantities{q}){i},2), testData.dij.totalNumOfRays); + assertElementsAlmostEqual(sum(dijNew.(quantities{q}){i}(:)), sum(testData.dij.(quantities{q}){i}(:)), 'relative', 1e-5, 1e-5); + end + end + end + % wrong mode test + assertExceptionThrown(@() matRad_collapseDij(testData.dij,2)); + assertExceptionThrown(@() matRad_collapseDij(testData.dij,'rab')); + +function test_collapse_dij_numeric_carbon + % matrix + testData = load('carbon_testData.mat'); + % Beamwise Test + dijNew = matRad_collapseDij(testData.dij); + assertEqual(dijNew.totalNumOfBixels, testData.dij.numOfBeams); + assertEqual(dijNew.totalNumOfRays, testData.dij.numOfBeams); + assertEqual(dijNew.numOfBeams, testData.dij.numOfBeams); + assertEqual(dijNew.numOfRaysPerBeam, ones(testData.dij.numOfBeams,1)); + assertEqual(dijNew.beamNum, transpose(1:testData.dij.numOfBeams)); + assertEqual(dijNew.bixelNum, ones(testData.dij.numOfBeams, 1)); + assertEqual(dijNew.rayNum, ones(testData.dij.numOfBeams, 1)); + assertEqual(numel(dijNew.numParticlesPerMU),testData.dij.numOfBeams); + assertEqual(sum(dijNew.numParticlesPerMU),sum(testData.dij.numParticlesPerMU)); + assertEqual(dijNew.doseGrid, testData.dij.doseGrid); + assertEqual(dijNew.ctGrid, testData.dij.ctGrid); + + assertEqual(size(dijNew.physicalDose), size(testData.dij.physicalDose)); + assertEqual(cellfun(@isempty, dijNew.physicalDose), cellfun(@isempty, testData.dij.physicalDose)); + + quantities = {'physicalDose','mLETDose','mAlphaDose','mSqrtBetaDose'}; + + for q = 1:numel(quantities) + for i = 1:numel(dijNew.(quantities{q})) + if ~isempty(dijNew.(quantities{q})) + assertEqual(size(dijNew.(quantities{q}){i},1), size(testData.dij.(quantities{q}){i},1)); + assertEqual(size(dijNew.(quantities{q}){i},2), testData.dij.numOfBeams); + assertElementsAlmostEqual(sum(dijNew.(quantities{q}){i}(:)), sum(testData.dij.(quantities{q}){i}(:)), 'relative', 1e-5, 1e-5); + end + end + end + +function test_collapse_dij_numeric_carbon_ray + % matrix + testData = load('carbon_testData.mat'); + %Ray wise test + dijNew = matRad_collapseDij(testData.dij,'ray'); + assertEqual(dijNew.totalNumOfBixels, testData.dij.totalNumOfRays); + assertEqual(dijNew.totalNumOfRays, testData.dij.totalNumOfRays); + assertEqual(dijNew.numOfBeams, testData.dij.numOfBeams); + assertEqual(dijNew.numOfRaysPerBeam, testData.dij.numOfRaysPerBeam); + assertEqual(dijNew.beamNum, reshape(cell2mat(arrayfun(@(x) repelem(x,dijNew.numOfRaysPerBeam(x)),1:dijNew.numOfBeams,'UniformOutput',false)),[],1)); + assertEqual(dijNew.bixelNum, ones(testData.dij.totalNumOfRays, 1)); + assertEqual(dijNew.rayNum, reshape(cell2mat(arrayfun(@(x) 1:x,dijNew.numOfRaysPerBeam,'UniformOutput',false)),[],1)); + assertEqual(numel(dijNew.numParticlesPerMU),testData.dij.totalNumOfRays); + assertEqual(sum(dijNew.numParticlesPerMU),sum(testData.dij.numParticlesPerMU)); + assertEqual(dijNew.doseGrid, testData.dij.doseGrid) + assertEqual(dijNew.ctGrid, testData.dij.ctGrid); + + assertEqual(size(dijNew.physicalDose), size(testData.dij.physicalDose)); + assertEqual(cellfun(@isempty, dijNew.physicalDose), cellfun(@isempty, testData.dij.physicalDose)); + + quantities = {'physicalDose','mLETDose','mAlphaDose','mSqrtBetaDose'}; + + for q = 1:numel(quantities) + for i = 1:numel(dijNew.(quantities{q})) + if ~isempty(dijNew.(quantities{q})) + assertEqual(size(dijNew.(quantities{q}){i},1), size(testData.dij.(quantities{q}){i},1)); + assertEqual(size(dijNew.(quantities{q}){i},2), testData.dij.totalNumOfRays); + assertElementsAlmostEqual(sum(dijNew.(quantities{q}){i}(:)), sum(testData.dij.(quantities{q}){i}(:)), 'relative', 1e-5, 1e-5); + end + end + end + % wrong mode test + assertExceptionThrown(@() matRad_collapseDij(testData.dij,2)); + assertExceptionThrown(@() matRad_collapseDij(testData.dij,'rab')); \ No newline at end of file diff --git a/test/optimizers/test_optimizerFmincon.m b/test/optimizers/test_optimizerFmincon.m index 3e7c70aa3..6a617f762 100644 --- a/test/optimizers/test_optimizerFmincon.m +++ b/test/optimizers/test_optimizerFmincon.m @@ -1,52 +1,52 @@ -function test_suite = test_optimizerFmincon - -test_functions=localfunctions(); +function test_suite = test_optimizerFmincon + +test_functions = localfunctions(); initTestSuite; function test_optimizer_fmincon_construct - if moxunit_util_platform_is_octave - moxunit_throw_test_skipped_exception('fmincon not available for Octave!'); - end - - if ~license('test', 'optimization_toolbox') || license('checkout', 'optimization_toolbox') == 0 - moxunit_throw_test_skipped_exception('Optimization Toolbox containing fmincon not available!'); - end - - opti = matRad_OptimizerFmincon(); - assertTrue(isobject(opti)); - assertTrue(isa(opti, 'matRad_OptimizerFmincon')); - +if moxunit_util_platform_is_octave + moxunit_throw_test_skipped_exception('fmincon not available for Octave!'); +end + +if ~license('test', 'optimization_toolbox') || license('checkout', 'optimization_toolbox') == 0 + moxunit_throw_test_skipped_exception('Optimization Toolbox containing fmincon not available!'); +end + +opti = matRad_OptimizerFmincon(); +assertTrue(isobject(opti)); +assertTrue(isa(opti, 'matRad_OptimizerFmincon')); + function test_optimizer_fmincon_available - if moxunit_util_platform_is_octave - moxunit_throw_test_skipped_exception('fmincon not available for Octave!'); - end +if moxunit_util_platform_is_octave + moxunit_throw_test_skipped_exception('fmincon not available for Octave!'); +end + +if ~license('test', 'optimization_toolbox') || license('checkout', 'optimization_toolbox') == 0 + moxunit_throw_test_skipped_exception('Optimization Toolbox containing fmincon not available!'); +end - if ~license('test', 'optimization_toolbox') || license('checkout', 'optimization_toolbox') == 0 - moxunit_throw_test_skipped_exception('Optimization Toolbox containing fmincon not available!'); - end +opti = matRad_OptimizerFmincon(); +assertTrue(opti.isAvailable()); +assertTrue(matRad_OptimizerFmincon.isAvailable()); % Check static - opti = matRad_OptimizerFmincon(); - assertTrue(opti.IsAvailable()); - assertTrue(matRad_OptimizerFmincon.IsAvailable()); %Check static - function test_optimizer_fmincon_getStatus - - if moxunit_util_platform_is_octave - moxunit_throw_test_skipped_exception('fmincon not available for Octave!'); - end - - if ~license('test', 'optimization_toolbox') || license('checkout', 'optimization_toolbox') == 0 - moxunit_throw_test_skipped_exception('Optimization Toolbox containing fmincon not available!'); - end - - opti = matRad_OptimizerFmincon(); - [statusmsg, statusflag] = opti.GetStatus(); - assertEqual(statusmsg, 'No Last Optimizer Status Available!'); - assertEqual(statusflag, -1); - - % TODO: test other status - -% TODO: test optimize function \ No newline at end of file + +if moxunit_util_platform_is_octave + moxunit_throw_test_skipped_exception('fmincon not available for Octave!'); +end + +if ~license('test', 'optimization_toolbox') || license('checkout', 'optimization_toolbox') == 0 + moxunit_throw_test_skipped_exception('Optimization Toolbox containing fmincon not available!'); +end + +opti = matRad_OptimizerFmincon(); +[statusmsg, statusflag] = opti.getStatus(); +assertEqual(statusmsg, 'No Last Optimizer Status Available!'); +assertEqual(statusflag, -1); + +% TODO: test other status + +% TODO: test optimize function diff --git a/test/optimizers/test_optimizerIPOPT.m b/test/optimizers/test_optimizerIPOPT.m index a956d526d..c7ececa8d 100644 --- a/test/optimizers/test_optimizerIPOPT.m +++ b/test/optimizers/test_optimizerIPOPT.m @@ -1,28 +1,28 @@ -function test_suite = test_optimizerIPOPT +function test_suite = test_optimizerIPOPT -test_functions=localfunctions(); +test_functions = localfunctions(); initTestSuite; function test_optimizer_ipopt_construct - opti = matRad_OptimizerIPOPT(); - assertTrue(isobject(opti)); - assertTrue(isa(opti, 'matRad_OptimizerIPOPT')); - +opti = matRad_OptimizerIPOPT(); +assertTrue(isobject(opti)); +assertTrue(isa(opti, 'matRad_OptimizerIPOPT')); + function test_optimizer_ipopt_available - opti = matRad_OptimizerIPOPT(); - assertTrue(opti.IsAvailable()); - assertTrue(matRad_OptimizerIPOPT.IsAvailable()); %Check static - +opti = matRad_OptimizerIPOPT(); +assertTrue(opti.isAvailable()); +assertTrue(matRad_OptimizerIPOPT.isAvailable()); % Check static + function test_optimizer_ipopt_getStatus - opti = matRad_OptimizerIPOPT(); - [statusmsg, statusflag] = opti.GetStatus(); - assertEqual(statusmsg, 'No Last IPOPT Status Available!'); - assertEqual(statusflag, -1); - - % TODO: test other status +opti = matRad_OptimizerIPOPT(); +[statusmsg, statusflag] = opti.getStatus(); +assertEqual(statusmsg, 'No Last IPOPT Status Available!'); +assertEqual(statusflag, -1); + +% TODO: test other status -% TODO: test optimize function \ No newline at end of file +% TODO: test optimize function diff --git a/test/phantoms/test_VOIBox.m b/test/phantoms/test_VOIBox.m new file mode 100644 index 000000000..e63450854 --- /dev/null +++ b/test/phantoms/test_VOIBox.m @@ -0,0 +1,209 @@ +function test_suite = test_VOIBox +% The output should always be test_suite, and the function name the same as +% your file name + +test_functions = localfunctions(); +initTestSuite; + +%% Constructor Tests + +function test_constructorDefaults +box = matRad_PhantomVOIBox('MyBox', 'OAR', [4 6 8]); +assertEqual(box.name, 'MyBox'); +assertEqual(box.type, 'OAR'); +assertEqual(box.boxDimensions, [4 6 8]); +assertEqual(box.coordType, 'voxel'); +assertEqual(box.offset, [0 0 0]); +assertEqual(box.HU, 0); + +function test_constructorCustomParams +box = matRad_PhantomVOIBox('MyBox', 'OAR', [4 6 8], ... + 'coordType', 'mm', 'offset', [1 2 3], 'HU', 200); +assertEqual(box.coordType, 'mm'); +assertEqual(box.offset, [1 2 3]); +assertEqual(box.HU, 200); + +function test_constructorInvalidInputs +% Non-positive dimensions +assertExceptionThrown(@() matRad_PhantomVOIBox('MyBox', 'OAR', [-1 2 3])); +assertExceptionThrown(@() matRad_PhantomVOIBox('MyBox', 'OAR', [0 2 3])); +% Wrong number of elements +assertExceptionThrown(@() matRad_PhantomVOIBox('MyBox', 'OAR', [4 6])); +assertExceptionThrown(@() matRad_PhantomVOIBox('MyBox', 'OAR', [4 6 8 10])); +% Non-numeric +assertExceptionThrown(@() matRad_PhantomVOIBox('MyBox', 'OAR', 'big')); +% Invalid coordType +assertExceptionThrown(@() matRad_PhantomVOIBox('MyBox', 'OAR', [4 6 8], 'coordType', 'invalid')); + +%% set-method Validation + +function test_setMethodsValidation +box = matRad_PhantomVOIBox('MyBox', 'OAR', [4 6 8]); +% coordType: valid update and rejection +box.coordType = 'mm'; +assertEqual(box.coordType, 'mm'); +assertExceptionThrown(@() helper_assignmentTest(box, 'coordType', 'cube')); +% boxDimensions: rejection of bad values +assertExceptionThrown(@() helper_assignmentTest(box, 'boxDimensions', [-1 2 3])); +assertExceptionThrown(@() helper_assignmentTest(box, 'boxDimensions', [0 2 3])); +assertExceptionThrown(@() helper_assignmentTest(box, 'boxDimensions', [4 6])); + +%% initializeParameters - CST structure + +function test_initializeParametersCstStructure +ct = helper_createTestCt(); +cst = {}; +box = matRad_PhantomVOIBox('TestBox', 'OAR', [4 6 8]); +cst = box.initializeParameters(ct, cst); +assertEqual(size(cst, 1), 1); +assertEqual(cst{1, 2}, 'TestBox'); +assertEqual(cst{1, 3}, 'OAR'); + +%% initializeParameters - voxel mode + +function test_initializeParametersVoxelCoversAll +% A box larger than the CT must be clipped to all voxels. +ct = helper_createTestCt(); +cst = {}; +box = matRad_PhantomVOIBox('TestBox', 'OAR', [1000 1000 1000]); +cst = box.initializeParameters(ct, cst); +assertEqual(numel(cst{1, 4}{1}), prod(ct.cubeDim)); + +function test_initializeParametersVoxelAllIndicesValid +ct = helper_createTestCt(); +cst = {}; +box = matRad_PhantomVOIBox('TestBox', 'OAR', [4 6 2]); +cst = box.initializeParameters(ct, cst); +idx = cst{1, 4}{1}; +assertTrue(numel(idx) > 0); +assertTrue(all(idx >= 1) && all(idx <= prod(ct.cubeDim))); + +%% initializeParameters - voxel mode axis-permutation tests +% +% Strategy: odd non-square cubeDim=[9 11 7] puts the geometric centre +% exactly at (row=5, col=6, slice=4). boxDimensions is in [i j k] order: +% dims(1) = x / col extent, dims(2) = y / row extent, dims(3) = z extent. +% +% dims=[3 1 1]: 3 wide in x (cols 5-7), 1 in y (row 5), 1 in z (slice 4). +% dims=[1 3 1]: 1 in x (col 6), 3 in y (rows 4-6), 1 in z (slice 4). +% The two resulting voxel sets are completely disjoint, so any x/y swap fails. + +function test_initializeParametersVoxelAxisPermutation +cubeDim = [9 11 7]; +ct = helper_createTestCt(cubeDim, 1); +cRow = 5; +cCol = 6; +cSlice = 4; % (cubeDim+1)/2 + +% --- 1x1x1 box: exactly the centre voxel ---------------------------- +cst = {}; +box = matRad_PhantomVOIBox('B', 'OAR', [1 1 1]); +cst = box.initializeParameters(ct, cst); +assertEqual(cst{1, 4}{1}, sub2ind(cubeDim, cRow, cCol, cSlice)); + +% --- [3 1 1]: 3 voxels along x / col -------------------------------- +% Expected: row=5, cols 5-7, slice=4 +cst = {}; +box = matRad_PhantomVOIBox('B', 'OAR', [3 1 1]); +cst = box.initializeParameters(ct, cst); +expected = [sub2ind(cubeDim, cRow, cCol - 1, cSlice), ... + sub2ind(cubeDim, cRow, cCol, cSlice), ... + sub2ind(cubeDim, cRow, cCol + 1, cSlice)]; +assertEqual(cst{1, 4}{1}, sort(expected)'); + +% --- [1 3 1]: 3 voxels along y / row -------------------------------- +% Expected: rows 4-6, col=6, slice=4 +cst = {}; +box = matRad_PhantomVOIBox('B', 'OAR', [1 3 1]); +cst = box.initializeParameters(ct, cst); +expected = [sub2ind(cubeDim, cRow - 1, cCol, cSlice), ... + sub2ind(cubeDim, cRow, cCol, cSlice), ... + sub2ind(cubeDim, cRow + 1, cCol, cSlice)]; +assertEqual(cst{1, 4}{1}, sort(expected)'); + +function test_initializeParametersVoxelOffsetAxis +% offset=[1 0 0] is in [i j k]: x/col direction -> col advances by 1. +% offset=[0 1 0] is y/row direction -> row advances by 1. +% A 1x1x1 box isolates a single voxel so the shift is unambiguous. +cubeDim = [9 11 7]; +ct = helper_createTestCt(cubeDim, 1); +cRow = 5; +cCol = 6; +cSlice = 4; + +% Offset in x (col) +cst = {}; +box = matRad_PhantomVOIBox('B', 'OAR', [1 1 1], 'offset', [1 0 0]); +cst = box.initializeParameters(ct, cst); +assertEqual(cst{1, 4}{1}, sub2ind(cubeDim, cRow, cCol + 1, cSlice)); + +% Offset in y (row) +cst = {}; +box = matRad_PhantomVOIBox('B', 'OAR', [1 1 1], 'offset', [0 1 0]); +cst = box.initializeParameters(ct, cst); +assertEqual(cst{1, 4}{1}, sub2ind(cubeDim, cRow + 1, cCol, cSlice)); + +%% initializeParameters - mm mode axis-permutation tests +% +% Same cubeDim=[9 11 7] with anisotropic resolution res={x=2,y=3,z=5}. +% The geometric centre maps to world coords (ct.x(6), ct.y(5), ct.z(4)). +% +% dims=[4 0.1 0.1]: extends +-2 mm in x (one full voxel each side) -> cols 5-7. +% dims=[0.1 6 0.1]: extends +-3 mm in y (one full voxel each side) -> rows 4-6. +% Any axis swap produces the opposite column/row expansion and fails. + +function test_initializeParametersMmAxisPermutation +cubeDim = [9 11 7]; +res = struct('x', 2, 'y', 3, 'z', 5); +ct = helper_createTestCt(cubeDim, res); +cRow = 5; +cCol = 6; +cSlice = 4; + +% Tiny box: only the centre voxel +cst = {}; +box = matRad_PhantomVOIBox('B', 'OAR', [0.5 0.5 0.5], 'coordType', 'mm'); +cst = box.initializeParameters(ct, cst); +assertEqual(cst{1, 4}{1}, sub2ind(cubeDim, cRow, cCol, cSlice)); + +% dims=[2*res.x, narrow, narrow]: 3 columns, 1 row, 1 slice +cst = {}; +box = matRad_PhantomVOIBox('B', 'OAR', [2 * res.x 0.1 0.1], 'coordType', 'mm'); +cst = box.initializeParameters(ct, cst); +expected = [sub2ind(cubeDim, cRow, cCol - 1, cSlice), ... + sub2ind(cubeDim, cRow, cCol, cSlice), ... + sub2ind(cubeDim, cRow, cCol + 1, cSlice)]; +assertEqual(cst{1, 4}{1}, sort(expected)'); + +% dims=[narrow, 2*res.y, narrow]: 1 column, 3 rows, 1 slice +cst = {}; +box = matRad_PhantomVOIBox('B', 'OAR', [0.1 2 * res.y 0.1], 'coordType', 'mm'); +cst = box.initializeParameters(ct, cst); +expected = [sub2ind(cubeDim, cRow - 1, cCol, cSlice), ... + sub2ind(cubeDim, cRow, cCol, cSlice), ... + sub2ind(cubeDim, cRow + 1, cCol, cSlice)]; +assertEqual(cst{1, 4}{1}, sort(expected)'); + +function test_initializeParametersMmOffsetAxis +% A step of res.x in the [i j k] x-direction must advance the column; +% a step of res.y in the y-direction must advance the row. +cubeDim = [9 11 7]; +res = struct('x', 2, 'y', 3, 'z', 5); +ct = helper_createTestCt(cubeDim, res); +cRow = 5; +cCol = 6; +cSlice = 4; + +% Offset in x (col) +cst = {}; +box = matRad_PhantomVOIBox('B', 'OAR', [0.5 0.5 0.5], 'coordType', 'mm', ... + 'offset', [res.x 0 0]); +cst = box.initializeParameters(ct, cst); +assertEqual(cst{1, 4}{1}, sub2ind(cubeDim, cRow, cCol + 1, cSlice)); + +% Offset in y (row) +cst = {}; +box = matRad_PhantomVOIBox('B', 'OAR', [0.5 0.5 0.5], 'coordType', 'mm', ... + 'offset', [0 res.y 0]); +cst = box.initializeParameters(ct, cst); +assertEqual(cst{1, 4}{1}, sub2ind(cubeDim, cRow + 1, cCol, cSlice)); diff --git a/test/phantoms/test_VOISphere.m b/test/phantoms/test_VOISphere.m new file mode 100644 index 000000000..1af3cb242 --- /dev/null +++ b/test/phantoms/test_VOISphere.m @@ -0,0 +1,186 @@ +function test_suite = test_VOISphere +% The output should always be test_suite, and the function name the same as +% your file name + +test_functions = localfunctions(); +initTestSuite; + +%% Constructor Tests + +function test_constructorDefaults +% All default properties from a minimal construction +sphere = matRad_PhantomVOISphere('MySphere', 'OAR', 5); +assertEqual(sphere.name, 'MySphere'); +assertEqual(sphere.type, 'OAR'); +assertEqual(sphere.radius, 5); +assertEqual(sphere.coordType, 'voxel'); +assertEqual(sphere.offset, [0 0 0]); +assertEqual(sphere.HU, 0); + +function test_constructorCustomParams +% Custom name-value arguments are stored correctly +sphere = matRad_PhantomVOISphere('MySphere', 'OAR', 5, ... + 'coordType', 'mm', 'offset', [1 2 3], 'HU', 200); +assertEqual(sphere.coordType, 'mm'); +assertEqual(sphere.offset, [1 2 3]); +assertEqual(sphere.HU, 200); + +function test_constructorInvalidInputs +% Invalid radius values +assertExceptionThrown(@() matRad_PhantomVOISphere('MySphere', 'OAR', -1)); +assertExceptionThrown(@() matRad_PhantomVOISphere('MySphere', 'OAR', 0)); +assertExceptionThrown(@() matRad_PhantomVOISphere('MySphere', 'OAR', [1 2])); +assertExceptionThrown(@() matRad_PhantomVOISphere('MySphere', 'OAR', 'big')); +% Invalid coordType +assertExceptionThrown(@() matRad_PhantomVOISphere('MySphere', 'OAR', 5, 'coordType', 'invalid')); + +%% set-method Validation + +function test_setMethodsValidation +sphere = matRad_PhantomVOISphere('MySphere', 'OAR', 5); +% coordType: valid update and rejection of invalid value +sphere.coordType = 'mm'; +assertEqual(sphere.coordType, 'mm'); +assertExceptionThrown(@() helper_assignmentTest(sphere, 'coordType', 'cube')); +% radius: rejection of non-positive and non-scalar values +assertExceptionThrown(@() helper_assignmentTest(sphere, 'radius', -3)); +assertExceptionThrown(@() helper_assignmentTest(sphere, 'radius', 0)); +assertExceptionThrown(@() helper_assignmentTest(sphere, 'radius', [1 2 3])); + +%% initializeParameters Tests + +function test_initializeParametersCstStructure +% A single call must add exactly one correctly labelled CST entry +ct = helper_createTestCt(); +cst = {}; +sphere = matRad_PhantomVOISphere('TestSphere', 'OAR', 3); +cst = sphere.initializeParameters(ct, cst); +assertEqual(size(cst, 1), 1); +assertEqual(cst{1, 2}, 'TestSphere'); +assertEqual(cst{1, 3}, 'OAR'); + +function test_initializeParametersVoxelMode +ct = helper_createTestCt(); +cst = {}; +radius = 3; +sphere = matRad_PhantomVOISphere('TestSphere', 'OAR', radius); +cst = sphere.initializeParameters(ct, cst); +voxelIndices = cst{1, 4}{1}; + +% The implementation centers at (cubeDim+1)/2 in [y x z] order. +% Round to the nearest integer voxel for the membership check. +centerPoint = round((ct.cubeDim + 1) / 2); % [y x z] center voxel +centerLinIx = sub2ind(ct.cubeDim, centerPoint(1), centerPoint(2), centerPoint(3)); +assertTrue(ismember(centerLinIx, voxelIndices)); + +% Corner [1 1 1] is > 7 voxels from the center -> excluded for radius=3 +assertFalse(ismember(sub2ind(ct.cubeDim, 1, 1, 1), voxelIndices)); + +% Voxel count should be close to the continuous sphere volume (4/3)*pi*r^3 +expectedVol = (4 / 3) * pi * radius^3; +assertTrue(numel(voxelIndices) > 0.7 * expectedVol); +assertTrue(numel(voxelIndices) < 1.3 * expectedVol); + +% All returned indices must be valid linear indices for the cube +assertTrue(all(voxelIndices >= 1) && all(voxelIndices <= prod(ct.cubeDim))); + +function test_initializeParametersVoxelLargeRadiusCoversAll +% Center is at (cubeDim+1)/2 = [5.5 6.5 4.5]; the farthest corner is +% sqrt(4.5^2+5.5^2+3.5^2) = ~7.92 voxels away, so radius=9 must capture +% every voxel in the grid. +ct = helper_createTestCt(); +cst = {}; +sphere = matRad_PhantomVOISphere('TestSphere', 'OAR', 9); +cst = sphere.initializeParameters(ct, cst); +assertEqual(numel(cst{1, 4}{1}), prod(ct.cubeDim)); + +function test_initializeParametersMmMode +% A larger mm radius must yield strictly more voxels, all within bounds. +% Using non-isotropic resolution means the two radii map to clearly +% different physical volumes even across the coarsest (z=3mm) axis. +ct = helper_createTestCt(); +cst = {}; +sphereSmall = matRad_PhantomVOISphere('S1', 'OAR', 5, 'coordType', 'mm'); +sphereLarge = matRad_PhantomVOISphere('S2', 'OAR', 10, 'coordType', 'mm'); +cst1 = sphereSmall.initializeParameters(ct, cst); +cst2 = sphereLarge.initializeParameters(ct, cst); +nSmall = numel(cst1{1, 4}{1}); +nLarge = numel(cst2{1, 4}{1}); +assertTrue(nSmall > 0); +assertTrue(nLarge > nSmall); +assertTrue(all(cst1{1, 4}{1} >= 1) && all(cst1{1, 4}{1} <= prod(ct.cubeDim))); + +function test_initializeParametersLargerRadiusMoreVoxels +% Increasing the radius must strictly grow the voxel set (voxel mode) +ct = helper_createTestCt(); +cst = {}; +sphereSmall = matRad_PhantomVOISphere('Small', 'OAR', 2); +sphereLarge = matRad_PhantomVOISphere('Large', 'OAR', 4); +cst1 = sphereSmall.initializeParameters(ct, cst); +cst2 = sphereLarge.initializeParameters(ct, cst); +assertTrue(numel(cst2{1, 4}{1}) > numel(cst1{1, 4}{1})); + +%% Permutation / axis-direction tests +% +% Strategy: odd non-square cubeDim=[9 11 7] makes (cubeDim+1)/2=[5 6 4] +% land exactly on voxel (row=5, col=6, slice=4). A radius < 1 voxel +% selects only that single voxel, so an x/y axis swap produces a +% *different linear index* that assertEqual catches immediately. +% +% Offset convention: [i j k] = [x/col, y/row, z/slice]. + +function test_initializeParametersVoxelAxisPermutation +% In voxel mode an offset of [1 0 0] must advance the column (x/i), +% and [0 1 0] must advance the row (y/j). A coordinate swap would +% exchange the two, producing the wrong linear index. +cubeDim = [9 11 7]; +ct = helper_createTestCt(cubeDim, 1); +cRow = 5; +cCol = 6; +cSlice = 4; % (cubeDim+1)/2 +r = 0.6; % < 1 voxel: isolates the single centre voxel + +% No offset: centre voxel only +cst = {}; +cst = matRad_PhantomVOISphere('S', 'OAR', r).initializeParameters(ct, cst); +assertEqual(cst{1, 4}{1}, sub2ind(cubeDim, cRow, cCol, cSlice)); + +% Offset +1 in i (x / col): col must increase by 1, row unchanged +cst = {}; +cst = matRad_PhantomVOISphere('S', 'OAR', r, 'offset', [1 0 0]).initializeParameters(ct, cst); +assertEqual(cst{1, 4}{1}, sub2ind(cubeDim, cRow, cCol + 1, cSlice)); + +% Offset +1 in j (y / row): row must increase by 1, col unchanged +cst = {}; +cst = matRad_PhantomVOISphere('S', 'OAR', r, 'offset', [0 1 0]).initializeParameters(ct, cst); +assertEqual(cst{1, 4}{1}, sub2ind(cubeDim, cRow + 1, cCol, cSlice)); + +function test_initializeParametersMmAxisPermutation +% Same idea in mm mode. An offset of exactly one voxel spacing in x +% must advance the column index; one spacing in y must advance the row. +% Anisotropic resolution (x!=y) makes a coordinate swap detectable even +% if voxel counts happen to be equal. +cubeDim = [9 11 7]; +res = struct('x', 2, 'y', 3, 'z', 5); +ct = helper_createTestCt(cubeDim, res); +cRow = 5; +cCol = 6; +cSlice = 4; % (cubeDim+1)/2 +r = 0.8; % < min(res) = 2 mm: isolates centre voxel + +% No offset: centre voxel only +cst = {}; +cst = matRad_PhantomVOISphere('S', 'OAR', r, 'coordType', 'mm').initializeParameters(ct, cst); +assertEqual(cst{1, 4}{1}, sub2ind(cubeDim, cRow, cCol, cSlice)); + +% Offset one x-spacing in i (x / col): col must increase by 1 +cst = {}; +cst = matRad_PhantomVOISphere('S', 'OAR', r, 'coordType', 'mm', ... + 'offset', [res.x 0 0]).initializeParameters(ct, cst); +assertEqual(cst{1, 4}{1}, sub2ind(cubeDim, cRow, cCol + 1, cSlice)); + +% Offset one y-spacing in j (y / row): row must increase by 1 +cst = {}; +cst = matRad_PhantomVOISphere('S', 'OAR', r, 'coordType', 'mm', ... + 'offset', [0 res.y 0]).initializeParameters(ct, cst); +assertEqual(cst{1, 4}{1}, sub2ind(cubeDim, cRow + 1, cCol, cSlice)); diff --git a/test/rayTracer/test_rayTracer.m b/test/rayTracer/test_rayTracer.m index deca5b62e..049f3c985 100644 --- a/test/rayTracer/test_rayTracer.m +++ b/test/rayTracer/test_rayTracer.m @@ -1,239 +1,287 @@ function test_suite = test_rayTracer - test_functions=localfunctions(); - - initTestSuite; - - function test_siddeonRayTracer - - % test funcion with dummy nummerical example - - cubes{1} = ones([2,2,2]); - cubes{2} = cubes{1}; - cubes{2}(:,:,2) = [2,2; 2,2]; - - resolution.x = 1; - resolution.y = 1; - resolution.z = 1; - - isocenter = [0,0,0]; - sourcePoint = [1.5, 1.5, -4]; - targetPoint = [ 2.5, 2.5, 6]; - - - [alphas,l,rho,d12,ix] = matRad_siddonRayTracer(isocenter,... - resolution, ... - sourcePoint, ... - targetPoint, ... - cubes); - - % test Output types - assertTrue(isvector(alphas)); - assertTrue(isvector(l)); - assertTrue(iscell(rho)); - assertTrue(isfloat(d12)); - assertTrue(isvector(ix)); - - % test numerical Output - entryPoints = [1.95,1.95,0.5; - 2.05,2.05,1.5; - 2.15,2.15,2.5]; - entryPoints = entryPoints - sourcePoint; - alphasNum = sqrt(sum(entryPoints.^2,2))./sqrt(102); - lNum = [sqrt(102)/10,sqrt(102)/10]; - rhoNum{1} = [1,1]; - rhoNum{2} = [1,2]; - d12Num = sqrt(102); - ixNum = [4,8]; - - assertElementsAlmostEqual(alphasNum',alphas) - assertElementsAlmostEqual(lNum,l) - assertElementsAlmostEqual(rhoNum{1},rho{1}) - assertElementsAlmostEqual(rhoNum{2},rho{2}) - assertElementsAlmostEqual(d12Num,d12) - assertEqual(ixNum,ix) - - function test_2DCube - - cubes{1} = ones([2,2]); - cubes{2} = cubes{1}; - cubes{2}(:,2) = [2,2]; - - resolution.x = 1; - resolution.y = 1; - resolution.z = 1; - - isocenter = [0,0,0]; - sourcePoint = [1.5, 1.5, -4]; - targetPoint = [ 2.5, 2.5, 6]; - - - [alphas,l,rho,d12,ix] = matRad_siddonRayTracer(isocenter,... - resolution, ... - sourcePoint, ... - targetPoint, ... - cubes); - - % test Output types - assertTrue(isvector(alphas)); - assertTrue(isvector(l)); - assertTrue(iscell(rho)); - assertTrue(isfloat(d12)); - assertTrue(isvector(ix)); - - % test numerical Output - entryPoints = [1.95,1.95,0.5; - 2.05,2.05,1.5;]; - entryPoints = entryPoints - sourcePoint; - alphasNum = sqrt(sum(entryPoints.^2,2))./sqrt(102); - lNum = [sqrt(102)/10]; - rhoNum{1} = [1]; - rhoNum{2} = [2]; - d12Num = sqrt(102); - ixNum = [4]; - - assertElementsAlmostEqual(alphasNum',alphas) - assertElementsAlmostEqual(lNum,l) - assertElementsAlmostEqual(rhoNum{1},rho{1}) - assertElementsAlmostEqual(rhoNum{2},rho{2}) - assertElementsAlmostEqual(d12Num,d12) - assertEqual(ixNum,ix) - - function test_rayDoesNotHitCT - - cubes{1} = ones([2,2,2]); - - resolution.x = 1; - resolution.y = 1; - resolution.z = 1; - - isocenter = [0,0,0]; - sourcePoint = [1.5, 1.5 -4]; - targetPoint = [ 10, 10, 6]; - - - [alphas,l,rho,d12,ix] = matRad_siddonRayTracer(isocenter,... - resolution, ... - sourcePoint, ... - targetPoint, ... - cubes); - - - % test numerical Output - alphasNum = []; - lNum = []; - rhoNum{1} = []; - d12Num = norm(sourcePoint - targetPoint); - ixNum = []; - - - assertElementsAlmostEqual(alphasNum',alphas) - assertElementsAlmostEqual(lNum,l) - assertElementsAlmostEqual(rhoNum{1},rho{1}) - assertElementsAlmostEqual(d12Num,d12) - assertEqual(ixNum,ix) - - sourcePoint = [10, 10 -4]; - targetPoint = [ 10, 10, 6]; - d12Num = 10; - - - [alphas,l,rho,d12,ix] = matRad_siddonRayTracer(isocenter,... - resolution, ... - sourcePoint, ... - targetPoint, ... - cubes); - - assertElementsAlmostEqual(alphasNum',alphas) - assertElementsAlmostEqual(lNum,l) - assertElementsAlmostEqual(rhoNum{1},rho{1}) - assertElementsAlmostEqual(d12Num,d12) - assertEqual(ixNum,ix) - - function test_rayHitsAtBoundary - - cubes{1} = ones([2,2,2]); - cubes{2} = cubes{1}; - cubes{2}(:,:,2) = [2,2; 2,2]; - - resolution.x = 1; - resolution.y = 1; - resolution.z = 1; - - isocenter = [0,0,0]; - sourcePoint = [2.5, 2.5, -4]; - targetPoint = [2.5, 2.5, 6]; - - - [alphas,l,rho,d12,ix] = matRad_siddonRayTracer(isocenter,... - resolution, ... - sourcePoint, ... - targetPoint, ... - cubes); - - % test Output types - assertTrue(isvector(alphas)); - assertTrue(isvector(l)); - assertTrue(iscell(rho)); - assertTrue(isfloat(d12)); - assertTrue(isvector(ix)); - - % test numerical Output - entryPoints = [2.5, 2.5, 0.5; - 2.5, 2.5, 1.5; - 2.5, 2.5, 2.5]; - entryPoints = entryPoints - sourcePoint; - alphasNum = sqrt(sum(entryPoints.^2,2))./10; - lNum = [1,1]; - rhoNum{1} = [1,1]; - rhoNum{2} = [1,2]; - d12Num = 10; - ixNum = [4,8]; - - assertElementsAlmostEqual(alphasNum',alphas) - assertElementsAlmostEqual(lNum,l) - assertElementsAlmostEqual(rhoNum{1},rho{1}) - assertElementsAlmostEqual(rhoNum{2},rho{2}) - assertElementsAlmostEqual(d12Num,d12) - assertEqual(ixNum,ix) - - - function test_rayHitsAtCorner - - cubes{1} = ones([2,2,2]); - cubes{2} = cubes{1}; - cubes{2}(:,:,2) = [2,2; 2,2]; - - resolution.x = 1; - resolution.y = 1; - resolution.z = 1; - - isocenter = [0,0,0]; - sourcePoint = [1.5, 1.5, -4]; - targetPoint = [3.5, 3.5, 5]; - - - [alphas,l,rho,d12,ix] = matRad_siddonRayTracer(isocenter,... - resolution, ... - sourcePoint, ... - targetPoint, ... - cubes); - - - % test numerical Output - alphasNum = 0.5; - ixNum = 1:0; - lNum = []; - rhoNum{1} = cubes{1}(ixNum); - rhoNum{2} = cubes{2}(ixNum); - d12Num = norm(sourcePoint - targetPoint); - - - assertElementsAlmostEqual(alphasNum',alphas) - assertElementsAlmostEqual(lNum,l) - assertElementsAlmostEqual(rhoNum{1},rho{1}) - assertElementsAlmostEqual(rhoNum{2},rho{2}) - assertElementsAlmostEqual(d12Num,d12) - assertEqual(ixNum,ix) - - +test_functions = localfunctions(); + +initTestSuite; + +function test_siddonRayTracer +% test function with dummy numerical example + +cubes{1} = ones([2, 2, 2]); +cubes{2} = cubes{1}; +cubes{2}(:, :, 2) = [2, 2; 2, 2]; + +resolution.x = 1; +resolution.y = 1; +resolution.z = 1; + +grid.resolution = resolution; +grid.dimensions = size(cubes{1}); + +isocenter = [0, 0, 0]; +sourcePoint = [-1, -1, -2]; +targetPoint = [0, 0, 2]; + +% Now we will have voxels centers at -1 0 (default matRad world +% coordinates, meaning the isocenter will point into the [2 2 2] voxel) +% A ray starting at -1 -1 -2 and ending at 0 0 1 sees the first plane at +% z = -1.5 and the last plae at z=0.5. When intersecting the first +% plane, it will have passed 1/8 of its length. + +rt = matRad_RayTracerSiddon(cubes, grid); +[alphas, l, rho, d12, ix] = rt.traceRay(isocenter, sourcePoint, targetPoint); + +% test Output types +assertTrue(isvector(alphas)); +assertTrue(isvector(l)); +assertTrue(iscell(rho)); +assertTrue(isfloat(d12)); +assertTrue(isvector(ix)); + +% test numerical Output +grid = matRad_getWorldAxes(grid); +rayVec = targetPoint - sourcePoint; +rayLength = norm(rayVec); +% the ray will intersect z at the coordinates of the z voxel boundaries + +entryPoints = [sourcePoint + rayVec * 1 / 8 + sourcePoint + rayVec * 3 / 8 + sourcePoint + rayVec * 5 / 8]; +entryPoints = entryPoints - sourcePoint; +alphasNum = sqrt(sum(entryPoints.^2, 2)) ./ rayLength; +lNum = [rayLength / 4, rayLength / 4]; +rhoNum{1} = [1, 1]; +rhoNum{2} = [1, 2]; +d12Num = rayLength; +ixNum = [1, 8]; + +assertElementsAlmostEqual(alphasNum', alphas); +assertElementsAlmostEqual(lNum, l); +assertElementsAlmostEqual(rhoNum{1}, rho{1}); +assertElementsAlmostEqual(rhoNum{2}, rho{2}); +assertElementsAlmostEqual(d12Num, d12); +assertEqual(ixNum, ix); + +% test the old deprecated function with dummy numerical example +% It expects cube coords for the isocenter + +isocenterCube = matRad_world2cubeCoords(isocenter, grid); +[alphasOld, lOld, rhoOld, d12Old, ixOld] = matRad_siddonRayTracer(isocenterCube, ... + resolution, ... + sourcePoint, ... + targetPoint, ... + cubes); + +assertElementsAlmostEqual(alphas, alphasOld); +assertElementsAlmostEqual(l, lOld); +assertElementsAlmostEqual(rho{1}, rhoOld{1}); +assertElementsAlmostEqual(rho{2}, rhoOld{2}); +assertElementsAlmostEqual(d12, d12Old); +assertEqual(ixNum, ixOld); + +function test_rayDoesNotHitCT + +cubes{1} = ones([2, 2, 2]); + +resolution.x = 1; +resolution.y = 1; +resolution.z = 1; + +grid.resolution = resolution; +grid.dimensions = size(cubes{1}); + +isocenter = [-2, -2, -2]; +sourcePoint = [2.5, 2.5 -4]; +targetPoint = [10, 10, 6]; + +rt = matRad_RayTracerSiddon(cubes, grid); +[alphas, l, rho, d12, ix] = rt.traceRay(isocenter, sourcePoint, targetPoint); + +% test numerical Output +d12Num = norm(sourcePoint - targetPoint); + +assertTrue(isempty(alphas)); +assertTrue(isempty(l)); +assertTrue(isempty(ix)); +assertTrue(isempty(rho{1})); +assertElementsAlmostEqual(d12Num, d12); + +% deprecated call using cube coordinates +isocenterCube = matRad_world2cubeCoords(isocenter, grid); +[alphasOld, lOld, rhoOld, d12Old, ixOld] = matRad_siddonRayTracer(isocenterCube, ... + resolution, ... + sourcePoint, ... + targetPoint, ... + cubes); + +assertElementsAlmostEqual(alphas, alphasOld); +assertElementsAlmostEqual(l, lOld); +assertElementsAlmostEqual(rho{1}, rhoOld{1}); +assertElementsAlmostEqual(d12, d12Old); +assertEqual(ix, ixOld); + +function test_rayHitsAtBoundary + +cubes{1} = ones([2, 2, 2]); +cubes{2} = cubes{1}; +cubes{2}(:, :, 2) = [2, 2; 2, 2]; + +resolution.x = 1; +resolution.y = 1; +resolution.z = 1; + +isocenter = [-2, -2, -2]; +sourcePoint = [2.5, 2.5, -4]; +targetPoint = [2.5, 2.5, 6]; + +grid.resolution = resolution; +grid.dimensions = size(cubes{1}); + +rt = matRad_RayTracerSiddon(cubes, grid); +[alphas, l, rho, d12, ix] = rt.traceRay(isocenter, sourcePoint, targetPoint); + +% test Output types +assertTrue(isvector(alphas)); +assertTrue(isvector(l)); +assertTrue(iscell(rho)); +assertTrue(isfloat(d12)); +assertTrue(isvector(ix)); + +% test numerical Output +entryPoints = [2.5, 2.5, 0.5 + 2.5, 2.5, 1.5 + 2.5, 2.5, 2.5]; +entryPoints = entryPoints - sourcePoint; +alphasNum = sqrt(sum(entryPoints.^2, 2)) ./ 10; +lNum = [1, 1]; +rhoNum{1} = [1, 1]; +rhoNum{2} = [1, 2]; +d12Num = 10; +ixNum = [4, 8]; + +assertElementsAlmostEqual(alphasNum', alphas); +assertElementsAlmostEqual(lNum, l); +assertElementsAlmostEqual(rhoNum{1}, rho{1}); +assertElementsAlmostEqual(rhoNum{2}, rho{2}); +assertElementsAlmostEqual(d12Num, d12); +assertEqual(ixNum, ix); + +% deprecated call using cube coordinates +isocenterCube = matRad_world2cubeCoords(isocenter, grid); +[alphasOld, lOld, rhoOld, d12Old, ixOld] = matRad_siddonRayTracer(isocenterCube, ... + resolution, ... + sourcePoint, ... + targetPoint, ... + cubes); + +assertElementsAlmostEqual(alphas, alphasOld); +assertElementsAlmostEqual(l, lOld); +assertElementsAlmostEqual(rho{1}, rhoOld{1}); +assertElementsAlmostEqual(rho{2}, rhoOld{2}); +assertElementsAlmostEqual(d12, d12Old); +assertEqual(ix, ixOld); + +function test_rayHitsAtCorner + +cubes{1} = ones([2, 2, 2]); +cubes{2} = cubes{1}; +cubes{2}(:, :, 2) = [2, 2; 2, 2]; + +resolution.x = 1; +resolution.y = 1; +resolution.z = 1; + +isocenter = [-2, -2, -2]; +sourcePoint = [1.5, 1.5, -4]; +targetPoint = [3.5, 3.5, 5]; + +grid.resolution = resolution; +grid.dimensions = size(cubes{1}); + +rt = matRad_RayTracerSiddon(cubes, grid); +[alphas, l, rho, d12, ix] = rt.traceRay(isocenter, sourcePoint, targetPoint); + +% test numerical Output +alphasNum = 0.5; +ixNum = 1:0; +rhoNum{1} = cubes{1}(ixNum); +rhoNum{2} = cubes{2}(ixNum); +d12Num = norm(sourcePoint - targetPoint); + +assertElementsAlmostEqual(alphasNum', alphas); +assertTrue(isempty(l)); +assertEqual(size(l), [1 0]); +assertElementsAlmostEqual(rhoNum{1}, rho{1}); +assertElementsAlmostEqual(rhoNum{2}, rho{2}); +assertElementsAlmostEqual(d12Num, d12); +assertEqual(ixNum, ix); + +% deprecated call using cube coordinates +isocenterCube = matRad_world2cubeCoords(isocenter, grid); +[alphasOld, lOld, rhoOld, d12Old, ixOld] = matRad_siddonRayTracer(isocenterCube, ... + resolution, ... + sourcePoint, ... + targetPoint, ... + cubes); + +assertElementsAlmostEqual(alphas, alphasOld); +assertElementsAlmostEqual(l, lOld); +assertElementsAlmostEqual(rho{1}, rhoOld{1}); +assertElementsAlmostEqual(rho{2}, rhoOld{2}); +assertElementsAlmostEqual(d12, d12Old); +assertElementsAlmostEqual(ix, ixOld); + +function test_vectorizedVsLoop + +testData = load('photons_testData.mat'); +targetPoints = vertcat(testData.stf(1).ray.targetPoint); +sourcePoint = testData.stf(1).sourcePoint; +isocenter = testData.stf(1).isoCenter; + +rt = matRad_RayTracerSiddon(testData.ct.cube, testData.ct); + +rt.vectorized = true; +[alphas, l, rho, d12, ix] = rt.traceRays(isocenter, sourcePoint, targetPoints); + +rt.vectorized = false; +[alphasLoop, lLoop, rhoLoop, d12Loop, ixLoop] = rt.traceRays(isocenter, sourcePoint, targetPoints); + +assertElementsAlmostEqual(alphas, alphasLoop); +assertElementsAlmostEqual(l, lLoop); +assertElementsAlmostEqual(d12, d12Loop); +assertElementsAlmostEqual(ix, ixLoop); +assertElementsAlmostEqual(rho{1}, rhoLoop{1}); + +function test_singleVsDouble +testData = load('photons_testData.mat'); +targetPoints = vertcat(testData.stf(1).ray.targetPoint); +sourcePoint = testData.stf(1).sourcePoint; +isocenter = testData.stf(1).isoCenter; + +rt = matRad_RayTracerSiddon(testData.ct.cube, testData.ct); + +% Test force double +rt.forcePrecision = 'double'; +[alphas, l, rho, d12, ix] = rt.traceRays(isocenter, sourcePoint, targetPoints); + +assertTrue(isa(alphas, 'double')); +assertTrue(isa(l, 'double')); +assertTrue(isa(d12, 'double')); +assertTrue(isa(ix, 'double')); +assertTrue(isa(rho{1}, 'double')); + +% test force single +rt.forcePrecision = 'single'; +[alphasSingle, lSingle, rhoSingle, d12Single, ixSingle] = rt.traceRays(isocenter, sourcePoint, targetPoints); + +assertTrue(isa(alphasSingle, 'single')); +assertTrue(isa(lSingle, 'single')); +assertTrue(isa(d12Single, 'single')); +assertTrue(isa(ixSingle, 'double')); +assertTrue(isa(rhoSingle{1}, 'single')); + +assertElementsAlmostEqual(alphasSingle, alphas); +assertElementsAlmostEqual(lSingle, l); +assertElementsAlmostEqual(d12Single, d12); +assertElementsAlmostEqual(ixSingle, ix); +assertElementsAlmostEqual(rhoSingle{1}, rho{1}); diff --git a/test/sequencing/test_engelLeafSequencing.m b/test/sequencing/test_engelLeafSequencing.m index 98e0378fe..a07546c29 100644 --- a/test/sequencing/test_engelLeafSequencing.m +++ b/test/sequencing/test_engelLeafSequencing.m @@ -31,7 +31,7 @@ fn_new = fieldnames(resultGUI_sequenced); for i = 1:numel(fn_old) assertTrue(any(strcmp(fn_old{i},fn_new))); - assertEqual(resultGUI.(fn_old{i}),resultGUI_sequenced.(fn_old{i})); + assertElementsAlmostEqual(resultGUI.(fn_old{i}),resultGUI_sequenced.(fn_old{i})); end % Basic additions to resultGUI diff --git a/test/sequencing/test_siochiLeafSequencing.m b/test/sequencing/test_siochiLeafSequencing.m index af6e91859..60f6bf7db 100644 --- a/test/sequencing/test_siochiLeafSequencing.m +++ b/test/sequencing/test_siochiLeafSequencing.m @@ -1,4 +1,4 @@ -function test_suite = test_xiaLeafSequencing +function test_suite = test_siochiLeafSequencing %The output should always be test_suite, and the function name the same as %your file name @@ -31,7 +31,7 @@ fn_new = fieldnames(resultGUI_sequenced); for i = 1:numel(fn_old) assertTrue(any(strcmp(fn_old{i},fn_new))); - assertEqual(resultGUI.(fn_old{i}),resultGUI_sequenced.(fn_old{i})); + assertElementsAlmostEqual(resultGUI.(fn_old{i}),resultGUI_sequenced.(fn_old{i})); end % Basic additions to resultGUI diff --git a/test/sequencing/test_xiaLeafSequencing.m b/test/sequencing/test_xiaLeafSequencing.m index 9d6aaf943..a4fabcc2d 100644 --- a/test/sequencing/test_xiaLeafSequencing.m +++ b/test/sequencing/test_xiaLeafSequencing.m @@ -31,7 +31,7 @@ fn_new = fieldnames(resultGUI_sequenced); for i = 1:numel(fn_old) assertTrue(any(strcmp(fn_old{i},fn_new))); - assertEqual(resultGUI.(fn_old{i}),resultGUI_sequenced.(fn_old{i})); + assertElementsAlmostEqual(resultGUI.(fn_old{i}),resultGUI_sequenced.(fn_old{i})); end % Basic additions to resultGUI diff --git a/test/steering/test_stfGEneratorParticleIMPT.m b/test/steering/test_stfGEneratorParticleIMPT.m index 5ad451e76..4fb35feee 100644 --- a/test/steering/test_stfGEneratorParticleIMPT.m +++ b/test/steering/test_stfGEneratorParticleIMPT.m @@ -1,4 +1,4 @@ -function test_suite = test_stfGeneratorPhotonIMRT +function test_suite = test_stfGEneratorParticleIMPT test_functions=localfunctions(); @@ -83,3 +83,24 @@ function test_generate_multibeams() %assertTrue(isscalar(stf2(i).ray.energy)); end + +function test_generateRangeShifterStf() + % geometry settings + load protons_testData.mat + + % Move Target shallower so that range shifter calculation + ct.resolution.y=5; + VolHelper = false(ct.cubeDim); + VolHelper(2:3,5:6,5:6) = true; + ixTarget = find(VolHelper); + + cst{1,4}{1} = ixTarget; + + stfGen = matRad_StfGeneratorParticleIMPT(pln); + stfGen.useRangeShifter = true; + stfGen.rangeShifterEqD = 2; + + stf = stfGen.generate(ct,cst); + + assertTrue(stf(1).ray(1).rangeShifter(1).ID==1); + assertTrue(stf(1).ray(1).rangeShifter(1).eqThickness==2); \ No newline at end of file diff --git a/test/testData/FRED_data/MCrun/inp/plan/plan.inp b/test/testData/FRED_data/MCrun/inp/plan/plan.inp index d726263ca..3997f6c23 100644 --- a/test/testData/FRED_data/MCrun/inp/plan/plan.inp +++ b/test/testData/FRED_data/MCrun/inp/plan/plan.inp @@ -1,30 +1,38 @@ -nprim = 83333 +nprim = 100 #Bixels Field0, Layer0 - def: S0 = {'beamletID': 0, 'P': [-0.900, -0.900, 0.000], 'v': [-0.00100, -0.00100, 1.00000], 'w': 1000000.0000000} - def: S1 = {'beamletID': 1, 'P': [0.900, -0.900, 0.000], 'v': [0.00100, -0.00100, 1.00000], 'w': 1000000.0000000} - def: S2 = {'beamletID': 2, 'P': [0.000, 0.000, 0.000], 'v': [0.00000, 0.00000, 1.00000], 'w': 1000000.0000000} - def: S3 = {'beamletID': 3, 'P': [-0.900, 0.900, 0.000], 'v': [-0.00100, 0.00100, 1.00000], 'w': 1000000.0000000} - def: S4 = {'beamletID': 4, 'P': [0.900, 0.900, 0.000], 'v': [0.00100, 0.00100, 1.00000], 'w': 1000000.0000000} + def: S0 = {'beamletID': 0, 'P': [-0.900, -0.900, 0.000], 'v': [-0.00100, -0.00100, 1.00000], 'w': 1.0000000} + def: S1 = {'beamletID': 1, 'P': [0.900, -0.900, 0.000], 'v': [0.00100, -0.00100, 1.00000], 'w': 1.0000000} + def: S2 = {'beamletID': 2, 'P': [0.000, 0.000, 0.000], 'v': [0.00000, 0.00000, 1.00000], 'w': 1.0000000} + def: S3 = {'beamletID': 3, 'P': [-0.900, 0.900, 0.000], 'v': [-0.00100, 0.00100, 1.00000], 'w': 1.0000000} + def: S4 = {'beamletID': 4, 'P': [0.900, 0.900, 0.000], 'v': [0.00100, 0.00100, 1.00000], 'w': 1.0000000} def: L0 = {'Energy': 106.0783, 'Espread': 3.7329, 'FWHM': 1.4576, 'beamlets': [S0, S1, S2, S3, S4]} #Bixels Field0, Layer1 - def: S5 = {'beamletID': 5, 'P': [0.000, 0.000, 0.000], 'v': [0.00000, 0.00000, 1.00000], 'w': 1000000.0000000} + def: S5 = {'beamletID': 5, 'P': [0.000, 0.000, 0.000], 'v': [0.00000, 0.00000, 1.00000], 'w': 1.0000000} def: L1 = {'Energy': 112.3394, 'Espread': 3.5049, 'FWHM': 1.3972, 'beamlets': [S5]} -def: F0 = {'fieldNumber': 0, 'GA': 0, 'CA': 0, 'ISO': [0, 0, 0], 'dim': [15.4758, 15.4758, 0.1], 'Layers': [L0, L1]} +#Bixels Field0, Layer2 + def: S6 = {'beamletID': 6, 'P': [0.000, 0.000, 0.000], 'v': [0.00000, 0.00000, 1.00000], 'w': 1.0000000} + def: L2 = {'Energy': 118.357, 'Espread': 3.3099, 'FWHM': 1.343, 'beamlets': [S6]} -#Bixels Field1, Layer2 - def: S6 = {'beamletID': 6, 'P': [-0.900, -0.900, 0.000], 'v': [-0.00100, -0.00100, 1.00000], 'w': 1000000.0000000} - def: S7 = {'beamletID': 7, 'P': [0.900, -0.900, 0.000], 'v': [0.00100, -0.00100, 1.00000], 'w': 1000000.0000000} - def: S8 = {'beamletID': 8, 'P': [0.000, 0.000, 0.000], 'v': [0.00000, 0.00000, 1.00000], 'w': 1000000.0000000} - def: S9 = {'beamletID': 9, 'P': [-0.900, 0.900, 0.000], 'v': [-0.00100, 0.00100, 1.00000], 'w': 1000000.0000000} - def: S10 = {'beamletID': 10, 'P': [0.900, 0.900, 0.000], 'v': [0.00100, 0.00100, 1.00000], 'w': 1000000.0000000} - def: L2 = {'Energy': 106.0783, 'Espread': 3.7329, 'FWHM': 1.4576, 'beamlets': [S6, S7, S8, S9, S10]} +def: F0 = {'fieldNumber': 0, 'GA': 0, 'CA': 0, 'ISO': [0, 0, 0], 'dim': [15.4758, 15.4758, 0.1], 'Layers': [L0, L1, L2]} #Bixels Field1, Layer3 - def: S11 = {'beamletID': 11, 'P': [0.000, 0.000, 0.000], 'v': [0.00000, 0.00000, 1.00000], 'w': 1000000.0000000} - def: L3 = {'Energy': 112.3394, 'Espread': 3.5049, 'FWHM': 1.3972, 'beamlets': [S11]} + def: S7 = {'beamletID': 7, 'P': [-0.900, -0.900, 0.000], 'v': [-0.00100, -0.00100, 1.00000], 'w': 1.0000000} + def: S8 = {'beamletID': 8, 'P': [0.900, -0.900, 0.000], 'v': [0.00100, -0.00100, 1.00000], 'w': 1.0000000} + def: S9 = {'beamletID': 9, 'P': [0.000, 0.000, 0.000], 'v': [0.00000, 0.00000, 1.00000], 'w': 1.0000000} + def: S10 = {'beamletID': 10, 'P': [-0.900, 0.900, 0.000], 'v': [-0.00100, 0.00100, 1.00000], 'w': 1.0000000} + def: S11 = {'beamletID': 11, 'P': [0.900, 0.900, 0.000], 'v': [0.00100, 0.00100, 1.00000], 'w': 1.0000000} + def: L3 = {'Energy': 106.0783, 'Espread': 3.7329, 'FWHM': 1.4576, 'beamlets': [S7, S8, S9, S10, S11]} -def: F1 = {'fieldNumber': 1, 'GA': 180, 'CA': 0, 'ISO': [0, 0, 0], 'dim': [15.4758, 15.4758, 0.1], 'Layers': [L2, L3]} +#Bixels Field1, Layer4 + def: S12 = {'beamletID': 12, 'P': [0.000, 0.000, 0.000], 'v': [0.00000, 0.00000, 1.00000], 'w': 1.0000000} + def: L4 = {'Energy': 112.3394, 'Espread': 3.5049, 'FWHM': 1.3972, 'beamlets': [S12]} + +#Bixels Field1, Layer5 + def: S13 = {'beamletID': 13, 'P': [0.000, 0.000, 0.000], 'v': [0.00000, 0.00000, 1.00000], 'w': 1.0000000} + def: L5 = {'Energy': 118.357, 'Espread': 3.3099, 'FWHM': 1.343, 'beamlets': [S13]} + +def: F1 = {'fieldNumber': 1, 'GA': 180, 'CA': 0, 'ISO': [0, 0, 0], 'dim': [15.4758, 15.4758, 0.1], 'Layers': [L3, L4, L5]} def: plan = {'SAD': 100, 'Fields': [F0, F1]} diff --git a/test/testData/FRED_data/MCrun/inp/regions/regions.inp b/test/testData/FRED_data/MCrun/inp/regions/regions.inp index ba495c73d..4124899e6 100644 --- a/test/testData/FRED_data/MCrun/inp/regions/regions.inp +++ b/test/testData/FRED_data/MCrun/inp/regions/regions.inp @@ -5,7 +5,7 @@ region< pivot=[0.5,0.5,0.5] l=[1.0,0.0,0.0] u=[0.0,-1.0,0.0] - score=[Dose] + scoreij=[Dose,LETd] region> region< ID=Room diff --git a/test/testData/FRED_data/MCrun/out/score/Phantom.Dose.mhd b/test/testData/FRED_data/MCrun/out/score/Phantom.Dose.mhd index 6a58ff0b5..6bb59c431 100644 Binary files a/test/testData/FRED_data/MCrun/out/score/Phantom.Dose.mhd and b/test/testData/FRED_data/MCrun/out/score/Phantom.Dose.mhd differ diff --git a/test/testData/FRED_data/MCrun/out/score/Phantom.LETd.mhd b/test/testData/FRED_data/MCrun/out/score/Phantom.LETd.mhd index 6d103b257..e41f5d4cc 100644 Binary files a/test/testData/FRED_data/MCrun/out/score/Phantom.LETd.mhd and b/test/testData/FRED_data/MCrun/out/score/Phantom.LETd.mhd differ diff --git a/test/testData/FRED_data/MCrun/out/scoreij/Phantom.Dose.bin b/test/testData/FRED_data/MCrun/out/scoreij/Phantom.Dose.bin index 23a525c81..db1804a99 100644 Binary files a/test/testData/FRED_data/MCrun/out/scoreij/Phantom.Dose.bin and b/test/testData/FRED_data/MCrun/out/scoreij/Phantom.Dose.bin differ diff --git a/test/testData/FRED_data/MCrun/out/scoreij/Phantom.LETd.bin b/test/testData/FRED_data/MCrun/out/scoreij/Phantom.LETd.bin index b3ca89c51..251319483 100644 Binary files a/test/testData/FRED_data/MCrun/out/scoreij/Phantom.LETd.bin and b/test/testData/FRED_data/MCrun/out/scoreij/Phantom.LETd.bin differ diff --git a/test/testData/MCsquare_data/output/Dose.mhd b/test/testData/MCsquare_data/output/Dose.mhd new file mode 100644 index 000000000..ec999f39c --- /dev/null +++ b/test/testData/MCsquare_data/output/Dose.mhd @@ -0,0 +1,7 @@ +ObjectType = Image +NDims = 3 +DimSize = 10 20 10 +ElementSpacing = 10.000000 10.000000 10.000000 +ElementType = MET_FLOAT +ElementByteOrderMSB = False +ElementDataFile = Dose.raw diff --git a/test/testData/MCsquare_data/output/Dose.raw b/test/testData/MCsquare_data/output/Dose.raw new file mode 100644 index 000000000..0ed83be8b Binary files /dev/null and b/test/testData/MCsquare_data/output/Dose.raw differ diff --git a/test/testData/MCsquare_data/output/LET.mhd b/test/testData/MCsquare_data/output/LET.mhd new file mode 100644 index 000000000..b8d7dc22c --- /dev/null +++ b/test/testData/MCsquare_data/output/LET.mhd @@ -0,0 +1,7 @@ +ObjectType = Image +NDims = 3 +DimSize = 10 20 10 +ElementSpacing = 10.000000 10.000000 10.000000 +ElementType = MET_FLOAT +ElementByteOrderMSB = False +ElementDataFile = LET.raw diff --git a/test/testData/MCsquare_data/output/LET.raw b/test/testData/MCsquare_data/output/LET.raw new file mode 100644 index 000000000..f902017a3 Binary files /dev/null and b/test/testData/MCsquare_data/output/LET.raw differ diff --git a/test/testData/MCsquare_data/output/Sparse_Dose.bin b/test/testData/MCsquare_data/output/Sparse_Dose.bin new file mode 100644 index 000000000..43c914979 Binary files /dev/null and b/test/testData/MCsquare_data/output/Sparse_Dose.bin differ diff --git a/test/testData/MCsquare_data/output/Sparse_Dose.txt b/test/testData/MCsquare_data/output/Sparse_Dose.txt new file mode 100644 index 000000000..0cfb5bf7d --- /dev/null +++ b/test/testData/MCsquare_data/output/Sparse_Dose.txt @@ -0,0 +1,8 @@ +# MCsquare sparse matrix format +SimulationDate = 2026/2/27 15:41:31 +PlanName = matRad_bixel +SimulationMode = Beamlet +NbrSpots = 14 +ImageSize = 10 20 10 +VoxelSpacing = 10.000000 10.000000 10.000000 +BinaryFile = Sparse_Dose.bin diff --git a/test/testData/MCsquare_data/output/Sparse_LET.bin b/test/testData/MCsquare_data/output/Sparse_LET.bin new file mode 100644 index 000000000..46b84ee6d Binary files /dev/null and b/test/testData/MCsquare_data/output/Sparse_LET.bin differ diff --git a/test/testData/MCsquare_data/output/Sparse_LET.txt b/test/testData/MCsquare_data/output/Sparse_LET.txt new file mode 100644 index 000000000..be74f82e5 --- /dev/null +++ b/test/testData/MCsquare_data/output/Sparse_LET.txt @@ -0,0 +1,8 @@ +# MCsquare sparse matrix format +SimulationDate = 2026/2/27 15:41:31 +PlanName = matRad_bixel +SimulationMode = Beamlet +NbrSpots = 14 +ImageSize = 10 20 10 +VoxelSpacing = 10.000000 10.000000 10.000000 +BinaryFile = Sparse_LET.bin diff --git a/test/testData/carbon_testData.mat b/test/testData/carbon_testData.mat index f9224109f..18fa371b9 100644 Binary files a/test/testData/carbon_testData.mat and b/test/testData/carbon_testData.mat differ diff --git a/test/testData/helper_testDataCreater.m b/test/testData/helper_testDataCreater.m index daec8339e..023240316 100644 --- a/test/testData/helper_testDataCreater.m +++ b/test/testData/helper_testDataCreater.m @@ -48,8 +48,7 @@ pln.numOfFractions = 30; pln.propStf.gantryAngles = [0,180]; pln.propStf.couchAngles = zeros(size(pln.propStf.gantryAngles)); - pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); - pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0); + pln.propStf.isoCenter = matRad_getIsoCenter(cst,ct,0); pln.propStf.longitudinalSpotSpacing = 8; pln.propStf.bixelWidth = 10; diff --git a/test/testData/photons_testData.mat b/test/testData/photons_testData.mat index 25df3452c..c595876bc 100644 Binary files a/test/testData/photons_testData.mat and b/test/testData/photons_testData.mat differ diff --git a/test/util/test_compareDijStf.m b/test/util/test_compareDijStf.m index 8442fe4fb..512262fb7 100644 --- a/test/util/test_compareDijStf.m +++ b/test/util/test_compareDijStf.m @@ -1,4 +1,4 @@ -function test_suite = test_matRad_compareDijStf +function test_suite = test_compareDijStf test_functions=localfunctions(); initTestSuite; diff --git a/test/util/test_comparePlnStf.m b/test/util/test_comparePlnStf.m index 5c60df506..bcf74cd6b 100644 --- a/test/util/test_comparePlnStf.m +++ b/test/util/test_comparePlnStf.m @@ -44,15 +44,6 @@ assertFalse(isempty(msg)); assertTrue(ischar(msg)); -%Test case for wrong number of beams provided -function test_wrongNumberOfAngles - [pln, stf] = helper_getDummyBasicPlnStf(); - pln.propStf.numOfBeams = 3; - [allMatch, msg] = matRad_comparePlnStf(pln, stf); - assertFalse(allMatch); - assertFalse(isempty(msg)); - assertTrue(ischar(msg)); - % Test case for non-matching bixel width function test_nonMatchingBixelWidth [pln, stf] = helper_getDummyBasicPlnStf(); diff --git a/test/util/test_interp1.m b/test/util/test_interp1.m index 841217541..00f937b2d 100644 --- a/test/util/test_interp1.m +++ b/test/util/test_interp1.m @@ -1,213 +1,219 @@ function test_suite = test_interp1 -test_functions=localfunctions(); +test_functions = localfunctions(); initTestSuite; % Test basic values -function test_matrad_interp1_values - %R = realmax; % For R = realmax, the test may often fail because of Inf in matRad_interp1: should first divide and then multiply. - R = 10^100; +function test_interp1Values +% R = realmax; % For R = realmax, the test may often fail because of Inf in matRad_interp1: should first divide and then multiply. +R = 10^100; + +% Pick a vector x and sort +% First element of x is in [-R, R] +x1el1 = (2 * rand - 1) * R; +if x1el1 < 0 + x1el2 = x1el1 + rand * (R); +else + x1el2 = x1el1 + rand * (R - x1el1); +end +if x1el1 <= x1el2 + x1 = [x1el1; x1el2]; +else + x1 = [x1el2; x1el1]; +end + +% Pick a sorted vector y +y1el1 = (2 * rand - 1) * R; +if y1el1 < 0 + y1el2 = y1el1 + rand * (R); +else + y1el2 = y1el1 + rand * (R - y1el1); +end +y1 = [y1el1; y1el2]; + +% Pick a single value intermediate in x +x2 = rand * (x1(2) - x1(1)) + x1(1); + +% Verify interpolation works correctly +y2 = matRad_interp1(x1, y1, x2); +expectedy2 = y1(1) + (x2 - x1(1)) * ((y1(2) - y1(1)) / (x1(2) - x1(1))); +assertTrue(~isnan(y2)); +assertElementsAlmostEqual(y2, expectedy2); + +% Flip y vector and check interpolation again +y1 = flip(y1); +y2 = matRad_interp1(x1, y1, x2); +expectedy2 = y1(1) + (x2 - x1(1)) * (y1(2) - y1(1)) / (x1(2) - x1(1)); +assertTrue(~isnan(y2)); +assertElementsAlmostEqual(y2, expectedy2); + +% Pick a x value exceeding upper boundaries +% If x2 is outside boundaries, we expect a NaN +x2 = x1(2) + rand * (R - x1(2)); +if x1(2) == R + assertTrue(~isnan(matRad_interp1(x1, y1, x2))); + assertEqual(x2, x1(2)); +else + assertTrue(isnan(matRad_interp1(x1, y1, x2))); +end +% Pick a x value exceeding lower boundaries +% If x2 is outside boundaries, we expect a NaN +x2 = x1(1) + rand * (-R - x1(1)); +if x1(1) == -R + assertTrue(~isnan(matRad_interp1(x1, y1, x2))); + assertEqual(x2, x1(2)); +else + assertTrue(isnan(matRad_interp1(x1, y1, x2))); +end - % Pick a vector x and sort - % First element of x is in [-R, R] - x1el1 = (2*rand - 1)*R; - if x1el1 < 0 - x1el2 = x1el1 + rand*(R); - else - x1el2 = x1el1 + rand*(R - x1el1); - end - if x1el1<=x1el2 - x1 = [x1el1; x1el2]; - else - x1 = [x1el2; x1el1]; - end - - % Pick a sorted vector y - y1el1 = (2*rand - 1)*R; - if y1el1 < 0 - y1el2 = y1el1 + rand*(R); - else - y1el2 = y1el1 + rand*(R - y1el1); - end - y1 = [y1el1; y1el2]; - - % Pick a single value intermediate in x - x2 = rand*(x1(2)-x1(1)) + x1(1); - - % Verify interpolation works correctly - y2 = matRad_interp1(x1, y1, x2); - expectedy2 = y1(1) + (x2 - x1(1))*((y1(2) - y1(1))/(x1(2) - x1(1))); - assertTrue(~isnan(y2)); - assertElementsAlmostEqual(y2, expectedy2); - - % Flip y vector and check interpolation again - y1 = flip(y1); - y2 = matRad_interp1(x1, y1, x2); - expectedy2 = y1(1) + (x2 - x1(1))*(y1(2) - y1(1))/(x1(2) - x1(1)); - assertTrue(~isnan(y2)); - assertElementsAlmostEqual(y2, expectedy2); - - % Pick a x value exceeding upper boundaries - % If x2 is outside boundaries, we expect a NaN - x2 = x1(2) + rand*(R - x1(2)); - if x1(2) == R - assertTrue(~isnan(matRad_interp1(x1, y1, x2))); - assertEqual(x2, x1(2)); - else - assertTrue(isnan(matRad_interp1(x1, y1, x2))); - end - % Pick a x value exceeding lower boundaries - % If x2 is outside boundaries, we expect a NaN - x2 = x1(1) + rand*(-R - x1(1)); - if x1(1) == -R - assertTrue(~isnan(matRad_interp1(x1, y1, x2))); - assertEqual(x2, x1(2)); - else - assertTrue(isnan(matRad_interp1(x1, y1, x2))); - end - - % Test Extrapolation Methods -function test_matRad_interp1_extrapolation - R = 10^100; % Choose a maximum scale - realExtrap = R*(2*rand-1); % Choose a random value for Real Extrapolation - - % Pick a vector x and sort - % First element of x is in [-R, R] - x1el1 = (2*rand - 1)*R; - if x1el1 < 0 - x1el2 = x1el1 + rand*(R); - else - x1el2 = x1el1 + rand*(R - x1el1); - end - if x1el1<=x1el2 - x1 = [x1el1; x1el2]; - else - x1 = [x1el2; x1el1]; - end - - % Pick a sorted vector y - y1el1 = (2*rand - 1)*R; - if y1el1 < 0 - y1el2 = y1el1 + rand*(R); - else - y1el2 = y1el1 + rand*(R - y1el1); - end - y1 = [y1el1; y1el2]; - - % Pick 2 values exceeding Lower and Upper boundaries - % Pick 1 value in the boundaries - xLow = x1(1) + rand*(-R - x1(1)); - xUp = x1(2) + rand*(R - x1(2)); - x2 = rand*(x1(2)-x1(1)) + x1(1); - x2 = [xLow; x2; xUp]; - - % Index vector for out-of-boundaries values - outIdx = find(x2x1(2)); - % [1] Real extrapolation: we expect a constant value for - % out-of-bondaries interpolation - y2 = matRad_interp1(x1, y1, x2, realExtrap); - assertElementsAlmostEqual(y2(outIdx), realExtrap.*ones(size(y2(outIdx)))); - % [2] NaN & 'none': we expect NaNs for both of them in this case - % If numel(x2) == 1, we expect an error for 'none'----> Implement this! - if moxunit_util_platform_is_octave - assertExceptionThrown(@() matRad_interp1(x1, y1, x2, 'none')); - else - y2 = matRad_interp1(x1, y1, x2, NaN); - y3 = matRad_interp1(x1, y1, x2, 'none'); - assertTrue( sum(isnan(y2(outIdx)))==length(outIdx) ); - assertTrue( sum(isnan(y3(outIdx)))==length(outIdx) ); - assertElementsAlmostEqual(y2, y3); - end - % [3] linear & extrap: in this case we expect they work the same - % If numel(x2) == 1, we expect an error for 'linear'----> Implement this! - y2 = matRad_interp1(x1, y1, x2, 'extrap'); - y3 = matRad_interp1(x1, y1, x2, 'linear'); +function test_interp1Extrapolation +R = 10^100; % Choose a maximum scale +realExtrap = R * (2 * rand - 1); % Choose a random value for Real Extrapolation + +% Pick a vector x and sort +% First element of x is in [-R, R] +x1el1 = (2 * rand - 1) * R; +if x1el1 < 0 + x1el2 = x1el1 + rand * (R); +else + x1el2 = x1el1 + rand * (R - x1el1); +end +if x1el1 <= x1el2 + x1 = [x1el1; x1el2]; +else + x1 = [x1el2; x1el1]; +end + +% Pick a sorted vector y +y1el1 = (2 * rand - 1) * R; +if y1el1 < 0 + y1el2 = y1el1 + rand * (R); +else + y1el2 = y1el1 + rand * (R - y1el1); +end +y1 = [y1el1; y1el2]; + +% Pick 2 values exceeding Lower and Upper boundaries +% Pick 1 value in the boundaries +xLow = x1(1) + rand * (-R - x1(1)); +xUp = x1(2) + rand * (R - x1(2)); +x2 = rand * (x1(2) - x1(1)) + x1(1); +x2 = [xLow; x2; xUp]; + +% Index vector for out-of-boundaries values +outIdx = find(x2 < x1(1) | x2 > x1(2)); +% [1] Real extrapolation: we expect a constant value for +% out-of-bondaries interpolation +y2 = matRad_interp1(x1, y1, x2, realExtrap); +assertElementsAlmostEqual(y2(outIdx), realExtrap .* ones(size(y2(outIdx)))); +% [2] NaN & 'none': we expect NaNs for both of them in this case +% If numel(x2) == 1, we expect an error for 'none'----> Implement this! +if moxunit_util_platform_is_octave + assertExceptionThrown(@() matRad_interp1(x1, y1, x2, 'none')); +else + y2 = matRad_interp1(x1, y1, x2, NaN); + y3 = matRad_interp1(x1, y1, x2, 'none'); + assertTrue(sum(isnan(y2(outIdx))) == length(outIdx)); + assertTrue(sum(isnan(y3(outIdx))) == length(outIdx)); assertElementsAlmostEqual(y2, y3); - -function test_matRad_interp1_extrapolation_nearest - xi = [1 2 3]'; - yi = [1 2 3]'; - x = [0 1.5 4]'; - y = matRad_interp1(xi,yi,x,'nearest'); - assertEqual(y,[1; 1.5; 3]); - - x = 0; - y = matRad_interp1(xi,yi,x,'nearest'); - assertEqual(y,1); - - x = 4; - y = matRad_interp1(xi,yi,x,'nearest'); - assertEqual(y,3); - - %Multiple y - xi = [1 2 3 4]'; - yi = [1 2 3 4; 1 2 3 4]'; - x = [-1 0 1.5 5 6]'; - y = matRad_interp1(xi,yi,x,'nearest'); - assertEqual(y,[1 1; 1 1; 1.5 1.5; 4 4; 4 4]); - - x = 5; - y = matRad_interp1(xi,yi,x,'nearest'); - assertEqual(y,[4 4]); - - x = 0; - y = matRad_interp1(xi,yi,x,'nearest'); - assertEqual(y,[1 1]); - - %non-vector x - yi = [1 2 3 4]'; - x = zeros(10,3,5); - x(1:5:numel(x)) = 2.5; - x(2:5:numel(x)) = 5; - ixSmaller = x == 0; - ixInside = x == 2.5; - ixLarger = x == 5; - - y = matRad_interp1(xi,yi,x,'nearest'); - assertEqual([numel(x) 1],size(y)); - assertTrue(all(y(ixSmaller) == 1)); - assertTrue(all(y(ixInside) == 2.5)); - assertTrue(all(y(ixLarger) == 4)); - - -function test_matRad_interp1_errors - R = 10^100; - - % Repetition Errors - % Pick x1 with repetitions and y1 sorted - x1 = (2*rand - 1)*R.*[1;1]; - y1el1 = (2*rand - 1)*R; - if y1el1 < 0 - y1el2 = y1el1 + rand*(R); - else - y1el2 = y1el1 + rand*(R - y1el1); - end - y1 = [y1el1; y1el2]; - % [1] x2 is a vector: we expect error - % First value is repeted value in x1, second value is different; - x2 = [x1(1); rand.*((R-x1(1))/10)+x1(1)]; - if moxunit_util_platform_is_octave - assertExceptionThrown(@() matRad_interp1(x1, y1, x2)); - else - assertExceptionThrown(@() matRad_interp1(x1, y1, x2), 'MATLAB:griddedInterpolant:NonUniqueCompVecsPtsErrId'); - end - - % [2] x2 is a scalar, the result is NaN - y2 = matRad_interp1(x1, y1, x2(1)); - assertTrue(isnan(y2)); - y2 = matRad_interp1(x1, y1, x2(2)); - assertTrue(isnan(y2)); - - % Extrapolation Errors - % [1] Single query point, case 'none' - x1 = [1; 10000]; - y1 = [7.3; 2.4]; - assertExceptionThrown(@() matRad_interp1(x1, y1, 0.5, 'none')); - - % [2] Single query point, case 'linear' - assertExceptionThrown(@() matRad_interp1(x1, y1, 0.5, 'linear')); +end +% [3] linear & extrap: in this case we expect they work the same +% If numel(x2) == 1, we expect an error for 'linear'----> Implement this! +y2 = matRad_interp1(x1, y1, x2, 'extrap'); +y3 = matRad_interp1(x1, y1, x2, 'linear'); +assertElementsAlmostEqual(y2, y3); + +function test_interp1ExtrapolationNearest +xi = [1 2 3]'; +yi = [1 2 3]'; +x = [0 1.5 4]'; +y = matRad_interp1(xi, yi, x, 'nearest'); +assertEqual(y, [1; 1.5; 3]); + +x = 0; +y = matRad_interp1(xi, yi, x, 'nearest'); +assertEqual(y, 1); + +x = 4; +y = matRad_interp1(xi, yi, x, 'nearest'); +assertEqual(y, 3); + +% Multiple y +xi = [1 2 3 4]'; +yi = [1 2 3 4; 1 2 3 4]'; +x = [-1 0 1.5 5 6]'; +y = matRad_interp1(xi, yi, x, 'nearest'); +assertEqual(y, [1 1; 1 1; 1.5 1.5; 4 4; 4 4]); + +x = 5; +y = matRad_interp1(xi, yi, x, 'nearest'); +assertEqual(y, [4 4]); + +x = 0; +y = matRad_interp1(xi, yi, x, 'nearest'); +assertEqual(y, [1 1]); + +% non-vector x +yi = [1 2 3 4]'; +x = zeros(10, 3, 5); +x(1:5:numel(x)) = 2.5; +x(2:5:numel(x)) = 5; +ixSmaller = x == 0; +ixInside = x == 2.5; +ixLarger = x == 5; + +y = matRad_interp1(xi, yi, x, 'nearest'); +assertEqual([numel(x) 1], size(y)); +assertTrue(all(y(ixSmaller) == 1)); +assertTrue(all(y(ixInside) == 2.5)); +assertTrue(all(y(ixLarger) == 4)); + +function test_interp1MultiSingle +dataY = (1:100)' .* ones(100, 2); +dataX = (1:100)' .* ones(100, 1); +query = dataX(1:end - 1) + 0.5; + +result = matRad_interp1(dataX, dataY, query); +assertTrue(all(all(result == dataY(1:end - 1, :) + 0.5))); + +function test_interp1Errors +R = 10^100; + +% Repetition Errors +% Pick x1 with repetitions and y1 sorted +x1 = (2 * rand - 1) * R .* [1; 1]; +y1el1 = (2 * rand - 1) * R; +if y1el1 < 0 + y1el2 = y1el1 + rand * (R); +else + y1el2 = y1el1 + rand * (R - y1el1); +end +y1 = [y1el1; y1el2]; +% [1] x2 is a vector: we expect error +% First value is repeated value in x1, second value is different; +x2 = [x1(1); rand .* ((R - x1(1)) / 10) + x1(1)]; +if moxunit_util_platform_is_octave + assertExceptionThrown(@() matRad_interp1(x1, y1, x2)); +else + assertExceptionThrown(@() matRad_interp1(x1, y1, x2), 'MATLAB:griddedInterpolant:NonUniqueCompVecsPtsErrId'); +end + +% [2] x2 is a scalar, the result is NaN +y2 = matRad_interp1(x1, y1, x2(1)); +assertTrue(isnan(y2)); +y2 = matRad_interp1(x1, y1, x2(2)); +assertTrue(isnan(y2)); + +% Extrapolation Errors +% [1] Single query point, case 'none' +x1 = [1; 10000]; +y1 = [7.3; 2.4]; +assertExceptionThrown(@() matRad_interp1(x1, y1, 0.5, 'none')); + +% [2] Single query point, case 'linear' +assertExceptionThrown(@() matRad_interp1(x1, y1, 0.5, 'linear')); %{ function test_matRad_interp1_multiple1D @@ -217,5 +223,3 @@ x1 = sort(x1); y1 = (2.*rand(size(x1))- 1).*R; %} - - diff --git a/tools/matRad_fixExportedGUI.m b/tools/matRad_fixExportedGUI.m index 363db3fea..2669e18d4 100644 --- a/tools/matRad_fixExportedGUI.m +++ b/tools/matRad_fixExportedGUI.m @@ -20,7 +20,7 @@ function matRad_fixExportedGUI(guiFile,replaceOnly) % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2019 the matRad development team. +% Copyright 2019-2026 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -152,4 +152,4 @@ function matRad_fixExportedGUI(guiFile,replaceOnly) savefig(h1,[filepath filesep name '.fig']); close(h1); end -end \ No newline at end of file +end

    E%+\F. Etlzh^aOώXw[+lRR\g6MX{]gs!\L>9[38}?Uo(c0_f}5CAViI-|G:4Catʆ_VYO. !@Ϳ#Rא].ϜGU) :JQz +%Z3޾;8v7.Mٖ WٍVQ;^q]e],L"ly}$ϧNۿrثSʹ́{s[^f(P$\!s[vD6~Cxïl>ƍ`-E(Os9O|*U/|7寇3LDI6gcBO–.قQ¸^n4Y}z %g:=>ɩKi%/-ʼn/Dzߔq :c%Wj9 H_\mVdOZՖo/й&3 ϦY^yMOW4m\X_U Z=&6gX%ּ9b;m6>0k9l|V5ԐOvx@%秗^ZqV٩Y]MIwUN>1GUsgѠcG&2t' шOÛWi>(0k`M1Bj:(fWy_>z]=ZlWӲZA-?VւԙОW3@Nx?sxEVm^֐X鉮a||671|@rPFQzrѹ?^4*f{čFYz]K.]l:}޳S;qSI]o 蝶Wڨ\;j\mm.*&Rf]l ՠA^&}YJ:LZ:xm>unm)9 lfk^p[p?|n>8P/ԓ/U{!%m*VKYFsIk|^7,#_T+}B1ѮiBO/YT$ȏZ=ۊ+zo!_V T@&jHws(ʓC&Wsҭ[oF331:Ao([N}n{n0O7+%1ou ֙B3Z`b4["t{B rK&XU HtPLlE0;~ϞbG6%%Ҡ|#ӱī.Z/#b{Y, 革F{J4 m`z%a:o7Q/-F:-&tt `;r +A9iA/1jfMB4]ٸ JUwŪ^+*۰s5X}2 2]2هJ:.8ZMDgR R g:ʲwCZŲRw:it/ZodlTZ̫HءW~֋?|w`^쫂SlmBVLNlԆ)|XQ% ;\Zzb;Q>U<(e-[S)m~Bx306,oɮH睋-7 Ai"-OFV梈?JZqgþg3<4wnIWBou>]>ܦA^10Ѫ5%Cbأ$8װqo+}!/Fq`jwxͶvSvŪg>=$"gߓSfӓ/ anqr3 $[#FU"ǝ!y( 6`&:!*f&#:i;m=!xsl{4EB2֒4+'B}TH:?d6)!C݈Ý礎k󐼉~-*N{_y{@Jk*Z-!`]_m잇:j;vKojmY +sѡFaeGMnc@鹆,ZQasc)2viSJMXB"CH4Tj1ln;d׊Pz=J}_'EukոWqv6D> iX P0 ,\WY<ϳЯDkn.Tי9z7:n1c1?LWEeBhcɞR6w¼4kFK[ H$v?0EVL5dtC/ő}R kX~L^zn慝\6ͦdVs[ˀnm[a@MdZӜ!دlfoU,תw`ťT\z E(ߐ5QE=n8r'O2us|U6[x*RNłbМ\j0||#赐K4/k.DO2(ˮ-e{9sl'S ٽ)Jk(;L߽:uxQ|J#*O, lhKaSǰ{N6xm c"tˁv&߻^F'\`0>[Byظ`X\њ+J--&K?M +R]*4\bOvs@ 9JQA{" B7͇@Gr𣒾5eޙ(?-,uG 1lTRޡu0ف59Njz:cES}r]#Jejl +gµ1sSAZKn"b9Vϟ~ԁuwh@u@~мZS|@VC@{@*@3XHi~WU%ߔ(\t6nd)p9!5gxv~Oz7 3E])KRG + lveo-rxU]WޜhMMJfl+<4-3_G +%"/TKi<ۓ -{rf_ιS:6{}בcO|f擏DŽqXƅ5ɺjn@?!݌ge_6IX\]h؟b%8p2u)=-,3s쬄L,}h]+\UMQ߷l"}d(q"[?jVXπ[{:յ%_nfeǚ'^Dz+'lwNz@H^l0[`sdF{ņ7A ׻bz"5zP]%Rّo~>zE_}9'7% xW'lu[G Do ѯv^ ggjȮ hwB]Ŭb?LF>^dQq7FZS.~.nqki +u{>3D\Ɓм;mb+,27>2(l]}ݹtx!}hXu!fF=kתim>;Q=nR?{'ieΞ\+yͲgEZ :hG<OwrfxFFwG}S#ZQE3Yg%lm{wT=5i֩DOX~#:ĞMmimD6j ./{_rvIkԮ[G`l-5k[DgOnNuCEᴛMkW.Uk|4;-iJ1,f;G$7[>3Df ];SDZTb}J}"|m.[!,QH 6%'}~IJv~fZmނ +Z +fMH c(sʣn8;c7wCgᚏYlE%*@ĂTumf# [2R4@9(w\\:f4*:nhv>r]֥dtßW#|zKծ[Xe$WS]ܪ}~&wc๨ XfwA=BInsUmyV|J^,s^us}-hyU8mЋOemn/嶺jКy{[ST:З|ZΊ]A!ttW(aX^ܶ@!.Jt nĵ*(UHWc>K7#.tme +v[3HtV|#2'q+&<&`1¸IL= ,Ztg}5C7\o NwVIE\K l +,*Z32rA q^g OԚPfYWgQe&_nMw"vj6.sOs v_[qڼqٱn{o۽.?ϱ)yN,`Ҍyߍږc(țņV=!fgrG;wݴP<8ޱ pwq;`Bisȣbu-ME!s4.#lXA-K` Xv<e\lUUF3q$m9ߊi}O(`dkGЍdCU_aW4\.:L,4#_dGj+AxJPU/GEEJCf9L4d@ۡiO-Niժ#jlV2*>?n6q+ig#AϘt62$%d1HSZWB.Vwy9M#r ?ioQrw~5/3yR&=EkY˚﫣63E{ϿȗOm7Ln-d1,;j>u5RD$V.0zRYHg\ "^ѬRB2Kʿ ;=G wo5h4]TMظ`jhV%ѤmG!JqKkY,MqkY!N]vt\|HSoCP. Ub~ O˺ڣ䀡Y9Y3 +z(G7))7L_Lp9h^H)I^S#FvG|7:g7㖩'r6U Ŵ,iț&v +N/T|DϽ[y+W"^.FedZ ls nWk):v/k| 3Hҍ'OrPA͌hK_INiTfX)ymm~gŤv7AygiX)3MFbS &@5Lq h'ĢТ=&8XhlZP1 {6 Ԩ/Z37wy +6 '[H~6qkA)Zg ]d> ٿd[BP{'cVa{#>%B!CH4لSW(Ô~ O><@/ӿx.>߀x)L2Gī?;3Oa ƒ>DB!P@T-@7Ty;_1ֺ%c|6C>/Ry)xInYÿ(HD?`@Y:E[J0)0olP@6 3Ksp>`>x&{ +]xB:c;.7/Q^"@2VPPqpK#~jxJwݚZ;r_=d6UR͒Y98#gfݙ(…̢fPngَe"$WMCwd^`:V/`v`6LPfۀYezxvb_cv{X)B;]c7GnVw_ٮ Z נRASAeD۠"=cPbTzTTp8PAKyNpVB^ + ++mn$| +==p7xXn]:1<[H.SWz~%sW(E$CѺRP,Kt~&ݒe#k \:lOέx~ +aOH?^^?zߛxf3;ϳ'7O~bG@(Quƍ246i_[ ~[v]"g%ne ea󁚝taKhsΟ'$3o/WL~q6E"V \۲/~-Ɨ*keq:k?"]^&sbqM͒w^vzs\kyȮ&W5쯑(#]W#JJ0Οx,Рl7,2KYbz3nԥ>Uϫd@gv'4&Y!Jn*tU9zN2+d]'R\9uIeyNj8ydjwwݬ]ʺWtRqݐޔjƩaQɫ;*hx(! Of-_+5~.l~Wx7|H? fI")S.GY;ޜi[z^Θ ~SčҨ{{Mc|MGr/rv +]UߎTᶲ,,o.\ '>qa@Vp+(1f+mSȚs^@̙Vä gZRkCs Im!N:3d?4D:ڧ\i txX& +ZCvVҐ3n_} ;^{:y|k&f]csj)SIkB em_V|o5 +Sfc(R$%#Y~vNx eCUɖz3)Gx{]i*vj-teʼ{iVYR.xS۾&~/KIڲE%yZ>`զ2YPי.] +"GAEAAQEq'}?Olr3ddXލ: OV4+̒`˱j'w(Jn4ҫ[ܝUJ')_阳ol /Nm(vjJ'A#cV`^sYe9JJC + 7veR-u>WćHŴ=۝/U6OocۦZ"n{5GHt~+=sI5mr?Sh([\Bkʚ5rx&Lef )G[.%bv#Jq~5 0{opQ׋RlNѰN'-=[h}Bze6R5¾.ES+"n?%e['IWIR{3}Y,*_k-LJ@b_`iن6Lγ"V. %\e3Sݐ{#U$/bUG /g_ݨB +*ZQ!~~nɝG4ɸвpq8e`ui:ѻ Z?-}vuZ[⌽: +lzܙiI}&ġq8b*dxGgr F>˶Ÿngx6ҷ-VnkeJ.)cJJ 7LdRpਂqێ63|̹n N=e(2̗-T7̄IΘ#F`2ݓ/E}ԗ6"=Jc2T3~93)C\\E13zrc;>ތ B5yPux+ҙttU_ %TzPex %/;mEK~PJi}G@;A'Dt X\pc x(b>\3f6fVx22õwA:y6^;"ڠҮ붫$f*U[rqΙL'oWڡk@ X K#T|b3ṛT–,%3D7m>Q+J7wCn_?C2fmK_&dB(/6dA[:Y* b +>QB|Lq\a+~ĬD6:wPru\H~,} i3[lo<DK77tnu[>nc~՘vFekfcp'w#FF@p #y2R~s8 .ل|u1xxGlqru34 !APip@#ȘeuT~khPM !@d~$tMRXJ'g}s:$>in7r };Tg'} V-|sV)R-<}ODxcQj@[WcM@8 чWSnbcb)K@O@,Hu܄!N2 W)Yd-y[Z]rD5!y+srUqW^M/tzqѺLbV<T.;IQZf!@kTO#?j=H[qӡȠ ȶy +cu!R4sѼV/J!SƦD컽?G7'=R1D6Kf hZbLq=Z;h(?4FޗVP5Yw k.?E[/I-{«iΤ4+MʹD}`+۔f6)R$yUw"%v;ԗmϨ]Y7'c;w D9ʯ],&U 4_v=ǣpK7~:pWDQ&Z߫[ԏPrk\NzBWfD.G5_F_>LBZ:Жy ɖstn!7;,||[ [2??7x9U6st(+R2SHc _6A8c=+[${^byju~he0msE_fFA%:<~>ԋO^Z:|n~-_ØX!k~N+#ebc2&' L~S*?tY,3d9ŋOmħ2S8İ-O7O("ba!S'I1۵d+m~Z9 P:/_9zV}|PBE:%gPzܻG-Ȭ;=Y;O(X(ȏJBE#è4a&vA\/U`a?쟳I-o,Y?j,ԽbHT^Nx oچQQ 3c=?˦<02^iQ-0sf-~&ɨss~|O*]XG~7Ue Q_.Yu8F{0{tXJ;yg_G>z;{r1?W3ҽt5]_|3]>b=ǖ~^sYAyZI{bYOyyWʼ>/r>,?d 1ajoA,?aNWe:OIP&UV޸zVZe׶՟v2IDkݾ[+74GٞzeC1 ?=@̭O.oߣٮxYs0`ϸw̨؎Q,G!c#Ke33EλMz~neO5)2hjԣKW9&X}ܻ.6h25nmWVmI3*aWսr<ٖZQJV fSRT{7-hg,LJ-;D1귉RO˨ñĢp 렮f˹auuݙm]']7 +ܼuOftв_yPdK +NR4V#w l$/r,Nۥbvs|2e$?tӮ:rz"4<L +ܴ :>t*laӅ4,t~W}ִK6}Yybzޏa2=X7!ހk%jtVkWngT{)= jEQX/ e]{ +i3 +G+Pta$ML[7~.lؼL^n :qX3zrNL1礸8΄۔?Ǒo+oQgHn{SGMpV39Onò؄Qs򄺞W U (X o_t3ܢA>h>|v=+ {p#f4'HśC[Ɋ"d3VGs:gY&l,Xߙ4"Vx~z%KlN,ncbVD`TV4PS:PE:Y2{_nrZ.u(^)Ir+u2_`ڭғwqԢ+ѡ|[r}E!bwHju76؍OOt#t=.sLs84x綑MtG%` n}`0=%'Y?tilUocک@_h 泻7fk_!&~U,(aM1rȵKTlf{}>y=[Lm*8PED`6/`] &4Of!3p?h{vjWѮh-bkv|T*Vke8kNT9k?O )[8<.^B;3b/S'Ch"3({0/#t^:α>[cwpH-W"/% tn>Ifm#C\9 Vc ܻrVHL&8 ~:iµSna3_?~S5`WFXƗ;6; K=p'hZකTRRYk[ܠq'ZfBLO}*v69Rw>؋J8{nxҤ%q8 TMDd(Jc=DS)@jFbi vl@T~ˏu%.LFv+K+Eyf7_D~ pV t9w3FopT9\ j;ŰrDrnɎuNqQdR,5@ZPDV鵾ŝk@96ײ{8bkq'4Ӛg+JݢsQyO.n]Ν0EebMqjVC5 jt?~FҘXE)欂(vfecdW}2iM"r 0k {wOׇiCOK@_?#(H,@~ nhv]Omtɭs#yyҌkt"\Qjۑdpr4%K-ckh8:Exli R U6`khEV*/C7+ޛt:vrc KT\ˠQ0Y)6o^Νa6i]i|7:[ )6. ~<2)Bh|f.mY3Aw;jڳsNv\lb@.=e܄lvi3+lw7[> 4;M )LGogR(aŏq9ֲ?SMNQ%Ġi6#s,fcHp3U}!KuϦDjѸ@@ >H5RnI#&QmXֵX瘝҆Vz$ŭ)r;Oi?x;ӷ!Trq7BsTܭ8i !(/ZNm;2\}9%Ư)9I^iEPH:d<[L}VmkKFU͆YCy~? d>"8z' 5N=ںMQW>g}>vwȴ;ZѮXZ-WsٕÆk~Q X,n]|+t?VG3 _6蟜۟F`dxyz1mvBoWեfIC+/^[ޘGy9 sp/&p՗Sr&~C^ȣ~|qXף_"tOM& l=XKh"y7Xjڙ2:qr"yI/- /ţ}1JryHv0,tg u@lqz [~w鰟iKӡd~0ʔgۢ>]kAkd}?N8t$U&n)y .y2mCCX2yUZc=k4銏h!=hwߍd:`?\Ƿ<ΥAnkB6lv}r01G{mghCWLNdEHE< nTmnf1T~E+8&s:֝=߲E"OKL{y c %FStLoP9ɥMǖu/VWԑZ Ut?t#ZeaY1ggWZi}kCVd=7.ڐ2|Jf璵T*eTKk oo#hI|Eru聴 F8 :-l!7wCk:Bj{P +Dq!|+ + k֦Y u<|<̾ nR164Bc`dX{j}umTiv`֠O.C& /TWm-r?4nIt+l%( Ͱ3DzdzfԐ2LҮ{J>}lߺWe2ʾ\}%bwnYtn3][\ބiAeF|E=ˇ#+ ͩzT6Z8 J0b>S%b(ůS?J.@|Je zslV'Du̝ۥ=W3grɴ9!N+"x8 + z<e*K y2,8Jm!ֳrp;eEC-F9f$OT+e)%%~bjgjJ3EN0"r=nΧSLx QUǧSJ;WyլWJ:k뽖a\#&wq;0m2fĦX<,R\DpMmdeHg7`L8/_luft V7s3%x+~JYnjC|`7Uq>iŁkk$t%*Kم `s,bU/F%䶋t2_19\g/\lw!p˰\y3O3Ϩ Ǭ +*كn# +ѫv 䇝EEf<`U.Y]-2´9.C:dR,p~8!hcs;Z !G5!ԲheŽ׃.gB>E._2n4YL;Py}_.V-M=%_kM-C)I,a) +1psN BJ +vM>bۅLZP:Y$w PFX:+$ um rvzf!*x ȱ`7 ^}i>Y!U՝3'.rwXҵkW)->GԆoTz,;:b/4'LI>CP|``ye% L{yeqo+=%q(~cP{ :SE8;y.IUvU[[ ;\a}<0+IJPgks>^)hN[R^``i+K1v+l!b;^kKO}cŠUEK|P1w|G>,$0˹@A2D]n|!+0ӂ?q)|9sҦ?!ʰ p8rz[@XrۍGQ3܋q ҒM_^)0#{|]|~_ A݂꓏ +Ⱥ_)e@: HMS/V;Bn)@/J^SP >)zҮn; !FL?l9HwHO3gW98':ťSvz ҟ0x3a;5Ų/MDcY.x.?%rPT.w^І9;1ac/Ug7y"9._N[X)Dr 8?m7* 8qz]|o HjLyKX:ʣ;OWUf猌޸6Y6bpp e?F>{: B[EG)^ h{ eq~ڃ &‚]5|p}2O/w-j'7A_F(b=M S\4 V62 )W +ucVn\lؿ;i GLN_8z} +wr&y@~ +@Y-ys3ԏ_=IJvZ>XbiY? ʇ=^f]>IoL'aMi}_ynWAoNe"ۇPouezC;%lbPP={wXnp8gZ+W?lRޗ /ny?e^Ny&fqgc9^?qo`?\ҡ귒χz siZ]72t32cwoEb!բղFl:jqk8!0nlՖbe7j*N9VXXՏ3\n[PX_gpտ7-I2Io#Xwy݁cb娹k3s3/VOfXYYױ  3=i`=[]wbsU8f)oӲKAh]"ia4~r4! ~WUk˗z)&pg +GQ&#c=ae'wa u&vU;o*0QU@Y+O)ْuq-QgEܬf.EhHqyT;YfE^O'NK94.wn1?b_eψk5 +^{*ӮSJfX@{HHT#Y:+qG>[ e[ ,RoK$(]ˎ`D ;)irʺdހF/?m;lK d=Qr\RMjI^ZQU)VD.{+`{GL~QslF%N0d7y ,&.3s>.]]< +N a\V^˭Mgcj:nZČMON{t1< Z1)ʔ-F|Ux9$yHU靸pƑ#٥VX|rlj}%3~\*a=}{]Z{t l5T/׮V" '$\~?I5AxE$~RG|$n&6k}Sew.>h=ƨ'[ oekD焷;|樖eW2[3SCgQy(sLv)-i&#!;;6 bնp `'be:WAgk^{3/֛)[nlTDz1Ӧ*|̏y>F}h7:vjVic_z gZe7bQxn' L;+oBÊsixO8b +ρr|G6Y쐳' +-|WȬw[;v} ]*]x8 |j^Ê*:jIcup{K.@ʧY)GN&oS}l} wAY~TnCc \˙{l|~ +WBXxu'{((T)ëj\쇟LʑG6 +u`'Iٙt"\g;'; n +\NwGXB|@P@e@vllw<Yl()7+{ :m7|"k };Yj7,qxZ{ןeYT$>AIzv -XPfk2je@[*PL7\T{>@ +9@^#=UD ).@xhJ@ S00.Lg0C[YkbW7&P.ݬژo vi-㳜C G MQRE\!=Eb?MYt|\Ƥ@F~\ȡȢHd)l/8Kg֢n[7h=KoqO$ffm7Oy#6-W^rCBR0x[jfr#!JAn.Qr=hr # r%W5ujW.aCil1U{c49E8||,YtK622xtRyMz[ ,XH:?Qm"BR P=h̉<; wNKwWѮ ΆY׈5m7QgE+u7z6Bv%lDc)j,:=)ž\Oax)c` +I\ɯ_(,zv]ncxB؎ jw+8Cz&X׋j{vmMfk;gQ#39<Wo ,g)fE*ڵύ%,\~/{}2ŎIb +MN,jJTZ,5@1HPOGUy6 ^ "b_%dٟQ96f8_1d.z|J0z/]+kmn17yT%.,6ML!o>@VwЙ|ŠSAY3q(2ΣKE_m],vŵe;IeTU=J@APqEq '__UUjZ}ާصIFfDD2q.rG WB$DK#F`<)3bZ;Avk/LLL,jwq>JГrD´xh))`r KMǏXޭt2/0obѻ[OI-?b 8l ܋jxhxhB Z+ܓ +T=X QD],ЋuϮI_b"zg?RL<  [e:XC;/6.v]g*;/צI>4͗vnNK wXc= ƬM9%yyu5+|,׊̥/kغ_\U IoY,Ь*}xi@K%,[B6cǽsH +>uK^c.bz[5 +x#Ag$˺7º]y+ С]RVY7"ǼLP *n*${T22`Ccj Rgu7PA&~I VR'i٧]]|J^gry#mV F#w(+l&pQ6:1Mu.̺ae%׺vG7cn+H*YR_111BmU TݓM*0'ŀFtBYڥ ώ0$QmOf(OTt&w.|^o"ʫD=Pmi4,q9QW-ݚN| GuMC=ψ<?ʶ!y 8n 1BIvKr@M!VI0NuʹvV\=\%P hAܨ}p ƈEpj\/9:6P;W0(5p 3 I6>́u&Aƣ2@ ˾ +OXY3 Vʀ DjDu}T:7zĢ`G΢tV{/9a3$Ѝ h{;>"%idwӃM(d=x BdNrHz$*k5CT+%9,-Nj@-||I(ި+:ϗu nkĞ#0PXACl <1IX3IY#8E4_̿Vq&E -&YME@SxPq(Q L5|Ki׊vKxRvX!q8\:9f9 8$@de 0>S]d.& ;MRwdџѮ$ l$$ƅ0fWٜÑT`O&Rϝ^Vذc]"X.4-đ*F +[\耑qH:A7PNM&c阺up&IL2+jDL%v G=V֫g3 CPkF51lq4Y.@E}EO:dJ QgjGS8IJáZ8p^$p.Mcѹ^sxL27&=%tY!s,QуhG`|oǒL"TT]Q3{f@rА?Q(@>B]&yC?#97|ˌM|&M˙ҵRbd|g u\Kvl+9$F>#Kd".Gٻ:J4ah{R Add'|{&jeh%@:m0h$}sk_Y2u:@{slEdi./3Wɧ|BRKfGjhF}J0#GCF i#HsT4I+F@E؂N{4@#P +^ +<u@ GcR9l{dg>]͓Y9\KU8eC{|!JqՄqNH=&Bdr{Wl+ţ&Y+7D Tv]Zi@ )?Y ±JnJhye#nB[~:8pqmzޓ)&M|U-,zOLz eܝ64@`ou\\+Yn7/ +/*{8};"ć_m">1d !8´ +ܥm6>G)_TMpLj,yiev{`<_ :`1y)[gҀ͡r!`]s]چ&O8#:[DPv#rf քi -$ńapoL.=oofI3q`~r#a-Æ[F'sY_!,L;[cr#mǡm﵄8~|ǃTp]{u^;։ve{)kՒlX]2)\ dYgTvMYh"A͆`Tý }xwm;)Qh[ZtMO]M$.#}e6|9^Yevs#aN`UnRQ%(Q@gܢ}QXàu?":2ZFLE;[t[]?ܡ|˨1V+4/d ׭t3v j&dDg;k+TVc%+Cj6vr0ƣ}Zwd(mjNEA 6Qd צA݆yf]?KZ6}<ɒJޔFR+N@0%lPNjgoz^n͘vczޙԊTXbL%w^Vͪ+*z`U`uZ Ԋt6Ȑnb / )d煂*EjŅ^ 1:K֎CS+%vͣK-O.)hφP9iej&BUbIwMfL_(qhQ8xmR]~bWBǁCy,V,,.ʜSd r2 s2Ubx$әӗIPv3NkZPR$u*d35CvL kgknK +9A"qm>K>c 5DL+Z/>MOiX!)t9gJՙ'IKEZ^]mR;b,b,Wdl+zS<^e=Dv}M +d𲜉 ZV¸7|w3H,4y\ibr"z%“H+#×e =0?ozи1'0>BI:M4Ss;H{Lͳ +vk CɁL#!%k9!Q 0ƭPL]QEX"dQz#|o8i;eu7C02 2v~)m,,sQ}VG}J)Ug=8Ղh}1> Nw"|E1LKM)t YĶ7B\$B,axyt[r-t0WET cm+_~/6qrv6)k۪$eX$+!F +;f#$ϼh^(NEkKK^6b@YXhnr|HvK-UJbR pRsыHKmj8Z7IJ CDKii9XNNfsje #)LcK6Xxэ\>8]:3ct\e emh{g8LW7scA>ONǤɻ҈H I7E%B^O%)ZXΣHq*:D Zy1=0Bvܸnœɝ]źdT 6ɺI$$z d գN.Mz @j3 ɸC7ayT==i kMSǪR?6 +Pԑ oxsþC"r7V|1X4%LFc%ML7ܭۡ!3\odH lJzicȊ뜛Vts$e\p#WAN=@܈( ZX{1l"kwN!hǾ:2e80A!Dz]Fhkkɜ+Ƙ}.ɚxbJD>6]K*XDk8- oXq,MNhCz2bt\SؘdzhE)О' Z:6hp NT1MU 'g)@I$'0Q"urYY2EUo ,VS^,CP$ NLz3M.WH>' +` `ÑpXa&)&{&EZ&˙+˩f|. 0PcZ0_19gr&۱DZ..v)Xkk'7Vagq#';69˞LwgmEgr$d;Q5<N68\$5Ol$hSJɆ)f#F2ZcA&hEIȆ sm"˴ ,VAph5q8G+3 Mm-\5Ie~b ɜa3V+$fcUyGxLU1OYiT_[NLݲKp3 +Ԑ$&(@,fn@ v>@4I J2 Lil D0N-@P-yࣺ VJŽg YPYX[XA zU?Br% %ws^E? w#V4bUțd9 ʀv0@* H$IfifN"恈\xL oo%ݼ߁8^%sAz0UK]KݡvArf{P4ɴ\X`sVǪ5Pа(:OpXZ5Dzs[\.G89}jNIb_$ńMu,$@I&S@CV!dy hCYK'U|QOAXQ:-8_NBz3Jw}! +A :v)&$3_6`I`E0k. ӹ NS31ۚ03:7L1m#t`>Uu&uFPϏwH9T}PIk'yK11"l~t(${+lXЃgo~<< xPǮc킨8-,xjZ gZĺ!>>uo kc~ Ѿ'QW{F#|r +jNz:F2\ig05% +-=؊w%ޢ)& Me  + ̋DZy:LR՚h!68qqAe'#C:l]5uGM^qG VVgIXOG.&y p8a4ۓǹLN"8B e-kUFj [ +ozCw˖&qX'#=l{zJ8 ;Hwׂ)*t2tEbTtnع~~Ʋ>=^lds_^ޣhYA'(gEd_ EwG \_p!oҺrMrK SgyT#N]{W`,hAlҞ?7o6l|fBm~! 9X7o6OPP~$u!78ldn>pj8ڟChl|fj7bSȦ^+kK~%'eowi + #6[ᇶ5fq7l|S&1 ~Z_^A=XXY P qxdMؗOom}ͼ]_ f6B2SuSrJ%Tzoe7o6lޚ]{`P~z^~eCR˞2z+6.h£/+K92)h+|%ݩjT^Œ?_/*_k09C/,z /V HkS! ~کtP}wXl6l^P'ݷvcNS+O.dVvT>8\/>\7_h~sF4'$|r+bimUгRvX?:i+$;a ͙w>@q#M΋q/_׿LNRUs=s6M)&Ow)D)gq9/)jjT8T %CS0_l m.Oy14zN`=[Ʌ-ºd݀=}}ԾQ␟/W|\f~:Uϫf]/9N7)XUiHNx˽)+Pѿvfcӌ=T`L6io?oʆjg,w|ΝK//\70&>臨C|n\zʢU|lCA2 S/a>X<9kB: 0yӞ;_LŒq?WaI}%n* k4Įm=8pvϯ$ +7MT%\j_ L& Zf+E\ Dc.J:g̺U˾l;y竏[ +֦kiuTOG]G,]$!ڔR~P1.8߬+?_fKkk@bh 9>%CNgC5qJ{!I{]VգXj +0}d"7 +3q2md;߅)N甆',bJI?x& +R~X v/* #j0u]6EWVrgl؃£skA%vgskLk`{p(h=nz7ho"?鼙O00T0! +%ku+d}3^=6YGa:Sbx{'o jT{bҏ!$wlw?\g[̮$\و[:ߍ_4n byxJ ʨ ;G8_ؕUAr|?L;I(+NQcH>rm򉎕vy“aRPt ̈́ +[*sF?fjkF|ć4}PG5C=̨(46~ۇ^]ZB,޸>=gwC +}JaDafG;dk0c..NRB;/Vha>\}/]\CMƛ5"?ll#Q;{뉓S%Okdcxi(T*^G1_xhk2gjh~%!l]J8=+i +^#B:Ҏb +2򿼐r>m]k-ofu)@,}4-32d=k&$ovtQPd*q۞z3PMLom>yd/U}nSx 㱊C}6S@r9^䢾ZN!CL280 hfTأH +ֳ\`zz'WH&a$E SZ9Vx~K6?&7bÍ!d_?\j+~q6Gs!W>uG]a/|EKO^k_- +g.-[,<ԖON L{22 C :d6Ƴ+&* M,$8ft£8V*md.k(tSܖApe8yV1g!K֚v[Mtl`"vwMʀ9yM?m,;ayO"fMo_sΔ~Rzn|%ssrU K.Nм5T|yU]P~z!0XZ9:>Wjx5n ~}2In7:ox9K{|L+;#Sc샯}`RVÞ|鳇kt +g{r[?7X+;l)Z)pݯS|zɩu"MiBQNf' slo6Wr]9^f?w3(e%ڂ;kM*a> zxpo'b Dn_9kqÎO$,\H?NCiIw_qɿ<;1AWK 7`fE,CXF9QMcTc[(excoVPWD{ٕȬ,+0ptf(OnPp7?;I:\J +gx_n +|~`ؤ^cLcKGbrEBVMz'ph)7kp&~.H4xlj!z!S=voXFXgWj] G?Nh>(oj<kNJ~īZʭ{}`ȑ҃&ҍsj7E{ @G?61r{9y}[O4<|yzr`D% T4Tr1O67m4'/w]X*{h"竴mہ'#ՉX{Fs,ԺX{SG{3K . ʇ`=8iy[TU܇ujf؞7U/kp%>9$KA[v[}BAz|{|G'ljOe*'~+Z_D8O/jZ/N$&t&]uKy,b8ؖ/M67p o^e +E)-Z?jvMo.l"I7727=!Fȇcspz^>"7g{k8{ynlӃ7"cjxr %?QCLN0gg<֚]^>;#jVϲ rUWXٕrRA4^lxX{-;{y-pV[ħM[>`c|Y*ndhԨvtqn^$/7v jXQwp UjIPY?;y0CǕA2Pd{S'BeS=ׇM-":vYfgeNB-(('ԾPk}.`t;?حfA%R7pGIɒ@64H?dj<Г7l}6ScDN . d3q Û$72iÂ禎hf_]}5/yV̩R:irfZ-2~z1{]Hy2fgm">2)/na\q +So SLEdzf*jd%oыAX_\ϯc|H\7a:Yy܆tZ<ܕ6D~j^zj|<Ni2=nH#R &nŦl@&|UtAW^ϳh7o6[D)[hi|Ӕ=IG835,y/W(xfP>G~jy噪 + ǫw|KM~@PUo/,3|5':Ԥ'ʽs2>4"tQMMSx")B_&ĐBc:/[>T#Pߟ̆ԊDNF^*Ɠx#\U>Fx:|Dc_4 dzBR{n_q&|8?W2m) 38ht< ui4Iq6YsM˶Qo6lُrk)v !&r|pvX D#91?h_Sf{zm}K^}R򍤽?f?IUU.i&Qr +$pk)2quN '$Rc9bN Ѓ, >ibj>·K9z,@]M-x0 Ϋ{jźXIL?uX@.פB-чa?I?ہqgygV7o6ñNhi1#kͼKXtSu|XkqIoRe!h!\iTV HJ`{>O+3_V-wZgP +\2p9*\f?Z?˿o6lw{bPre\jɦ.m ^I^궴si\}qB#- 3\'ɩӞL巑N}((M9;'e&v~n7?=È V]۾3O>dsLGZW/j m;'t}k ?C3k;'<5vvOr#0Oi?6NmÏh;/ߥ6`Ds$?&`s?vWv̹0]9c2ӬyOinܷJ~#i\Ru~|rT_ +䮭;][!ƕykMy9}Xu݄o Co0Ϸf[8Li9l"|Ik5??I?n泇Y1[3oiH^OZӍҾa0f)^}uv~o߶C3r:/sJWjIoG)+? 'vMRNVk#𬹺5hUߛft_ j>n'UײO3Z7Wc0[.LҍI3ۓ7j16ǹ6/wX6|?n翹'[ݫwof;jkwˬ>Zv;4eNgW:A}ӉqJvKSrޫhA~{ ?hwcso~o4~?haϾ'z9M Nfzd-cp`M0؀18{~ ;%uR;9|{͚ƟZ]%*K/Cc26j‚Eu,5nv^O[ q^[?l +VZ躣%F SML_3aQI]Q,:sc+M*G͞juؚ=p<J[(Bvm̻ZZ3S'GpX J56?O6 +:I֏P{g Chf^ӟwz/Li C0SVd R]Wo܂SnŪ'ύp?-CA>(bһ*-:H=wqYN*e(W +m]d"{01!rD +dҮ̅ruRޕ>&` .@ ?uR8k$w8;fRE1S?OO* +fWs;7m;R;Oԟ=Z,0?q5KƄMKg>0zχB5\~FœDd` ;/*/wy>F?jOq4~zδr;cdKlT|I'_>p*zJ@U|\/O!Z澢"o;] ":_Yү8qtABG Z.rQ zR0"IO00Dtr/ZLEsW])/nj|SJ>'Z'#R#/_E>=s9Bã +hQg⯳BM hL?PT`I}}WS1F$4+[ $oP"lɝ,9~ϴG6+t,߻>+;L}i_dZ X }i;c|%SI`^hм,-Z{h*!ȀСwYZL_\R,2󰂾?;j:IzT"Pԟ\y:KU# SߛO[ˣL\SS;P>6 3ZRn⃢5B:y4H䨗2*y,VD"[ +ѮU5gk(s6֤:P{8'3q p]!eRxW`Yq)wi´p1 >5RVu:H~Ao.cU :'u_ifo|x2i]L>wJX]3?Ux)2-&L'`Wh0`A +j䮃L37?S[U7_Pn .u[QU>k*s?2WI);^(D+kgs `Pxlìڧ?F#tAhxhxr nѲ -E&PZMTd*,Z@%%L!v1JDOSzmZƇ0IsP% S6(f D+C ҂y 2!+ErTt@i[Q.f l<3||M5v)ӺiNJbc7su}rV,[)p wef ߅@#ޯ +8J,_dEO ;:C=%?Kb*-\v>4hAfܼ4߂xOPyj2WlCv~1a._Kx6ۛLz}o + @&R~dj׏nOF`hD_gEx!K=GYRoMT1snF.)}8ErϠ\pͦ1r\xJ&RJCE% +WQQ^', f({Qx_Qq0f+%"ϝsbA 5>r0Iv#;)qZ^l$a}ͭʴ:_DH%zWs sa8 bz5{H i^ U$s}ټ+@Qz ^M]`G׳#O:%(H/!XP-;C@P7=t(DP~\"?[C`ja^NCLЊOk#]jWf k3[G\Yg }ο+kЯ.h- +)K޺CD~ߙm ?T9pAYWkS(Ca@Z*K2 ͦIbJ2g\7hbI6cb$H?i1ydO$HYnl%ʇnt]&۬T,%pap: ӴDR 5x}C$]+哢:OFgWD@j>o L}8>vblxQ}AMY͗1d$=R{dxLuΉژ|uq[`Ώ jh? +*Ig +wH)%<E˻LF̓L&>`}#:aܖb+ʤfҟ}<@z] m̱,XL^ѽzQ`& 2>oF|*qNN~ć~NDߛz L+ߥ?1gba:qu#lF}hI 'ig \ɋE:Q[f^..tQ, -muw>Jٟ1G3ȎjkݖϜ[%Fs? =gFKM +@C'>VQ$uBW`$m7ZX~5Īzp!YSԯrW=$^3?#0 eL\Z  +>s  􇧘IzC +b&O|&O zA5Ur1B X54{E/@C{^3 Ay<^!ҶZD0N/ƿ/|Y.aÏi8L6J.fBU<|06-]8TGhzHO$fU;+xVSl֤͊k&r%EaS5˖iB=Aef=doXސPX#iKHt7S@0ʂj~лT|$@jubzڴ`jCBR~XR|iXlsB㮷V[-m;%kkЄ4d0<=3hQCg?D@p F`| k0C@6v8fOFN,tx7=9lp@HЁCx.?=,h/'H =H0O>ÿ8?a@_~Q€{>ƽ;FqZ,CY}>9PϥyILt:#R4BPtNXeO +lxGrr)?\dOqS +AVdaVc\K#k[~ SaEd/;Nk~pʞG4*;oNOrxr ѡQ?5:sx/}U\<Aꂡɶ8rkn,.`tw~b16?򿏸TRS^&0kE*V>']aHV1IӾLt,ay S +1"Zb>?]KB0elXqyǼNF.1+Zʟ*9\xG"Vbog^oX/XKHz|v4V:z\k~掘O +h w"h5}|N8o?uRl-mwޤ+Ul*Xpͪ8sNKk̴ )ZVݦqRQ3z|\a4Th0]y2_䌰vRNkʸhc*"/sk;K.<*gT=V+Ydqtec+W#yX1i[%]S7k6hVl x8O^.;k>Lk=-ϒX#=HC#wҀOWy-"U^b}U0Vlo֧ݎXc'{{ FܩdžD4[0b] i81 +ϪX{W#ǩTK4yu++g {׼{Jn}X'mu4^?>?OL4o'cçƴlttdDSFØO#O z0!׬r*>çEo`ʜ&D8coiL>i ϩ橿u8BgkYn|7-GO9QS1d-v"N/Nc( +9xBmEG Cq'wvo1d#(jw~Zɺ/ +>@Ǚ&{^PTG-TQP+DqГ>Vhl7P+FX$V"iq2$].2VVCas-zBK"V9bűV!6xU٨b+At!3u!Vh(XP!FFKÛ{AW9{-<\Y|',9#5!~"ɜTq5~xL?czļ@j= =F*^>HKOyPbB*jJğ*% #%B/M +t`65ȟnHoẔ]ɝM[\@oB'HEL̡RPNjAly 5W4"Y/"n":R/l͊8|!יa8C6$eְydNy>&[z_ R1I/h/]49͒%z7/FZls\Gw>}WIf@/kׂZ Z4)^ سl0@! G: xGONeօK5;_3/Y#5h\" l4%]eA_YIv`ty +L!lIpMI}H }h]hz0m"ߺvv\=LIn'8xԤP[RD4rUXU% 0CY%3յ<&b`l%ݱJ4`y^֗ +//Yygv(, oR >LcE.!7wt@:(dLGd׃1;z 2Əͽ ˈ@Soc5,5~P"C910$ ̦7Ehm giDI4vp`SM-:_nŖzFT |]]dV:sy~?>̎7 ݵ,BoNnlxZ7{ezЗJ-̣@H$^>5׹Y,11ج2y?m {Kʩ!e pUA_A\DgI brc{X-UFu"{Y[jymP]̪[^@Xg>n>}V~I^%EGOFF;kKM/3zvðH^ +^7(*Zus\0dYҡMjc&S~j tNX`JtQ(`+"0c0kKf4V I$iFy*'n$B("o %JILUnvzu󏒆i#J״n&4<MDQ-hm#Xx[Mk>9kv)P7[5!4gAj3*8=h-iӱK(YF\ Y%gC?&yA0>s0fY?4̟f{V&@3h(Km y Tv +-͆# vEq*Pd Ёh̬w݂>>*ߘɣ 1w + < thgc͚V-F5imh7dJhn&ZhM XA!hk7ELE7; :enhb6=+,ƜakX6bM$#-bRn![|:#pZp3++ٽ4k|k r>Ctqr>Ztqr>Z>;C9y-߁tqr>ZōkpU;IO hJ牛7.F1&q^VQIo}6uuOy;y)IuS1zU;EiUESh +A">3$t>ÛI* CTeq2{nZx=e i^a`t1:0ޘWy3xP +-ϲxI-`X}@VCٰ7/:qi.ykoV9;hB3d}imyZ 'Aurcň+tb)с[S@eYK]V+iߕrAk&՗EJB >0{*Ab6ɔI-6̚ 8c8o]g64yc,jٛ*z ciТ@8r풽!ݳ7cvddzm7kgoP=&{qn +reo6 +UjA< m R;5ő69nǑfoNq`^!9l^ĵ{? \cuN=ܖUU6(c[; q[hU]ð@?Urlxf}-BI8`cͦfcxi?y>W2OuwߕyxlPgzvy8ݽ(ڨS6V!P>j%weށ[Bq޾+ Ikc}ezƑ*֢5}!TSdv@^zv^Mlkg.dTB4 hekg#N[k7Tg@[?_}@cp=v,C#Ʃ]Q"#@3 N3Ԯ<Ŕ춧Oc@~.3i,S`F׿wl97u6[ P,n9gAm{섘WC#hevdG=;>T$Hխ^$HkMܬ{v `7> J,LJQSg]'GQ(o\h!&Eyd>M#Eyz?PgOQ*߶daPz6@c]u-{6V)UsH2J/P\|hsU?N[F!^!(Σm|z0Og|ˆsl]hN%tRWU0J,z2.]'$N߯.Ȥ55;9sLY]?l*~'WË7`KdN[v=|K5hG;:zר؏w:g٨ɬעojbu_}#{ +.J>#ȹ][Z]q NUWn $mDcZu\K9JW?ZksqWlTL d?E6j,wzl[39X#b\9\zObZg +_zgsz3\ۿ۸h mܮ`VwhåzMi`|ZY$)2gia^ Xg{6;ef>m 1 w)SF^epo}z,>fEa߶Fz>}zOOOa^USCew}z+,$ZاNO;i@m[اbOo ZXاe}V}^~ 6 .}Pk;{*ێhWzϠ{߅}z=mhOOOQhB}Ym쩰φGa9W}_}e)ӫ?2RKTbDhߕ;9e|0vBU&F.i}aTK{#R)tRDEކM>PFc K3&JAލ2zok1a6lLg3 L6ПجCsЄLq|jq#y*O6k|.6*NNہQA>[g.,C+^5;I-[Y'!mvOl +?{O>Ş AGmoǟ5;c{rI].5&BYpRTЇBl佭>;'zG!ÍRC2Lj}c֖4O@VFFeVZgmPlh6Ɗp?鸞Mz.M@ՃBkC12fJ{ Vjxڷ9 ,p׎b#!5[z_24;[FEGvSۃh?17w/AWN;%al_ ntngCbz|t@h(0*sˈ4ճ%m{:xg^~{zOG:m/;x}g>;/]M;PF٩N޿{qgkj}#QVվϳ-} +1#fPnj7&-ZKe3Zd\)(Z*t&zTU+kX,4؁tZ*;T6Z%͚Z*4- +(em k[ѽ" SHNֱFTн&w3@,ǨLJf/uGp|=}]i⧵U۶Wlz.ô@0e-!юu0&fub-F=P Q]9ʏ\6\;U̟ïR}Ɲ: gKUxy~CbUU 3[Ae׸pzK ) |F8z}1|6ĊfsTӥJ[CJ/it5Wz )DnDFnᎺYMѥs "koSv$kg ̿[mq^k7Zv>a=wʪRO-kwX ۢ >F/*tK,`ЏQzO͗OLp¬ۜ7{ԐڞL˲y:$ҳ8i\𤶸:b\~ppLS_e]\848lu1ݼSIjXMθj?CyIPN*٦3Tݨp+( ƴvlMnl~3`lXƄT10qDkp5w :7li{[en_ e;*+LҶ^ |b@\VewE[%eէTqI_݁aݚUT; >Y-ntŃ)-הͶ{Bퟑ@ ^֗Ʀ#{ + 30lD(6I1a^{MuTUz71&mMdstUc"*oB~7Kۨ-= eRhuum\}{Ь.r@XfYMd]fyfDs֪Fh[$ڭG.TUhv$FIzD<&%vð[$ڭG[$$ +];sYIzDLKkuצ%vHzD1#=k +_Rele?rY%uPX1w[vqeGYၭ=wP~@ %RɪbC˜ .<4OIXy᡽Z|!1}| @m{mzIȭ.<4O!_xς_C ͏(/<,(SC|Y/9r:OTaz.E 'iuq5FmtG4cŇP-4wD%-{3=7/22 >3^.}Ժ?ggY)ATb ) +׸ +#@ P/% _4G,S3'z _*ܮ+y#мE`>=s%>Y&ߥk z9iJJŴ{WId`-FYq\r2fw/r6,r{~=r8{eR +ү>a+= E>wGO땖WOٌ,vSyXl^/x9cD@)H*LIl( /B2wy]ե_}F pͭ '4E7u ].Nۄ5 ~;AtC$=KyԝpMtxg?=ӵDmz]ݯkMnv@7&rQ|]:ãKEZ 6mtK=hcSR"+M[K_(oa"` L~C,DcW#eV"4u7I#">H#4g{Qd"xsW0fy |㕤y-ᑤVwv+"t.1Ĉ}c]/^*:/ +LRD1 ð=f `^9"8œK&ƒ:wV%z6b(- T# dJ!} V- E N;bEt+ntf$JrcF`a91<7LBvdVl̏$_Uhk~ٸM>U|)309 |N, f +DZ;ݍwi0zsk.y4㒯%i[Hc@p9`;6lʬm5KX@wE_ֲo>"6xoJ. +|'# dٖGjYp}55F$3Aq`|f- -vxiHMBu̎A|wrJ2 +,(SkjLǰ) +^ﳐp')0(e"rfyn!q+15'>8 pZz34+ŴGzd|)k~A/$Fс=>H Ы(%+eng()= +|~!~Y|nh7B蜊\!>0b~M(,\߃I\y_pY"XO9idB<}МTs }2zVq4̸}:աw2&x3 Wew{]OpexPzy}fa@J) ueצʉIC<*gL:6ދ{S5AX'(Gd罈QGJEVp[Y~t,e#lE1iqk;&tBL߼g5LE^B +VB}?{4~_ Cmpowmt/y]LYZ +nb ZMB:{Tb֖͐D<"YZsM"dU$MGfR0DxK虰cqxRԧ??^Z"hc%:,(1Quҁd8_=PJnX|#4(h G!&|J3\]\1Y*9:A,fKRD'H\ wh,FQ0|̯$hpBӷ8 ZZ'xy8 gJ%'a@ + &L!]J! +!/Λd7ASDQ3#{txʋg[07Lϙ̳~Qi8kB&|p/>#+kKj"0M:@h쀃7t9g#vߡptHqo:%%zpfؾ#pPvfN,F]]_̵+&Hݘ`5#M,gJ|>stream +fOc\a+&~,uK.Qɡ#NQ=$%4!uN#eߐ[Iډr+QS̈1k&𒲯Ƭ0};K*ƞ^eEc-/iש5APFO^-'4[f!wZ,^`H1ç #>i| +on:}vxSKК&9{tE'elj\M8߃BY$$J=*yGSG"K=*yG.#?zT(R +C +&K=*yGpI=*yGe6 APQoTcg($ۂb`'SKwt +.M4p:EifC4m1$@a1@eTr Ų!6Γ8 +@rBSAɠqO^0Z OET4{|8F#44$4Ţ!& 2tEp1 !GaeQ<0E͂ceл} +Ź\T 6ǂ_˂$Yxq10!a R!&F,:FC6tzq1c!G!6t!EF<4Ši.<8q@C(0 (P<Ө rQ,%pl(309eBQ$e &X6ALe@sQPk(zHeV9F(N-FD+f1uEE b z3B'A&A)-JQJLvBa;Ii,r((Fg8,Qd`<S`)1 GBeQGD8#JM8Q:"ϑД(Namp4h)DP`&&b(IM6bb;RrGL24Ī4JS`)sX~r7jyr(U } ʡ]UP`!-$VQID3  ӈJH. "qƾ(%MlQ M\BT) tﮣq 4ּ;A? 10£`ߨ$prR* ( +endstream endobj 109 0 obj <>stream +%AI12_CompressedDataxneɑzE0 X6{$.KwŻ7oz~W /uۯ^y)MzG_<}O.~ջ__χׇ7O/|wOŻV>|᧮_\}x󋯾z/E^WO_\./}y_u\7eLew.{vx׏O~ˇ:yW?8ŧ/yqׯ߹Y1/]x3r3>?ى3<~X@70 q~h'ڙ1?y]|/1|6o_=g}_?7_?yɏ틗͋_^d^^WU'7B:@/~1Mo/ŠgO޾ӻWx|՟_iNrL'ٶ?}TS/qϿ2WO_z}ׯz4+~Ȟi}t_Wo޼6̟WO_=>W ˧&/^~_S7N6v^Rj_>Ջ'[Qc4x +Mka᫇w{7H tt/LAWz)W_ ^2$׿?Krh$xzqO''9.슫ܵ;w}'}7o]p!ӄr(aBW:M wEC1KC*^C.SH1SI5UNtnݳO>s9\s=_|76W| %Tr)Vz*Pnmj>V{PomkZlVZmvծۡݴv]=SϽ[_C}]pU*W]ݵ:^|]uWׇ뻃;C8C:C9>ڡׇp{q7&ětoMi7Cϼwt{t~RFkyGa]ݍ܎>F߮Gi-y>Q )f11onceX1a̋ݍYsuf>fpL<40wlݘ11cx~XcX/_]w;0rtpj[璞}2;ur(wc'{2vH;Ǿ2AC~.ݍ]5&qϮ@5ʳOƋ~c_x7vWǞ;\>c?O{p7Xgi'#2DqVawgz+^1C ?-:}1os"}cosX'}'Q~+gziqLs}ggs17}Щ:UG.ohvдàmWƵC/LhP2cLb*&L(-ZJb j5NЪtJ(PB: mR$tQq(&Ez$Hh(P!AhSg(a#43Be u<89[_wn ߏI H$!8YK/'fs~gs~W =mzY>ϱ>(?#9zS}@4L\8q6~~~~~~gqd[:VJ;56>_m_^*l8()qa"+4lbhHD&=hȒD@ S]#FHзI)#:0#zhR;| ZGԏߵtʔh!QH4ȴ#=#)PKc;˲Yyx +Y~1%!3$_ynssmϕ=ݞA~ox͓7O:z g|w)=rI'?0$|Ӂ#xCχ=.:7syno1y{z>1':-_slr/+_?ռ[o:E1x6?dR@S=k?™^ +OxO Rw/.Y?إ#܋z/ +53f|w9<e_L.r,L:c'&}rz-ܣS>aXHd5*t΢5k0QnIaM1DmT4DzMeѧvsсurnY|wxwSBVaQĮ:A X<#}cȋtwOA_lb4kO7ABU)B$qbA3 R6fTִk[5+ֺ9s-Uocp-ag 8a8M`iX-60j`|Wf)P[;={}`=Rc=wct_[}aVX ;Cdu2"X<ᆍjwlpvSDD$6e>md#[&6A։6ݒ@ óQ̂Qy0"9Z<w熟[~DW #٬n$~A`z^/_//?a`cfӔπh#ũ؟/(p7xzxcCh7X/_-|joT~zz]xZSv7/ `ZkB uj:ޒu,'{+{{` [H}扛'-Ogeje? 30o ;?`qYe)EC6IZd7#-{G?zH>.𓚧W/L3B"ם9Ǯ\a1o`3O>Ҿs ~\x>06PjqDeqR3+wfڻKi/^~5`J*MXrhiw\> ݲ߭q`RXgZ=ЫO>ϭ t'Th>Izb +?>}y87aܜ|n~xX>ooyo1w㹠o?nqB|Iv7*jVG}HyUwIJ1jѝRT+PyJb6{wXjRQ\CZ.o^v)F@:O [2[]#[jLgW,8hL4)G#/Pa]E*ZLDE>r@^WŌQ'~[?V<cccoQr(!&U9M̤yIM{}(]mrO{c. }6pDq9qݜs|cd8qC&]~lGI[1vxF8dJnnue_}NqoՏfsF/ǂ67btGE>oXs>X(g옍qu:nv&.HsBTq8?NIJ!9AJA嬄0k!j qbw s !2ﳈ,bϷ&Fq Cv!HigN 7$ Ǚ|.9ÕI4X@}Or*vR2izz%ǧT>shݚ|v8㤝9'sӗO>3<)wۓNBu']I쭱p_7{m{.֝.NgyͷG`,-QviSgBwjuޖn+azQ-uŋ{y&+GYxF?p8G\7{stt*X E sq?}oprI;2L}lEFewc߰0_ e6%'!od!]tsJu~gae@r.>v?#oQ|I]ӧm[us Ol.wSt y}zHP@Cw*\ wP7zP)1чEpٷLKT_7{/ѿzv(9#:eq=m:e6JrЪfʣqLqa7)Z EnOi6ݩrSMK.\9;s> OfNѹfŝ袎l9ׇ;`,1 آ>,K,KWx6ҵ~׺-][tk-]ܣ +K%>jz]jje &;K"5K`h%Rakv3)]Ca/1CE7-Q$р^2b;RtcV3>@m/86LgМ ߰/HvQOhO<ՐglͨƾjCa~hKy3D{ P$ymɀ莍=~^v +ԁr9T:?I KZ6?9N5ϛK"KCCg :{<9VJ5lNedj +Cnv zVJqv6AKc9"Ŋ7-Q$р#]O0N t9@ +Suۡ9~C UWG?1Tڐ1Y1 "-mNv^( ~^[2 ccEwǴTjGу`b0(Ág Ri/aD?/  1X? m!v@S]b~1kl#P4ǡ<^]#7 ]VDӎ # 'HxN hڑ!Qu$R")GE3ʴ'5_zd|5+5^6iL2TR,)E<_=G͝2Tzq'TUV?Hܻ:H'ƋϹmŎ+ni^ʛnit֤ҵ~mR;y,,o:<='Ц7ѥJo|i?6Y~ +͗ /٧h) ~s Jj{/9?Ak҇6ve;| |ӯ9S,oOo4O:y3~h 2 cdQךbd¤>.O{9}MF-ѹc1_K;FBs9%tGp$p¼68)q0JVzʐ5{x}k\c#&G !$E)L ]vJ{}:QF^OV@nU޿~p*|dKݷeba Or4w|[;/sk'WlNJQ7]!zS5;. mTӭrd.1*%Rypvc|?1%L0񴣽olEʓA$ `R +;6XDf]\2KS7̣ckpe4f {ɛ 5+X"9b]hQ7J%gdfIPcT 3NtU+%p(ҦW[&d愭+1;.ձP7עjZ,)mzG8r%av(rVN~zLbq50T1ӿwG ě?IlPxJ6GKaꇘ.XKSP]uP cA^&R/ _8vVpQςf89޿.dfcpʵQg H6EAm;z/{(S$?[0G@bjlt /Nx~asԶ*{y[<]ì>)i'&_ٯelO89]o(ap!NYkC/F_n\JХxBQ\5𴛝uR.m ?"caR軐iJ> (X]Ȗ~~pbM0RיJyHԮ]0ZQzfz-vLE^}J+dU>&+Dop#Ӿ\EփiMmv8͛ H]W q̐pSs4Y8BA`zlcj71bNj{8} /gU"U[Fn3Bc'\VdmjJ3ԩ bplhn$ucKELBVNa΁XȋB b[[$q+Ň\(@;)G VSaMl'fNX"TBrUoGP!d |ud`c +/iBXOir<>BPrQEA+ r͐@n2K56B}Ht-b#8Eĝ*~oܮ),9 Cͩ"Ș5-gZ V\O3OOpBrusvxnJ1PdΦ$) s!8@bALԐ"%.Scڻ؂!,ؔC. + 420FK`D }n,~!):ũNU$>?o連lfks4G0o=BVDw9tphD-DI*+ qQL!^1!YcsDIJ0tg_*}d0=my%s` +/rAAUqHԜ;Hz 4"[u%u%F!)SИ8-{_>URBS%y,- 9[^Z,#C %U2D +D2>5cdw=i_tܡ. +I5`uҒ9I/*q)O>{Dđ~0\b8 +mYXfs]O؃Y8F2eh 7f'̻ +g+c }"i;A={%>1DBX +JZ[D{X`,cNa[*fR@S"*)Yni nJQ.{4V#SB&1R!,TQ^@ckLQϼA)[f CdhPc.Nb sik$[204*tR05%<= V94](ypHj#󶢚WTdڴJf W;*g 2!,jEWs2 kv2!mpq^jjp]j I=PzuY4 [o\Y J@aZZ& wX +aq$#;imggw1AOpZ< >PEI eY-~VMiAa)v>y_`d:gF4#xRd"irY]TxYjDɄfx:FyhGyvS-BK2 7Ahj!y )'2ZNhY-\a)=+ܹHly̓c)\+'=yhϚjDPJL&i93nL~ x&'PNkCX]jT4 mWe(]FjP2w"x`,NOp_Q[6D# ++ r[r#H" YIc'q\xVEfX Szeݬ| d,'7-qmDdIʮ2Z&D41C_KwY:iuR#К육r`ĴQYM`f"[HZb%mGʥvF>tZ AF\Ҝ"@+ )n!tI*~Q3Î5ǯq'GWН,B\ٵ U)WD`(bJ +A99!A= A1h&,?ï/kFxBFK'\`߮I' )z ~1HP : +T |ȴ,=TEK hyT0 S#˴Lj(B3=E+B)L3?T+IlS@TB4d5,ZHQpIX4?YL)!D'bגŔgli`)%+RJcR2=-btG:וK@KQ#ZE#"p&lX$b]l/Tµ8xDAP+ +FQ)K>MGISg8R`e#!T#A S:"mQY2lPMf[j֋Z,JBN$;&\r_8%պ"!AEP5~1f~)徦+qg ɜ)[Q:"g}ɚGxˑFz/VA)/y3u-e_4Yٴ5=&,>6oM+$mzo8jџo_gvb >oMG5=&Ȋ 6?3y8Ar)BjBzFȱCŚop + gaF6!O!.2L@0.E'^1Q7G4]$4:dCM*Tt3+LB'sǨd5 #y8uuwMS]ȷe&gF]^5q<'|״D]NDwM3]~4I 9n,j>8 n/:' U +*!g3aHN77;By%vQOA͈-q-qx'4MM,>#9xsЫ4kߢ '<;)K~pdShf{ TЬqC]oYpĵ3J0gԚ4b.`rR%8$=5\r%@dY#mijR!D](g3iWIď|؟cGNDZ4[)+]Ӕ +f˱2pHR]R.pu2_{9ud,Au0HH~f@{8s4F>wLRn{0~{Eb(F')e̩B}v|⮓A.X6ZD'3ELR\,PufƲoq8/z(5Ԅ2}4krCV`C-wb%*;|!'4&9p f1,Z̾TUB +:ɹdxx )bptpwV +77iZX٤P{ +_HI2Jp*xu̖>wL}rEF{i,ʅ5}Q&j>A U;}!ðMypipUdFA/V(ke01)_'puRA/h^yiY©6oт=Y;iIw5-[i.ℑQ:)|,fz=%1W)zCpa־11G`7arRZ[=|t%CQ;0Xք8{r~l?eJl^];ΐda eN7CFXhN*,}fZ,$EBq(evhSA1L:ggVb@+v9k&i-=3Π5E7PuFcӢn˥ H}tWn'b**a:jNNc2 B?P7\ڨ !R +[$j2rЮDĚRR|$8)mai30ەKQWb;R.8 )AG#k 25^+G96kqCdG bZ9$PG^$]oGm.H܋j]W[V&S6 0e'e]xdZC©z W11,G%)tPԴOn 5 4qj·i9176ߣ'|Nx ޵,@;KGÙJ\GW?k>}~=}WO[g⊂~+] oJ_/Ӌgwoqt+9C曮h;JY.|䐀)Cq8<_I0SP"&:8~&ȦGBj ȵtɪ~P a f@"rΊvAe$3%JnOQˉ#I<|a``p b jp.NuD3 az-&[0WD0Ö,c>\2\I&j +ÜfOVs=01&$yE9(_<4'Hr)R$j4R`Ndxqru^xL x捝!'HN81 3#.`!.k6$؞ёgzܴ;-5Ã<<8&52]5_kXi#E*^f' KY%//h'pҽtw/y +o2(-ѵ! gIO`bA8UMJ +g@R>huO=ė!@sԾ1Q$W^&Y^Iw WP\EB<xSXYIp7 8͞`ep b.+f4c +kFAYB j =eɽ6=х`UIܽh} ՋlMSze`#@î(WIQ _ٷj'@v4SawA_M";̞=" )&`ffD0%?`ZdFIF'((}-!yDElrAR@&WMbfkwš^lj Ej\fT 8@RV,"?oxPDDroCX҇$O\q\#Aus.*EL rAćn{yPJiq#rHB7 j. *^ $^eF3Ob>G?f* dϑ1;tE5x4xJŹ1/=|y8"H^p +v\36^~'D8 9>f2t+ãgg fBVR#F8eDOȥ@> Q UBJޕ_9}K"c^",G]Boy,=s2AEn`W3Z"9-գTEAk瑁aeTftNd2L-Tr0DC,X'~Zסs!œE!,2PH6%9$>R(yز :bvUL WZF8מ^KI +(}c|sZFv,&˘H00A0{_&f]wkPṘK" "߆Wҍ{])&sژ/L;qؾ)\-{ G'pNV>4u/s UxhSrȰHS6 }`qNQ`l"/*P82GFKb +O eB)031ǐ؂l񀲿!0|l`I9 SLմ$ +7#~VQDg+Kge]9ZAsoHƪ iY=MuLo].?qW9רxJExNh2;CTaMr*NC]Dܣx30- <n +ΏҼ#QkjTj\p +oBņjtȴ0L@~Z/{tA=GyY_Gd{,My*uDʖlӋ,ҞU+ƯBhˮP7y?M}ERT k+,Y +"(s,) +@`L +6䐖A@M+P:fI:3H&) 4-L-B; *my^%"ͺXPK-&n cU`|9Ce)* %NPe&ʪ鉁ҒP'0\,@u[MI"ٸPu[6D!$1% +cFsSBVoz]Qy{}'v!?UELM30[\eNCz6GgEJ%87ŝ@3-M@8v,irE(A#*%NAL\7N\8*y_OAc%|/X] :F }4R௫ؤ =غ hZP0|vOͦNThpC@ Gu)`.`n_S_, Qle (xV`"P: +iB +ܾd<j氭re5(pI(9,a8ϴh,6D=NܐŅחu@*2c0X"=S'20 +E`LTH3 +Ge8elE:('UDWa|.e䚯p5HS6A 4mnPsf߇{Gȭ{^өړRQE cӉYRњҒW}z4xv +AK[106p$gD "Z'ipTbud`ej:zY"1xD +h\m䛽Ō +Dɐ{ *TMBg CW}y.B2p6f7ңr֐7?R( 6uzl[e1\y&aQHJ4E,14hC(Q'7agGe3 pŖY\*KIu?*zGA~_Lq ל (pѳBpYC*Dr4 B}Ӗ-}-RDzG s[,z bL@ؚiZ1D><1HY.X*ZOHYJ* ZcyQfTv %`V 1N țceX*>wz0) 8QEOCH/נO-͌ .ه|̥[7E4x/OH +Ó k a@0W) =lޅ¡91}ny?i'ΧHxB;a.=g.5t^:`US +f]flQ{KVA#Y1b#@[rsm2|n\놶cվ .#7Ef˗,.0 Cp,ht_Mݗ){CTwr!8-2T +^*ÙD 093h7AY^]c[xׂ?|xi:.f|M-[d4:JĀs2V5(PIܪDG/:4]#=$ŠU :fH-YG=M| ~|-QqG)/_|{ͪt&, -EhHzSd؊Rո=FĒ`=Y]Х,N<\!ܰv)cAe̢C.ED8mKnܨ?,~0AFL:nVRA5=:%M0Ļ=GjȔ a\3ge[8眱3Am~wqXz݆P$i,hI]`pq>M#Nyԣbx5 sܗ7f&#B#=-}@f95 N"сMR8gecfZ LtPĐL,s_Ӗ>,ȁi|Sb Š4ζ&t;(z"4D&W8n"$%eUT`YMOr{iqzQRpq*`1~N0 g(ٌ@%i. 梲'v,1*ްyzfHp1Yc^8|aYjNJLjIJ t'Xj@6`R^j iQݲwTUbE4R}mvRG9P[{ \] X /MSzCCA` ^-|&˃ҝ xI^$C3 )&U4YJ56M~@})jK˺ۭΕ($^.sU3JVGV@WQ]flFM.Z,|N, $PǾ 2QrD|߸j>d+!ՠ `SspFU"jZj` ,NM@J +9`uhZ#%QK[6 BCXִ Z^VX:+sUhja<|]hE*:0t&(Aҁߦ1׾B@96mEY%Yr.hl;Ź~OXź~H0B˚YIlZ^,s2"5^ސwVQj[vEⳍGԩH[Z)JM\g0&P0k+]CѢm^W2wU%Vb,8Y*YE +Oόģ]I :.̼v*u=NlaWL@pu2ѮR$(|B,/I + ʸLiv&&+|%xӘޅ + KU;1#3$юzuUҨ1a"2ܤ5E#fd $b&|&G͸i֋t*M!>'dB'9uwdz!L@"MII#?כѿl+ŨjbQ2Qy.iTf*8).qTWhŴZ5ΙI/-q)H:>IG x^@pScL^:|a2lf [Y܈3ɋS4 1\E jpeyy%kYe)Gp6)[P|]+&h2lKPT?J@5ayY!,;ev!}Z}%IEpyYb5"-n4١蠄EKEk2+<[J _D_K4b:-2VIV&'|&sRt}Z*Cd[ $׶94 bsI)ARϑg 1W ]V\0DvibN;b'_,ahXb]AS mRU9'bӆ\rpF;nQ1dһ5(I!SFjzW[ZlNyr SK|ӨUbJ31DJq-Ͳ[z մh1n \ZE@[geًkYG#y<|ݪq@|QGg}~,*MK2_&Im5!d19QE7#w)Gt_S0$ꗄp<=:huvkb!8fzPk5WZVGFˤpFG[U8MtzP(9ьq+tN@MwH a@3ZwvUWAia68%4&eCD( +Pk͇M>(EPOOFk$ԙdEhұyek]+lՈK@BHjX-F+|jfӣ|ûIyF2`[tU"]L5ʥQrr,r-E@N*J^0Ps?~.vM9_,4gU/$`V/k0 fUr.1J;4Kx+NPH鈧 `P_RkKObd9)ջF1,MטX[J")9%$tK%ڪybF!R>=%j43hSJV6V&Ӳf'i|VCnO9cX@b=d[cQh2b54z*v4~2p6 {*oR0G/Sg$ 1qvRTSr1 >G@s:f )w%w~-Ȁ,uhH3eonbZ-mh[m +Lߌ'_dS!fvm]fF|),}xAcobR-i\ W->\֒b~*-!FsT3i5;zD}naqV+U .ITӷRZܕc\0+y-_wðs|4r#ZYdpoMvM*6|j{XU3xW50<A<7&0+4#)+dLyx &'NV) f qNX):JwcT|y}4$Hxf@Jv{1h4`.6<|=W8Zɛefd//,e|!ALZ?Bs ԒrIc9Yg֜Pl% fܭyIL>,uij/$EeEbb֬'0z83cRXy6Ted_7? s}p-{0M݁fl3~Ž/U҃"C '͜'i;ZQXx 7:y8HI  ͥΠs!1r_/{1f`X*ce. gW)"pO96iC,4ddp l 5R6f/h͗tWE\n%WLP} K*7 N3OD4Θ㝕m`gV ϩڃaMm$<:O0c당YϮ[%Fs>-MNP&ؒ'^ <йF93_2hKUY;&W$ q9-U3WObQePPEsYÙh@`ӯjy(೘(έBL}ٹ`=D~ͅO(?\|N\=u9OY>hU1Y`3V/Uy|ntf,NslyTiq"*,Aӂm!=WJ ˥2?VjR +ɶ=Kχiw*nj +V`n}7 j]cOYIk4TfSHS󂸼zO,D`TR$Go&lDK*U/kd!Ec3tc򂹂'dԭeWwʷM0 T!{6=-$j3$~LJ~5!Ƹ,wW\-֩'Rf7E|~k1H(y[S<œm L1 +7Y5cKc6\b fNb)>`>=tONRr<<ḙbj%)yqnr/AϦLWDB>K֋< Y>L/q]|<̄1ЯNhEiŪ_?XR,Z*RrSAmY qa)4vMyAt7LJk5u*ǡVwEHrdYScSP]Q"YjIUp#8?P\b-;2[):aG)ѨpX]5FZFU`E4ױXTx v~O_ۘ6b:-VD9VtbQ9DC뮳XİٶȲ W*-UefĥADV>qp#J`P d~x:SΫد^nbiL6(yc"ZЃl+&[@e;樹8PpfEϵѡӰ؈ea<1q`!r4!`a݌| UӺi +j{ amu3 + +M![Mi#anVMj[m=!~+MXt\OAۅ(eFf#b[[2cj:CqKlm6;[G6WcY(CFmAa&C[H9/LL|D%S@KSѝ#2WTbef? C֥2KΘXUm[^N8i2hx眳x[b׎Y{X办ih8V>PV oR4MȧX CnnFoSt˳ =;<)gcľjc!3 煐Hyn>?k{K?4wDf ڍg% t;$Wyo,\_X\K &} a+X[/)_`mSM&@ nKF]1`kb,%yL8tkv8GJBW̯6f=P`-Dʴ:<`g*ۼHcAP+ }i%ʎ1{h41/[nŮ;<3M֞'\c`ؼG3[zdT9iumui7R)_] _i3 $c|[x$)âC 8K!\6TX(Z3ξXP\= 5(NXa8`++_; -'c` W}674B#XoM3mb1V&M:[fP-aݨ*:f[/>[뱬&srX{36J]y+TB6ejnSsKtű2a9f\5j6ZVH-x59945 9n@&g^a,`G:W vnn~z)jrOBj3bvrUP!BV0bĆWQc(ϻ}[$748ӥ@ Gos"jcwV[ߺATu+EաG5ݽC>MR$ԡ6t>K &mt1Vz72v@ zClkZN  p{jus6\#" -2Ų+AۼS9|9=fׂ# -`Wb3Oc䑍yHgbj:/A|zVآz0pg_56wM2TtEp@JZ-wZOO5rP 5w.*r0iڪ!gx3sU32w9Y§UR*ϲH˓(qJ;:d-!oFl_P^WgBl&/T'VӼ@j,PM >'ȃlMme jl4taf6lfMx`RSfIޝwAa,ɢ%mD?HՇ-DEB`wSiEYw\pKBڢ:9y₨<܃fE.|]̂瞇ajU "kki[v1k݆lm7/5gᨇ_޸何 y˜פymM%f)СeMyNj>!2bp)eTӆg1jkC/@ܺÔwOhjr"0d7 'ptB흪nit {m t6{Ip}-Ϥtz/Y+o4ݪuZsu6w3ܦnֳ7k]!:RmDۼqW|wY2YǛ9LzvPW3:4fuκg͆ྲྀj_[XpfևJ%0c-PoZ)K^c0 ]pu&ٍ[O}i}{ufwbFlk&ڭ)șnuyjfӼlA5K`Hg[<s ++TGsa0btѲѫڳ8J ~ p{փw!w'M+|8+W41ϋ5R>u7sͫOb~+_.zg-;9h;R58-eNv`ڌ{5TdSAC0bt);E.G4k*O,srh!J:afC}œ!U-݋٪35j~$L'HV%wQ a߻=͖ԯE jl zlɗ2[G0kԌYԖ|Ѿ5Su3I޷{&YvB` ck n6ryyŠIK|P-stcVsׅm*Qpr!#N\6jr\-)UrU9*L ή#Ң{&gݹf˕䘖WTZ* +lת)pV[X8SK_jdksz,p? !vq6sZcd'c¼S*gv""nkʀIdǾ&K䴨iYӽ%G/%Skx[`C]B41"Ms"HZ<,朥{ЈKBZf l YNڰ(5Kc,?::¬}5fHfuޮ__~!̶F_a:N%ƗG_бBҦ.app1k^}B/h~HLz܌1 +}aZ;,E~Ć&H_/1- ͐ ꤵG8&$q{ wQ]m>k-pٴԭٵ,uD.K}MK$Dk4S + z•yet3Lp٠ @O^ӈWHe٨AŹbFNC"x=j:];xF_3tIur`g򄖪`HRׄ5êO7Xb_i G0 €D{"!˪QaK/X~۪bU--CfGO<ے8 F|g}gH!ū]ȩJ:- D9]sor$>Wd`ULCnyM044- "_"~ƚP-θk1,k9FBX7ϙΰwj&*#Ha.7Jmx +aS"dǝe6ƢG鍊muynv,s~Yj%Lanδ.-'eɚ,| /J̥(ѐpJ @ŚWj@6?f)NW=,7 +0l*8DO 4u҉8׶B[&Td K;$֍V/b: _P|c@jr~Mn3ɝAc"I2RaC&n3ӯb\:nݴWd!g_afxj8auoZ{ڲsvϱ)a7jk"^lgN«M +F6QN]i@.7NN_Md)[Щʬ9Yi#K vdY{ TMu9ZOv7bMy]!Zo +]7iq;ac'T ϣ!a +]sͻV4ʼn`74공OZl}>4Kޥ;&JT@.obzk7l ݑČ9>@ɡ CXfd45.K!KAzǸ6TN Hyز[7mbhnZ=i=Rt]Yr 4S"`l +(c-#r"WF*u0@ -^BTRwK V(TT{9S}p׻_U$_Xn˰^9 | 8{@VwZZXj575\k!34DZ;!859Ԑ1hTJhU"YoU3u F%s2M0Gʵ{1ئj_oz„ٖev_R81lMR* /]#a%lCXNDpp`ىdb6TP n=+vRldF‹Zfʟ%Ը@Y-k-^Gb]#@9t vzٟۗ{I]w;k`^9!-K޺'Y]?;{zg!:.}`h lcX{VLMQ GnӁ8X>fʢ.1bOAOb\jHR lhx(T̎緩޲`P^P, VS?,$yxa\6/,V mB{9Wt*T-Hc8ϱxJXX#Ʀ=b[PRs;Vj9.2X(ۊ5x.>¬]<}w 5׳;4SEYc<ú&3#zCMGDww21#ȧM +?lIrta}@B>n[3z +d, rifݱ7bp8axwlϗa[nc[#{r DF"B qSeoGA=!{$maR~9w8w>3;(X+sYNjhXz̟h=5O|wu҆\\'Wf~g}'Șuw2)rWɢZX+J2=|yhr`M2v^r#7I!(OR;9TS祓]ʚOe5;~k-w#WŨAUN3i >mߟ3YCuxFU2 /i`APZ,C3k}6R7c +J"_]сQ~ {p;n#ִҲE8.UAi 4B"GݺYDGݢuCVgX^-ړuB;YvLO4y6bQ` eQ>:R(+ >*h\ivʃ`pT#,Ww\q6de߾jiŠ=tOcn6q'8: biH5֮!44剦()4lr \_Q܍j]8k~鳬srj{.¨6 37} ͒*n [lէ\w$S/flyj4H 3D-=HFTVET6^a=f ) +bF1(!lM!Ņ@pvfg]71,:`lp>WC--ý4P+ @Fx%D6Q(+!fo[4 lq3QJa"0q4FQUI"oij?gE8܏k2I`YMI•){H'Y/ K"x*<V"y߇環~: 7h%ɋ04H[Ij56v887o צA&,=S]ѻO!Om17dOs2O5q烊LF +!3@2fl?# } ye'J1j#WW)5} bBja }Bpqw498E+$G;> D=7shcXwkr̺$ &;YY1~ZPUF3Gš@P/Jb١Q2o7_ڜlј('7'uD06f=~aG[b5fhEWĶYK kҒ]VwZ!7 +zxSC(ɇI\H O.b+i Y. F&6ݧi6)'ĵIrjhe+[Ѽɐ%, Cs4}޻} K$&2@-54z^4p%Gr$[ok2Jp6B/fss7KPD([ZsGin4!Sɉ-c ! +&k4(GMg+|+kcBY@Q6/H3f]MI뺞f@!, ++i0-,f<LJ+*M 30?']7Jg=1n\ucױdϾFHne'ecbuKS!AgV$t7lO go}l;W!b7ENlg7.g:fY=4,zel 蔜%)@nGrsy1߼=Rm$oFG {9Lgr6dଶ{Ső4 :VaBRt=9QyHG?@~khy$Iٝ-ZW" +q|oת͍39"@^u.5VEbNXL['~Jꃇv}nٕR~1`uQ۟2l]_}ZM0Rot룷VhL#[u>5Cz9K1LAQ<+yZL4yT763!~-,f1Bv,} eZy9y:7Wz2q +O}߇o'y;F)6{mN:N dpSniՑ{p}x֔/((u,e_:<`ugyύ>@u1 Ju*f Bןl_|˿z񫛷77WJW׫W/|DϷQ-?gop?OA?74&Z",y0ؑ,3ot緿!+ w/'J2Dt6Q/^ /$'.re 2OkXPhI?j (Dir߲4"5*9yZ '~Q;2+ AƔ3}D<$@dbkr7Ff7"y0)gО1<F:VT験7a+"t?I;0cjHF,1 `g<$pJL$ſ}WC&X?LbI-Y/ Qp^S8ƙX ;`UL CkOHb/>+}Ʈ5Ðٰa'@: $Pbe:/T"d8%xu;bbȹ(jOZj3$7QV ڴ% `DtPɔU@jMb*R:L搞14/hq^6~>cr{60 BPQE"L$ G>%/g[HtBp/dlҟ¸}+)sã y~:ntmע>opLJxW&T1}f6}uY,nI1]J+X{q)DW/~9K^=C;I@2W_ȮwCJM<*4^b]? oUMYFBO:[TG*kRΊ>у̀xևpRVtMM$F"8 l6$D_(rCe{8U V#D#h+HFWz"A27#v߀rJi~6~|MCu)&by(rIxr"M@L%²eXPMES7yҍ?>4)CcBJ)OS*ecl0h$"D:UWMfBPKt.N  iRq#^E?PU>WK7N&.<PImY$&5{xM'!o֣_`gv؃$,o K&#N|H;Qة̼k1$>"*u_Bu(M'lJIןkIĈt+.و嵠 +2Y[?P|̿ 7r/mٻƋ<Y͈J$Yallp -cNX:0`J = +%b|%op^Pz&Dٶ(KᴢҺ"ŪDQ3C8/BvyU1mlQ\0i՜Dv辽q&eS>b:qT)Th [(CIf%1\kmS{8x`5yr0餓,X17 Qր9gU +]Ҕ 8shd=φ,4EDb"tɳ0 #:x9Eb>(hB҆y}G-wcEgqԇ&wUh"PL1ܤxߕ+6V7SUiT۠3]O)qv\W_\sBgbSgPxPO6ڨDD}H(:˜a7Q`uqsv]hH[bY[2y |HbôNkD΁n%1C# +8Ï' :[4#%z^G 2@ +1&w-)dbvt჻Ahy(C`R>%CkezUNhN(3_]CXW-|dawrBY'!pSKFЅ SE`Bvj݂ x7=E0Ha"Vga3hvڌ$eMbeGF[Ɣ&!T 5[!K--{4X`}GjGER0RAiƸ[l Vnp/KC5,jpw';@, x`daEw'%2Ū$xY6q>3;?ؓ"1…͉ظBC,z ,߉զ drF ,$f~Q +fXyG]XfUŶc'q!bl(%fc qm3SEՊ$0Yt<+oD6'&3n'ћfNaz{0`>tۣ\9&U#O}2% il"ʕHbyH<eɐ*dlyb[ U֋f5w0n&^q٦g nqϘd YlnbD'%ո wWeST%=3Ckddgm +lXo=@O4.D?YIJ9Q6 %nuuϩ$"DDx1N`7w{xSm}3W9q҂o~n`S5Z#x6K4﮲"s-l !!l_Ƽ*䑳:Q$ bjgw#0F!*FjȬȅaK&uѰb!ʻg ܥdlY͏PMhfʭdw! +q׶SbA74Zj ..gZ7=i8㐈ͰF~*-aŻ8[d(q mپ .B!]<w%̊][Ff[.7ё1"vd C , „9J[rQ S[ad:,cI_mtAf"Ki7.1rxtU|(qK0g! 1K4Dxy \"q4qMccn!&pL?b&"q%RaA9g}8ϑX;-ڪ>+<8Hx[afd9-J`T42,q4gm;e4 a^.R6}-B6Gt +č& ~(8&TR7O.(J (^%K&t&r=[?:Zޓsf)EL*~\\XFo&ſ_\ҧ~?-y__:#/1 ⫷o__Ӄܜ7/nίx7xf#wߞ?8n׫3A߾yzVyT "{SSDo7_sjr㗟ӳoޜ՛\/ý'~l_o: }uދqsSۛ/~u]\=g͋_)u~랧CS 7߯e/Wi];7"f"o>~/C|HV;޾}fP`]_^퟾xs#OCߞGvpQ޼<=;t3y3?K:o/Oo]_~C3vy_^_7h:.vMv~oN'Wt~}qNnF窼q:c߾]2ooN\U\o_y {g6ǴÌh=o( Gjśߟ^|xc),:?ܜ~ٓŧ7oyufdND +77_c]Yڙ~ + s=ˣ~Þ\rK ۞nj.S0~$N7~{돰;̓}[x{J`߼c}d˫/\9C|S~'_>Bj,zwgoNs\y`%_zv\H`Ӄqȍӧg9%]<},yz/~[gpЃuzN'ioίH*'T9}qve81;T1Wͷ\_^}[J-dws0ƽtAe<x`ȌN77~~q͟_W7?R~p~KNw8\åy:O.s)IgyLms=?:}}~ciN)$|b'[zK<^|ȡi=g(&ܜ 9uyc3ɱ/WgHHߞ]o]$>mCfﱙ|^sOϞ d|B|G'ۡvfo勋/Pػ/ryxݜvk/v7g{5W+a޽ӝ\bئK5ǟ>B2]"Cf-ǎaZߠ}y5__0|4~-)~{OA'{OY6ݽ' (h젟ht_B +C/Z!75aF*54aΑƇ{- G2e O &A.?St= nҏ:}>-O}z챮)n|BM>;I +=!>w8}hON;U{'t +i\{e>*̧s?=4ONSp4?_p 'J?/Om#Lνa'MQ]O嶅*cyxy~7Oȸ}9jE'n<Wvz遙߿:}BO:p8$O%1}:9zrn9vr}>}1Dna'>Oqt'$K$V1gߜ^]_~q~y~5__՛ߜ~ϲ cL<; x\Wwt퍄N헧@}umowi'1vK_ޜK('G=u?/o_Uiߪοޅvo,\`*\?܌N/tڛӛP0Xc-O従}絇9[o.s5ǫ&xvۏܚCMN/~sCmƃIWvٜtjΞ`RrK;YO՟s\>=e.0}=U}RK^,w'z]zr~.=ܥ]$.O2rlr'trv CfgO/wi)u:.=3tK* Ƕ='ߞ'~{~׻\啇=E!__C_}9N&onc!Oݙ ۏ=*Ɂ=a'v{DHG3?ܚQz@GgߚQ؋=AZQ?ݒpL>xͫ7;ټ 0?`@~84;,!pOЏgqCzBr{9w!Xȋ//\dy#yQ//ti]^|z E=>9ϮPyUnqz 7/Xqߺ|l){dWҞyE~g0vqg7/>9Xn|]ts<<>ycN7u텇:;Z^y`կ/|҅hW)gNo5#{}"uq=侊ǎovw7aWu_ߒC; dO X&.ΚA& +2y,vp0` OoWOm%;TqHţmo]ه:;f +/_I97cW?<$o'lx϶It {Ss P~qCNSAy@U'Sŋ]NZajߜ?V?oO}Ss0ǃy[}p<:o} 鎯>b'm^9={sz".؃LlGT&0}Sx~̻¢śk~_..wCMu;?W=:|絇hmWo_X;{*X0dD,~f77{zsVsps@> =rۛ^xԧ +aOlRO}ӟy|HaB{.=t]v~󧋛/^}G91,}L==`zO6IE O%\p$(ZpO?]t'yPB}7ꣵP,ԧg>ÃTl췉zyLGA&D=?1l_ 7UV@Wpc +-Y~|'?~ݫ/iH|3NvO?῾A(lG9k;zA_?rxVIOa+GvRQAPNB(t2 +$֎Yɖ+$-ŭ*Sj(R I(5~>҇=Q${%lir +ߋ[LeSm5RD'A:^eR;e;vsv2RORZ'/p2F DI'u04&Z:-,vzD ҰbC9 ͆~&,ߠ}!Z! +mֶaL')hh %t6Zͮl%o, I }FBZՍ~~[q;[ h iw + 7G95!mQNJ(Dm1iq#:cbGؾ1|h1 +߉'| Vi8t(G_*haBρ~6V dǠwZ!Eҟ N.q+{ KEg=60<ڇ&'%Cn&C r +]Q:oÍ!B 9"/jR%`G{Dbٌޱ 7/ڄD?yG+2M}OXPL/6] ڡj\xbFNKcAwqd7:WL oN M))F*:SЭ'ZN FL'<hmL +.T'[m%p\Ly{L+DFi``6V a+N}㔅}C>L̫5aj}/NC4^>X:?6>yu6,B(iZځ׊c$@g(oO頁֒ u Ϋ] +q-ė.KM.@nښxt&?T@!*"@qL5cQCѧyכLA՟!_=RB->q1Z-#C|(.#4 mm`=vo.K[\#WhSgFimB3fEO܊6,4>}tp4jr@6^<䡚=jxG_긻 kƄVnou#I7E%V9A,EvD[tλz۰\S >݈q(2lXC`yB0¦/#%">xGxa{ ai[%RZ,rtbGfSy^n7H| DDuh+)Dq­Ƅ{ Ʃ"FgRޢ.iF 07 t:4y{XBBwrf@٪U6tL{CGID,d7C=1Uv&Z{l6iΟŭ8'*J>FN10>%:0^ x8Et*ΊCJ% $ IHתk16H$ɴco])<`鐸.iEҒ$R"jii)O=(-%5PhU_XCݹ+t| RNiy:e&U[hbt:HG?=I5'{&`訳$ŋuHf?dU*1唓F[+,^o@b%I# F0Mvбs$ڴHJ%?āƝ=[tЩ9+i7u91^gJۗd,ŗHKTW|fqK_b#V~WGw)C(lC!c)z # Ɂ ҋxiaSM9!Dt H.ǖo]Ow+qgy40_X' 1$}p iAlF8ߺ(TX(*kC,+ؤ(#ͅt}\\4aIϗDjywbBB͍F\=WYh09 FQN"f;DTVs+M 349At]4og8WWGn}(YZjY$\XitAMPN#G F4gcr -Q5,0L '@afX"X f s@ +\N/z~N a*8u-a o=YjN tSEtetE;81īB]_]P|+bp[TPt( ? MѢ7tid `|V"{Dw|riH6>2Qm%[AQT`ah:!tX4w(ZaMeRUbe6Ugm(v-{EIʭ tH*"]gM-X'sI&ЖuXY$.HHVG}HP$䭊팿RpZ僎cYEot4tffbKhIgÙ`]]ׁ}U԰@*kNA\yD!Krwg%E D;ӨI zޒqH@ZF +.NU֙UB#`/fpkM%DK=wȒigC :]-MxqAyb7Tn_ +Rac ~"TLGD<;-Ml,2"]o5/b#XoH C2Ay! +ny ZB"`M@b p$[!;+4hN>: +4ר'l#Uhr8r A&t.SV;@lbS,,2];@7lBw6_%+.b-|߰鸐nq'Y{l6h52sg?nv1Zj_bnEo {ZnxEMp(8)2_(ܴͶL15$U!vLܽ` lj5b"!pP +odrδkUa0 Y8 t1g E+6>/lW7$a{\Dbg~3ܳDMVyt?`Ax΄L  [Ks>dʟD 7 &i7jjǐcީ6^!a{Q`lWb5b ;`0&v%QK ]YQg kkrhwk0$Ko4{"-7]pZAvd;9A݇+x*;,>Ta~Uu߬MfsfG@p}:>LT0  +l` i/U] ` b5kFE-_|_E  + NUClkjB? +?6$cXVͶGnp5DNA,xE.fu|5VLkq]gf;1QRU "*TMx}t˘njQeNmCqt& +ZB ? c0jɄG~!I0!ڽ"a&q6gZ5*"Jdf\X0T# iAѢrs@||t,Th3 g;@ #>!b5[`u ,9!^&8 D^8 Jトw' I{)(?5jbju^R=<:3Jflr* Beė 4cBp- + +b8A,!Ba2/$2.;K + (G +%s#ch̄^i.V*. Ql&ppЛp"Cq2#@`̲"o"LLP{U*h8E~$>:{X|[d#oaކ  w1(X, 2l3+]FCD})!V#Eh/t! 9@@[й8ŅDgͽ+8-PRF8(GjHYBHn;kk9a)CN  rQlR 4uSG-/A?D: +]`ʠ]:iFaY#5*3}%b"g,}oCa``0f+h6?rOC"  [UY +kn1ļ]yctS[1~&U .c8wqW  b {ZKBGzCdp +bgŦ*RƖCH88-(o,[7ӎ!f>%(A'쿤? Lj g\w"1٘! ,[v0Wa| .\}l_?JK]0@kIDAk΄H%ZBZt$o!A;|}DA?XFZ&< #-nj ိXRp &ѡdG^T2# ~rMy|n0'kQBd-<1+7V3% ccIEne g75`7@U(*l&JKdacq8"2ݠfI4!@{cԊ2H[ "%ghf7LS:>p\K_]+mA5V2";zSaf`l!$C1CEbq,@s ?V Jr +4Jv[~ed`5C>dAy98-ۀ a''K,džێ 4 aBnl.a~"Gj{ WSEFg RӌIl~nMj7*"Cшy7D-:*;"|$ A%n+~d^8 +l3I`OM)Ya拻9"|9˅GL@G*8X "\:'jkR ;WWI|*\-  +Ё3 8gL[N6<Jm ?"Q%yGkO -76׉šF[WNŮXĤ}(C6q=cWOH}kK<{1.T%qJWlUd_ioDY# >3yR"HcӬ":~ +xL<9_.ge"VAu_"\ː%6eP %nYS+2FL#p2A=pVZ((}Z-NC/"WIrMNf5e{E@縉l.v,vz,ęȅ&gn%1hn [ M,K mQ3IjUAgL79D`K'ZxD^[Ĕ`A7H@ +8jc!(Nm.%?W+G$u,6E"$9{æX ++ԑǥpVG< +\}t}&Ӳ{1^ +g|Xu¦HI; "¥`SY!;Ftt%I X!!/(oP8kֽKnqVcq&CTf<:.fHrN+;uZc3>ni{,#>fb?.-.G'n!vi# bfay!-C(m܁M}\Xt!ޚ3klm29v&Ǿs1^*Tbª%{`!wzChvlS);j&g=vC$kӔ'& h:D惌#RkؓyyǬĪ};iw`Sqc}'k$N#Jst3G{Xnq7`&Ͼ]4M8`@gtq=,0Q,^[V# pƩڊrX=Kvt"MpCI>| ~p$'a;gI'~a@!18ٙ.de~y` iLwqxq8@Z,.(['j8yAN-b\:_RFڪ@;ٓ0KVC:^9Yc9&Nkf4I +r'h?~SJ + !mH` +,.qME?%r8>ʈ,AIl޻Zd陯Þ͌4#1ԃ6Sp%P_'.*(㨪$ c7i˲˽jo,1Ľ} +~ bhBb- |'/lZFUjen8"^&`:P}GJhE3/dO 6Y;hmx_a!֪pH!潈 a#Աi_OѦPb3YE$M(¯ "8*s*`ci{={h0Q{k$db ^ ^%gڒ^`촳2GXc-fV2~K`[)aaKۭ0Qx؟5z Kd@μ?VV0}B +-(hT.GMjݘh^H5]1MC؋Mw|wY-DI)2R_%:{?IPs6 8*t LIw;y؋KXhT/hK4ȕ2$"M8e 7f9@ 5*}1bL:R"O)/ $:|]^i +~#oLHjѼxo@Fsl<;ZRܒd Tl KEiK␌>:gt X5R n^AaH;@N{rXSX*~*^2XhjT[Xnw >G,Z/`Yw(ȒO재Wnl m`PFb-KuF 5u^Qh?pYB-[+Ӂ>ܱ*Qf +AO28S9( t`Z8t>;uw2#H@ +hҰiVͨ&)[x=RuQˠ%D*,bh!m$Haڥ&$ԁQ%I"g +bQR+P-/y(9~ oz nGJA/)]璗IT<{UN7it5uCv^񶅂b }]Η`5GJ+٫⁣H T8$U OzQ=:k WRP7@sL1DhJzF +԰c,捈0 aWĬlS6Tр-紱YM4aut'tf`sJQFrn4#3Ӵ @wpPEtɎ6 vHzzx!ۑ,W)=]POt pKp(yEEhF EK^JF\b۪' O*PiKa$Dj}UX#YȳK$0N6 g!Egq2)K9&2Uh`lSRkIjNȆ!/I &3f`y l]A\S@d*B5Sn%o/tٔ!4;k֞;*GB./סBIPpi]X;(1fkP3y ['^ +kjg f~~E; Y'w-PHg%sy))JYL89K4 rexRm)99zze'aٟ TQ9geByUtS89BaR[T0; L7fr;WS XNF>a"2B77͞tӟ>*5-T$ָq8A*tA?KEU&]B~w,%p5j'n?O%{1Tg]TW 2KB 4T˙[I询@ÐeXE|zU5oQƻD 03$.<.Y㪲R!5@i*0B1c&Rљf+-rcITNݗ0K4v>ڦUn=\1V¬ÿO?_ǿ~owi?ǿ˗wUR/ur/o|H/Zo`W쿼 :/ƿ_˟:KFM]w򗔭6k\ϭ [NVQ#h&,mpZ&ʚ6vo5cCgNG<6fc +*z7pR921Qr?ȕ["HȊ\JmD2aHHNF/1gM-pElb'Gn^nî +N3 M4Qv Q6u UHTnqL-lEC)CSj+|je[Nr# j_aZyRJr BQOG*$61 '+Bn6u4t{$s"ddqAGt&b6a6JZ"ReJԙB@gu,!.Z.[.m\LNf55FH֣_0*q&HVCMwd*%c0fW2Kʕ[7մGp8-H)Z:$sYYo@h\se#AZ]Y +M-!Y9ɒ2%/YrէЅ%AËrڨ k5'wraCFvK"]NGJ'I'ZnnEK dP%?no=v  &xe]G& uYfX:P.(Xr|30Sc|8˪ D&Y`S^}ᩊ͡Ap +&z* +.V$B,ep?K94*r3#lrJyg٣BBBJr=ݗRR+]}w #TĐ$BH6wmS[^galUiO~Cm<-(Nys5[7jBy"śHF +8YE+i +j8G׈\+GxaM{fQលO8ϊsçݢgֲx?Of\+$s cރxp#Qν|tDӳĄhHPE$Rg"'xLzQpjblez#TOx~/"@-my\&Vx#J߁haDӃhZQJ=ed"-1˨꒓KK Rw["J雽$`Q-<(tN@jJWT4WǛ jQׅje)O`F$kZ%}E"t3YLg +y4,5?QWd(]ѭw"4/MKyb -ʗ-G)lvDB/A5H\KX[bhgN +m>ؙԋUVshڜ!5G8YPm9e$'ʁ\BYd'YV"馷vOO);ң6+dp\Rӆ3J2'qtL*,-(.e=wkO' |`kK}|RkS뾎]` +TJ6DK7Vr2$Ua~HuXd$rEocYn֝οru9k,WM _NJ6.~}Ҡo"ؿPF/PO5ۃx4s-1?D5jsk!B P[$Q0\؄έs>= /Hk+ +$HPq>3v#2Vn]ЄR #=iѻף'sE1aqY?| [4fqY`:.$`  6YP?>jc:"Ɋ 4J.lΖk>}lWF٠gIJCx[2=fCTl$"|]b}I\Kn /BuVx{|)vQ v1qp׆`6< .,LFꥉ2w<\TͶ̧Z) bZ.~}WO bj5aEB2OY[iԊkEIӄ +Lloc]H^Pe@)UǕtR+}Ǫc,sm!9BdGLVXdmP+Q߳âGT'zsW cEjLB;Ҩ;A@,?Oqs#\@4%4v,/Ŋ/7:Ud6z kqfɶѴ_|j3-/ꓗsv╠)UƊh/+#T)or, eqUyǞ{ٯl7=z`uZor=1G_zG^deø0;f w^tמlpa8F!ӳ'LiR*?O w1ʠoi!X:|DL3F::SwҦ3ז$!D2aUH\+zlL 0UvI@:B#͟%3 +eUbI:%z"fI=PPQ"k΁ Bt(.TƮ2K dHn!B$Z6ij/,eq%x\ALnBM5Vb]E |=etč[s9fJ[Cy^nR,26k}œS2,W{[yC,< u xg־؟ȟ{7\N $HrُUm)w$58tEc5M`~^cx}-U>:E߽V&dUOL"үjHBW0R_oO9~TP{trCƽܮr{|;+ g)ۏDXCmS~]̓rywNq]8 ,eykzPIYC龲fDp戟qe pei?g?7g]b #=sV1onܜC#LP[9>\DD7H +sCC^lg +{\߳uFAU]x%0g @-hYU2cp'aAI_bF g %I)Gϛ\4*Aھj`(Q}UrpZ#=&]5(y}kD輣έFMUAٞG7]$nS`p*NERr ‹Mhҽ[hq+O(BX&GB[O<:Lygd".c"nz[E& Kke`2JH (0ҊKbP`wt+PFXj(c f1!%{%u,iR0)aN׵|)p1O,zU*F㊠8/cOem63zx1C[fS4;45xjVx8]7w`+19n,5q8~· /N%f9"jp7;lId-KP{Jdk}Rշԏ]"<2[2̘"!FI+!C*__Séxu8PՍj<؃jBm{c3h:ӉU׿!Zԋϧb37/B<%CEKJ? %#Z v^qW: u!Rh`vT9 ^ꪥH#(J1 H>6)cGxf䡎-ÐЃEDN#`6^Gi-[7mAou%FUO*qtaZWY>\&^)&HM9dG1.dLܭW^G"#Ȣ`G~YX8dV,<§ +yP +I8{2EJ]\xǞG_|)s&#bQ?y=* H0zlXzX&›޲e'L[BS7|e `ݛ\s`eup6e(LIU߳GU$]++p"^GC::F|7LcHw5H*IVGt (#x/)/_1>Zd^#E>ZNzw2"}ScpfE-f㞫קQks~xj6\ŔF0R;<$E ;Mp 0&U98 +gstbb&%(ކ$7OfD($OR;&`B4?Fɜ[%J:E24@^0un (ChI*ɮ'Tx_xGF* Eo!= !1"}]}`zL nIfvT_usQ+a@O32 $R)6v$~&1_L{I㽇t"RN`Ls0Xj7txu HWX>UVȧ h-, +#Ql 'hKGC&$5).R yLW|~ȠғXIuKϧÑƦ؏u)#:IEJCeIv% x4 Am?1f 2 ~LX1cXjJm VvOnq#@}= +g[_-WEWxE(|g\D Ĕ~\wqU4x /vyN#͸39ro:- qZ0[˞n +9NNu +G`Ih( +CngDMW "%gQ]e}hi==^\_tQ:TeFՙ(PlAϯXOT="* eC#oЫ¼]/ |DLmSxz*UE~pס#rXt,Q"vI.gδX9SpqI ir}_X-$lI]$ߧ Vӄ\$,EC !쭄F-% P-j6DiIa$Ck{Ǭ_fmUgr4#!`-v"iq'T 6})RZ8G)Gæ&U4nVe-PZkl2c:vD?򉲡 (=^"6"=F% r|욤sKHsM%TȩKwB +ƗUZgN:)~y~_odlv$`ۀj?ޏ Suuoϓ)Qld&IRIR;H?k/=φQhnu;=Zʆ}=GA =s;Z&TcFݫhӠ*.PK`fP`";֎@}["є:eqIR?|Y/@MIfkp[|jG^_=) +endstream endobj 110 0 obj <>stream +lͯ n&}]񆁘?8{="}T o0{I>E,kN pR)] * +164 Ac>{P(s&YN#涢}Aű )s\WR'fI;Vyq]V @f*E{] _b~-X}8Ԇ0<c.!TӛPof-|hmɷwbo*ĝ^qW-p$=O0@pȣm9}I萖<c?]s杖Ư`,ky5 +IF5;&WbM93TkFtEI_6՝[ +*7cPbAVpX {9}SS)@mO<]NWY +}'B163Pd:.̰fv:{g<ƹIFGG ?<#z[2#NkŅw {WASC;|/o8@81|HBWkMQUȁ#T\t?˲~^ ·o[Px#%W!'2/8KM~ύےY§T.ƦODC| ,V$|~E]Rr6K[z2VV+u>R[)lyL+Q/EL;hż: 祶 xPŕ0rzm1rb/ vba&;@>yA#) =KaHIi)ל<˘`)v9"=pm婙z!ߒ& (OهNFGe0R' DW0ؗK%K!%Ԭ>o +V]E/ +:pk(xsTO+A $^0H)gH^rWA&r.$1t,i9H16m z(0&s<&k('7~N.Jttf5ee1RLLwև΃^V8koe>Q|s|4FLQ 'rPu_ںhsԬ|tE-f:ӑd#boM<߮maUܩmvwOi9Էr*\#ajkϏYk]L;mB梅 ⧏=!eGURڔ*Ix%rr|KfgZ +dW̡2: vz/X֛2O؂RA bDL]1ǖUš3} g B8:GK RDy/-ʙ1 *-g8 p*jtDYJVL0R[ 6C\s6Z}Жi5=AL::k&9_CqGon&GA[t$[X+"_;*cɩRo`Nv3SHqw],1Ő\b%K5YW)es2[sKL:e4I aC ~K;) =ķ:2[rC3w!%~*As "O?VdJ\ʖH4Q]-i~Րʾ>Fv/$*4̰-x^nJ-K|z0.2XH*J`堪V2s:p}zSexJ GCa; ~[5>J:,5|c8s&?@aU67y^Mŗ e/=qyE)|/x336%L9y!%0;3 +bsrgI|z#lR%)`o +YVQګ Z.{_?~l=,o.xQd pIXrVWS]=r{ѭ{GwoY ٚӂ+&[DI47Ƕ0Wyޯڴ:l3v-7=P mf G1r`hY*ʪ.,UxT zF-2pe槴VHJX4sV._s +a>T늁r`zg_z}&#{ٽ#tUHUBS,= c_HS٥{O/tŊ- r`Gs DI# q#%8wإŽ=> +'q&q>~kRlln%Xz AqĔ H +:kFlǙ%\m`YQHKXA:_Rt(M3_U~)şy5VƟΊ'&xF/J%X2Qv~p{^ȎocD!zr(":[<#I[RR#>v<{8t(PjDK]8bPnhgd +n6kC'R@Wtcd?^,&#qhdv& `o/Lս>FMAҳL@!c8UY ] 7#L齡r9ukJmw+v̮4cfPGگF4c#"ш^-YEPE[ؙC #Ɛ=&q/9Zɭd䣯mw{P%/7!S|({꧎'ܡx}֒!Z3I8_8xǞpB;5fq$d[1RKz $Lm7 +3t$VoHC/C3ǣphQ +gNe>^E*Գ+ץDKeg}^8|î3 +) ?[y%j|C6rUgf+le8:4!оpt˕sxe:CbDK_Cӎ3QU h3,7 g꺽XTRR 4(*. "걕,.1U+I0Իr2ef_%rj/;K' =ueaH4(5TN +h8/w Yh=Mb*|͝Fc`<1"J3K1kMSd> +:33 NlON E<  #yI CG~WG2RS yL e&y q +TNҽZ+!ַ<f li%^]Ӳ낏7Ͱ1_%Yofg -7g~b^L_vJt4;C(/h+Wyt DQHI64cOփi(DWG)#LI}Fyg֨a5KݻRDjM-ꆇvTᢺR)T]rмdgR,ǤT| i-xMCMrTrpGŎ)Hm f (Vݜh>gX-Jy1wj#,Jޒ)YfGNwo}Z WL:IB qy`V*xejVD7*dINͫy k>b&&Cpo5m?bf%( KOy%=M%c{=#(,#JLtD (fthzWqXc~|]Y To8)J)vT>PMT7_T`J j2rJ)צ/AVj$)$Pc|wXR/Y4Mbhf@*Uf8" +i~=-jb_Q5K[[E+ PnH #Ш*2h4]y)5^1[JvĮc$aX =PE HEwFA,mCGu{ku-)ta +'?K +4E )P_(ǁ +kYY=bq!UiI{\bP IP#Q,#Sy'V15[a * 0UH܏ +A'Ha.;:vlٝ=P~("q_҅4g6E.i1\}):t?p&W/ 'I$io eNi  5j˫?>?V.!@d7w{i,2av^Ef,c7ë b5ˀiPtg@ɕF/x+ͪR:PN[@4 U-1R5e ]nY( 7g/Dz |UaL)70מt]m~J_@q Woт[݆檆#_sђ{}3> +?ˀ}kݴufCObJ*4~~K\R~1=~0޴3}g2%ab[[z{?{Tчg&HN ` -P~HȱK| ]++2!Z6HʉDQŽ*l= +TXD=iZ(k*A:˿6g?M%DDʫ\uWچWLV j* g'f#%1"D[[yN"](| Cbk` 4ipҧ 3OϦwCUaQ׸PB]mL=~=>grhX4IkV=j8d*(j47$!Ӑ,/[5o*$<$-ʂr02@Pyo,9٫LE*ȶ]ʔ}%VC`ٰ @lHƆ䑄0P!h4=ȇX2ؓF{IS C)eRIEriy4'hÈJP_Sog#{oSƹ.WheP畞<ʙkBF^c_}=G¾Mh XN_${iK9h[itsG29#%F5!&5TYi'ϴ"2s82mkJр >Ua%l>Կf"95i(s.Nf=][`QAOl:_-i{A{dl>TϿu=_i8?.?z{x擺gɎF|W}W(0kT#l K {Ӭ +-o@ТAͷo{~B| ҆IkPZ s'HXAm̒4%L`!RMxn^0BU=U &Br\S2 p'm: PP=YZ94pSR`e=X{E0cN#m jIt¯SF~+gz]RYP.YWhy²={xxi {)V,Fq,̦ +'&-.*` X ZDmk %=ʷǦs[u`v"<+-dc{1}\-j! \̈&HsQe@ovߕOML#b꒖m7sPƀ46l̷G0͛i❍}ϩhGzqzx +jz²V<:bU/sM 4a0CYz&Wh x^w4g5mÖڣ"XYWyq@[lT458?ijυ3)sMեE_6y" ~w|!>7sU2 0|M]x5ȿgxӷK+FW"o(r9i(ґ?R֣%Pf 01C+*mo\% u+f _7Л4LpǾȚ-un?LH5TMO(׌F$hC34۶<"|HY϶R>?޽oᚦHL˽ǮGb94vPrAXW~qܤ׫΅ǯxИjFz +@p3&ba2-0FV#(x+| C:{t4/Znog, +*Cn39ׂ=vJX]eYRZ +X %%yS)}2.hCr]7AO⠽$FZBeBòF|{#e+5υƺ +&FM[EeǗnĶO} (dB]tUF0m7U[#[&aYҫگ_ߧh[^*>{hҍu 3_ꬼ׶~G-]zeOJ=ʍn!t(*mHC!m:uzY[)-G?J4/^s-L}|8`Fw>dlްkɆ=}Jm/ L4$9-?Vu$ph<} |ncOHUǨVPAdzE$tT!֛Gs7Q\YĈP@& J*y1GGr[^"*0 *=$|u9{>LXkcW'GSCʣ4Y *h㇔:&lJ*hYXOANB?.*)f㐆&?Qqc\uUW݀a3e(j۰~N4D5Wɏ~ʏDCzqAC mܲF=DO“7OU\,][~Fi5G(ǑZƮpfu|z> b%IߌO9@x[-uRI 1uդōT2z?wV]%$=YɫVu^=uԋ=}wL#aCEU纭F}:dg^3`bٕLp8\3,vD==;o7`(S#VL4O,q}sDWƤ_$$KN;0&i󿏸\L"a" +ʻ@3J([ԫdcRp%m^+2!s.[Uv1l|$DhSjX܈|PG}uhx ߛbkjBڿG.#ߜ 0#g a,KC|}}vd]¶` +`Bj5ɱvEzh%M۲^8>.ceT`֚:>)T`"UBڗtj[-kP8$'^MԷZ+rmRabbvBt'{>5J댪yu*Yu%l8m eP6hG\4 pS`c%ӫzŠKJ?W6M0[f@@`YO`}{4t8Kq%n +Qc%ó8WhJ~m1" +[tOB7)v3c\bmk|;bՈ_5pEQǯc|tS`h8 +ϫY:}DDz b`_ ٓ ^.Hn)yQ9!AsOD94T9"qQ7*K dlFGaahHf}$ o1mSTWcZVjRbnaxT ʵjpӳ+Džՠ[{ί[!47]- +r9=Ģ{@}WSh%w_ԕƧVD8pWׅ%ZmvSQ6uPꮁNJMVxɤAŨVPY¶/Ib|+H@(B+ L#v;o8][{L;(eB-P `'럏 7~YA<"5L gUg p-d7斲?40'} pq%Xܢ   +Koe2X_lxWY_DXMf_+ b+y(}&( Wޑihg | ݷ2_|q -bH7 + +zI7񱉲BzWPhu! +ZD8෎]\ J8 ;AHR2a:vnAtucl$ r1)%9QPFž> !'P2_G=oWp[s[h /N\|E)ڝ@J Ů^X h; 4({Nb1&{uwiy~-F:[#vݾ*e*vk*Qݧh{"3!=hQ +p,這 ž'[BviζU%yC4VޢێͦkQVFm~HIaՎ#.c$TctEhG'ˀےeIxIUsO"^ Qk2fLD^`q􊤠:r6M6:>*#1pzz>*x+\~z땡TB9̼4@:JL"YYݜ5UIFR6jI x3 + +w- a @c~u0$@'gVJHPz;ZLAhęrY?e8yEX#@-=íܢ;z"3! 1B2ɤhVJZaWi$`OgR]eUE+כ&ۏH`ĉ s?0H{aV#dPĄr&2r"P&+أJ=b|Hb뙰6$iUjEuWIۅo^{x/4J Ngrs$rh" +Cqe↻ Ɉ#a*YDw@+ćEv<ċ_LnϞ!Loa@ iʾ盛B-2IzBޕ2Y%$KRj.{5khπ|>yjxsdߴv $ +X΂wDAQ-.چLб`ͅsK(x]ܚ3">)ǣ(uDzAtjfv}x kVY;"(M /  Gڬ=QIz|sE(ud%{E%6Gg1%-}iw;#zu]3RoΧӥ2y [Aێe}Wʩz-TU9zҰ!r[ߖ?H mRnjElo R=.' R::#}:l?rj>Dns/eS7A3%\0\)֘Ҍ8yyA$Hi@ų|jw-p3 ꧴I%ETCL5z +jka]1C͂8@Yܚ.X&Kp ]i_ɷ !PGA^Z4A,@u}Km?9u&{wz7By"zz,({XN썲;NbArc! D(qPL n+#j_uJP$U_NFK9М)[e:9zIBo@-x']:v$uNe˯3W~z{ r_TRJ}=L?9yzH8Iक़Jv#Yz(t8Ǽ]y+<RG?XR%0[Xe l9--拙ő}8ݲ"fȞ}.kU_/O>/Wr pu05E}>z0aRzNا:a +Kn!5^+G%H`ygPx۪ԥe'W%Fw u-YʵM5S{9n\ +{7uҗW=x*`.G vIZ}1$%ܯίQGl3njc81O].=+W*8ZUO -2o#%ʿ|lP` Klc1ID:p `o=HP|T+Je60QJ-JGpk~қlP#jj9ky *nw:%_h-G ;Do(.<&ݘD$jŦ"YNqͦFvcРBG8ySzR8M=}Nya 0#JVг.;hZϹd[f8@4p/Gas&^a~W|k/J+zT=ރst-Hr +`uRGg02J7Էf7V>\B{۳ޗOW Y?*y)H# x~h{i8+9JCPV-DhD\e4}HBҡP88w 9QLq#a ɾjMč՟wR/c_ Y#A0+&I>Z8ujwTMI 46~#: (E݋8eh Btq0!y/g x3 J3dg^iZIucӅjߟOܦ88i*x*5%`& ,ƫ x&:~QiկrK!.JXa$ـWh 8w#iz)/0Nﻣ@6:z$kHO%ZD"iƠP B>vD`1i|@,:#4^C}J,nF1qhc#@5NrGN޿9H-iYȱv,N + +("Ӵ"yoW9 RG\B[ϭӈ+}cPUQHåVBc*I{Eu\Z,Aq +z7kx !yG ޗ}_@@?+J zTǮQע9]xTT&OU.z`0A:$^$p ta9m`r)g!%&1ژzNCrIӗI;[X'bBMv;<-P~,=?,=|DkZŰd]V3Ce{YENPf GS[~Y̗9gɖ }!XiiI6_fh;шRz~5O#<+ UHLW +) ݭ|\|wțդ3 +-\pl&uhjYEd]a]YF@fEq}Ed#A,R@E+FNIQ|s=]>Cv';x'n/تWKq0pB+2o +UiZ$N㗐zcK> כ!׽žECgR6dH+FDk-מ6&s*or;pw!,a]\Kr`QG(ӫJߥ='W؛o=J˞.Iwkz)D m>2?XN:vͻ4ح9 *٤D g5Xgo֙S^1BO^C{ ^e]+/=(%JqeܴF8FG}]He?_{* 7KA)D!#Dt4B~0:z.!FΓ^:oUgC +'uG=eƀJ+aYN\Ǵe pi#SJM{1Eڂ5dgg0J&C𽢝ѡx%,o*ځU:$Gy'z5-lp[=Z4}kغ!\đY3QCE%lFJ`4D)3՜hkYC[՞NcWP,\VCY 3GFѫTeO_]n +X]+,Jhavv19j)ax +BIMDqQEHlr5RQgϡDM0YzN˨z0υmm0x4QJ#Jz*A~[^w /Ы[drEm037q9xwCMpMK܏T={d\i .{MYڎPN%l*zn.Pb|I2 U "Ewd#JuhǙ}jIP&oz٪@yDUVN,5\~Enۡ겔:4:܍?#u\۝ifv` 5pa+]vV+D`|XNZhJCssD*N5E_o~u/Nf[i&\ҋ5\鴀hӸ;Ҝk4 Ȓȵ!07^61vDrr ߼tHMhX|J}ݵW^ 3%Ci&BF7酶]눪]^cOcmCrkqj^l +Y[Gnt"עJA~kk`X.u-Ѩj~.Sj pVs5s͞f=Ah!}w fӒ OEtJݺ*/j}_(sJ|Z*"hRj#E]M QNJT dpÅ27aEoaQ|XasպY󹊞 G}~*N)Q[7-YqdߌdY#H/x^Vi>$.:Nj:=݆-*HpED&/x7N%+i aXW< K 8I/-^sp@)2EVI6g2aK"ZsZ:mݗrľ\QYa[BDD|ׅA{B_, +JЍŕV'.EI].U 5]v@NhT7Waנc֍lo1̔~fEb8y*iqq{&X`\wP'%_ .*g.dړXhGFp @2`) AP{?RwN ϊC%JY=sǭAޜt9+0n?eFe"T0 _V 9+'5P>0;Z7NTlVy:/5ϭVz\:eY^4N]M>A-Iđ~7 atU^63u,F?tuA>@"ةЊ:~)ݛ;`&Y !ڭ:AV2'D J/r*#|CDEeTtXw@5GXa@'%:2L؅JtƏ 4i숥$Zf->Arz?~Ȃ_KП{.\` nhBEG]&{o +IzrU@1EZ0$InnYMI-40Ŏj%FF@"ԜnI}me4 Qy\@LLb ~^ XK{DYK;?wUmBW"J pJB0̻ؤ^lhE% ہ,S{ YBHUv*5RGs7)M/>MAgM5ٟZAu)l)3ކ֋p.@,4$\BO3ڶ _\}D9c[lx4cB0؍9eLa35έ:*˲X#ua-?waU6iٝJE4 V2 +1\SWZXaO]+2;Q/P6{X6 VKkb,Kqki@R/i?{ Ne d->Pw`$V!z3-Vs㮤\W yiާy*o\Af1IYvs|:'zZF95'  +2X u 6GŶ,Fz"O TjSwH741 }Pet{K{} + +ЬS{s-}XgvԒ&G1!*94DiR} Bdr^dсS56 ރ|*ݶz^4z CM1rn|J/Qli6 DĿ~Zaڦ 8J1RǠHCN=IGUVȯJUIv=E)Q@%C3̕oO'}{aCSCB}rXʋ/&?ʘ;GcRIUJ} |'1a'S6wwHwqp_M-Mεh|wT6 @Y>V>ux%\uI_xa#BJެICXhavg8(z(O j~٫k#@L*Wj0'5[@7AԲM 朼8I z'-.92j+Ě_/HEDE"dW"tIbEH='0 %ї)vnNb +똹$p\]8>+cih]$]rۉtO$Sc| ]['*~6ߊK9?g>k1Fy0OS #7[cF5*8?6g%0&#*td_xj>hm;--$$U$-œ ;=o8*nlkTWyF 78>8rD|42*Ծ9XxLJC!:W+ n'>m Nň_7$_5SaAXRk/m9 KKJO@jVih$hY ]mcB'R3gҏri޸|q3a@wJ6#~ɧ!kԍ =ˆLKҁP(f[b4>1X|zCXPGsRN8cZWAPу(PKxI<2Ƥ[7T˺R,==Jq>0"h*0?4 tFE@{A57GBH HPN7t-+5MjRtPPƔVVn1w'Y.+us(I]9VӻN5sDoܟ\C4{ >]+C!Rom5\\^ٳYGsqT^,PDY%3T5jarq[!L:5VZYrT2…"SI'aO:e`&@|z6׆U:6,ύuše'GQEUXaH3#Gf岆ptNc 532J< Ҳ%iKVN`4b:%m+#UQ`08وހ *h+*3MX-SKnb~"<(NgqzH>Cak#֓ՋW[Ns0@)>艤KF]yeȖ+z=!4 dÝn#jUY&m@Q,WA!QFgosݬ‹ +PlA^2#u]gmtTtJ&# +*.v,I~(ጯv p.Q e}ȳɞ6t:Γs_D#P'&8$duxmj_O:_,F[mR@4'|DM)-:e%YhB_rqYWp2v?%HʹJ'HZVM-*jqvY[~_* 7hD6ot$T'V +9]Žl +#ZL I} ZQ 7xk\JKp貇{3;a +Bln\Ѷ;(7H>"FնO^xc!AaqX$9\t}ȖeH#hleZ^qt؆{ 4̆]q֍rzyF +lj󴥖 ۬ Z Z ("1̱hw`ʯ` %RbF%*0-~řj}Uu㋰b ?)ŏo-ԼKj;d[6wU{(2~*>14YiihPRL3\XX[E$5Ž D%[UGIJdw(A7Bc8]l(c^T6I_7l65WA +HziӅae;ܮfvl*ezL. +E~SrY)itCL/ uqᚋj%N5lՊo%(!QϹ<4|jm'G IGÇzL@ӲQ~>*X'|\Y+1t y ٖQ|*WA2O8:1ƉIHwi +Xr*=1 ^U˿#\q`փG"DllJ*Y%0>-r0NI'pqifuCR +Bp\3,eN)`7b̆(bE7w+ifLeբUJNVOP(Xt2 M$qTs3Y +dP+Vj| yPCa{AIzqK8TPT)*%J(ANzN07Fc] k|!ۀ9F +ǢP-ÜN*vzT;唠?D0RPTNv3oON%_kh)||rq}c6=-)ECPqgz UM9Q-;$^|4F;ƴaYзŝa wG*gQՈԗjBZ-} qA/ړ-vlv÷|ݷQIZkbU5e +)*BRɺ3`*0ǷUһPPޞDŽMq tk= ff+ֽA`VB/cpuq a ,(CfkN1eGd/v\b '@F#+^I~`:jv֣14`OU̻U}[x/̣&KKlGWI5 o`K,xv_*.b]j=!Mqg)10ug{oaiZ4NFvжʅ(8Tb6դ9/\u+-5ޗ$ -%(HΩd%ڭ0JPZ[gm vU(Q1%\D;R^za<(%5M%ZYX֩V5tE_[ŏ^Hl*pm>oWKj)VN~T܋ߣsރ3D'Xy Qv# }/`IYP<MGـ}a߭WO=4T!B]*-}T e +αya:6DMEpG. ',,r;y4UWx^(C-co-#Sߊ4,kGwjC5X ?%nk*^RX6|9FT3QC/z\f~ x(#CJNuߧwN-XNZ`~Ycwqȯx8Ƴ)Ǡ׃ d2 cFyxsa(²E=b>G/2LRtFHl48rVr|"颍a)~3T*0P։.\==DZҩX?Ya[W2K(R@0"Y %==pjIEph|k˾ygHO/qsX&Qw(|.1вQ *z ¡ezE94~Ez.+z/IlP5F|U5QIn:w߬ɂާ%}yZ: ZM]c=f?gHnFB_Ÿ^uYgu*1sf Kk,%0dU9jJP WiQ 5puYHh͉VCRT8">a RZI d= aU*w6 ʌG'TW\Uk~ BU&t\:UGX3p`(N s{h^4g*4Ƈ.U{CG%?rBaޤ/U8rF@13,\XR]M0͖XF\h3_;6mLkxEQ;%_}fHG.m2gС$S> iL@칑i+~E AŝA!~=㰣c v +*SԖJ'$r@g'xl/{> "RLt#XTj@okKnv(QSn7+q;`K-m]:J 0ܑ̐a0vh{/j>XEY +9$|[ Ax2&օ{ Qmp mT(PXNBTϭZ=].=b f0QԞAS9Ii#-ћo + +jޞ`)zZjץErkzIPۭx6({MҲ'VÔޛ3KlFSõc kMEmvųsC= r֦RCK L5W=A$P^5ޜŨ!9aծJ p@Be? G1( G`p=J,Z&3C_eaz&%J0+'4KKDSˤ[#G] jhgžLEGu)יivk`I>f8w*d0Ssc8]c"k46ac5:F,4K*Їٶ,(F  K}JN.8a¾>}2$w4"s&Ke55VRfKx? ~Ox(X%<"yN-oe|4.j):v(Vp"n>ւm’Rat% OI:JI{ŝ>K"A$Dˏ<{u?t_r]Oh%sjz[s:qh@}<5`ꬊuO^|%GQXb 3r+|E6׿ۈഝ#PPapg_V,C0I=''sщ$2$ x/e)E( .:p ?^et_Z%tp%_S)dE8`dE*g!~ + d|1s U c-&~6#LZ-H۝D#_cNcm)fw:lR"J?jzvmқڢQHQ_*^-$vcٜӧkj𑳅 p>=oy|T[QF*?y>s},7Bd$D,2 o_Ù@&k5&)B}}x/Q=S9>6vq-cj{FfIyz-3F5=ծrrDSN!UHFÄIP +7$U7jw_ԓ"*s(*C^]eegwMhXAӱkP/G)1pT'nȲͽzch'FMߧGSs<~ΉT[@Wvr4ES,[R\TÔ +'TQ,ңb`HXNiQ'*"Vۋy.,T05N 0.Ҡsk 6 [M,TF +C9J+YMe$+] /L/\)fgiR%1Z+<4PԜ} Dc4c^D濧%/ڦ#%Z_BUAĽAb= n8uB*\#8܊1WQiUN=e>gr<뿒?Y/7jԂ^b:] ?vG +J/ Eި+<mdD8JLEHe"/dЂ=6`+[oGghntVB.zoCu.05QZ䷳{| +%5rG*2l +x&JaEVwV PGT\L5]L) qKhRpNiwM_U+hŀ@m-Z4n2aHߤE8wE )7сn= )Sap92iaɒMc\#5mQ2ݪ[O!A%ako FMW) xaQXkZi=҉ͬYj"չ3k83k$, XUԊT1&5;|` _K0| wGow,uÿ>_Co辿v +5ULן0;-DBFk"o_?]_)ɮ]^4o*4͎l*JqX6Bq霩&R+J`% @@}4џ}N;;}% +aRVQ%x ™LskjieЯ6H\ގĢȅR;Ģ)VqAdZ+r$ШDP^5FlMP2 &z&#x+h~2٠G_ cEZj9VtktLeoH +R~B A} @C@@&92f{Ա4B[: FNat!_sQ}m-F+TA9mڶniUaR{w_50OSeۑ$@KT{MmxXƃ0fPΪU/MO3KPqS )ǎ4! VRç Oc7w^t]í04 3QI8Aff1NZojj̓?YN}(ZB̨V!X(['/3>~1l^,\X.͒$$]FG7,"*#26f8ˁT) 5&V7 {N:=E7 @50{H-F-2keB`apbo1~ۙ1>z]œq!6 h-kъSm0yFˌ >5I1.*A^yE\'V;'ISZF{Y0=x} +Ξ"y›8 , ?ϯh`?Uƃf? (CD.Ȇ Bàu[I7n +94CP +=@'AIT#%}ak?5n"KH|q-3[4 eoԚ*}t:p1b]kQ9ug2trGD: ݵQN`Bп7DU5{d,܅a&]+LDAzvgc^;ZcUx91.>'3S1Vi{H?wrC:KpiåCBxu<4pۻ_lNF ) T1kV@5~B0`Pa5ߪ+AwJ%_Ƚr ~뵐*jlLjA榹0ugAB86 陭Vhr锽a +Tpvw1F AzM[B_D,ZqB~ ۙlxs t`o]B+mFV1r Y#2 +۷U=̋K5w7PzF<2ﱼC 7P)|%C&/H#:Tyi[_f@K?ٔ%gEK+M}{Qf)*RA5eu͔3ب^W?{dsGq!i/$Y.E 1K| ڶbr:7DL&fgQ$6iwhyS*zy5=PA E#hwc ǝaIPàFK胳{Z91] +Gac:Xwr**a"Ȋ!PSI/G9Ti)`!%',u|w9YB=tV ixR1uѻAlNH݋֜棡;L fFOH/\~L*rI)"[u]͸F +T816Qewߨ3zsi Q@h)<:ߏFdzjK)\B\iBpeSX/YFXI v /=o9H6r 5a1IM# +`ErjZ3QB:oX|gd1o* +#x}qzu],=. %!y$ljQR^T*zFVPbhYB6T+߼Ŋx!lM@0jÊAS$5zyY+:E:k.w_ܖvij|u Ϟ AmAVoHUօ&/j÷,J5`?Įυa/!Lk]ewcFbEc(p +FZ|IVAʧ2JU-ID1[WHt ^P +a"jnY*K:o,M'2B{$39㇂萀6 T؍ކtcp<ѱ'G51YYBr@*8"ץ$9} іV#3M5ReCukЕRbr2YsBvaib+ZaK:MzG&Jܾ  Ć3O贪i߸TGSg51\F-{ತf0[`S_T-,mTPh[.,5W^_.+N, PDZL]eeP_.ЦuDg؆V\3NUAl>ִ_IUsxx^)EMlI]GZ׿}g;)0{^/;ڟ߿r6?1>I&QRR`4"_E`ܤ$va!J d88؅Ex>RTdͿ "%/D\*7d|4P _E\n]7+@9Bf^kpAt@ujT>5VeBWyUMAGatnT~g9PZ܂l"ҟI% {BbWIOOK }L{dCON-!FIjoW +K5~=Tj,:ԭW RX_&SCЙE]]Xڬ)%8)%lUɈza2q073$r{g+ ^qp1٣y'5${>kЉWof@zeuQ؎񡙁+pXfCX `-"vz0q.VFz RmN4+y9nLD0X_kT?ԴI*N4hepW`=Ci| gG֣/IA kC$MR}ϵ[T?Sq3Ǟ*iUDDxvd?S_> _Y14?ClAG150G!* >yM7@(!&He :wÀ +%̨D+7 ;.fI}!Y{){D+:4еi*֋+ Wo\9?/ٿ*_rȳ^ +so~;_9pMZ%:SAˁ PZJ: MWrZ&z4;5aa,xy]T\]_-_]ukv 0^?*%C:.nb@e8]*!aZƀ-eVD<=B{aD5HV8%@A으Ysm-Ey}>/|qx { 2omBxz-(Svgh _V{$~>IƟؗlpܜְA4=q^J'G&F)#RZ ͶlɰfBa + GtUQP=uBMؑF%a7BH^e4_΂ +dЂp !aqIdW_/3;j,#dž@1܎Z^=7o2]V澚$|ܹpy>>Μ' O!?9E-̷;r_E<+5gPyodz, ˫Y#.Or?d|۶kWSO8媹 #pE}bZT˺5űT}?"':%_)ddz+4uՊW^*4uԲաLNQ7_)IK7t^~gHov;D-/ +jO +ku9dpi(}<̡͛/BHw1j +!S!tf4UA1ۚV-/>Tݟ;-U94oD#+Wqwp1{"(rl7fM ¹`\a!hV, * R +o}.$2XP3On DDлHAX1}~&Xb_2w|9Sݔ k:SVmn^,'"Ƞ`ri&`k NmHngaL*@--%1eWD0=X*EHbF~LHneil[Dq8=JIA~ 9=aIQmGtU%G_ f`AJ9TÚ:˾# 4=)|TG<E躹qiJ "^,medzTըå-?~-a"r-r +-t9L9}gʡq`5/ Ga_=BV,|)ڋmv#:Y +P}fVTbsvO  yk5fiWk(8#z%~ aBymqTm!yas ZFGU&$(@dGeMjr-Aؚ0\<-QHCVƷ>x_.VLzyz% ů@R-΄Ⱦ pG Bai 03H!/ah ޺ {D/!7\a)(3bw$V2UDBУ2vRbpW`usSa|mmb[BZkk-vߟw6Afu}D9?|AچV!S2*fk@m+=DVXCuIWߏnŊ$ItVۗq-s XefW±۪;z#AKn 7%" Thk{D-Du.ofeIWHڣy3Ɛ6mw|q/nܤrOi:x.t:ֲo"0~i_2VfKx.P`_964ޝ#4 #O 1;(p@3t#bs$ +lsQBƵb f)j_b(*ltKQeIFFaS̫+XD[NZi2C7dh/p(Nz +l8b<|n CmLb|2$w sU۫~^]Zܧj3n -<z%?212=V#L?gV$,y"v,_{Gakb3wx/ݯTa dBiە^R!kÅ`z_qɖJ_=Зqѯ8 rkW*ntt }7"NS@PL!%ѐC3V/P$^;#} ݸ}ĵQx#h8b&?}{ Uxd@=Vx`6ˠd#Oe0_ + .pdy)7)U gCSF^O]1j6I B+^u~N6(m3r/kjZTaIgJA̩6=Ӟ >eLײHd5N+cXoP$MiiQbRHKP4(i%uz%w-?i +iZJ|gvA$n{M)6Vr6Ԛ$DS$Bj *HRƷ-y*s~au>wJmvoR6|s,<Åsa(袰޸EN:~^g?JOq># /^l`h2PȎoi(}q|g#qG Xa[jfa<OhQ ^IC# +|obheOO^jLp^CB⑰1pq&#BxIsNd#w[~ :|nc +27{ +B7'Vޜ *ܯ#F`xMh( )}5Mq㤫=OaM%tdyӺ"yQwAyu8szH ,5j2^ d5נhh[e(RZ103j*a ~ɜ嶺ouzQt(U7<z{{nN輀H +>gP)j*kmv$^Zp>(Klط N+uL$ ͩRLCNڰtR"LߗM \uv}!SȅsAʕ sjo=hǥA)Z)=A?%7- {؀gtkp34Ɗr3e4>,^-%@l (XܓTJ(W3-и Sڃm&K'([]jDǢ:ǫzZ䝍ɗSW_i@n6 MB1VJXx(9Vn3ښBzli h r<ݠLiҐ +L(h7XTaEi3z즴 +(n &ԜSՄ}r#qn +闽 H +N[ȴƕzbV|ӧ`o5`󩦣Ef,zي&QNPTAwDKJ&#Z`|VXPt)CpzFyOY/K!ezWEMYIC6{pǽM@u) "&+Ptf("N n .'`vSݮaخElTl+DJ4bO?MX˩b|E5fF,M$&nafTj<=w߰%0]ׁ|;#~uʐ(nD xg +7#ăFmIxL@;$\@M.ۓJg㓨X[NLӣR.Ӊz=XJ(GRW,*?`Z<EN~m=9Tћ#P-%dՋ0eՙ@ߎpudшCaE n=0sZ־a1n$vآaS?Ӟ ݌F¹"HƨC5=we,LG4eƅ N}(56&.H310 +ߑ[]tsBx03㹠??k`G WG*&,//И_k8|^=QO~ luBUB>N۬_uy0<<wUY/<0(a^&y9sbjw(];X)B֣|-c:XZF +*-3zzkYVDy\cگΒž\say.L͡2W #&qCyXNzρ0PߢkH,u^-%T OceS;w-Ӎ^gb}&4[2\Bɕ{T j^xNyDy@[`>dyZSC+P"*/sAΗKc +u`6X#'M`񿹶E>lʱEf + $02U:~w&}@mԕ%DF԰**aK+W0[eI_AR!ؚtjO/ u5q-@k AR rq+MkKaUR,ji€ +Z=e_|{6,rʔHUt%^9sZOWM)!{(o;=4x6YbvY 1njV /[gsc_""2Zl,L %22`̘tQ2aI4rc] otA9J0_gƮXkc8M@#fe9pu(8[bѨc9IT]aڴ*N^7"a(^\B \s_Sr|(qr5ɥ=Cp:K%$T\ +Eij֩8z9"oX@&[4ݓWk!92"k<(I$IAMc%@KB%#gYFC ;+J4J`TЦIΡ5~aG/xlKȨ_rJwp@cd.OOR #4EOHd B0AĩcTPw2MA Ȕx8 -y]4GZAyhD 0U+ R I `*Ԥl&1٩ ++@ +߭T9w+ޗ #8@h1BRGъ ŕ%DdLf& +J\?%`  ABmI jfA:$BYDYhv#0` ][lb.¾-?w)BBW0Fؐ0%ڇ{rW5igc(>+i32HxG H+*,b &.0CRk(GBz25syoirP\8TݕM%5?M ?գQVqJjp#$y,) s*X,*@Tuǭ&iL|C|48nS-yn0vfKו]>O[{+xbk[Ƶp:fSz?_\8PSl ++ 8s&;Jz7ǣps^oq87E#|G,էydWɪăVGYotC7sO޶{CIMG?0#Aړ%?T-S n``wmM~(<]}Fz2{;h6@4) yx(`r].M8׳K'߽wpnԡG{l8h6O_ 'eğϴw[.^g9׵ +_oW?7&bnOᔇ qz+I¢-夗}qiYUBu}Eq\X϶6_s\϶6?g}*6?WMvj}~-ͣTu_4ݫh{.t^RF;tgOooEf͎626Աn[5#u1t>3&L}Rğkx$c﷏)qvFѫFŰ")ͮ'(9u6F_ǒAT`RdXJ-AFo\@IN$6$JUH%. X[8)7ߍ?j@@KQJ Y+IAU44>P*$ A\f24PV Ui ɤ'敐+^%7t4 %ԭ>B/! 6<|%Lܣ>"+=ŤY2~WH@ *ȫQ0<ꡚ7VF6HWi"96(p ?2ۼHn)-n80D*.'RT1ihe8G !_RٵuLwPYG(d&xޅ3K*,A+"VF8T =E/$([rXX)6R %sUL}ct;x}zF&ّ KI1bq]Hѥ4]ݿFw8KmG80,Ƭ )s+ ^Q6,XȅhO۰]gx{(1=uX='P YjŷTɢ Tt 12\ג4dq[Q5%|[1ڈ-P5?{+j(tk 'ƈHmh#q HiQ"N؁rԪ@teQm""K\II!&M*Y OxUrEJ£Z%?<_2>V2| CPP}iNU +SMyihx"!\4cOP4 4!+͂{{Kgs̨$?@yU$<&R}/E//BsiLI#ee®H!ΤLHECMKqejGEPj՞#RKRd}=(+IJ*h 7Լ%K(*@Wq:ZH<n/|^E z5WR  ot"&K.S Gar4T޳Hy AE -ZQJ~o޼3=X,0H+,09, o/#%b4J +Rp625̝UxN +MzI|e쑆q-Ih@il +@ssזhg<سۃS7$cEOM[P* EDr%ȐFIP +T6@b졞 Z)9yyхB1EpRzxE^=KхFuHK#CM>L47@S@z'aȋȨƶuZȪV*"9>r9! +r;] +m,` cȄM] xWH*+5l!m"P&BR6g`a%(tǪ zoi H K4B?ߠ:#k6VJݦ()P6 =E[Q^G )q_ ұC=.;9c޼jw\M#>)VErA .77'[r6⩙D@*4BMrBڍڴIBAzd/Ktt$F7%זK^v$GWE(KEOXvA5C徳bnE$%Q2ud=gD2)̭8'Q"_ѾcMăpJ˘Qm/BĔv׫ĘPÁ\9 +ncFV2AI-ocHB PoAz +^ ~|2Af!-҂,"ֺ6z@pT5.\J}X;`s%8푏_Iv4JSI聵 2j8aJ%R|(GD#s0\F)<[րlp@+"| MXl6&8ejfiL1JFOĪUBb=ȅ{lj,M asJtKrQqF{9 F挵#4U@ +.FJۙ IƨILnOFLIkPGYkU&Y4@e3;ֆ|6 +\G}B4OJ٬@C2\f~*llbP214zߛ_~DK3- rEK`GN[.4t5T(9U|7^~*SѶ㧢mOEy~*ڶTh;O%m~*SѶ㧢mOEy~*ڶTm@#SMw?Ѷ㧢O%ivKm:~*|6?m:~*SѶmOEy~*vTh;OEێ?mhSѶ㧢;ϩLNKّ=tyh2yJ j$+mHW&>0>-k3O&%z222y8KX&oh^Ԏ&jEdtL푗ɓd܃- aQ"{Q*wa[2+RЖL˭%&#ڒ阦29@]8T&4h-̞ޜ4ĎLNsIerq*KC[2y^&OyyQmF6pu  FBTq+v)͎KzPxђl2¬H6%nkol@<A rA/H5yKTWMM+%+4ȦQٶjm%^de0eXZɺ$9N&NŮ 4 +C.VVK47-A==(,cW[랲P֮8S!$ܹzJ F H"5zhK(_We<$a/Pq:-[ގf6r5J0%T~APq"$/QqJdKÕN'č`֐+ 7SJ-(A-3qV_8Q.] *ؠ C+j8|f*ZY͏MwRvT.wy4qWdm8Շj'gz0p+akv i%2|dU+%H)JȊodU +@JKbN_kf:ރKjZDvݣKlЅ)/W $Thc568GTB{lHj$w1Br $ +Ǩd9'z#ְ's@1gn S" d?GVDbŠlN|'2YdK@D R&5G䠓j (&7GWrj)6q=sN܄Ϋ2: "Pkk]JZT=: F$A{y3 j&x-.MPOn@VVF\yR듫swZlØh" ̴q^BPC 9wPsdrSbœ9y2 (^H'`|wA(lt{4LtQ)zAA/GiC_xo*1U(aKu8BԬK lzkV95a!SCE~ߌ 4s-x+p &yc<q^ri.!!σHT`ai3,z7/:HHit$dEY.2|4Y|Hx:N(#8Sܡu-`#T7{ʶ{r~Ge$((^6aS h|wr.TR*k^x_9^6Z8RLGP1w +X#bebPMܝl +*Z$3C,qD* nr?-. ތ;f^IhsxH aޙGa_7Xr|demdr超^ٛGy\ZԴD=+NBUD:ݿ;p&\(-ݟOBf=;բ9 2M1LYc2{s8=Pμ`Ҁ5Fo.+'< +yt2?] ޺.:wm^ilLF9d"׿t/LS?y`M1dEYEs Vj2x}95se'ጯe&PGd ·nC pύv<&cFIqoFU\E&Xķi ] ~Z&% 7kE^9\i(u?7G77e󤊩n&[_zs{[֣G(wKMVݴ>U֦}09i_ Gh}}yl‹zPΛ\5//E4v>ve׫t$:{7_f`eXډW?kҴ|+սIyS1_߸"l[7?"nBfBj)w6d&tb J+ż.ƁAbI*Ź0>9UhE<kZH/RĈH@a]>(bG:a|r+vm X%YYm.#E_iAl/]eQ%5FS?:)*|=w0R~z]o*s.]EzJB8A^G9@Nuރ&Qb + b G6y/$!#UrvϭY)Hs\hYRQ hG>DYKͮIgFtM^Q̵V\G(g!RRaNPoeJk8b0kPJZ)AYȑQkFV%QPZƔEN!eVř޹MpwcoF"` Be'oUѩD9PbAqfgjDVQX)oMg8*j,ÐlmD.Cbn^AvB^jW/żT-o2=w);ِynk{E)*zv* +d@:f|E.L BkY 4*)rR9Tn;{<82PB"hdش~Vxp]R xA[D?HA9A.Y:vzvhrp@Kr-1}]К\2vsۃ,N\VCȿG8pT쥎[j˥FT%!a/)ɏb?,>Jm)RRI_r*@aq@6m8PNԗ +d1M iWbWp…c9b#;u,:#ܥ3ОHwĭ#|bم8'=TJr P\d; 1Kdrצ(#.ֲ FomzD"W̕rr8UgvR5 + +-hQ~QW 4}̍ 3ϔFXTPp> 9mve1]O<qUZKce8E:xEWQe6\/ d9R9}9h(Ms+S|8'nT+R>;^ĕrFe 2(7jcJqOCQ + &g +I*1B +;hRDTυ@MiVq.:LJJ+,5^|IsS9 +3H\z|PLpC*I9T!,#\*FD%$IM-8- Aq= + VyֻIOha߻v,DYT%Drn;NVeТ 'g@K-Yj*6H._&NEQ*ݼPJ0sn&{&E1s]T0ӞҲ78gި:䐄A!{c!1*˅DOTGfRXASJg-j41 +Ok/+%o+*^ONL3["qR;)hK@l%ƞGtbaHS^=#R (5UEn'gܷI.۔KQDL$}aO9vI<ڡյN=<1%X-t WDlA~ѡbrB@*p:X$QL%YQT D +OwrY`٬+< +7qtҲLihZS0شo&* js!/l1Ƴ)a+nM;]qJâ^)>Xb(SP8XYRrhRFOۇF5zJ+⚨Tΰ(3RfvHZRvn=GMIqQ +&cO2Ȼ$Evls-b(nN ɱ/"[tF[4,"92t0PIda7pJzIw zU5S}|T{m9`2)D. K:o>DNZ-U# ;aj\qL+tVJXD=LnNJ.SJ\ǻ@I*`xE-J#& +pxVozUJ7bcgb@aIOp3AY24+Q܎F;rL`x!fP)vf!e۸leFeJr9פب8++gu]rTJ{URŒ_ 3%)KzJW9$dqI CE{fFtFezy޼D.]2崍5iLN)}lv;2xIMv}f%zE -TOH4&( e%+jIBZK2$^l?NRF,VxD*(琛SU\[O"[FgnǪiq`*gk y4T%'$n\A t$-)l +9*N'X8"ZB[uv,BGi4e̢79{8G(!aF `.29.@^mPyG8=V|"z4;4%Tt-܅1KFt?7WdoH2d0KyڔWVwn2C :PWWF7{F٘ZRAD;n wOZŚdwupIc!R , <˄gԂ!)#g:I+~!,ٜ̃}r1r.OcXrrr + dͪh9ppke<ΛD/[-eȌg, $2+Ldr6skEƬQ@3hijI4haw0`7RPE*uRr"$%a*V%i(h7Q9dkQfKTMSAv4͞{K-Py\]|q[oRrA`C {W@H +j&h遖xͪZ/`QA٤:p+3g:7XlũQ'樂'ջh׻S뾒'L2jN[Ht\WN99 >Ȯ]33UgfΖgl8,s>*HPJFTX/)73qT8h򠢴O=.RB)zhɌc&Pf,B >2o^Þ(=`!xҰ4d3X[xM|ʚT2e|Rh>hՖ8+솽f Vek,|J \X44| +#/ )BI%YN 5 6D +)2hlZ&zڧ%Vzk_ޟp+O*]a!?wp61\Y5s˞l܆ S% FX)yZYYD-krqsn̳ EP<ҧoDE`^*ZJVpY|}.1`[Nf(BsX#Jj\kh )yU$n B%KSW595FI"r+|ٞ"wSZRy"ș/հR준:\$/,/(nr,q`JIU]l(CuZ)!+2S\*K2K%E*J̾YFL''u dq|.Z\+)9f@]\`*-IBpY7P}u@,@cЌrTYd*3R+JXr뗙d6w[A|,͈+rQJaSBIA/".fʎh_>ѩb7"HRͤT|ᡭY + 9ߖ)%Uu n{+[%*)Miq{@ ȾkB.`--K!A#0q +⋍Rr<c%[C(C Ӵ>E#^7ߊBh\yM*v8ŧVq^1NL>DT;+A'AՐj0NݼghJW\}E2 }%7TDO|YC) 47)uCw^HWJ8[ca37Q`VGO\a"*E^Fn feT&rRE]7<S(njaq6@}0;Jln8n@EhE מ‘Q Z5(eXQp1.Dy"? +t+!5?FA#!ͺ nܡ {XsRg0$*/t'͕a2ɫj + (|-\oYΆX n4"n;5V) +s3C`A&?{()G +^Ϊ ܲ >05OjlY3RTLZA=+U3 X4Ej)*(0q$끈ɊUFy\!Q@ +x7?dLdؚf"tPE,3čCcYTCR@СuF;Q +5](2zA@2`ops$.rdE/:Mp+edXRF60t:Y`T<aI >R'c(^R28c@p9,LF^crʰ`Aj40  h\7Tm@ʌM^y`=yP)Zj 1hCInN#&=cAUP%QVR!'5(kVH>)Co_Aorw ӈ< +p[|˷;GK5uV@ +PP! )~Y .~:Qʙs>qk#.>)}hF[@,rWZB^0Ue + +!JIz^`2!N hḬw*?BFH,ϰ7ྡ܌z)w@29O^5BAC%GNL)>\#QC1NV-v5XWIIQs$UH=B;nքAP,YK"/H%]pr1*@ƚ/PD] -Y$!s}R5xJRNv^J)+K}F nkdpSg\3ci-K~$")"lRtkN>G5ZwɎ9yح. ;gAv8dh5X +endstream endobj 111 0 obj <>stream + mXN:?*e,MV̭!F!$w@7H#rgXd -J Vf{TmV C}f5YSzwkN=[ ǭPQV#e i^kB+Ș\-ϱf>l@s~5!pTadX-O 7 I;t_WD3rVj)MAQP)?TP=U{XcﰜA ;Y`JJLXc'(8j.ʏ-PL;B; Pg"-HJ +K qQC.,ؑzvלX ~PS>y\!KQsOrnDtһ4gErD>bR1FQ9eDKCy| =+xLqO-$н :T4S~82FZ|RN6 E'RW)ka"tN.CS@4ĕ',rj`QFs},rߟٛOGP?o>>tz>>ҥaouzxw? ̛~kx|h MY}u{볕sgznKi5:>gol˽i}5sGT2&w'nן%ߜֻhp<yK3NfŘ O%o7L< 'pl}'/_LrXџl;[rؘ1hg.@lzpϴ}{?Y:0RGF[Z"X5O~ 5: 9O|{kfX^e~?ϙ*Se^[h^=ηoЇeaV_"\Tynz&;@zRbqP5gSB/Q@#ϛzt;jG~}\"$o:hTqm(St71`gͿݜ1|s7'{MXӥK[arg8 ^ ' [8Fswܞ;wiϜ[ϴ>u? Y8'mB8`t0r$u? xk6mmo:NXwwEs xR-ĵ7b.Mwo19q~GC=3gzlzlΦ{/҅wxyFwVc)5K՞dp4{ +Vk+4-Zb3^a`S>Gܢ95]vR+ H]!O<:fRo{~_OD_ݼl?pէ +EW7/]~.{a1p6{bCEK <EeAX*ҳ6_qYq9beA\eA\>Pgdj2}sp:_#Pb믬9R'#XNܲ ѻ5"yM'+Uo8@._O @Df^"+V3*~E"#g>igsA?O~.Tc'C?RB0'k  yr;!JF) }-qK5#@ 6(s1G{V.@0,D"%?BGG\w7= € 'My<NV$Fj SF/ 0Y}\eEbO<imi|4dgM剆Fue7˼%_L;I YQVfA>g '_-:fn$*X"IK纮\ȓ%U '_Ti|nÙÑ9GW&>y2 yss2G8eebּW3 ҷ }'L9'D}I_j1|zAT￑-jNꩪJuw `m#j*=JF13\X "!Us}ڄ +JyGwWJTZ}4}\ӜZ<"$0[\rhL/4 xp1sR b12KcrS#YZP0.IEH t-0cP0uBSRk$>j[ł>. EL Y`=v$Z^$Y'Ȗ!ln9*?In/(ނ0[DMŦ̧R.wLèZUbʓ%5"<8y\ĴI%Rj~Dއ(stoW"6>E,,EL߄>*BKV"/N:2.( SE4߄2IӸ q_OӨFsuqAO<^[pI%$ jְG;Ѡ:=cn +KOrc>Nrrb$ɲV *L*UܨVs{'p=GܿB$ES䮀nԖ>T3K}>IsO'7ʱo'N^YNVVV^W~wX;:-/m]^CS\]̗J2O^&Sv֧d @6Gr֧Wà Q.x_ܲY 'k?dՋJצ̗g{wpdsx_qO宣O]]$+".O{{ʈm<wٻ5ҫ DMUs?8\-[l0m/$}=ۛIP5>v3Կnd; wzm㘢ov EfC`ΝC7_fHzpw3ŕq^|_s2ӽ0Bo$dmk;X}3I~vnz@e8Oևp2Kn&">\ܜROެ?><ܙo{ټĽ]2<wLw9;?3>G*Ї&&{#@:K'W&ۗ3@'w&SBWN:3 .?8Nw2<<.ڤbg7'nXxT<ۛ7r>v+om[j8Hv̜u%JCW՜Ue>9-vKZtZidۍ&c-Q-QQQ户fW}h۶cS1neF6Rڹ]ta" ;KUkcܧW: ;3ξpof樱 +@Nφ+dCh*mtx0wF*7;}:aJ{8Ěΰwso2;!q$d=h2XT/oGg1'#xld. wo?;s_#!oߺ[~O\e'OG[3|]^Wu<IA|tKMOΟ&qWM *d4|* ~eM-&5lÙ-&7H/zRP2:o'Nzdmyf{w(A-ܡ= 6|~P01{(-7olr'# [돿ɼlmV*7N7dlzI˿tnV͛l8yVAh_0v93n NGN}9v۰D+95*G<[/hL +b\^^5(*vf?y?n?ftػtt{p8yMtqRq~ꘜt)$V6ʃh{mi:H.z~,0mҲ>$^sd^b-"nhlVY_Rds8?s|&b2qT[{n I=HxuGLF礴[k۬ly}BJW{{;K{>OFbxoxa^X|Y\mutkELzU7/eXx|p5:6Q綼Dԧ:7ڝ'{jjFo>^Tٌ;u-Nj0l{)V/T 1Yg6W;;]3Hoۆӻ>9M*K<М{{q HF)=h3hHUQ= +AhQǫ,QFGZ5^ "n~d8*Ʊ6Vu^՘/hx4HZK{54uŀcqn߃h10I8w>|Kʬ8$U_\~p<|bw_Rr;X|q-쥼eIJT|Gޭ+d9(lL{nA6p}NDh2M8kM@$~xx،ldSH_ EsLftөy_nwGû7.ߝG7:v|'uٹ{[+ 7܍:{1NC'k;i(ɋ\⑼>gߗvvӕS:rvo?>X^xϾ=rۯ~;_9u̯OӧN-8 ?$8w|3v>};r>=į~;" jյ|Kgzڧ/W.mE|:c)g}}xŽxy;&|uOn}1}]5{穫;np_ѼhkŏڽK+><S}Ɨ./g7٥+_3Wמ>kٕ{\n/Tm|g] \vΧ߾㋫Oo1,=pni/up_n۩ե*Ϛ |Z}0MCK穭/S.zul3ߏNlUj,[+?yqNA8ԅ _|~|ԝǧϔ􏇇~\?>js哛/w?O ^ŗ~潥sK;vwљn|qG}r/?>Սgߺ=:;[ݕkVԹ{ Uv}oR[[z;4|f֋o7|youu_>*O0?^:΅_~xwc|q_]=M;\~R1ӫ_^X]`m˕O]ʋsWl7 K^|н滻zY|ˇn3w)]Who+7>\>{ais<_w`%xgXpI]ps?<^lͫ_OqoOpGO?x_V'8s~vR,`w#^}C}|𳯞xŝ޸Uި}{Y>={b2~g|:y7Ovݭ/M.+>O_{x坫/>\~_)ϟNߖ8Gw}b5g?>w,m]xٍ?zR\p9s3_Y}/Y2g?~e;)[ON8{ns0gGu½.gz_xq2k?>5;}}}~vaAy՗{UK+/:ɹ_=;_ gwQ/lrfK >?viy/5vn/]^ykYd?޽|wwW?߼?9s[׾ ԅo^_aN1ot[WΗ_>Ͼsn~g{ݽ[?_}O>8:b6=|yuyԵgO=|ӗ'>=<_~8-U(,*! bwwfͬ/Nٟ TڨK*~?X7%[{"H| &_ҸR2j`+:26o|V G0ἅeE F_tLe8J]ʺh4\Suc+CBS{w~>|wxǭn^ޖJ/塊ҫ{|zH4K :׳*x m_F./Q@)v~ά =nnggw3kn<ރn?f%A@h/ɧp}>Mf=N y_njiɡ.Z:XI"c\>&p9B+Hz +^ehnt'Ť>| =ΚQpvߎgukopo<>~I)bīFvF$[֤7/ѾCrOU["P! 9̑dȴqn=ͺr}zl yG*f; :x{7`hr_sn_8`'%{`+yJSdz|La<;d}ϙȇXjؖϺC,GXd:6)>&*&Y--S=E/MI3R}K7՚)OT{:`)$[XWwljl]H'м)Ӽ{_)}5-Ll>tҷpZkndkp'E/q5 +/6rB]sлނ5=]7slvu9E?3|:.vbpg{JӋ'0WN$kb*RvIAn!i ́ӊ Kݦ %u'̡ $ƉfR=Hk@@{X8qpL,J4  "$aϫ(3.=4ٮoA2J?%3 28TfH_y;' FE H*WlKaڛːiLo<%*VFGŚD̜ʀFY8Xi$KkX_Z; +DnM7D#SIjDDQKzoxQrYL^1gE|nRe YybX7z7Zh;S[AZ8W*R|UV=MES%ZU?3x3r +&w7MMU+8L|o1:yxWaYgO[.l?x/Sald +kX"OW +} hJ=f 4wF7xF!g:RG^ca6ILi-\ԯ IsA$r䫹~ml52 +.uz/p&̾ս;Y8/[3=IۣʞfZ?uF`o*[גhgdRVC 7x$80{kǮ^iXv/G躿.umokxIz㭎ar]'"AG۸f` $S!=kO}#Ex0 +HuKjwظEdkP=$SHN&Ŕ/Kߓ6G!KMz \>R]y#ZNIm9\SR:oY{HgŻOeh4)-KVrt v[ Z>Ç6*g*=xSyW)%l4:ix53I4>ԊMnZ&ҡVg?JKxM$Fw@Uz_aQrgg[0[v( x-7_3⚹q"idˏ[{iw(It.ĉ pYYw!,k(HI*;dIpf.Hx;x//ilpf>R,-Qh v-џVV{X-U#s7bNN&rL'Ryobu%MM?MKJe=i!*.j;Z +M{U2WBk {#KV4x_f%- +g=ϫ Bi"%\s<n j,K~L1;@^KHT3ʄM^!1c7 K&&ܙUo+j+zBSh!貦Nwhך?"{D- It+bB:5C<!ٯhrO( AZbCo*'0 ӁHGʟ%.;ⱂs\ #o)&7PwIj^ϞkuZ뷡.DYQtgFB8&_xB_-s |+X6.p8ݳTdGyiʾ '\զ(-IM}+ ~`'*3ǐ//k M7+͒;.`=ee@+ 2 6}y!l52{<%_|sOۗq9痲쬽Zeg͵g`H_7k(OD3Q#3ʬ55B,z ĨEh5<J nSfvvb'd%s e2$.nn\P6ݑVnQk,k+Md8fr(ϡgZgX wv7݋/fji*.HT$?hz&lrQ^Dv2-ve:i{଱+vڷtt2_LueYQRQ8G߭52Т R.~ B0[|z}ioeSlFn_9TkߧDgR"n"-CL +@[?2ldϳ?@ƞmе +s.ҕ:lUaZw)f<K|NxB\ozܨApךzz@ڗ15؍PH̿3xu]jh|U`ك*m n V.8DcA''NEQGRl0"Օb_WɊ%ºOہtHudʖjǩ@Q5;@-J\*`gAReA@މ PL`pv9rY?T60wQ3wY~o#6C:xrkA+r [|QU/,ťF8ccK\gl]D+]E_!U20oD2E޴z;Я{4H5YJ}%4khQ\U6#Q:͙njH2ްa'撉c򂺲V] t&y9-P/'C5O>+;&Us4t|]` 瀞kxW@z5jcyo@ m9+1w.n6:&C^H |޶CXBt  [o23u"K6&#g!c0=:+ 5M Ů)z@VΪ<%^`) (L3.#aXզA<~V>25uwr֨1ޝ1N/[J.ՎZbe#)h}RNq3kkx 2V{Y=f}&߈?;ޘtCZny:^ij#nte?֣Ȼvoß.g^XDm*P?%5cE˕&6FoV]{T=J+vn 塕8(kZ>HB#Yb<^ =вJ{++6$A릟wWTѦ!+^+$gۧgoCހj:ob_vVtQBjӌԚ1bj1jv#k7܇U~)1_ ߡi6>pגGRIGg{m1% _I06#~ge]V?$߱pY6"եU$k5͟4dQ\rHAUy ?G_WDQ5*z$NNǼ;tQfހtԵQfA#}xeü*sb)L\GfN~^VQ}Qh\/ϼ&=hmf>%`R +y5Kop0+Wu[; 5XmqQ+[$0dwuɻyj1F_٫}%$n#CFl-@u҅6tp_K6%.iݶ-)~qMtRd%P|BTxl"j}flӽ-l*GuG{ZQ%V#>Ziӯ;Y5Bn=os[FhV(6Wo%lZL~f̪Z#[.;"7mfql9>ƔfLs^е&L0=S;i:5D գD{*-@D8|7ǪS"6u V= V x B]z0:PoԥUR+vQ#ݻ~ڙ{}[=ԝwMbײkՙW8U1,+t@=%ˈtE..~1/-?%w~\v?A{ʀ']A%-A~sDSZoYlRE­]UyϾejG.wy)(Disq$* {绥^[~VqwGSy-r73'=I.˱y<[p\v6gĞr(MæKt;K'{~q\*]>oYg!d CJB>C_kN +7 SWQbGri.DT* V?DGF:tn2 % \\/a'9%ECz&"7fc5tDh}*faHƀ^.YԼUT$)a^3sn%lKrjQcE۔)txF@CCemei)FӝE:@_s=\|/K83Y^ŦΙEe]ifڈ-vM]c6MsgCZFrp(FƋ f>LrhF.߻D,D#p~kVKR&0m!?-Mdczbi"1ݑH(_^{Q%9Rl|`Ngn t4:Zt{$eZ$2pHE,m~t Tdܙ1,olT7Z@?1exѲ|(o2 cBΘn٬kį$Rq^NwQF]غ2Ì8 +U +=!VD2nS0Z)(wK}XEoa2.gv꒗y cL[:,J5,qc5&&jk:ګnȑUZ u`86X7>MOw5E=iccg}U@}Ok_N6 q3ò8PwZ>Djd¦zŎgpk=˰Fk( !byџZSLVu`lwuż6tiQ+F+0)Tz:vPgfM1Q߅ݻ=Zt>X/zD T-0TZmְ82VYDCܩX2e5Scbg)v1ƸiRL$~I͸VA:>>(BsF}Yvad/zc +![[:殐uLO$8ڇAK0res_% }kRvb _$θS!M2|MW*~DO~\vG^ds$/%W +%͜Ϋ4ؑ? +}VfZX+`H[TyrRze՜7ӲYMkX?;> FE7Nk*imɧ*IPׂ;fTS8PŎI_hRJz &w;vtx!BI7/zBy1 w^dߊ(&2S?Ma`:KUAl7çc$#2o$2&\yݚ9_TwfoKIRM31<4rr=[z]\Bz(lfUL KY,˞g SI }wzbW]lbxp,LggwUvȹLzջX)rc'S#~B/9τ-bRjmϧ#t1*ʻԿKČNE +XajHvkVIU>̲lqd6I9%qg *PYU=]c׍iUq}Oj]@Z. "HY*|5Iޝ_NBv_i7$>C,H˛W/"ϙRjZBWJ pt?%ka=ǾBg:6bL5 'މJ*uF' UlcTiOY!)C^v0ݸN3`]$?߅ {[=gگŵGjK^S_II]D2嫙=Q ;_CmS9TnntMtu~4Z BӡVKϏ8M7Fyro‚.NtA׍LԨXast7dڧɔ0OP'S޺94nImt P<:p~&ӨW9q)s/FhS·ez'2ݴO:uʮnPj`DueCfU_.r.}WdAgq&-v> dޕ)JPvLlojZ~kb{1U/a*V-00{b; y6$o8`n*7oi.aa6gg&b5u/$|FR g[ bq +vYqâ٫MkQ .(c_f=g=;Fao$ձ^vO& +XTw;!1~3lrRtZ*K:9Ɍ8fP:oG 7* f^xŨ5ĊѠCu<k+5*>4̌ Tov0^Ox[#b~2 "xR>G\AQ?@di^;䫜, pěiȭ}/<冲Ҡ] $Wd[WrZ Qb2<}Eh>z-M[>;e\:ʇ\(Ï{8,UGt0."图"/rF +MRfbh{<$/xLƗ04k^@5][×/+WY̬ZڝkAwC h~vRdzj{^/p+kg5W[SVMprkOU:"$t%Ӂ74OɳQo>0RÞ +d#[5]W^@-# R>$ԁ#}urǑ/O|~P\onْ,YuqhS=-}5)?BNk80""W{歷]t%}%V\8˙jS6BS hU# 9*O-ݎb E=JG珯Hb%M4";Y9U2"&}T=dKm +1ope{BtbVrtRg"Wz#tͷZpx7PڔWQhCe՘O_ND UBd]l|(dꋭzK/QN[&WyU`w*|~i}y9Bб~n`5@+\1׍W"p.Gk],wdUڀN4G(?ej+QS I-GQ٣ +;.Vz2]C2lYfT_kybi z|SR| +#z]*AfE_B&vjrvw? +i%Bs WKbV({~ "3ڑlh8Q&a]RzB^S%-3賢'KR2V4K"E>%?V FIVtJR{pO.e[kA"fP ˑT/vs,?,r wG/rAt3 - +CY}fz0k~̈ ra1/NŋfoJh89i D٢"^^L,(QĵILJmG;ȕuMbCX]5TiFڧ91  Bӊ!&A1Yodb*1ʪ[<={,tQAXB|ҡy鞨䯽:ex@H,۳!/bII_D$iDiOq^yMʼn~P~ı†XXVE6qz(Cmlw!Zy,EՋAĞ?>Lo mRȄHWP'͝o2WD+xtZƻ +.O\Nθ5!E>Y!.buޛC8wbW{͵jLIm~8U }SLFF96R_E&²?“٪N9gT,Vۗ%n]JAKʧծVgEsoG70_-%T,~yϘ 931U5)OeMᵾW=l7^̵.>t0xeKX;|ngԓwj { _9whwTiK,^FUF?(1Öyk:(FWǏ41yEiU|M uv%g6(V&˚j@})uJ._&rBV*J19ȃٽ1'&ǯV=^9"[-ݼ -&S%@dn<Jd-'V ,h]@rw(H_y㡍d+柩*C4' jʀWOѰ@oR|wjC]Q;Z+{r5`-l)Oceޔ.uf-KHʚS4|ǨVRd?]N]2iEl[I>PnJ25Ldi0p!Zlx\^Sҙ3s(g$_cǼO7T"'F]`6x\l-fvTTuNw5[\Ď9z\Z_F1Vp&|H[Gȯ^^AjkTm Q;]*I U \\@㛲uW0JDݭ¼5kW1/]0_F|@VhWx;/g4*}y/3.%샵no{!XZwicJ^-Z8i +[7ymV>{:Q&7A6@%Ц<<;8´wjwmX)ۖ(4ovNt0A`/kɉzeYKmZU!3D) wZ9WP`ln,o@[HlYq_z.- M>Sk|,iAKNѧv͵瘤M(Ao6 +n%9+Z;H+ҭ8ڢn}EHaٛ-ǶFT/Ti+{yu/s#'歋%?hOSo&]W[bݱBZ禴;ע$'/~DJY{h)wie5H+x+ (ӈm,_‹-P2i7vul/?K]E5V&L5pht&@*Tjac̔Vo^@^U T#!^Pw_YEƬr"iKY.]޽ua?MTfCQ6SyީIil;ʹ#T**u<0@wN|t%1K떜>X;{#5ea{8n[KG'TLe{s*VY+*ߦ<C,SdRmee1TQ=LxWgh54cL*E<2r|GؓaKlwr(kV*ct3sRcGu-kQQDQVz+~総I&٦I3hC8TVLlj%+Zģ +3G6DU̳xU`0[̦fwFK!CQ+K2/TvhԎ_E$nnv*c^/QUf/{flaMśSk>,rbe@ޤ]VTI1 v;ʋ#3ё3m&>|Jj#.G4UA LKQ٧]藅@WMS_i96DF[}\6.GYLmC75J,@mZL71v6ZyoTEl^mgm;L M1-;zN ;5 >ĉֱYkBW#PS|P|]dӔo868Rc_u9}w=⿶c<< l5bՂ{*[{,r:^D7pb&^v˨Ry= N759aѨ rж w/Az Q%6G70epڭO =]1{㟣k ҬEKGRZiӷXK sFIOٵrGd[$jt#{_zmͼ٥Hz~ #חIQ۱2_,ؕ%̄`9Vq(('Mi?1eD/&;+ཛྷW珝Eʌ:Oa^xJn]akIhqIQ5ZkH9bg8!ʹq?&\lIɴ a񔡴4A,VN_wn1wc!?jLga SÇerfV el,y Ƃܡ2㍐xJp4粆l_F/'+Q"| }fuAL@2O*QyqS^54j1)XC5b!wTL +JrR2KnT.*XbjhC͚Sb"mqTg^v~SÏFK+|kˌ-/͐@>W쟉4ijS߅h<<3};7_НpyZ~ViVapggQ\|J*9ɇ 腝a'>zaag=x}cZ͇--J;իX!+pal"W&(ɔbHkNٖ͖%XEmfZmT0pz2.&{ j&ozcLJcEůdsEӓdp3emx$kaљJ@zЃfu|ROo_ ߝoQCP k% pT#"Bײghx{mp r:FRIض3 7^ ԅ(xZ~7 +'S_# j2ㅻx7&~V sl|9zUnxbV#NEoȀg>)xJ\܋n1;}J^UZ~YX=Ad1vKd)exId0N!ۊɼ=gڎuxVC780%fG $T!iRzߞpK$)ݭ=vګUudڝ}TuU%=0PnN8hyP*X gsKzYiw9b t"| 2)j< K$l? 6ʹFov & dUs6\}fFW +fCfM[m&$ ?g+{P=_W!kh}) :õyPF[ȁ¸gr`/[Xi8uRԔ| ) )H%\-bZFCIiLԁݘݗzp8tuMw6i +m- +q/g%Hzq.b^>T 3 +1p! D/S ʧiLX Zߏr#yzCտY~$:cQ|VM{C4Y?E_rnRnolwYjodZ[pPuʟ1 +)!κxb>8ޝ>‡bɫ82j+x6*ܽϻ,.2'$=\ii{l1C#pTbPȑ%f-/YFsQu6#˚Olj`+kV $UcJ'җF·[.?-Z0ODΜ5bv/eJ2Ŕ͇-qѪ.zyG GӡpSt%_ITCC) JakwUe|ooWs:xϫHCX5>duA3RU6ӯ]XDXUVj< M?3J: O_BJpԼN|1+gbܞ gB|ϭ3aiTS 7 +pJwIÛ*{z\5pۘj%{-tXE o,+"{ws~"J;6Grl Qw(jŴf3p'T¼e4Rx5K~4-Z3E"sa3pj{σ3Oh[ؽuLeN0#[ZŮ3ӴUTi7KxN؎v%s/=TX蚔3S3S)fLj"˙tbf` A=AY3*KoT۠h?E[pB5. cH+09F8 \iyFXQ ~QXʭ@y*TeE.>2+r74uTekq->W-z3 >#gzcޭ5!rl$fdMz񖿕 hwsir!Xd& ,A#v#&I\R\{BZ‹dxIݴX.Szyzx2._[q Rث8?&4RDZ"`xjRN]Fu4hx[U OFCLT5g:U_HmZU'7tX4ٚ!ԁA5Gj A+]Mw{b][~. ĻXTȯX%{Hc9%L]EpVӚ*MצKwd(oo%;0Bf݋u*1C-tc/n<+ƱUɨA$zdt\UM#^36>3kqZ` _]7)" +u9$w`62LQ,|1h0Z?AԞY]SJpeZG!ze>`%*mb~rkc%.R<貾K#>+ikA~yQfnG-Ew.MD\n}qG^~OLG +\GY&M?+xb3,f=m72pn9 +HYq_]Y嘐UG.roV-Fu\טt߇$E#)iy"s註ML;s~*^hu eEga` }Lfxi?9~5e삏{7zMa gs`r(n)b/5gXc +HWrJx\{ +[6nr F;E!%ѶsoSyyss&A\(R\s+vgk}sGlYS}%7H&;7/ D=TʲQ:X*gwޮmuΘ6M/{?ʡXu$ !N^iВ{Tki(ʬjr,IbX.gLCr/NMR +l-h#G_3hGoqIneI-?֋'|v\ $XCLk̎֌"`1m6hNnW:9Ј/H)9 ^Ω)ӟo&lPjMǥ.+$1 + ̝9ɂ$PbXCJIפ>VQ?}Y*Rm(>|k:K[8k_3G":};W޳QrINGTP2K߉/kv X"R +qv6f=w7@I8jtҒH%om%?:sq7PKh7dybO=Ͼ*1rW㐪4^HwQO6nz﹪syRM$'CӍ\ j үD m'TVR8m6wU7VpWunk{I^QqlԟP*OVek rL~k2jkiuTsbՃTP^#"-y=?j;1]-iP{[n KtLOeYܫ3k(T5K<]rJ\UזԲp*m\uO:Zw2&>Ȫ'v'FXQ[ǚV uL ч6ؑB@빫 nT[[J,B1)R7TQ:OSrMZ#k8G)V7 Kҏ4lSW'Bz♽Y^.ڡE\lɎ?PZLbRpBp^/)V(7K|.bt CYy޷{m>5QjxZ3O]њJ_j~%{+= +9Q +IVd5oE{gD*J Ư̳[Rky3aWT8JyvwUgy={7ơ?'6ӡ^(weRԜs?-$p\ 1friNޗ,}TE[W6.G=a<Nӫ銸2O$~} iڦ–ez?z7[qW^ # oҢQog1ױa(Ϩjo]wnBQk;j Kik^5˰骯YS8;hVi8Q̨6SgwS(Bi5yj {O%X=/ݖvixR}o*-ߨ<GՌ:{ n(MG3cP$'U~>i ~ P7Xmal ci1O%m\4j?dwm IeRq <37-qOs'o{bhYZJ~q.ɭ +mi 0oAMj|LeK7 L=Jy3=4Krt)\-%!3QDo+( "ok=[B^LOsbj6@%]bm zDP!`}5!;M1j}I!@vuԙjSh7%V{6OcW_ >yԟi@'L~kF$Hm% B:G`Jd@Zϐ[/^nn9MįkjhUqvUې:r?Y2Nh2^tǫ!w_pd^l;s#ubgIpbY-D!-mnt. %fk{l9mx_ub_^Ǚ&Wͳ9Aаj17"?:Jmdx ?=:05!P2_@\a~>dYЮk}yj9f?QhqѐcZ(19*j8.}UE-A lCE<@V+ u!ܟzYFu4Kk4!PX^[hP zLpXUl!{2jFnא:teKCaִ,Kd"CٞruJN6I23ֹ?oKjcZI|c՝ g: ih/DҨ4*eU<ʕkB>9(:"v[2 +~#41 N[? +|н5FġgiqI(shG}iOi :5w߫V,.?7uuh_Te>€:nQ>Xl-fVr^u/՞挭9satyJYP3:]Z4D9UVj{vުyR +Wky6Sox,j-t#mJVhնǕpnQqkY_[ 0?pmf_`MDi HiIKx,l$T]tE1}3QǭXȘ.!\HV A?yw_?HgttM +c{tQמ.b Aƹ,~ջ>~k9h<;hIth6󪹱J%_e쨬`h˰9T%@}L!'}2zuBK:8}Z4avWtkVJ 45#/}dvW&ED=yܼom:5jQQ\ -zwO*XXsk䈞pe-ގ`ϰ͇lKAV|e%RJ4Jjt&*Rxڷh@R25Ի#Zf޻g}z|REKyr:¦P5RįK∽ {f*}ahnǪ_-t79@=9?[[^v_5[C/by.)C[7)k,ŧ.(ψYoeӵ>U-NCE#;7VY-_5YFljʁڞD;$vܨzt*McTȠӾTA-w^k-UqM{˯ D9 #c w2w'+4fLx t@uAIͲ4fEYbzSRu{}6´?PuoTcuǻ]ؑeJ hߖB^EiFwtk2fw>SPe9*;?-IwW/pCKHKMKÍz|~Κ/ B2oen5o~c( LU.:ܼ[N||兽57903>cd8G~)Š6imC: +V>*wѕSI:HNN 0˭%g3hBN%=J!9Yܔ.9~<_xkt.S{u;c_ݓ5^LՙwޱťiQ;$HzF~i):=[|0o7gREt$R'Aٯra'6cֺ0f#y&1R1`Yr;(w<&^z];;djn( B:'벯w7\˩Z7q\I lҼGwpڃ2MK*|^Q\~Ջtse&>u~傹z(R +A=hn殓57Fj~iۋCó0CGFe\)N n|`j8mw>]+0Am]׵{}x׍ePUdp64=RW[0]Y6^f섅 + bBNjªSaun +8Es$&'ߓHA%JSKEF+bcNBYkB6O@\Dʻ= ?oE)u?ifņyLԫ?[9Z-U80?wApyjlufMu}f0^ovWCN!c~ܨ]Àk'KY~V_IKҾބ0F=]iԣIb 7O8w y+[k'j`t80+В/Vf`k 'VGn}1W*IJ[bLڟJF^KPѠ/T.oXŴ$hV1J&8_f?3ft+#6f69$4#Ѽ9Kkմz. +]zL)x{ p?ߍLȯit[֖}s~>[ +ڗ數4`馩6gG"GbW9_(}iq˷l9#ZgJLunK>Ch/Wn?T*i:?3ށD:O; +J\» 1M7ӷ tK)S-c.4uqX-߿|-{ +#BV tܲ~ZrwݵS99y5V|Hxrk5S@]$ShK5g\޴YJdzCƯ IUx?Z/}qubII(A1ڥ9ȇ=-d}9ZlMq$g%eL~#Ib@sdxP܋NFnM(uN%*I_?V[*9J4m-9+t"61'˓M|PQiWGYɔ7"dj*ڊZj6ZG?:gw!4NȊX`ӱ2CZ5]،-+ECRp`da +ǶM9GlTܴ2%j?ݙKI$zӚY}Cf scAu-y'7'j;Xa4ξԐ6gEywRߙh[qBV %T.1Ko-}nPO h?O$ϳpn5D} 8{4Is,wAHŭkm菽(; +ya~:y<ڠL+ 3- Ѡp[)Znͽc#-BS_rߋѪ: sBnūd5 27A\ +OR1(?5_/Q~m* C-=?ax^_?p<27KV)fXG ^'"F zU}m#b'plG Sro|P}Y ݟ霸SC[ױ6$[) mg}gqsvVIצm7K]Вs. +.]6~^MiN8;Qkl,s6R-*gj%U }HPܴϢڿÄ f vUb m^-w{ ]i5`JnpVd3K74*`x^u;8c@_WX_H{SZF_M7'ȟI u; i禌 +8z'z-I +|"CEnfE ml+BI_38`+SBtԉZ5RtKֺr# gC7/x_-.%nZMڑc]3:|"a;aoܙ" +$GztiC5PC6.H#Tލ~/Vt'F!")V5[;F.Ygnq!޹Ո +°}]"(~zQ!g{U.@B6܀7,Չ:⊆ƽ`P.U7M޸٫ ڬMu`lߙDD0_ uKY-y[;nߒ]kG!իWgmPx`n=A|fUR6:?=EJ.Ga{7JѫzAb뤣@y(|IjF܉uM>Pp'BTu+F527=^O( W w' ve"J1^ԇx[ڃ{1'BƚGuKjG<~1@ + )uRk7Ѩ)q]*rP3go +KT7W ǰ%h@ + *BnSCQ7l oDˆpt BD9qzܘiRJ_FIG0V7b1*C=sҽXMjZk,6ߗ.vTS>_,ßV&{3kW/kJitkyO2[YPEZZ{!_oW{>vAHFvʬqNC6XDQ?QU EDD *7?qw'3I"w\h%a?;j[Oޭ5dJ/MM3UGFw0U{b'`S FS{mIJ#o^}RYea:]r^? ~W:xqNlMiϺ=9 TW~q4 +J\Du +ӷvWס6ڭSN%cD7Wv I\kRa(ygAWf֣\[8?MvʩmW3M$ \4PyB}zj!:3f^Sr^)qwUV#~w+7{pUNw [쵟/ŴX9钀K]lqm)^Cxu=Dyw[+c6Rw *SgwhRD{|WDOI]T4GO*Ie>Fx")nU66k?P&U CaU7#? o,P]ǣ W1؊=8ItRewz5w)'+)~{>e#rya}S{йJqM(S(50V[@dd^&zrt|͜J{߻ARe6¶%#͊kOASy4vɽڨ :eR֩fS3ԁℑkU5N܃þֻ^h1j܉elȒYoo?'ܾvnou}k?sV4SXEyu]:wtAxjus8-qƸҼyI`$MJh[9f{H.BƃCUL'y_&ak_[Ӑgc4]_^21?o^3qDoIGxoZI{}M]l-%BeWcUֈ2PRȵUZIHJ%|ש?úzZg,9>Fwd(`v+v%kytYڪڟ,=yJI[Ǭ~PtZFZb":>iN;⑰?hr+At6~wϋޫ:'G9܈**KQ)LvV@Dž=Zu3V6faVSv8ϔ7z-U:W~}x\Qo*+^Wy_n/DDb҂)tGފO[zj[v$s۬b!0clR?NdʩF6'DFAkStW\/8`֪Χ|_3j}KqF9DO@͒z˚5+~D*6S4FE[ $~BQ{L5"|=WuM L%|}|qsZ>}Ênd67jK{^hfmtnt0(k˥NѳՈjNiY&b FΏXz8[jfU&K_6ىvE`6_È%&"4$Hx?=u5/$zFFL^1+ۙdں~VVܘ<|ncQ97Oi(QBv_1DŊh~D)ئI;Zm2v%Ot_)yr}6- Iz!x{rt4NaRym@WYCfɴSJg'*18?5 +ϭ X-k@02uGzN!_;hU|BxXy΃=O!V'̺M]n-Ȭ@-Zy8[7 o!gZCC'>d`L>:Y_{6xf6#vsn5 jU ekO +>ƺ#tfʓ ?m^i 0ꮥT zي1i<42#g|=DUmlpҲԩեoYϜk+_{QiBNF5+n+1bޯ6&!OV @yy>VƐ?Z e7xg밢edHvcFtQZ3v^,H^fxi?5h2'ޥo gnT;gNJ ;*%5LѸ|@yՆ;Djdp|"LӭZŀWyhBI/KSf#s\_\=[Jѕ khZdjяeZ<~]IK Ξ?&2v]Ӄ%;'`R1۲"NE76UEgl| +)xo2 +{|C6Juլcĥzt,vҳg/v9,ݷ0%ք>Mi9e`"LlVeXՇ*Hphs(.첻ruY]|ypѾm&*zMqjtFq4O%cm +ad]_2%?n Ɛ"h-rߗ\uD&kJCiJgBm65r*WI=MήԾ?I?f)pjGs->Ux;?R( P =0T{2P0 )H֑z/gm ! l52C3ܲ,z>ӷ`}a{9m ƪ`vV-|У O-:Zs䤁S~8$2ޔ]prVt`?]dXvܕbJv(ه9OBũ@9\m9i2tIqz-6R,9A;N\*>+* ApҍC^ 7lU=]FnKyF1bb;5xi/m;BZE{bvN_=MjvVȰz]uR۝VF'w3%t`#4 iآME5I#+(&=gY0K ~& wWޝFkPJ4 M;^+( r $UyNaJgi]Z|ù_Ӱe;~ʏ]͔Rx壂̟J۩s #txVE:yrjXeqz/#m?܍sNRyv'γl*+~k*xBmByW%^?^+ڪeSC͵3Ziu0=?tV&-bŅ KCX'v'p#5)?zVF. ?&uǿ#U1^rj z^ƿPT35 ;[1m/[nr?$ё/g]>=Ӹ>oucJe{NkEϮ'M]/[ɠa^1voqKf&Du怳xz'dbwf#F-r/G(܄#y:GG H[Pdvm h2.z6!e֖A20su?xTs9j!Yػ8`,}Sf2܌uJ7 +dL}[#/x06jDzqûhwK/7ג5+JhGv+H2}>RB&sXd }a[HzO=tk58ur=\%j9{srMIF{=o:kwM1bۗ ?\]?83o` jd\a@|3;ڳQRo(^1F-\+xVԞvפ㮁=dJjJ{?Ep=ኪwI\ӭJްT<'H{s81J{i d.8O9#Ď[%{gBSPw ]B+U2ZQ-v E),l =m@a/+w|ta#Y(k^Bjefgi?@ˁV[3'ɼ'8vLp3 [aӱ{EܗA f+\Rʼ4x,pRdٿY%aыvж%{m-=`5rڢ nqۧ/& x!يn>gbc'ĉQz~F9pKewT\!y+;4W/'zLԯ%(DL17IģTkkn/˯em nn)mlȸVZj-90 NJ?JX;?.ƀ:6BO;2IZ2VW,Pcp 0ͪoև?J LN#36~X묤[:k"K_Uzd9-h5 !R5ZZbwZ%n35~:Y| a/splgtZ4R}iKh]䑞&OXݚ@0X8XfOcݸve^f\[,nkw,EF(,$(0C Lw~;5"{A}drF)e䩩u;kmJ/&l-'C2-`C1JQbcS#L3=d~܁|. )V0*|=8ʶr+4Gd c=gBwQ{6_9vAHP[74 @OczMcc5u>]m3)ZVHa=opRuZ7%.#4ٮsNNDz qו{?w_f5c;;dO~ǭ[1VVV1] Ĝ44L`HrACz'2f=;8eCNH'eaG, +.QewXT` )UeYU +r-6WC-Q;{`SS~ 4]nث1d' /+9f$'K%cۺe깈N,ܧ.{KqJ)&K4|0󂃝Ի<6o"RK]Id| +''7;{j="]Ũ7OP\'l\YI]X@z |k\+Y~Og0~^ۗ>&KD8z^fq 8"(UA8]um*ImиV3!?ow|/JY^@ !Hr̼J?Zq9\-_ b0~N\go@4psvx*ݝK }DT GmNWzφR*jdl_^7b9=Gsw!UJgD#ZF_^@fP~J<7B)%dLȥçbEV-ۅPm0QACKf.CcT%LX64TJ/hXs9RZIuk؇3Aђeܠ#+ouk.+wOݗA>xb_ZqgR*Oac. +򸔒SdMF&ƥ/뤼T +~k/L!96.@J_D +UęlԳTSj#:uiv_ʴԺcCo0!*i32z DE~֤L2|K_]1F:x_cnUTfޙyڈCy֚fOcM7fuc)뙊 Mj{nG +˔ +-@zܭI;uvm-} zi&~DֆQ`[q}F>𿷣sKA?6 c 5rHf'iyY ʝZO٣~e=wny}w=MG +DLU5]׬4"ixVzߜa{%M] 龭 kLJJ2(lU=yY?Q(R%ͩ%:AU(rVJ꒜ާ+\:wx"wɕ eʂȆJs(/ff(RC=~Fcz2վPUpvC#p`<+|E+nHoڱʜ)ںs}Ȳ{ڵA*Oy-WZ+#+M:( +bcxCJ\bXGzq鞕!Qu_ȸ&-5#np9)wi=XDՋB6^ǦqMłVNxqi| e1^pjO^ce6py/ހۊrl4aQe ˅So5c24^ykZP2v} R_޺"Y"$;/FN󴦕.׷=lk]WUfk6T(ի}hýh-sJR ٲQ-yk;7w懖_dVAc|,E tIzy\OaRunIt׿+31||s'Qzfe6ă!f(R)ѲMaשKqؿK$2igcL#خ:bEgޮW  Fy62:MݸO CA 캓ymQ7/d=(Ҕ1nI},-Ӷ6u){7޾Cmk@L*!ME&w*N?åW] 2B#lM'֤:h}[9:SeKȹQ/3oڧAo? gSԎ075%~dYp$f5V ¶U*l\*`e<ý=ɮ.M߸ >]fk*=Nj[3~'9 Qi+FXٹndfձY59%QWw;|R|V֟5fM`/+w r)` +h6+%S-҇R=JWQnJ>@٘3P+k.O6]u*s-둖GT{IZmhQ>xq +䉯p2bv!<ĵ(Rһ&8t";ENm*RJCנݯ"Q?neq]i+IYeV%+Go)26fvj)k1[Xd=R VӴ[%cvPeo#Z[UJKS׉#+#yz_]5L>гɡOe"{җOIBXڢRZU*,lG`oynڂl[J'|jAVeNf +u[}B7іgXqu)Eѻׯ 7)%=υN(f5eͿw} pl/-4EzfvAY7M\#}K|bg_{:U޴$P3wsjf։/ 6S-﹅-^xTt[)?C|X-͓n4fhH[n h^_kaחO.\NXB:@/SDTGu5V>dnśmiVJ5qM\w֛8zTyZ~bdyN\R/kRfC[LԶö|kbH׬ׂ,R]YhP:=Fך-]q&,K1u3ΊjmG5GL&J0bcK@+NYv֤i -Z6? b.ge&=6+svw'k% +_wd.LCKM4Du9ں&^j׬zZ26`ߝϙ e s hY߭QwJSPє[gK9`7h&x֛ݾ4,݂gyjg˪I՟p 6[=֝A, ޢg61IRM}n-nG=31Ҕ/hC|YX>xr%ʟ;1dzwF?ɯ_Iszz҇n慚x3$V(j;Cbאe).d B> ǣj{UFu/enGP]:q?ssvq_T_8OIL(tV=/^,Eot/Hi꣱Z"Ieϕ2Й']%ܣR'syI/ D__#4㗙(4Ra+Z,]lXv`0@ |n.N|_LJ7Vڈ˦TW{r`f jҹW] +ɳ;q^v^[?ȣm ذoѾcnE;ȑ}}1M߅?4Vִz#@]ҧ}t01Fm}wet˺W$~aFmv' +8ŹmUG3޷= ?wigJuU0"-(QH:w&*bNҡ\fAdSg|hi#*ӗ o+=Qj_=]f/7rn+d4B{V=q+"_NJ׷Iڃ9m*YiտV[nQ)jrK~X8dô)w;^W]4{vȕҵN`p;ޑ1p+]ˌ72 LrGdHADiv{7eSw2dzr=u˸Wc Gnu3iN Z"ulw6rdZR[un# WwbYث'TkÜ%vCgdmN4Z٧A@67;J0r'QI$$Խm9rcJ~vuͲz1X 3 BV,~kS')bns\:,>oˢ>vGnصu9CfFflf?<-%s y[70h~ ͷT1vMp=9d4wNn2jfT:}EVqLf0#tlt|Xa;_ }N~p ?'蕋Qپ%+V߽ &w:fXeӄR KLjiǙw hoao3,+c5mׁ)C]>׼uyF%4n7Q\8-\_cnALx`+L`՛̪zco 2 ]rc?\‰|ʱm$T7Z/]f|O{aog],׮PchPXB>z+ἴ{Cj} +rҥgS3ea9t⸔nIwȸ.2y$~i?5my,Zzc#) jVϓ?eYц*#|8 !t0E*Z6޵AD}\|2>5퀖wόָ۠nԿm{hZ?6u_+*wSW#`:4lٛvu]2Ae7Ǯ>|DxѾCm;LR^ϬR_k tmH1,ڋjG:Giʪm7Gs09Z]; +bz~^ܾ꡿;;D*Og,7,$Jw+%rͧŨu{=,*`rm+20w˗8vA[Li ԇ㳧HTLzHaUgYvC\_Hρ]]4zk_$uS%HqTm LlT]GW( j_m%l haQ>^nݣ~+u>'ѹU/d-j(ɒk4whUMVx5,L`~@ Y|YFYw9ҍѡ~$<ԗ OʒdR#ogu-@C囮Iٽ0qGns.w§ۓb@_Qэ}l2gep֧atnD9F>"Z Fgc`zAQs܀IYulsR$/Y.l +)*l+]8?5/dTr/eysЛ}86Y)z:g`M>4mƴK4G J?lBl-'jm^N[nH*G/fW-:6o\{[#@6O }4E?Mwx2 ݵz +B}h?\?p+}_f* +ƆVY"SκHk+(4Բd},;@ƕU%] ṑu4P`>8uWhO_5_%Y@/ z%\T&2ђ +k<ϻ?sa+_,u /EVuz!K${y\:!)~"pf-TWߙ'|zD^OXȍSJXuN]uv2:;)daJ3Gz;么(G-B! Q9=!-DGm6%Q+cwr*QgI ~6s4vgo>i? xMWէ(Ղx J I1vo ӒW +7H^5JalZ"t*D#\ڹz/:(.XFL +m䉜(lE/U~;$>hnjѽ;޷1$.鞞(oC1 dEEsv3VRVr,sQo:C莭8~eCmpy[c)"?X:n +ZGĘrSzاOuq>)K4"PUӋ_ xU\5d--o7/f o>-?g.f["l3wj;N?H?u[BMI@"SUJ%V*ΟϛRү!^J\mW_FsU|q +h#ɤh>vMd^M|Ifn˓T !;uvH{Ի5vQxMaW}pzVll*r6aɱ$lmEUJ'LXv{nrš|%.X3QATJq҄'/{WunV8$ ~F\& /ɯ J:[L_}Z BI^Z-]0% +"pĄ HTݢ8b-J0`fZՐpt}Rckwv !)l2'][mDI^ #_?N󫏵[tF%RQ t*O\(JPG/%㱡m>'uN{=hGv;ā՗#?'L>P'~/MVm,#zPGw +pȮ0_2KmU}u|f;(%+_ϩ|UL+1r&T)vVH񧞪2l-; ~fZ#-}9y:ONgnhi7tP\UVƒ+}o]ǾBTR-vN_^\w ]р]YEWFsU&5İ\Dq8̠V$"dTTLX+v3v5NfPX5$g*敦怃0ki|ŧEIyGmSM}U^%C%OZc"e?}&RK"+P<)֍mնzXnP ƆzX, +$iVtr7B͐D^d?g܄}zϴa/:_-z2UCÙ̼,r:8et4?UB16R/b*O,&Gr3$Uw [(9CVWMO5FA&=w +6 +p"zb\E<tѨPua=BR\R;_wFH]q1@]Ƶ[asEHZ6l{O䣫xR'2f_m]AԦm^ȧײN ga:!ZEYeGw]/em yK%]Yj+~!ui#`xc>W3F0``:]n -M/|dr/3L׺$eVR* +WY53!ffiM8Y^Bl#xk}RSchԳy̔sе>ߛ(hvԐ -JFQzj'j{/bUfYY U4՗9G]cȥZdר<3Q~M +uSj/5 Y.@Q>e-UY OO;h&LjgS x0>>Fg=tڑ1{J=x[ P^XLҏI2Q[O5ݾHY47ΜbB=\fdS̔^s{kVrq\Õ+Z?d9MXJ^jIt֪c[H;I+\.Gǎ<8o AEePyH-s5ل#&_[>t" kuE_z{4]ė úzK=WPYvW"Z|LJ})g5.ubHh.qe +#Mc` mF*ힷ*@bB^TQ_eRAm(i7~_^ +Fg ¹IaٰeVTk0SxwhZˬ $oY+`k>7LMP]׈CmJ>}ʸexBq_ +hlFeLs.'Z(tG$,K] @L<3σaЁNGGfeT C[ VGPcj{qWSo2} UQ +~ +\ܕ-:C{ /V4jR;y5}f|̺%̚v1n`}jDˤۚѨ^Ӟpf}:&?}T{a{U oX{[42nȵCjjTF~1玺׬pb<ԵlMd̦_# +ԥjhL V*k$MWQ4OnZ(6[jw]!>?.Mޗn.%ArB-_ʹӁG۶!52.70dlZtd|8hUi dPR7 OmG]]9MD:3Uq K{|*UKng:gPuPY?yND@ܹeڮ8i +>"Xe26%j3))l)}ca7aq| )u;qc.6n7..xU T>hi ɼfh5<4޸nJ./K=W\Lp4 `Veu ks~<"ó=IzۖYOi VtaV"sn[Kgup1'^ :7obŁr0f4iPQ#Cb2'aDv-eSn81:hY^Fjtjqyq;}LW@_;"&2}gju)z|3r\w 5e4mr L4#wJ 3~D h7-FZ 4 z"}pcG>1뮇mZwDnHsx*Y\.]*Qܧ^NԖT92ͼ*VZ7:k^bMmL i3 pik{*2Pr, gݯ5%^Ç2ܕn mK]_CP,4kNAłx*4[s:~vBYQ*U|J:tic/M=2rK EiQN J[֊Aocc'쟱&ʌz6Qs7c坼,Rv#$/}BlOt +&.sf*o&3=ٷ-vNzfSx{(m|u|m:m[ϼCY27eS_3¢~5n).'r}zX{`W20UdY8ݽ|,xfΒ=r%܍W9 yXaL,zE:/ڸM`q[K8/ BQ$7۝Q4-:X}2XH ++V<d%{2Ñi摴~+)Fq1q\0,Wo.93* yջZ,⺘a5䯨t@6_SVS#} m려^\ZX0Ӑd jl4bRLcNW-І}6 MO. u\5MkjyI&RGGQIVA}}b˱3- m@,gJezڌTǙV >L`>6}`Xn 4qvBW~'zk|ۍ't35rGH<=yH{R,QX:0P&r*G47q+cp.;;]Xެң̦5ÉdzJߦ `Mm-z۪XmW`Vj/9\:p3{Isۆ=` jGv'>;i1X5nPj:}#kG^L-jV7`SB+f9FA² Xk;шi^0PrRfR(qǝ~ߨ{u~+k V!nE]f0c1,}-ܴ6xŨ +/};;hm860 +m+WM> ˉ^Qdɪ`^jh`31 OsT ++^?@ >ڋ~ AjmBׅ_Ò(W5t\TjYFSEbFB斗oi|.܆f;1DA(lmIye7zi<СE1+h-i}-"]dm.7^GP Cοʃ |vDN(/C7{oVkz +>/zsw dнgpNd<~h>wX!tP u.iJ{p\hJ as;_;h~<.s +5w4>:Ikk(3OYP~hL2CT}ĩ>yꩋ嬶V&ϛF'=> FVH)]Kև4ۿ Bl9LX?z`=UqtX6x].1=\}|~"Fc9(갱0}rb +n3viv%!_* +i&o4n/H4B~H7Qٜ"Q;sT'JɎ;][J1XBQ(_0ũ.i|PccۏlI +vq;JN"EVU{i/++@VU-pI#Mbc {B= >nJV߶qqv*8"n|Yz:jqrfR:{^I|?^AU[y~KhRwߘ3xW/^@˷8t{w0(ldMn|C߶F)㼼},3ߦ+֨Y[e965lWmWJ}4amԮ/:/$ys׳7/WV;Wۊ1i,,~-21v]d@Z ] +Ic.EdTe`C u*RU089`G*,V6\W[9&=\..KDX~,TBFlzbzUS*v5:9֞mK2,]l:B>SSp ޱ߫Cx&0ĵp"{TC@8:0yUQOob7V]TJ#44~+/wܟ5jI}!ܹ/}^I{TA t:7;$SR} W>k峿TJ2)` Qpy6UwL7EMap>jf0R_Kl.~32n#lh˸fSuuK,s0kch}Z.OWW|mw1oPuX[{?}C`N/,=ܳfX8f3dkI LuevUߣ.2?_V8ݿ Rh1՞:gbz+dT"Ѓ_A_ s+qMYζ]S^kzL L=0n4dՈ"2I=j+c+ + љZɅJ#$8(/W`!`CYVQ-C]دZ[Pեm Vuɕ鶭˵Pv~% GT}br=Ǣ-M9ȗװpZOQEjP{YPG# wLq5`oFʗ[䅕ܣ]b5!y$7rR4.5¬T-f\N FY5ҏ~0˴Zmc;l=&y;[wZ䯖tp zY؄ɀpk:eދ9e?8_3`~ +GZE9hɴf&⤿fػ- 7ylc(}+U9q 5} p#߈+x_[&*]nrjUZk[n +~^7kGjG=FNe旑k@.fZ>_9hA&nU!>wN>gayq4@yQ٫a૊\y"1*G;TٯLאK]!nkm) +>7`VgZcu#W.|vb< 9u6TL#sǘ9v>!-錥3>$vI#Cd.Xo6O"E=ŷ”B R_y0\XP*\RFKnq;UzL+imHKx|ou+1#V#@ b7Ix!e M| Jhagʫ'NXLou3ZQGo?:QLeW`8h5ãQ87+@&%<]&y:6O oVؼ30Y}?7V#&hc? -Rcd FK<=񡝱}V2}ƽ鍹㒹.S7̬+-g\JաPP߀ +Qn!bqM:dXed`8:s^HY} 1~).,*j'.k#(jm +^ .x^Yzߤ !a`vWkzFL|aM = ]ҧGgж+dGI}(vTTY7s&kC]7ۻo{zn6P5Zm;YuG,U4kaWo: 6H0*iy}@ /I4dwc.eQP«KIJ򱀅JeGg@ oLx~dY_ op3/GӺWT@Z ܑRl~1uquYN?@J]DVUe;%ͼ_i%R~FX|\ʥva^,vOo<˺Ԝ#-P$s/YMҗZ|*ZQl텾nI2_[*#ԋ+]Քu 2$o~fZArvZ`Ŷx-6@f2U&S5rzJDrQ٠bJTtsy㾭_V<2O7CG-|lpɳF%Kŝ6oQˬeNƟcN6EF!(\Ub۞BW ])崫mGvWXoe?t%~{0%JIYeR!s}Dx~mV2"0uX[J_c}钷I;?Z{pf66x1Ε{^^FC,-4`+cs08i8t qV=bz=_ ++c*|%>-^E9NÅn +n/MkZ=G`nycAF6 =Ws;Zlv +"Z^9o3 4f[(ֲTI30gnwjvAuEqYdC塵)jUX5,A>B6G@B/Xi[T믫u.pQ|+b`f?icX(S_,})#:+e) ~_q6Q*g MOHqv>̝/&kK闣HA3Jφ̸<0ë*gnfd3%NR]}aa&+d*'®7޴]|K@ޔƊ%A:4<dm~˦*N鴔LNP ^W=xoe<5eJldѨ|,t0r8tj4T8E 9|qnJ`Ph}tu+nEb.|]>",ݼxj;7m >CB|22X᭾ahY @hqg0j$}P`T~go?>oB~LQכQ3j[U$46k ł׫I\śL&7{Гpgli660OR+tgd3$DSPד+zOmn|]ϪH:JZuRe~61{5 '[8"#-{ܬ@\mMTzѼ(y[#g8VAM6UC'V+eU0X|rij hJMo֢g#~f̡]3Uyx"%桟`]VlA-[O`L>7j7HjT[{`&kln܃Re>̪;Q v&ү4Hgc"h:r'$v/T^oٯX@{,܅{ +=u}F#,vT:S>]צYiX\,ZrpCЙT׸`U^v@|vk9>#PGlU+8Vfvezu:4 +kXK+Ѯ^0&T6Lh?PA3dJ6SUy&VjΓrƐ븤i5ڮLEIbJȾf5ߴ}T4ύ2CF1UF+|K ^ES7@kNEL-7O3{mPr6j4TE1yPٍUk^SE90l1{i;a6˲dUW| f< Dl"PJ#2ܡ%'P©4s뭪Qq~"Njl-]WO4zTiȮQ2 PMSk< SA<4EZTl:Uiyl~(eUR<3W| j__uH ԭ9rZh=7;T{৥F/F籢-E1M*ڹɉYtvKM pw2ٳn`\us>"bV/שׂ5tSZאFKqM0o !z6?Zd}U{Tr*3ý +i^d-M;_/iMFz.G3Fa.]"*h_T/p;nI׹P$)@rٜss:tWUwO]ؠEwy.v?ڇ^~lqGS(<]39ȱ`j;j͙mҠ6FPѾ8e jpz)<8d|8Kk +TYڮ"Lc)zu7~.ʚ6F[MbH-{y7 :^G帰2v{c;] 9\}jeNvIt?n;=XzM +u& CKqQ`3βᮠrKtvNƽ.i4$|xgxw|5(&o}닙gleg]}gпyUw;J5{,TCM7\$:kVѼ]Cp/i7JŜloҕ/Vd>v0KaXgwl>?9.WR8O|2,\w/Ց[QUzl=zwJ$u"t(W8ɘFmR#po`/`5i/i{[7;zv#G'Kmo/?'ztɕT|#;( ,I/oko0z/uwE駵s'K:rS,FUɝR5PpL$OofOomꓛ.%%Ԫx="0|Y{M:K̴6,IyTAdTYiN/~twዙ` +j**u@wF5JI b,dϘm,;rsyЦ#Rv8J b)Z_7` r.WncDB9][5:6`;iqv#P +ܣa}wunN%-EqZ+tux~8h+'9({7biMǺG< +endstream endobj 112 0 obj <>stream +Ѝ w kfƮ(.UOwlttTٌVhPOqD\/i :،s+KmMt5@-ٱ׫!Gb|~9֙AԄѐ"a Q!>.|g==QlR`J>߃qcE1f;in]ȝH23LPkC>ǡN_~b^]j2~F2j]o,4$gֆn'COʭ_m`& awB?Лʼ=^M'1h2DW.fTv{#^8 +ifm:P!Ր[9} :8RpJǦPrW8npQ,1ۉm^B?w7:"sOd,X[j?sۛ(b9/Gc*B( :xe| j%7ջ>{J8(>$'͐{iF49WiU/L0(Mrݨ@7SOϾMEn|]TL p'q x}kF0c뤵GA(W&fh6Qӣ^Ѧowz%di:vH qdEc;%yf]2܌bVވ6aYg"BFcW~ffsyAzU\>]̴i+vXEM+وK !:_hZ>2.T3H_3vEMre/+[:}vZ5$]\..xT&xPkۼr;J^+ЮgXKپڈQdCzl,rQ zpbǚ@㦱 d\rUR|&S"LhƝ`x0)8k`Us4_{+nDZ .u8sr(V[ȡWXp m81۶rvE ae(|ёS6'SK+]9M:th$ig}V))o>/ONECeS/ 7gz 85/$y7v!KlOEn\5,WWTqA]yst r:/u[z0{DVt8 +kUum7hݦh!<@ gҙ8ħ W`o(>; #-`҈]wtIv~t:/('h8vvdx FoJë +tĐ%:՞ȩ#g. Ħ狘G'(60\y~X:'v?)ȹ{ím[@UZLd14z8WG+4R#yM"ڻ4>x.ݪW`| VAvPߥ53ر`--a{ >UI.A8,xuXU!Ӄ=~V QgUZXB7_jЦXsi^˶xf;ur_fO'vA7&lֲVjX|le?nl 7nmPAe_ +.rkԊitQT,Z֨VqOec;Af[;A҂/ X֩՜vyΚل\díw ķv$؀*-BψV +;_EnkWulkS8LЧƮִX̱JY ,bvwzY|׹Ի3ݎd +WޞݼV4q[pЇ݊e'jYSfu0TEp{g➂!w'?"6h؜gn? HウE|Kj&asqy{v7&'>ps]oQC`, iKr>`;SQ2;5CT応p:X1wo9ߐf(xIy ==Z:Yt{lm6=k|3A9{wc?r]3Aiҗ)ub(,4Be8A)z׷04gjt]0~yIe}B%⨣k >*=j! uoko.י̦բ Sf{<>I}2'Mczn:Ǥ/T S1+.22b*i Oh 6VWd6dM i#G⻑\Goca-Qf 6Wϊ+RB]n]GpTV4m4.mCyTl7=c?7'.[_EyYJqV#_:hȶB'/h,@4vZ^?dRfye+^i@_h/84 UA[~NB] OASuQS c | `$A|zR('%xgmwCft\w;S}џ\y3_ivLh9zFzV V` +e>=_\qjOaqg=1d\3sOe+~DPn^=F gèКQ\4A:|bA3jgjOiMZv`ۖ{MrV{n*tlz<|휨 +9?*f!?֩:'>Du.vm2Y@CMي@jyە!pg]byHIJ`|WsC-i1p>t]26}d#;\ܗa@XCqE !Jma}}v91ʋ~TSh9oY]GBqIxt)BcDԀTylG˂djcO|Ƕv CdOFѓs +VtƜۿx{gpvdk6Ҫ +{cֻ +pvF&;s[N?U6}JAxR}H8FƋF~FLfZw`zSQSVy=`@;GRE4)cPp#)|;yvЗn9o __Ϳ7n$%̨%V:^dJz֣T|{EF9;f|prE|r>fӪ ^3Qk?_֛ORVInk")zv4V׌vn_q㫔4K.@L_v)aܹwchkhmAHs+fu  <'os\. +z;_vW'B4 [, +rIÝ] n uyzrxJT5]`(:Z,ӺQVݗy+<Pe)l +ѡ}?+sj~y?- dDyiiR":餾3W:׼c#PNR'&CbC=X#K,,ݯ zKdtאsp7|ěUkĐwKC FY;}.NTF^ωౘt5U',ev8.*v[|VSgQCn[}w8^EἹBBg;+Tx'/%}b]&m}Gv=m.j(<6%O/$dsp}ۋ.F:u?V:Sj1~ier_ .;Oݻ[&C+Kj̜O$5{Zh]5n15_Bĺh~5j<YwJǾAu]Suu}}W#ӱYwhUjͮI6/xlătu%TEﹽB:{ͧʤu +ӠGRo _p;VDOu-&NϬޤrEE '\<"ˮ3'j n$+ϹvL8[ QuXS{?ߚG"ZX i@h>&0,~l] IhLn̮@h +$eڣIڶ }eUӼFk4{q-՜du; BjnHD6ptvv#w6FEG9T%Zj{77]'[کcAE{51Y=HURYT׍[(r٣_`n&یzg;Pa:~ʤgNtzn^Ov.OG#:T#LG%+}}0ƣ==ΞRģ^ k?tntmc7G}7lަt>Vh.sĚn;8Op0U:A衆e 7U?+'s.~ZҜzCmeNumCtp$_Ip]6t@v1Er`;k ypp)mRupN\g0dۨDga|.9X SQim'n;z[/nC2OUNi'JAӒڿ4;&HC#V18`,;k֗~tԾ:y&I+jFgh0וEک}d9lKei߭Zcve? :6tFV?6[4W^YkM +ь3wt;cp? c+Wg_-Z{ĵ.޳ښZhq qF%x(Okܑ bəjx%4ҍ)=Xf +Sְukl*'b\s\Xcb^7_&4GG7JBb(| OYT!խV@%{W*pn^OcYq.}csT|}ܝjeQ{~SXȹ{+IfhgF"I _gV޵?7 WW;h ѦU+S!r,\-=Id֕bPJx'ZW37 +;6aV|+B7^8ԥͱer%d,˱*Q/Ɨz udt7U[NjǍ 7wq3hpc/̹V,?p1ϭ.zHYpjֺRÁ|Nmu}kxX]}ȟ'?/U%g6>`~8c֛#G_D3s\ +\C `n H'MmYG/=.nE!L <~Ɇ0CB-ZSt| ~!hS:F8Nvo9`{e?qT_<~MڗZBZ$9ضπ+ݜbG۷+ZJ{,:ַ=kP#0-BwO#״CKMLh}YW%bA4& h:v ^?WS(+m6B=a(̖e|Ӄm$iy Ѷ$f 90j-1W "!iI5xö, +-mJD2´k_<_.d]jPzKK/|C歨2 Π_}jaiWqKai 0MqԀ +=k+u>Tf«'*ɏ KhNu&m ]=ĪJpKMuTWZeUsB_vMF3/`87Tk +ͣȹ7N020QW*(kE-K}>HX<:߀Vl@q:hzwQQƽ:2h0P}Sem ۲J0~j UhhNv3z,Hv9ohT! PqM0HV?ys$@zlKlY2d.OI{i'~: XMܴPupeGw$oCԜ'[<[r$>Hsn/$)SzϹ}٤ߩ;L0.!G+It?O>)=,x ^4lד wr Y-w&Z| +jO>@dQ@ט:k$n< +>UsaH=_$mQ{3X4@՛Pw~ ddV)N#v;Q'55ףG^A\Umo?[>E?aV(y} 6XȸVandL k8;oq?ufmP<67f"*);eCves0(!oL{TB}pn\e6oHj Չ3#j+_Afl/A?7M?=B%λnzM`yt/~o\ +zbշf\g=UVX3;#e%MڔzS$pR|Nu."7yjj$Jk-F#jp5C|{&v'=&惐-NY]u–wa:G`J_֕k.kF&t.t#%Wk/2EBiuG-;^V<8嵃CȎK ̍ڲ;i-n'cm(!׽k,ۗYmiAaTscŝ^xMVK|tx.b.ZsQh`Cu$3"#o~nux7lELJS/;q'we뫭N3*u9o6WH7*?/zQ61eyHx7HK>lOiiGodQ_kW)kmתDuba+fahGWQ9'l:sƖ2Fj.E&a䨙k)n_WID./Az?SX=R݀VGIFmk;n7< ǵosI+fl~G!<[˰jAZs;{ٿYa68wfH`l(Ila"6etblk{K{SڀP:f}WjIl]Hv[OMpT\-4h; wݹ,b dvON\5^S'h;?ʑ%!,e@4Q!K6W]_l]*en3kQ0*Hֺ5U7-YͲ^Kώk9\>zx1/”:u!u$oݐT%^Y!q2I9br[^qs6F^Լ.iL xl,z] '*:`O4!28K$Բ,;av+t0tqٴ9s0妗uTOgIկJ{hiMo)W8k^ YYy~a:!I]Tk.Yx-FJ֪GR`1&VgSժY0δf +dQVn ap^VVW$S%U^9RCRV݋ ='҃Sm3"Ui.({~зp:TxwdJJy(1kWM~ _k3*Y1#`h`kP 0[6#6q"mܭW2Jo*Ot`P,λ-1eh7=-PwwMEO)|l4 B󀐭^9[eg͍Yn {NKk%c@q W$F~WgN!(R ɽ-as~IP3w%_C|n:2$pvΡⴰ xx&ُ@E9,vEG;K ԭ0±}J=y%ܻf(Dr^{V^OG'YHI\Ԍr'>|yR 1]+[i| +GU}%"xt +5spBO:ҭ*j}4&|~O +:>&_sٲiQ?kd/*RlsDs.9,k;R-۳H,'n'*WMK#aNUV\J]<,sB\{h;0M|/2;'WNkٞƒ:֕|QoD\6kUF0K旸5p+=w#NÞU*( ƜxL3;Li+Ed6(L +ƚNc]_(/zI~txylYF-J6C[гU lb2iϋKte[:vqfK%g8~gbaWkET͟}P dʩsoɬ_ дr+ꕪ_G7eaU\s4vHg%͵ńM0xRX̲I^.F=CTq>ieoLa-ku+n7ú7k/ŋ e`0Vol+J^%^h f7YƁTL'pT3z;aM=dN7HI_jߢDr/uvaVۙ鎸P B}i8ΡϮ]8Uf U)p$15l;j 5&$t@6孾|g f00aV8e^gWǙD Ffu5;p4]fb \m_@+QZ(^moڌsoC >LluM,tX?=ªU7̛}]0auUaq OD2Zy\ YeܟhHqd}R,,EWVzr5"뵷+Q=7(Ǝ@-Rp5ӛ./փݼeN+pdFU[~qda-,F(/ߡ$?.`cToVs/;ݿ7 pz{ۢN3Yd) U+7b :չ4-?:{x8(Pfc~nLze{^i`*lbgʒ΅fq<\36n:agsfkb>N+ޙߴ\Vd"5l#?%?JQ8co\n|$TUi9_}< ewWZ,BqFkM^ a[dn캳m}U ~lbwhij\~<\v"%OHI1lL^k~&bxikD{v8Lm`Э>injY2(5F:kzJ1Nm_E~R/vM;nK`JZktý +Ztݸm>OYKAc*43^5k 3MY|"5?4r؇"6Sy Y˱NCՈ'PB*Q+Mw Vě +T[6X&lTT"݄^L I\&FYX|5]rR: v_-Xz<Yo~9io0?r.vyAԳ-3"iP;gBGEj/>Τ{^L>pBV\JųsziV]](J#tX|hy#MǍ#nbz&3ZuKዜ7fTAi Y`࠯BNz'>1D$^mMV/olq>EI>;_(?ؠ\pjJAa]j9TW̽wz[̮@ Zpw*J9$;Q\jBayl2AfƐ;uM:L !Lcڒ`AȦ~]"zC3s~ 6w7!=^y=CA#/n\_ZX?=FڶSHi_ +KF4Gﷶ~n ]_4ׯ{o77CPd}:=?1?3 's%A{g"H@Ӵ`^.j9TJ(yJgSuJEa=ӵXC(C]<ǂPluOm_YΩ%8*-g[)xڽwͫYcNDsܛ};*,[Eߗ(kQ<l^ܼTG r1% JشѢua7>_ifK3^q}>r; nom(UTFjذEY\co}a׽ zw1jCZc, lÅZ-/Jѯ QyVP ޼Zvʃ+m1l7|^ev}N-duLg7{DL'`C"sn;$(q=B^+]¸,{m8MX]*}т=5o_ -P>5$V%(`@] V/UH\L_Au׋}"C=@ {z2y5\ +j{\T6&;3S] +Ix=`8ʄ=A߿*DϊneAFHN014Wz9T=ߧL,O?p_Ѵrc8Ohe}g.ZF>21 ̘~חq K$7-OW>`Z\1I;耲+~"C̅Q聞;F:9c&1ʹ+z)^ͯ)mp*~AYņcM<|*?3:BkAgQTKgd0@is5~UD) R^nJUz꾵 N5&ͶI(lڎVC #Tv^ljkaױf,[PZR^'*Ћ+}y C?f5: 6qk54i]n xiR?A;g#&"g8<rWQ8QI}=Åwp  0į°#|59Nk];Z!(YVëXyS} K#\6qtqxӡ 3 CTO=L׬CEryyoJcvHh x䭉>/*z2n,JzD.VqtPzlJyJAe6fF"<ɛ~qj2+Cc|sb;aT W]nw.;ѐJ17Ҹges1/+XϜNeۥbgz?/7stjcol{3{~ իTާIP¦Z-atj߲Lo`R)Tj-&/wٵؚKXAӷLiuRq g_yI]rmx,{wZ z whco?{7ТtLs7 nD,|k<QasTwAZX%#\VR;^|ɽpxדSDrFK*X}ٟ +%su꽧f?9]ޏL(bA0xޭ63AĨn ".10* sX,0qZV//U@~ s Fq;+=lr̗ |u;&7#Ve&0YiC0c}`4ha(<b.؟[H{ۿmcF4du|C_cbKr>R@& sdB#g [I,~L6|{iP֫ˇI͌e$qImb2D`eƶ)­0 +;GAKåh #_*B 4n7O3yG7k&kZS|TSh;R毬 puCC^m'ȌQzL";/I57SK.x:' [gmn@ }l0+-Vf[+7=3!77/wp=fBsҕ`P ph6:B!Ze@yvyln.%$$ O()Ɵt1+NS&߄ +g^ ۨq(_̯ś_YsF㾰wikO#|H~dIj + XtMָD`r{=Hϖ'Pyz1)~f, O:tS 8x^DO_Q\؉N α]*Z`lkz U2&ިਞo޺0udڋC8wK3oe&<:T?6azsދt5Z=u6?o +.)ƏFϯfCOW.aD'ʸt{ŨbLVIp Q:cC QT- \o<8ioZGDLr< ͵+f<"XC=e<zn8nq@?qins3gFYN@飺.4?c޴׈徼R=l3TtCjȏ&KGB,wN n٪<)z'azUqL9$uj}&b<ۇe5}w]桴8$tr>=ׁKtFe|ޡ[cʧC, ں7>8M|PFkyvB\&oƦ־dCz;wVRdJ uO3=TZAm&c$:տ鞺9_}~u<]E*r_iNfT$ +ws~ѱ3U!fK2L@ +~iR\:-eS +=(^,ӣC[f/uң;oBkH)hdU0r^wֹ ȰQ_6:am٨kZm Wۄ0Dz / +544ٹIQ2 +2w߶4-jkb@,UVYbP[;y!?屈`cW>6:pu2WszkN +Z/$@wdm,#~S'EVR/.uZk#N\>m+v9W-z/J'{Oۛ{Cu:ڷ?+.9nî9buYd@ߜFH2ϕ~nYVr_q8-y)2F1{/6R9] !|er1WZ:L`F7JisPxʓZNUB x+r#KTS9R.%uezsŝp^Z&mk2kViZ9yc"%Iig)p%3QmԎ31wM! q›VohI`K%}~QV2H]eF|V:GNe\\u%ѭґPZ.Tkr;VZXoV2W5!_튦gs}սQ xy 6fM`Qva˭0 ccqu6p+~)P;lyeW[0Ftn3䋬LIeP'sSi +yt wfAr5ۣ` +hݏۼs+ch{"-!ѧA 5@ُ\=\^L qA?A]Q{UK`#)}"h#rNu6oI%cX 2{Z'Ycu!%*k sgіޚMc`x:{qUȰHBl7|On#׹c욏LV.[ߠR+'xT`#y]JOwcA2sCiVJTMOh+Ǡۍj6sUl,;T%Q\dId{OL26V +PLr,;}F)?ZCvYL޾ +lBDJjG'7z/> %x\8w*==7s7O4x6\[xhoQvR58I3kF0CBB 5#uTM*1~>ތt ͖<}0WcZ>ΑU-v&hm9Qtt I{Gl7[MGaҸ -R`C6Nݰō]BΕnh?$42ʜPHK_ څZhX3DN׳en^CDHɶڗ_"w59t\29Y+StV>e~L:zjnes8`߿f"F2tQӥf'XJ۞YgdJ*5D/*[-{bzX0:_3uJa^Qi "\S5b55~K[nU~ Mʫ&ωcz/N_N(`Y[KHe[!zC묯iEϑ3˦A5*2šr5.@gPݬϺndHfSb%+y(ٜ+8~ծ8<3Stk2owxfW wN:+d]qs*w&dhn4qܺX%*^NusVAN!lWOO"ӑG\ܦw6䪾ߡ9Dӕ%QSղ*Td]0傗spl^cnnHk2Vf[MCD̎Ư7ʞ + D湬lYV+}~y3#x#Bq7\vuwضYOiq>jɒ:XyGNU1=kz]K5xM;%PtM9|(zQtc"I{j:5i Bx @L/4`5egzr5ի>+WGCAglxvhUT4 f +u-l '-~byxIcz:,lj(Mjiͼy#Q`h;yĹ,#vt#-Bg3xLi22J;jX ?nck'ΝWWkMV4?"%wmֹ笺 9H{agnK뷝rմx-cU׃cW2.2释:w+pf{ +-[LNuL5Rl&䲟iib&j I%m#BWB'Ahぐ,@ZEQu֪XUW`dZOL +jЦ `!Hѐ6fXX{UsBȽNoHn*8a]Q3obxL+$e 8-S%}[˪Iz@yq492zhKf~Ik81134%Nkv|<s<9?/Qy˜S{K.g T/u-W4We/K#Izp}}fYAӻ+ׇ˝FC_"ݶOndP+ &Ze_\$Fr+:ռ/Aq;Slgrܼ\ +{/kj +dM2ޡ Lښ^P7Xc΃_U`Xiu˹ij1mP୑vJs?V?v>jZCY?h.OkJʡ+yl+&IOiPZy 1C5 +Q2G:ZIo*iX:5cE ^&a0bq۠q&Vm\gD +-ztsIxIlf/%{5YU}t u9]k8&dҒ2U;zйҼҧE ƈs$65c9tgۭOfj`wOјxy^v/{`3RG|>gEಾzu=%d]\1 )YMqEpk$qГڅ?Qd_[5OW];0?V\/MoύQPD3`P'd>בsBJoׯQUuwX=V(rrkoxuIG-IC_~^`MV3A(DW˴[r+ LL VQ +:?yܴڻ ߖ_tp(jvʂY=9e,,nBS=fڵbn8sBysOnG^L7P{W6Vy"|߂SQh=l(e*-u#zCy ׁz|u BaQ&=A[ŏ۩ixB~Pݩ.Mn&"sO+E(Bl\1y."&\Cƭ + M4k ptXwv̫*ys䙗1d $Y:%FNIjcr2QnV6hxg5zyQ] p1$xU[w5 LtH0Ld%Zj&0ܿAMzUd[N'zsa0h BM^7w+0l;n9~ SV}y^APr(+3u vEV8!7DImvO׭жglf0G'qd4;'P6 Fg.3Z>nyx +eU95Q܋(qICdӑubr.ߩAoHz}kFZ3{nEO+[Kٌ +%#K!oW~3' +/1x-n^R(ժg'c;5ZRW$G3z̾yh^<󇡃 FADԡ{l`7+&8zɔe;][?GFn2K9;Gos0+' ?,!C.`~"[q(%cj-[nhB'҇g"oKh=Ie° a4H{ư@zs[nz=KѢg^[׼|Aw}•QcLn[mPNnɒ7")ګmj^__}FE4'6A5u0)Y[d{6L +E鱕-KukDj5zr %թ_BWq1!ϠV))JB*07<^t] +ҍJšX]5l?Z> FgEī:"0GOQYL7 ãFRߝ|o +j sFX~ @l|sߏt?JhD}eobal?,ES)T܆HaX.#*hI=C[cJ8/B.2xlfF?쯨P+<}E9Ef uVDkl.?Q/[bB{X~ +O;NoZG@!>_@ TZf,ǿ?8ŋ¦tZy4OKAp 6iM7 h%t&tlR,!搭 hMGq`yC!Qx8|^wY-%.6H*1`ON3"ZØG"3ˆF|y7jd?[)`TAYg~;[BqɂoV'?$^ӧu<iJgJ1GUJ@?QEd +I1o羚5JT-qzߞB\_#?j/$ k5q ~B3r`ŞTtLʒҶRwSw=go\Jݎ,rAZ+l<4% omQUVLTIVa=)آZY$)Q}yjk-hۛ3<È ߑ&dYZ[y`({e8曟,kebS۵ ! 038١En}β?˵C% }l ~ĕՇ`ގx:1V6pƞ]h@ Qg"(w* *6&7ٯs=P.( +jn̾<=5- +I) ApyB avhݾ@Kr%VW)OsS腭'4Bt~SOˇe#sۏQ/4s&uen*of?S*C Ǡ*dhE +8V# &M9tC̘{B| +bN-1+gkn `u@;‘4V98KEH+@v(M%Xa#=_piǾ2n1 +?S-Mbl ?}ZݩCc?SZo7-nMdQyBP;nܑwC3l|1ixR)>O,Er?4gj}iNX8fTl NiM?uO%iﴊ{Y3{jǔqUIm@!by.j=o‚ZO)鸌5!?ٓԜfԆNUŠЂ+tK`c=+=P =$ڮ:@a@ +$Ex*lSC}4/"kǰO;gg3gd:m3xA="N 3f-x ~)ňe6tuy?NVk+M/nfNvH*'ahȱ.cCPu ͵rȢ/)[l7,D^@-c{ +́iՎɱ%%vR}Z@IxRi_#ð<D2âhST"TF*M>,b̿7]7;eH6A{V16 RF\&PhC,v8ϝUN'e #p?\4*AD|b$uftp5a6 N\ fdR~>+{W?DŽ{bk3DC1,k(,*1['ͭ/ZeFb 5 .= eBE|5W:37-#ʰZ|Dk q愮301V3=O{Q)yH4TlQPE BxlKhj:6nlDd<!Cm2 +$&QVY+%3FE`@Mn1)g+c ӛB&I"%3P9*V5]2C64PľfGwn[ KowqhO[VąÑބANJ[ -Šz[=]c(-s*xqc<_}&#Sayh֐Opxm٧G=ϾivW7Un58d"4ߢ-kDu{^;Eg\7}#蒫nхEtQ_Lq_>Z ȦE*v5[FɯglU 1Os@T;@ L6kHsi'>sm!WhYeWF1{*W2F 6Vn +N_B e(6{\Sg^cxzsg;he=xq>DN7i0~;8/51MG((>+O KcmG\n,uI"$B^dyA4MT4?Kol^? HPD%K8~ X憝~ľ8MjRҽEJF +nauk+IL 8$oʛE33"]rV +[=}T= RBz_2:DA۫:BDǒ$V_Z̜2H\aԷLG#@ןJ:%Wbh|r@/v?NrkJ +PԚӷ:1Е e AK?(uWC4wģ7맣/W ѢA`h=׷?0~QfwtV֬96s-[HLaDc}zۡy͙7>QOeypGv)VSW6WɡA__ÚyU=,D,%ϚV|t }_>8"/Zb3RU dU.k搥`:# Cq=enT~}khDI^Lյ+!=s)e?9ԛڗQٲ}ug5q͐z4Ցg:~gt(]i3luԟK3TdMGdO\vm0/91'Zd.$bxXRΪ;&ؾjƤ;*6ɲwV{뤲7ᷩ}wk >ONDiRώ h 4s}mfVv#g[g-س:7 IQG>p̕1?(7gK\[F6ZPG][ yK݆qS&[4{oS-AV5ruiN&[KkG3-6|Xj[^5hoۀ=^1Cbw}HK[q7(-w,hfvVnpԽH2@^d.JqiqQxlyˡEAh8|k<$qƴ)3A4yIXZ)P9=1wEF[]p͍f~-|,MH),ˎW[f6w x#-o؈ủ9wCUxgڍ*Knjfo!Q؋i8' xq/yD!ů6zɥ`-N됇ޗm1#Z(*sQϰx3 ̷W!/yס5۠A٫K:tR#:[tFkAl bBYw->;^ZLH#9Օǀoa.~oP|7O-9ŏomX/|(0`5|W$o)Vy/وkbt$FgӰߠAB~qq8X-TgOD-Z[KX&{aTE?(tմe3dfQf_Oԏ-kqbU}F'}&i3ݝ֫ Qk[en2̮Ѯ4I_}e ۇ dF'ep+$u&VejlϰXk.LLqiZ):V ufZ7* +uwr) Vj[~?8~iRU%"(v͑ӆݝ> .tW/I=;i4ݱTi'R2",@(؋D'>@kZ^|dP}Q-{&'miɟ;Xy1/1.?-=ȥdz +:PmbCz8sF:!8{Ls/~G|iDQ\Z( Կbd 7 PR )t43B+z)_ʍG"N霽e'W)[(yaNL(Sm&2|.5l{#{}iSz:g?,`sEVrdgM@T+=ũw1Zo.*\m5RWM4v!ә?ja‱i3uֲ՞&mg< 2X~`u@'`l[Ӟ:gpm+rTwe[il@saݯI1ՂDUmԋj-†f1Nk"xa[Aټ!l3C-M1PN# ֦&:nƤ(foGs3[43ks'j #?:QsӨxsEL/iT6kE1',Y:{9VRlUx1a)ˈmW~aȻ&/[d +1Re {q._3UkC~=ɜq8R"mtt P;RiXhqm2ծrt.bU1ko!HIaO<#_[xF՗ +b㥳KXl.J^^Tz/H2=/rAGp= en'CAi+~D窎A -*x`nڈ9~w ի$Dʖ6 ft|Mwh:49\+xr3Nx@_M*fLEݵ- /@{_E/RQnSrϦJmC?9∮ OGKE Z7iݜ>f٬c ]] zzZ)xfסrDekS(Uv~sNYi/ 'G/ GTq/_F+rvkcmgG*Hh>￝"]sثWAscsUyL=Lk |.S -KE._e gF:E0B>+7ckHr|vz-"鴲ߔ38Dj%'8C,,̠GúRJ~4k\Ev0HSXNBVxZdحnq0ENV(2?NVbܬ8d4lqGퟨ,{VVVr1"^N}Rro1^Oucns~ܲeV7@0?Y͜}nMsibO[wm"W0`S'vW>y9kbJvZOk7f-UVT.{},k{d¸a}Zu8]˞G o.\|X76[d)v+_y0"DlĤLZYw,֩{_Wu(a|+W<f=xҁfDdڟrZV<ÐImr轢sS }ǜL\wC9Fw51~ԦG=꬏ظ ?~ v6OSBEJ$F.㭸do$R*YH'%u0吀 ~|N6?Q9l̓K}/t`ՙs*5<BBu,WPLm_ F\Keؖ55u~RKs9խ(B.2Ӿ"O->|'v>sH;N vQ}զzlXg:??~1b7H|l<@gآa}{r:m'C ѧEq _X2+?*#aKȍ=VQ}6j.AVL_ Vy54j#BV Vwʌ?:N+!8+rKL +59/Aa*|=P\6x+̥{sgpԞq½yůt:HK*:CGr|tpQKύ/lu*cp毝AtܦxV;j؏^2VУq\~1lZr,(L7^c?SSOk|E~GTm3X9c]Ifƽvߧ/,cӝx.#ޢO}j1۳)A|] +#I= fwV(F]T>ztO]pHA`tt p(Jݪԍ_ڸ2wS=q;=J iG_iHMNzi_[qz8ME5q(H߈q[{\4[RҰQG4(:\UJShh8БQAg,u=:v?znU= Wg_t[]s</bvL/0WDXq}vgexNyY1N%gwlW/^;_rL[~6Ugn>}x5u ^LT^ؤ@\ JC iJQ]\-%k~ @1/㰗Tлkl"aQUq5p^-jzkT0G$Z(#tcܛ gXA2Xu7unn;n"f'{'2?F,Svʖ_3eϰcz%SDPe/sQQP|tOJZUf:H~ثқJh7,hݺ$W/G6&^6晒V[*Ts2rb̺I|':ݐ% +Fa IDU2F*͔)N8t?iJ8>S0צ^L-=T5@(Cطj ʽ#{h>MvLSdkcQY.ZzU„qeu?Ⱥ3 F{k?g (o[>AOدi9<\v(0pOx 넳 xȗ-]FML(vͥb(󑌬a{+_6S^X;R>,n&bswl:IP5^Pd4xvW[ze84*[lj%^zʧq=joVjkbO7.סM n6okY bTd|wK])MBLdA )VF>u=_AY哯9?mh#Юآxآ};3qFy; ,>:7<8&ٙVM@Og)]_2ypgS;P9ܽ!. +>Q}mtlewEa?a|6_o6p!c%߉k9,>bB?@iJ3STONO-*\5c\j荟ko`0=n΅p%M +'ٕ[!\oowף,ۇ=A0G_gNӞ8>{[qg Z[b;o +ndl\#T=M۔0`vK~&&M1(S.tC}\fb96dFb2.j9EkF\j!sT?7x>Z= e('Ʋ*aBm!AuhUJ %ٯH@;!׺ QJ +tj1-|dvZAgP3 `Nļ[ϥQU +k8+7(ܽ9} 7:%y؟v,b~\9}iwbAy.y;p1-A4}K{ٌm9Zb"7zN7=}fX ;h/qceAR O~%ҵ][gQ!igʙu뵮1srxrw|jNX*S-tl]Y1VU17fkAnc9^O =9jPѥ]nn2wNH]`ܹT:GN_ƚ[,zHSh/á7ݓWԣd!A{=R^ !xYg{x8uWQԦk0sOݍJKVt|Jܼ^ȽuG%Qܴw?ϊσK4Hu?7ĹݻwGƝ?~VFy9솸?v=u.pph/:žKkj's3"s0[CKiν +뷻d0]7c-Wo?8^O Յe߰]/Wjs>lDTYOQ>ى`ؙ\gmLF=8u\9랞K| +z7ljǘ˿WY}oR +<ڿaV7#-,&_tP6j ʬGqtA?`;/i[r]y:6'{ilIg[v[2}7hռ7$K7||lof^U/|5Iȯ^q}X ݩAx ]s맥(E#LAHcټ!tB zW59dϙT>ooLs, jqLi;e59N]ٱ\{[BT170w̉n.PzŭG{c'} G 3xU<K>mħj-ƴuko˪ʙzʅ+WݻW,fuk1SK(zA- ~~Wt$@i=E9$*9\K)Vϓ]W +XƜGn_;cG{49Pi7~OvI;[ӵ:lK{I,/YPW +,8"'Đ_ƫ_+O{ =GI\ޮ_4{Wc{v[{ݸWokScҳTF֨4XF]c%S)`9fWXDa6/ܕ*Kv;r OJWJ^pn1 JX 0*s>ң<#b.'z 8œ,&vsPepqZnN@-Ӥ#rkKi(HAw-8;>jS:;nȪ{i2cp] Il[oqI|:ս"t{}$ Jpc68`Ʒp  3F?.lJfDk,⭍xf 6:W +KsÎ{D^sY돔B%x2$wrƟ:.OC\htĶFuOU@3qL0-UaٯNnufcyHR?&/BoyG6)0Nz-F@O6Lܴ +,rƂ{uרS  ^k{}ϼMpcoUq:tP{mFPl&7kJrm@EK^:c&ṁf1vh#n|go&lRznos7:Y=Kyt#(O\ + #FA?,o7#+3":mc)~<'/W+(l{f&wL8wOJ4aj*w o~ChCe9[Uq=,-DuC(&rL, +T 'E>l*yohuKc[@Ne=Lf׊Ů"Fd; ֟{b7k퉣?p~elϏ0V,wt622BwZE{j DDYa?nnϖ#)~zt TH!AJÛKGe] Mf8r{;`~SC+3ۋݜI70kl>춫7!wokPiF/bW߿h{BY_4`0K_;|{x(xgcTQ_zna=rgk |rE}~ePYs.TE퀍ʶi&MG7}Bۡm}΃BLd#?ې|]~>j:qZtZZ=EܨZ#魸oimk6Т̕5W3A-*:Vnya -㚫F`B*BPu_6;Ht/1pkR!܇s}nЯOȚj'F?ITy5n? yYK\]UXoFCdkTWcx .K +*f'3^W/ AkT$b.,0)oY}5klHt%NZjdw95xT3DݵuA}mDJk6t15OeD}ᛈXk?,zAߏ(yR\\;>07SБ-촘OohulTcS}ٳ>.a 2{&sb"t~X &euԢZ[Y KCM C4Mۏ Rrnr[egU<pӶf Y/=.ճ޾C쬲7V +1_9 ԋ2yGoPl5w9jVw4 G}&I(q*e]3Jupu:|rPׯFjw xDeF@ݟ{cwoB':痈|lra8ZY-鐝q3'PZt3{:9p7Mfۮt] j1[EDDbW0olonooh.ԖX7x+RRy<ϜԶX Q•sE56&LϚjA?⬋+69,}o[qDIxwQnݩS+#_hWTn*uG%B& cc8iMIo,|e:}B U[t~Br{zQT|Ovl0x0XZM̸*L[u ewy,SgkK͊꬈^16ʎ7SͰ{FbYpf9f\tno:6A"N7 lfb+rv5]}JAOmj'/VpAfd3q=)pk-㏰ +%g|==\aTnw{47X?vԫas, [ZJئm'6+Υe][#Q +N>c!Yq:}nlI`81,bâ4' eV¼9v5c"B\@?uF; +w\ w~:Bu ޾FkӰU[[ ޗ*+]9ƹlNnK+e~\gAuolW0_/mmtQwz6l4V˯*1<15tHxCs[x KT Q􆑶M3چ>޶u1 ʴmlƲYI6nH8e=yv}MR33'*ʚj)kUZ{^jx ⻒reEu6oԨ|_CT!J).mt+*?>>OaƭWIְ߱7m]Xj'Ofn-Z}_Ome /t?cn3|nsG]݂KEPl!W0VgjRѳ͝)򿮔< +&T 3}'{FZm%~aQTw?qTXRb@=2Sꢻt!S*[F"!aTlB_;&۪ѓ'6e~ +l))I3n6ҝ8e? [KK9Nܞ9SVnfꇵ(|\W uEi8D[pU'%Uf}wVV9Z矴Z/7G/=:ZUx(^e8?@~&Љm"e?պX4_%JԖߛQ>5} Q>=q>Bcڟ~+ +|?QpWQAta d7>›Ǫz;C}gA3^NƥB݉|׵`8j Ufr(bgnC~n'7&.gio?5 挡Ms"; ,xW5]B˺]I:Er kZWMW MH6's%Oa {`eQ3K&N}Fzh5-{ >>k' ={Q/ݛq:euD>JDuYݥ١VIC -Vs'ZNcq_*q¬mȳV\?3$\9by/)OtjI#S>Sװ +jYClԷlb19?PۣЉPV*^O^?f򄰑wrg9P | R7 o`8[@cb1ΪV+RXo "u:[ϓ +ڌ@>mHm`ޖ7ծt}W$v#|S:rgκVYͤb 5ڴec|kÚ9uk|*HL__ p|W_k4vpMrX%%[^BAwozO]+TMl'vS ,'ΗǬJ "enIx2uMi3 fPygli ݢ!FOT;ƭ\TRdʵYWyeÂQRRky pGGY nChL_HxeF1ͤujj'4FĠ='q>\z9^]:جoPIOvqqe6E7?S .i:rty 3 @pobvyǗC_6i{BncojG>Ҩ^6V$BJAb*bY·jvNϡ.\-J#Znİ)lz賸֕yFY,ڒrVر&iDEN"-=:Ƚ>aKWy2݄QrjPq"όڠдFgЭVx-2@JTau\4Ý[Dg۞̷3?Dr56[~a9$s񥝽;3X7I3 RQ.<1k$W1OW@ o73ʰ |O+#mMMpu1Ck#~B۟Wٯ rݶ\VJӸ<,4h@V] u1k90 lwncݶ*ɖPuC%LrVW)sgfV6By\vic~C]^b:e|0*ȻDjgM硽/Io[obt/s4Sܾe7@;}:ᢟrXZ|&xCvwwm} + +wQ)9V6< Ҋ+ jl+]-vg >͋~Y[q1*`U[Lk^ͽخ-D\t5d/dK{*^h"u[A)?K0ߋÑӑϬ?@,"I9D!` /LqqMlI#1DSøI&/N~i\t#HL &]6~դ^Yn5tdo;İIݴ@\$G4FjJ$qi+A2XOܰ+u +\2K/eY'Sl}|'\TS;)wқ f E6/FP|TpXDw2q=KNFjKOT3 \WJ/>2Wօ?-,)O/%bwȋl@SD>ӭΣl]E?u-j>yY|(VUFȍiz^@-/\2hHc{ʆ|{2FXI7!]V 1FY:PuDG~|i盕4u#:+$nVlż3v>DǯH-U â*}'ͱ?D M?רYۥDc=U042˵FHB"Ha(-2^ +z4JFZqJ' a;=0_r-tm =ykB0(.?m$UEo5vl^4j)܍)O}P]O,AR|׽}|,Mw$_`elCr`jdE<4)0 +)MFCR e \nɬQeOyZϛM;@0zڸ'\R|嶒-X}^)UbhdwTץ)m+N'YФ,?J v5) +q_f*3F!%񑋇yg#/fcS if0MTvB-qj.#e<Chc}'! [Hm +J>S]ԅD^uD`\F¶Τ3]zEkH(JsLwsN))W9L;JܟLGFwb׺rD9_/BI" )]\-UTj6;+[7>3 Z*9\_;+Tz7N&mK?oTPh8K\ߠ0ֱ Ya~\_Ko`9q?Jo tF;}eYV7P~Pzt@ѧWagQ5vyet=|_b]Z]ϳEHꓟDm BjuT}IU}US!<m.Aŋ{—#RƜ-m2Z%}pl(mܳ9 ":o 2PI>dN^qGq2~/OP[ufi<{322Δ\ԑR<^MfgϏ?}i&"Q~kSG3F1U#ސ\0U> +SOhx*t(;LPFvZ<^_2şuj(8yj {w+)Hͥuܤ{ӘFeEgzGljHgSM)m`^/Hg'qJT6 #\UR~$IcT鯽Wud\.Ff eaLZ []lwhGy8VLPxV7w1C4?!Z.jm"NUަ>28:mBJOhX5|O{ׅgmȷQ!>^[ԟ nŴ +-vEp#מ! +B=*r4&{ș2ޤ uY5'p` oWnUhHDNYXd>QS>zS~$3 qSh_ԏvh/i-GY{޷dPD̳,yK+LMeMp1!aު`vzQ}۰I{.<;@]_{bN}N_1Ee׋`YӁzFFSE2*lC[Ma;VdĐxniPyTiA *{TPښE 3]o]ӐGs%6SШr$n?s]-O<&#K;}>` +" H{ +#FKeĹE`0bઋQJ\x5ZyC.0^#4?r!蒗y/h=k qib$9ۑ#ZZ~)@_bYlϵvw_mo̼3x̱hGMzeOhX[ Ɔ)C{LdW//½Q ȣz*n8iU ?gSЮnTjiǨAg87Rgmv#_uy@2Zt##"٧y_l\X'i짇om/UtIdYgtG?@2b }a`̣yVW_k}n_(u+z5Q&2KѱU)G0VlTk{eܕXְX#z-?\ޖz{}aU$Nwl|w5i5 FenK2l,ܕV`*"9h,Z=T; 2Rؕ׼bI+ڪ t; +P})S]YJw_xiľ˵}C:iS771D5TmmJK|4kU-x%"N7bX<>Fܿ^V=!3W5,ɢ$AMi?{]7nd}fTVpy*P:vPSY\P +5ne~*#MgN|jaTY+ upi8}MW_0p1Zvա/ֳ[G +rki6O0m5iK'RǫE)>-TZ}t3Q[,h+; Bc萲^]8`%eX%9imwy˼sg}xrF2oqXx#hN",v~KᾣFg^+ԝor_46O[>7|$=pS@p-E5: ij #@tgWVtz6vOQV0Ay9VԔ3AJ Â|F+ +ꝑy 8"`mڅ'QS6T͊~XSphUa{[xn+A+]wВ2}ا u5ݗ#v7aj3ʩV"ZKW="&.T,Z|Ǫngf߬c-kT03-B3?%$SIVo[ =6c;nn|/Be&ѷ=0$1f{Xa_dKVȾ6VN-ߓrpJ/E;7hr~F`(H#,3 6qJ˩;%^Ͽ7͟jB ̥Wxa!Ylv3M kP69eCQLذ_̞?C`nL,͋nA0S S%A, uPġu窄 +0Txuc8qҹ/~ߣMھPCwVb[rZ'揰rmǴ72kXҦpXLg72Tc ShsW>vyacAzBWGؖt G%_CSv7 †&c ՑiFR}#^1ݙq<6WG@;pz!\Q~ly_B{e+O-XF)Mc&s*w˄%X~Z1yCK>71V%N>׵O4x;Qj_+k=2eVymm|*ԎjIq XYMH/^E4-T]ܺC7M>*)r-!gNq_ښ\j8Y)(kD-*`$EC1 ,$=L\ٻgr +ӽ.kaahg ]u-v:5u$ubƸɣ}z#?[ h.=bljU wW n痳UaEnQ?,ӀYM8R3 1NrkAj`.lj~7 A x^2w QWITm&jI_?ξn͊˺,Xܵ|Qy-50޹:(V +Εx?E~-Y/.~-"T3/ޠlxkbau.23[n*so +"_G#.2x1 >߽еCnN^O2EV_|BO}3LpfI/XBa aƼ3: ЛyХw$eܳRP%dSw+2"Cԓu4vЇdp+E ^Jv(d#<t[3XkJ{R^3rH6nW^M=Ft4 6-qnm?>uIbb(b~ʡgPh|r0]0 +1+`_?1El u [}0{jTS lf UlM!8F^L<ɫr %3e*{@sk%j=J&M=Y4h,48NݐU`hz͎' {E~H{Í߭.~MKҜ7vdfժ>㦂C5j2xR@3Y#n{ccPf#vd3_Y=ν?vF hp#9_4uYQT?;ғ|\_ F=>>%ǧ?bL* Պ֜ +<ӭsuUCuW}щz;T@_!.$ojvoΝSM}.&,FIY"j]*_Icmu֑ 'p1|{ NV)TӬcJ6GMTs #Bۖߣ]e9ߐ uV?V|x59T +slͯ7Jx";1O7Rɇ揌Umӏx?"ݔiOu>b=xz(6ӎ7icyFi*~ h$;PFϿahYVю.f8LbNTTˆB9:c: #7lw׍kX>UL!uc]ޟ^\ۦā-9]+z[=`ᚾ|yuH :pJZki>Z̯a57o L񃴮f4AJK $~}f )+8q}?p@;t HhZXS;ΈN'ޟaZJ0[Iev򪵯B-jk7mѻ+̫@ Z]db.UzF&>w +Ly:GKzEi@gf: pӹJlmEӎB$AP]!g~ Z,?Թn]yW2ca,ưR#$mr-RnY553:D8V8/Om.+=J{QԻ&C%}+;oIF^w'cmpX/&^jVCj4ލ]sO)v%/J&ߠ::N 1ds {X47FլYH́d-5;"v,?׮3zc)Lm+G$Ɨ_V] P9ZTt,B">h]n?\rΏ 'f7 O:EGWLǐt؉?'H ubhWc)8V}KUHn6Pqf ;ev*2?o}.թyZ}u^Ҍ,E7ҠTEJ SufdO;]QPְ6&[^y!&@Dl9cY՟C 1ݹ +{4:e#[\Nf!@#47]LhO);"%}Σ{ˊi—nܭ|,_Jxg, Xn35 I6q0Kr%V։> eOi7đ5苗X2 ih,3iMw_J//<$5EpN@eIe u+oO&?yzvØ+mLPtjLf4K\7ϗ*ԃ_wɾ@W/[2So(mOÀPVSBgfm!A:FTG$ Q6oOp `T}Z[bX%]\'2c؇NLj!HdRJY CnZ*XxuX쁄A %&3vrUNƏKܤH:`O[$gNV2A6?G8> 3:cF޺_l3JPn8V#5߶0' F߬!,X{/]Ѕ<$wR@a=p YF{V9Ub$wv YpKcx {dD,/oS733,*3EU4^/cSU#2=x@M b @]'}(ע j3>|<z_7 WXӮ9]mR̗ughwݶT(g,5}wV3 ֨_[ GG +uyt\u)o̝ E+<aO4j`ee!,;ݺ.`&/LG,zoMȔt'է |cjN4\ӏAr_c +JL敼T +/κ?@ߧW+}Zwf*D{}ykoiE qo:u?T6g.~7Fo V# Usa-t%u_{|}uKm9b3x1>0)$O\]|4+彛:96C-u]^8>8xZl}|ǫ1 +B%^kHY꥝|:@=>Q]0o\wOKϷAݥĦ/6ӖSoQKk;i>rիCŽrLr2SriT n߼k˱/)؄߷5${\-M<!u'Һ"gˁٮ2{0<FgeCAs~zFSz8R+1|u`si,Ⱦ@mYTeuVk' +6eԾ]pBpJWSg e2{Mc=w(ɿ,.e/b@Y1"'0io笒=$ ]WKKeEG"lusPK +RWuŁ`ew?BPIk4h<9%gf,3|DJ)3d߬!v^oW|H}wK},(!/vzs|gj*M|8:X4n5k? QqO]{q)U9ݝsq91k-Me1Pg9b1VЈ,SU<əV +_~lc1uDֲi.g7&aLKuskڢF mV@QН ZwcP1356݈F8Ԯl]ËܠՁ PKӞ +ƒ1qtBIF6©*7IOzދVJlOMjWw$C%.ʜLDzAfEfz-ۈI/ p:HCyj_3!?z1o[J5VN_|Ejhq&Cu/6WJe%˶/MDIe>6UË5XFsh>PJl%t`,#Fb?XRsSV:1Ƌ/hW B +9iNU@̗69 nڈ=kev^`vp~c}mA\ꯖ}`>eٙ<+nݨYJ\ۍ&HP+0 2^ge*.,E&ץ_[)8;6vX^ھ-Ҳegam`BT7F,ײ_"<ТOi+J%CX(/IJk=&|g}#-^NP{bǵځT> m^4szG|OhQ)t`ޯ#PH{ 8hPXބH[2/0$3WMF"o/ᭉGp#e}vvZnXBfNEp}qҿE'޸7Vd::^ +r}m+pjo_1nޙ\V(&og]w}nZtz5|?ONφ:JthOWpZ*âཿ_0?)82`ݍR3(ΤHDJħ|40^CVFvY3'EPT?҉8cK܇=UzZɛ/'e\˘o5R^5x P1 ߠ|9ڿnXf|hokYD]W+4C$5z%n`66?-2NqeT饏SF 3k[Սwl,3-^@4LGYmXAhiM[D( 3*eKZ1 +ɋPݬ^'ǝ "p̔m] iʔ(?I&҄rE/fmj(_T|K (ÙS\w~0UEg3Oa@՞Y6]t,U7/XοHmsC1Ir\ި +;]CoZB=N,~YMƫz?; <^ZCyLVG9os&#\p\w$:8FD 1'|O^>;uϋ_;'N⥡!ɛX)L1c-j̤Fh7RO79/'PO5#?dshi?ހ-Pct}vxzDVވ +Ph޻2lɰ#_iƜmhA[U9}E z RVQv4C"^߰;X ce/nYVi<<+iFZ.?896yBh?N99V,nښimw"uzJ'sGDuF,ٸ^{kOշqStG +v1tr=`}f wUn8͝GaJ;mkkjm{fi֟ Vl^$(%cl,y *@R2" ma˨tzNKF+sR=[µx4X;_r|b!=tJ^c3R[Ҽ@sudjq\ԋtR[ʎyCJ"|d@3'QT+=_=Q\|&׽Yrx+Z8yj7A0RÎv+{V9ywlyEЦ_ &H hKǖ0Ϩbo^E+ةJ6|g^OFҵǚ-8~7+&Klڨݞz_{4f5*mRǮwEUU% TrBө@ VPhʀUBs_{mȔ"ж᳅̆Sag+3+mLL򉯑kDxr`*S<֛*gO;pZ6kaćc`M(D em^y,o(Mo(?Z| +;G?N_*![ٛ{qu0qh?"N&2WbugY&(zr{ YN~LnLX>Wt•i_ghĴꨧ²~̯8XLjW ny":jr;z;`pl".1WZ=j`bHpJ9jԨvHUo6(B!ׂ/^NzӬU3diDe1g-aqitgs^,oPp1 eoW㹽yQ熆&+ۜ&Q8ŴY}0NUuFzꇝr7*?Z7gZD+Ch?ZڻԱRG]Ռk +FܣCր'?EMJ413OW~O|_H;,tΪa4zcqzn0kN~ki.㬯y`E.J2wB4ժl&v[nn5q7\sPfN6e(MgJo==x e3tԳ kfpBO걓9WꮔtV%[= t_ۺh+HsA͒OѺ/k-Dւ.θ'pɓp@x܅Y0C$=߸tIgbFk:lm5ų Oj-{szvZ{W3Q{b.i~<z=gCtw+R3˿!>>,ET +jV+\z˽&Ek(pwkχBSLMg/^=mRv}IC`b+e{e6F|yGxd[ZBay- Sø*^sأyu/-?q:.6f_rMXqBeZY)S,DZJk[M9Dtt ?n9Z/'vtm/j +aհ緉v^ ѸUulP2Iҙ%䐫,6U[[]{n1rM Eq~iPqsBČ{"_]#㰪Qk96|kh^5.B\ͺdW7hO_M rOHSzb`.l>ܼ Fh%<,}9QUokj^+6 0WP9zp9<EǤ[84mG_ {vT 4 +>vq_+~6v6TQ"qlEs='/=mS?(/ quBbRM:tۃ8N9{~gMpϮրs?mSvzr=g "˯y'Ly5\ȵkhܰ@ߺNnrX(;{irf7OTw ~XӛL)N2*[h6w/Yt_tӳlUzxz8{]"3z EZ;'fZ͈[GבgWQ6bI_ w:֕5scUikjhWd\֮DT5`Ax'׾H'=CkOxg@m@{s"_x'^}?~PK6៧z/L9{غ}g~ڷ!v)<]czMC4JzthSEJYd +QtDғ(9Zz #/n,)|l;|چQ\-mNL65f^/_N-7DHxUB*&V}:TaS˸pv +ٽH| $zk[G#cJ&9 e.-,n?5/{IR[_r)P٭&/j(aQĉpw.LwxFyrK ei_R/_{ͅ-ՑTo7{T+m)y+l%#+qѷVը@hȤɔR3և4MT 2tFGlv(} +ح/N_gV_ه]?zhM>Wѝ y QxдfdC$'{~Nig.o}2@jFfwY=AKmQ`ޮD胅^kC7sTIÕ1"} L_;KԊQXQG|ׂe3fEoGxٰy%-LF|t#zw[UCodHQ]W i:Q^3l9ߤ>Cʖâ^K7|ebA2ewAn:[T+Wv425#x{Lke^D$u9%d[Ihm8U6*dxJ< cD$zLa.K4m^%eikK\X~ל;}<3 P G9^Fyw}= Mdb,BH{uc\x]h>Qj%VKM|v(P?h?|7u>zyj*Z/$w{7Uye[PRQQgma+؂uZ; C=_tI@qy^;ү+X7װd?0$OƁm{-?^t̟v18Ofu__:sI:;fD-Bkpe56S+|͔UlE;+uOl1vpyo"JRb3ց0gcjohG +A6{]̴9?b^ip-n֛~pp ^ /) +`5gnFb,kl]iL`%7vGUj\`TIsd'i R/Al,j)F͐3yzVc12 +B3w.DmyRDla0 Kt^Kz#l"=e)Vi4Wꫠtl.󢌬C+Rv!~vuTl#FYS>f"kLg*#nM-t0̴= Qx}Y( E7<~c;g?6Ie +(cP+;m?z$){dRԎzxGP֜hz"ߛH՜shF~:mfǗ_eDk RD +4,$_a32-k#skwBi{# &:}tҝd?W'^%>~Ȥ\`[im[\?\nK3elim_}G[I2|H_W[m9/p\1o3Q 4xV,7G^|^f)'13cmu/7ט4ZQypYe02BQ>1M >oܦ\!ux{\#Ϩ@ ZRWݯ#)su VLy/P+`߫::昬`{K":IFcSwRIA3|YEa.zV9{b`ͦdr`Ҟ$mn@1eeaiw Go%9ŀ7j^qN hh|a,v'%h-4Uc*sMHo@f~,zj'`ơQf xަ~`.b.c̝ %_E}|V q|.^T^w& @DޛOX8HgA& ?жkoiDT4 wCNm~>stream +!{7Wq5lrZR15)ynDh s;`k.uCNj:oMqzAazy( eХ[C[&GbX17a ij`\.yi+}X,8F\OH~/H42šqhӯ _&CvZqJF-|v(0[Eg(Lp.LnWc˂.=6ai:#{f0R-,[ J>;ʚf> 4=C6 +"0Z7kvSsekۥf>z*e4<sw 3zp{tzwԍI14+ox;r)_m'܊^3Aq/[vnƯ"'>ٺ&=8o,ƚ͕_=|-CVTOnE]u㧅ZۊB.O:g7Sw75ȟUԸ- &RQ;?%HG3z-Y®į=L:Ju{# +!eZ=ytlQ/ڳ8а(zm\Pq;m+8tq+jymM]xGnA)ˡ" U42f[Ձ> nϩ1nq +Gk!ƥL DyϹ!GX]sp_-|c6D]lI hv֍d/RӅl؝팦ʙNO꡹4A1[?sI1FCU@!32=}̡jOylBC;3,NFˇ 7ۮ|5b0ޛm:`|v'SPBsj̘t*fn7К|5PdW܂ \lJg'ZȣΗql_쯃>OG1W$n\g;9߮ ՍWLmR;anQa k"imکبp|rܳxU'S%t-' aӠ7 k?֞%xNRwǺ,}'bpUqϰj3 +-Z7QBdɵZDo$2 O1 pC73>ë,a9Wwg[gDzY% X\Qs*6=93רTsycVWWr@8[`q™Bcڷ0qtiٔj&A{U׾ȶlν i3.Cc +kpv3ǫt[{SYʜU>]u.ۏ,4 GʳkYi͖ysJUaOs?R[AuVvt 2)̆9/~QcXe{'P6ӫ?iFAVg9K"_sUS,-SQ7C_XSaT2w._icCvZkwF˜(TmEmimv9Au04cZ{0=L! +sWRX8q>>;! * ަOn886Uo'){ֻ,Z78u+M7!9Yﴲ8W.IGu+@"441p'EBPu[Z +;jtZZ6X5@<CG2g[mЭ[GZHpD g42z8< =f= c,x6._Bҟ*rM5C84i? h|TM^O棹HrPibNa6uH!~Tz썤kR/l}/G_~)CbW4|z'.^lIZ[YcKfNq]HD(8'2  +7λڿ{PԬG3b;;]9]Z-lևIWF93muV +cYep#J3(JN1[2rX^{ u7 A>j{+we;_;M'nצim:"FZD@J~xƽI^D~ul5Iua%l16o ϙX%Fv-Qsu(8g{?ʊ)go*ʹԖWY2,SC >/PV|-:.Mb%l5ւ[c.cR_gjnjCZGa4iCֱ|&իŨ>@DkthG:DqR뗿i!Z(9]X1 EH_?ӝ*.{ +;%ɟAsa$ &{cښՊ WMv +Tag}#ӄ40xG#>&N5aH:ZƮm!T׬cl|,l}t50/|9W" #@wNՐ'4a+o՚sR^h@Z)SinZnқe_ 'f=V#hmwK9ެ!o~͡ 3Eag={L蟒KNtah(tۆ+:ʗ/FgK0U?54oo+Fh2${>ֹtթև|$Y :;5[o~,G! *a^oX˯ tDYߢ)SJ&6sx Kwfٳ~pkt0I'CI筎E+& :zac8A5rIu&Z;_'ѻ8ʺR鹘,x]ڡh&]!0FЁrgd~L''2N0\ TgF+rƮiNs@S.qW6/ Adhy ä֣~.9 Zfިtiܚ-$OO0/\"Q9D;tXs&E!/Bk cTX3-^ׇrvhE$|p6Ewj!{PZX1;E ZtҭCdg3ǃK{<]G)oNC.kʞK޹lyUhmw2_yO40z7Bp7:םiY\z9y Z6i:\ +uBɇ Hk ݤkski{q)Dn1 ߮[j?CSO! (V@,Z/XŢ5Kf0 QF B2 grFGU*R N غ]ߟj_5f>\ p7 V|mF\uEsN۟qEӏXyc{IOޣzL3C +Γ Pe',WӣkBH42笰6lCVtx֟ˏ%cSȩz~oڶעԮIѕ{`,btbˑ'##,,)~~D떨3ԥI䞧Bk 7o/ޡ>A$eMf]굛 +j /qxۆds]tVw׷U- ͝ +mg#v}==ƝW?cl"2jlڭS$>W{kSWc\dI֞۱nbv11D;jX`ʋp *f,K9֘UEe{h$񫍜gtcBuMto!.1j+O|Uxp[byeEXeCU?l޴J(Ҧ5'boNGV5䯩NA_,H&+i~]F0RtR/NR~'Z;_c VFsϕgYtskhB9ǻOnA6<8ZagoD}T] ]K+kڿa{)UQ뢷uKBgŀ|FC F׫ԧT^h^еa٪I𬐷hU825=&@'[u]fM(t7ȷ50E#.F1]xKngBE/piWUy-q{n63j2A0^GՓ%,Gu-EqRt쟆Τe͈g%Au9=ehƊxaXh)HZpC7D]({i|W|5[6Ue!B}#e"*=$dn6Z93nJ#ռk;&;AC34qNhbVnOXSStz\_tUk8({86Q{ =th~uk- "[ +n+2x:ө&sZΝWVLM{(7Sl_i! 0m2]5QEǪeؿ^ѨNkut N? +5v?F@/ +68sIh(#⵫7\1ǷtuRA\!S"!TxN+4OcI4s ݶoxeW}r}su2M14"OeV8 ӳ=#W_\M2sѝJ{0_[C￸Tָށzn!>kK,g y͡?b@sW+U #\>0KNEƧh&'>EIw˫"/soL_h2>>F֐W> /֛ڍuYd)- 2r%]3 {{hOe"P)q6qz=Us dv㈝U~{Lvh̛XcES;Y^.cDS x: +j7 I3gljݘlq0[ >-W˙UWp^[ ==ȷ'H\)pn;:AO9fK 0vӼMz{QO%(n)OڈfQ~?@~{#``ïډh|t5p͑6yҏhqK+:4!/zнAUŸku}zaJ fE|a@rHU'.CuE7d]h΋AƤ's8&M/s;}kSR ;]ؼO8]Aa.U9r3[4:'䟡^{3EN6O`pInm\ob:cq`@]Gx%~ 2k l=Hu@"-dcɚ(gFVr5hd'*ojlT&Ypʚ}k+x) LϨhp#Br5NvC jDu7A[.JpcM2J}H̑/f1k>_,ljetFD*c}8{EzlLmJwլ`'Xk| T3F|G%=쌻 M7Lޮl, FfcWtwu^[鈥d\_ bU$N˳C5[{`SVR Cްbc[3%Sq!Z6#pp2WD>]eŊE_.{[,Ocſ{,Nm~ +ڸ}e;9MJ;*XKp+>! 'u)+./&S4[sN ~GDlJ\9תMAg8xgގ@f-SZ)M~I8ۮ, n-B4loeŸ_so"]3eB걦Wr +)>97˴ L]8|{}3[ۈ8T-]WX!ie=m8 Z8]ޡ3{'l.aDPWVkt25E JPFefjH VIǥGf_\&뗐7+V0?1Jr3};_yȽN'N7=ڥ.+}k)6CZ!<ה}{ttliQ5jywJru ZÿG 3\} ԥ]-369¹B1@zFlFLZ H?O +kr6z}jfjh9f:16QŴ3rҤR4ۺZá7Om`(Vz sgʀms8!}f eHfB*U# f{δ-A =~Li9 +'o{!g*h5B}-PKfUs EbAI? iQGSP?3ł8u/:JNy#lyf+dike;hJm`Yh̱\qsa;ǯvcZߦ^⠶[B^aEx+!Y_DEQA 2)Z/嗼ŇSw =VFo{n >~qjšt"T4Q7F99 1j5W~Zy[ZIˏ|K +Ԯ ]46c0 sKÚT{mJqh}[|6mܙE~k@%ŸSu!u֕Դqw_: 'KOݫqgAr,ɂBjQh-'@P8 3\@a q 7x:T?J|cѻoTTovWO^*fUsyw1Kjv ȶ%tzZTy}uy /wh.=؜}k魩nsQگ5gD׷? ųaUUjF%bYgOyћG ܍#a_j~[I<961O,xyfuc=Y1;e~;27\  +tIcӑNJ^ c$ie:YuZb|dZZTKlS8}42YسxnF;a|Ƽ.|Ɗ-kp# +_nmi}:̿Η{ى3b1ĸv~jU)=[tjz׽ݝ3aP:S@-9-Txj;cӽiYEEj*m6kQA.mz!>k~e2,1Yivmm/ٯOqg:՘;v Lsm{[rAPՓЈ+ +X [#z~uJ+xAKoy{8~u<-&ى[>^WvM;.$t팝s>M(! B%H&fN/$b^qWܪP"{+g;3]=zu__by:[l~pRʂR ޏ.w۱-'Ws-u}SnZw9`ޱDžs S5&T81z' 7I`XiGZޏ."tpPfVړx6X73bhdgwCtm/)"q뀮|E=W'kV}Po&Xru]]sVQ, e7(,1O +fw=>Iҋ0\YL|*C\5g b b[Kc)^QV}]d3rNcS4M{s+:3O׷B(vn(0ή?9M?P9V' M f.0;^3I>\Gm {dRic0q~eJڃw X"5:6q&M"@ʟuǮ!Vy=U83y 3pZ 3khݽj`x켄|6d)}T|2.+Ǟ-_ +r\&Gscn{.=`} !G/}|~ݱ [/iW[t ,ݻo=G +=q(j(\<DD៿/O:>A+coZL]z/FdQuEأ,iZurh%ވqo,ˎVF{[v&/pk8GlFz_%l'Vu-ʁsAqt~vTwz!Mk"~, nG2"n3lUi:rg^/;Ҡćg^Cr KQݹn@FB]=6Ns&@tQy\:ltC9{\yzDo)օR"EmȾF!C,p$ mwZ0Y{P"[Od0cV {C4xe3;9i7HtjbG;vMo[`3u+osfآw=#)pi??W4)ZhpBhZz(y7:p-Xy[;DB?nI/iQxӉ=_[;2Gߟ9~ozIT!l;cA,_ꛩ7Nat}Mᖲѕi9 w # =F;n=j8e½DZ9vRż˥q6ևe-R^o2?ỏUás]U(Z[, +UQ:|Ecl&(j ]{(7 HW ^wa lK;E<,EKDLB`D psJBAU`͞nrܩXWѓTNů[6OmM]|ǡ`sN7oՔbgƣ:ĕ^Alw5r7RJ7_kn2u#aͲ |T rRvb;>UXBƭ'*S{TtN؟WW`NXAM\mIu}S/U6ѵM>qUYK3Qܾ_̄i!]3vJ_R!0vW*) m\4l96݂7*+)d[F-\"7tF.-J!X_%J_rdLš} +4ޢJ 4 !1-71KQo$22Mgc2c_ƯRmFS3G.N%A4&)K6d)I;}x6;Q.Ƶjq貫jhnXM%u;7X;ZeH`~(WnwԵٍغCO<Аo__WA?.֯um_Z7=}o]t~FէYHIDX21 XVCUث/j]fکlcyʝYh6u!Zbi^_ڗiF9"ו:`bͼ~ +kC(23kɯLt@aHvKRՋ7n,Hi6bǣ8[ŽD/cd˨J/jt_)@n6E'ٲ䁰|%",*w r\H3ernn?Z =󮪓o]mj[mUm/;GtkdO5ΕcR\M*<=QQQa;;j%;i-ƊnoUK/"gIN[oD[T\7ảVj4/!VPQ9M6{i#+z>6K%U]FK#M\N'b$7D1[.F'ӳw%:UmيT끰,9h{Mqۛt93zW_h/ss !!yne )L$yvrxZ`wo,ݠ,c{sܣ8wYq?$`d@R^O@tm;?ޛT@^N:ta;t,mS/նlgfyv갂ެ\!x=hɮ[R\6U: k>]zg +>QnDkMj7mNRh9o_"4\uQfjTElL[ -M(3]Q6 &CZȓc /Nk^<g:V4Dlm +vbDe6uXf#郆z܉x#0ǼrnIzU57×!C"}mDbo@ɂl|b5`;A.h jO@I/j6R=V4ni Xz`}bhy5dw0b嬰ӵmQƩC5wGlŸ:JH&k]e *eFYQ081ܡڙVZ d kQ~òW&[N?zhDP.ΓՉкy;(ﯴvۿ4d@bH'Bj _=t6;x0'E=J DPn;{52enңh Yȉ"0%!~Ѝ<+X\3oE&I0;·Wϰd<'MDr;mޗ?̼}2xJ6xnÕ^:^oAi\foq)-n$EEynf)yURPN<㦷Pc{s)+5Uȼ2kӔyH \l/:&R)*_G='!Н"BEM,;| 6W+ѻyl5Vn.o- 5s$tEh#ӉY!Ä(9 mDu% Jf>ǔ7¿]#nYaW_vͳ>[3^pUqj; -pK[~ٷ +'Tq T9g# Ǒ +K0P߿[w.v`A/"O_;B0ʹNoF#^`taQ{G"`%2bp?I٬Y09sV]~٪^LOLBvֳXO4b'r'h:rT {܊";:unџZHo86*S㯊D oG2!vΕv+x+jt+Klph5؟Eoec'֎ t7"էq6mD Q/ꝱC#*E0xc6fc*ri4g]eo3y%KΤ{\svL-2@1Narqת>=aσ=/5jmdkMl۳ƿnSnu۶S$ 5⋲ nPin۴ K>bۜ:1.ho4b +ہN() <7)iσ2~g +2UQ΢pj{8lA`>׈:@MK6wdaol3lUY-pB~nÆKY 8p' y Z:g1[/^LwYY jd(UGEnN uhh ]m332V$?(U睶?K +x| 0gKHZm3ҫvV*_WH+Z1^lr/n;Ӯ}qKmlE ?.b]ٽT,_wsqng{ےDXf3ֲut FWF[ӧ[YiWoGoK*l. ]@vX컋|isT^DE~+7*[ԩ~g}J[YTHڍΥϙ99s:y~ Cmzw!Y߹zSC ܫ~&+\: ٙ[aL۪OIwCarʩ=+ka,lۘ5<KzJUvwabLzcwcqz+$]x=A:-ၬ|M%BLxLs?Od*#$9K;/H\,`aC]9o(/% aKɽo% ΤӊJ,/#IZ샚,Zl;sh3Or +x*!N^iw"W|di1ݸk 1])ZqR$DAnoZՊW/\7sbm7m WG[@^i{ߪJ=_++WZfSX^7\ Q=T'okRz,ܵzSAB8v'lˠ4:.^,qg" k Kk=)7\E&\ &i=a#pvGd;v֪yoCG2_inߡuͨ$ :l^&L8ܝT!rs; n.W?Z~,Jdd.'u?hfmG~w-OX[ll:y`]-/JѮsͅQjFj+uSUJ}<,uM(AS19Rqq]KEu +8\s?UU5{pf5@\utK򶭖[Gi&c s^혛98^4JAX6A`b}N:{ Z9u6N X`» Hxl#X?P>(06`OmF"z|m-?ެAU珈=6i9fT#1iZY wPN-z|;G!$Z +1H&~b-S8}:xX/Eݰ9)u,>ɷB\+pӕ&Yg+f&cg97smt +q %x{zL.&ç;>:Meت^ZTkk3@]-z`Oqgβ>kcp7HAr +Ԩ*I)lIv{`4_qzecsXDbL{Z8孖d=奘ϵ=9YF޴^#li ~ZBSM}썯xYZ~:^$taYZø,\>Hϛ*ݚr aJ Y45z%{&R7]{fG+Z5\J"fmr3yLF[."wZW#ŭnyH/IςZa_[gƫ9B7c}B?.twoq&o˵ x4 SDi 6zH+L'_e_XA/ݫ[^-`=݂`j TǚZtuI޷nuMiͮ}wӀ>wQpl$6΋`ܭe9XqB%xи?03|ܓu=iV39g! Bc}߰7k +aռqfrS_1b{Uzͮe58DMnVØ{(Ysa(aOZwWkGuTuWa!i5h,Ec東ά*Vr<]WAcReSwJo48 V߅sKͯY^'~t; "9>q|@'P\֛Fy{=ʓ`21(D?!_z> +ޞ8ALRbb,,U/r|T4ƚZG!˧F;ƂbZfd&q}tI2JmFRNJrjl|4y ڵr``͏[l͓akp6:ĚnxQk5j V>݌ӃJ%銎Z7ž>KW{ oF+#2;(lJtftV$cap? ;M3w.li#P9/!s@lVѭM؅whpAJo'=pٳj8$M%}QW9[##MR]oå%n(n_2FI,> VZ\DX!*o;{Z Kna)IcFd +pN/W&"8y[h6ؼY)Eٱѝ5 ^+z-nG[:o& O. ?F2Ԫg`%FQFsɊ%踣& <$Ru/YFeF=l9\v5Ļ@Pu~bG5ʈ|,XZqu Ou+mɇ`y頻5hys#^Wx)9E*ЫP $c\'1V ҅P?i3]ے7l&€+|RM ԐGi7ЦœX;@-5ez 5e9=;[X8;+%k{ſ8=jǑKx&Hg97^\^PPu4J;eqC%(iyΓ_Hޏ:4vY4hGwgT`:7zR[+y?BEpn1kXU}wPSD +EILU\5Ǵ!%BOߢHIehժͩ/EJ2[) +eUMgI3F[4[á5'A+} +Ѣ{gҙMY)1lRveȬ _ ;uJG5^.4E}a -,˾}}2.Z裙"sH/67f>YРT>blS?.+h7GsHMF Xt\NP8h p| <И^U嬽l&wlc Fɼ"Rc6ĜgY=w +*{mOCݔh-a],^a+1cݰ +i^|;*PkrU}=$tE_ ~$c:w+Mjp9zR"LoDOjwB23_=AY+F,$ݏLwak{v08A nhs'P~sF\7\#sU?q|-m,qg+`"-Cnڳg$lۏM|ݤl/: +#+:nDSoɝvVrzZEZ|w:qD]RZz}\;2[~bl7߃)ֹ}r[ќ;Z.PVǼl/+^8h߳Uo,DYvѱM>Xqys[^y57+85<c|a,^YTӆI* O:; v?mTnͽK-3ߓ{ wmXxZޛÊBlc(p#׌]s +&df[AP@?N3S֩ypo318y=9F`PghjaQS@|ekbYkĢyRStiصZ]\9{7e18426c{]tŲ| J_+ˆJDno-$\k/jw)iOOBNt{gq=3G ^5U\`"fmNm*zՎ_0{n2'D7 f!\?ҫfܙ)o2İkmr:Fu?4bg,M)c'H3}{a[A)҆}Z5j),hFĽjxS)ز }>_fBRᄡ[,Av455uyZ˸ iڹ%6VThF vlZj,?~jCx3 e[GcvnUvwKh|4y6PS1u|׭0hc8جytպuż:V*z{BwĪg֬՝ xo'Nu6&SD67xbXn4YF@B۫ߞToM'EVvvSJ.UxJY|-VD.W'BbY^vy?YK)z.m~,|U}_- EyEW薾ފg󅂇J>] Kʖ~Z-lZ +cOXU|i'%*yD 8+|]v{ɴ;fh.?[ž~mн9f>MM^z UbMqlA։uI(n`б8i$hh8^AWFQIrɊijttgCFCL-0Nhd!i}~~L\kZxw~)k!۞w}h[Y"W!ۗ^i˭l1|a;nud.EQ+۱m#7ǿ(SG>c؍_:ߙ3F3uZhh#nNh]f[jénT(Op(_W* ئ O`B(-?8rֻK:MCڿSD7R臔?VJ<0[.!(p2gl̍q˞jĞ&Ϯi}[W5 )s=d|i;:tSC ȭ'Hz/ w@U Wq,:u0f"!kEx"h8>kE[p3+kl?T񨤀y o_kֲ) C֎+ >m_e}tO#VS~5O;ύ}KN=cH7+`Ws:)"z|Jw';.]cֿN7=j57,ObD%j9<Ûvȅ]¨mٞ8׋WGGh}+W@oC/_#w_#u\߅ E_|#( +K`OIlrOĠƲIH[ ZY}|^WzE_t{t}73 ޯ9 wNV Rk 6w= zo+ pDWژ]~r +%M]wZ†p킍tqǽUF>@CZ?w7}-vmT& #N`㺬7KpXX v Ķ4%+$zAv@%8vPNd i:y|kUqʸ s9N-60p-8ڀ m ȃGaJ(cuSOcEjhuXm}VwgQ0q7}8xvpT3i 9_3xBSfi/h|]NקTjrY;Ù >9d7{>jJMJF~@ٰ=:ozw^椟=7|iWl ^W~ B[sJeulfvjmv i# +U#tk)odVm5djk;+`}lhdc4wg}ﳋ* ׂAX#PߩO' 3k 0FݥvUKh'ԲEfIνmyx>>˻4<*;XwNVgX9E+<Xb3* [p~)x:[۫`\Kɞy% [\q?IEh{')Y)Zo, y _TBxIg?(0քVE!fB`r{^Lx +޼3*&:!V79#ymp4Cpdd>s ۄsBWziaIQEy"-Y󓇥EWr+?q'N}rȲ BiFN=><[+*Pւbx \Á/Yg)n}`AѽElc ۛrc'68:CIQ,ffГ/kOĤճВv +MjGˆd5 ]DVn_xVB$v)7#MnaCw_bw jڎr\ά.m!ڼAi һXX c8eRYBulzaR{TϮvҾThlHo +4~T.bd9.مw}"S3Keѽ][;?y"Tb0wnGr}1=;Oǔb4r2B z,WUʶBg Fcʏ~(wBxJ=^HΞb\5CJwM^V9Hjv?" k@5,~o]){(@sWK[OOvld$"a Cy&H1v=Sn׃<_*,8*dRK;tw&k*sGu۽vnKNs^ŷϱs䋔!v^A㥄oϭ2$..NS4 5v Cq^E۝[5@䄭3_'K"u +>6Um|'?Z_7(9.F17Ǩ?َ3GS4WBv3vo0}s17aJ< 7'x4 e("~B Zm[g{93 O]K2)^2ŕq|%k2v_x2= Z(iQG +ͱE6so>bU"$ֿ_[BλhS%o k-&fԥ9_%`B2:=t5 y9ذ_\5jP[]qi I8Vw+lD[}TG3PSDgox{[~aߠ'GTr`Gi/;y³D_N*> |+}0- +gߧRgv=y_\/f7w4S~;%<8lVkDbŁ\ᵤ7̠)FɯM>:[ 1%ʦoOw8Tǽ& +a 4Dj{8lg72%{.n7g}J&ۤE̢iT\[ZkB.M\:g{!ݦk## EzFSbM Q ʝNCQԴjA8ӈ"`KZ yџ1{eNțxpF|:X5iga.\Zgf6vd*'9yzGXm0uS_Y)Vw)K?fnOJÏh,Nԛ2Ģ}8H4# +=vfڂ{ך1kkh E& ]= 4j55 z:HcxCglwA9~RߐF +Ox +U@)&7tO-W(̔rǹ~KA69ƍZ*d Opl#i\\eOog2ek{@OXayy.Ky4_;VÝ7.E'Kk2ケ>[ +Z:Vͬn-}D;? eӢe+~/t+AYpB v! }߂Vϔfyx:ՃSYDzN42pjՃ7:D5 hÓ1`{SXZpz, IjQG 㚁0C3_ K{ .NNNܓ?pvt]6C9 ^}}hUænT,CSmonS*]cwM :9+K_DE,K9Ӟf>g[v%miE/F1f[I_[mSÖ/3˼Gϟ&nÅӬ˿+x8K7QޞEe#grg/M,Ƚ]e<^vikN(s+Kt]*tjF.Nx)g_gh\Vc61{clY,JZAY4ww/^B4뽰5Q5Ʊl3Bw}Į73(>ʎ3~-]IChҺz}dSZx1~y˭D7?͚Νx&xsv3/ ^5K=<5c;'#]'ڶLyYuJdP) B 8N\& + +J|r+&!R_w+'W̶9-0M^RpW%G{~{0r̈́i1u`Y..=ʻZ-%ny%P=V-M,H$p`r!^aMa7wl.vze3b'fǼh(X)va^hZsb;=,޾ +f8Φv(!_e$3}5CgV(ut*@i}YC-c ҪFkv ā/O7JKTpU*m _r+PlaAh%FK ׏D~z魿 dRG4Zj6IힺJ6 "GɞWɝkEMp>.n?_`Q'ptQe&B/Ъa +.-gPc *_ P]Qw-{lm]g_QO6 /G`( 1%>뾹~MTJ8McR^7PW^4m/^W2.u-Qzϛ9K۫GjpɂZQ6_CTZu$gyogjO}^J;FFf[i_'oLgu#)+M@.e{R7(۞Ziv f9Y=`8:U){!Z6wF:;࢜OlpX~8{_2TLWJ6۰ WjKN̬~(6` +nBKم ԤED,5AkX`yтN6*Bx>f~i~5ߨQYg}d +;pe +u!J +XI -4 2glɵ)罴h[2g$ 3m`c?¹}l +"Z{:wSq?j[o<[Q{$-e4f?[~k YvǜC䓫Yt5 ^tf3CkiǨLj 1⳯"pG xԝ +(T 2?wN7,%zl ZI6t-n)lI#6#)gXMjE-za慌"|Vmp9(F? )`0NNAz/Orty+a"nfw~}Kvpaaq=G:b@_n&{6kqE:f/s& 50d6߲HI NuwcdϑɌԵz̓D R k7}p:9kH;ܸYT ]@1;koI !x-SGqDž#qHҰIZnqPRh먩Ep|0k S{_laG+ỨaVePudεk?v:ԣuR1o&93bUL+%DPƌ |Ю6ؾuXi2iX6͑`JA3KV:`Vgl7~͓]w-E x9zۨn/U/\zl] iv?thϑ2qyji>]N#FXׄG.- dθq;YŜ KǷۊgwhH< $x\:ǎwt ?t^v㈈4Ϻ9< ؚ"e4SuWUc$M뫝fsmkpKEkLWm]pbS:}Vsq\tbtuvE!]ƀ0wM${@?L{tJ:R'8b{o;5߁N̺<̘fߘ!;Px bd^O'ov3Pтf-EY}:hy320gi~-9'^awNj2mEʢ|j fjzp-ũ{lXiڅn2<,ķXso!~'rp" *mX/,_+s7ˌ'|^3*2Cw:̷gŖZp +D}Uư̳w ͥL##4o.FvGsMtCd>V#^TFytCW~bTNQŘ zߝ(=S}+\i7隷ntuKu$w W^gҶd͝:Hy¤Հr W/.roHiM~$w2|߄3D۵j tU0[x1~(<3!)oAs'@+&c &OA),}p3q]@|O0]A1dyMYҵm~+B +-jXՆ,Bo%Mpl_AtNw KC"xсK4Lަ" fJ罴oG>p)oWk  jՋkWo2Mws +ě|p~bg~5Ő  _iH8It`!w ]l7)xhqTc!DK`P5C@׾aCM=7(>0 `3~Y@ϕ}0("kE~K˷X+׽X"w2&컔uU9]O5\~9!),aR-QI/LL +9K_븵w +Q[eE7 +QZˁtBG|YR_< X'_n'iL;EƱR-5_N. up[k2Agۜ4\tmjh?u=WM y+EssO%12V *;$%H U9h9\-7̷Z! XFzKa;m^Rj{jl63u@SmāƗbMu{[!JrWY-Ƽ)·^9ڳeќT2oưk +KGJTΤo(^Q0yΓekC\|3Yȕ`(Y-*}~|H$.Ia [)s? Hr!qDN?cnǮD8cLF^HmP끂9!} >{%7 }>&P=h5~vESQvۢWu+T;Ӭۭ}sD ZnwɃ:ߌK/;*[h`8>sЭB..mk.˨yLBfz觤JӋ+W{h7y(3}[S28wm>0KLnLzę 9!>8/[xX?l'=Sg#r y#}y-`.n7 Nq@{|:Nv VˮG + a+{ 8F.x .27439VkY<%r)[~_t_G{ͫeJm( Sv>v6,ˑsTo~W8;JnÀN(/n''Gy_?v+\-2%/utneeU6C6 ӏ~̱JY9t*#XuwJeֻz雩|h^3x.? ,4 +5)ۍc׸"[)ӲJpms v>5c@;U~v wCk7}zU9G}LT3ξ|oW?vbUuoU9|@&$8zA6BfkP?k]9br֯E,yfN;}};qiVzTią,h 0A.Ϯ:{s"utv0kdC~?M:5ºǮy5X 1li]<9gS@܅gguw_+:ku16U QwQ'.p|~u_^!҈N .E? +E:7dPPZ} ,%&F7C vjWl{NtZ`Zf\*o7q4fWd9{[m%6߀_У+$ĎS)F&֊gm$+c~n/?컁glHB/GTχĺuC uSxE"{^~tb9apH{jub-PaYO+4NZq7F=[NLѨ\w~x$>E{k-J9Ñ& oWH4b!L^Rv^DJtӀKM_6J;ƽտfqosݼq8)^%Ga5377bdbYoybh+=|.)tՅ߻`7B_׿EG8mjbj_~p;..ߝSeOz@l:ܓv[zwlSXv涟DIu8{~<:Ȫz)Jqo Wc87{#ojr$v&;)dxsc VivŸ)e}xVl4U0*3z{NVDZ(d)K~*C8Ǹ%G|=뎷"&Sp7F{nHُvQ  xӷSosOXX],rbQ.-ٓ0]p{To gJw P%ךot['*1J74j'8 k0t4m?]Qr/[ڣ`]h|B12irɮ߽v[kC(Pz5]Co^Hҧ?KsV `eݮD~t=h,6b7C&g.xb2un$luMsVҏ=~u7,sMsu2[כIyc[ +dJ5y%R#È{}mݣ-wcngf;k r&*dmU7CB}>9fyTzN#O}+Mt4-9͟Es]gtKiL$w4c>.O5>N{^zD穲nD32mAs.$C .ڀ>q= +M:u~9ϭK>2wc5kz/ vHz Ҏ̟u'2z$떨R~lQwEdݍN[)r&v)ٟ%GZu"򭫇_f!=)^*P٫.TCJVg;D4b$6 stW`m֊J:s}' =u;U@<)UhɜtWCpnVeD})љ*}s# x"?ÇM4MW_91Dx @ HFh׃%Ю!A=+ս?Sd5gQ3%ڳ۶RE^uLkg.+9!s=fm( EnlZˊbN(^>i^$,g3؜8'ۻڗuvύɜ+eZn7u~oWUS~,N̮顪lM7clK=8ΕkqKJ^u2W** +}VMjZ4A-&V`ۅg(+ӠBs٧ -Ň3S3K\#Ck/ˁ'4Iۇ&YXVk]ᬨ:6P&>Xy6Ǥ3i+1·Ϧ6uSdڢQ^g]ꋞt֊&=hjϾu}/?mNv!ly2IT02*ۢfښ!@9 yrև[y`hQk|} uH,B}7 kt:k3AѹāUYYXOH@TӁ^VHa7fstEЊ+Zܮ_2Qr³Tn]CDVj5!O 癮 YCb +8{f7|=O qb<%reKh?%C=𻋜K5ʌ~xꪹmr`;9Y]M{[=z:f,]c}>Nb[Ij-@cxE9Aon7j[D}6D绫#jdhWs|.6843\3½&*wA#*#`]w )RGJ0;Vo3ɦekHU"V-:瓸zt*n˿2l0!&+0l2<͆tm6ȟ+J}^+E\n^W^kô|`뽭Fz\8BmS_GR6vkwn}oYVS>vq:GeOsۚ†7>pfnvQRA`ru٪j]șDwj|,)vg-W+Û~Qy;mNk^JGyLڷeSQ&mûqj[n|]k_AW9݉z8ٗ t%rfsv (ځL-O;wYlLQg+Jg[!X&6rɇK{ylŪ +f J?.ഈʾԧ_wJwsa hre6:U̦l:--k>>4|%x[WځcV?[Bn^9om%~_*jm3 1M֖!fJ%qSd~YG-oE0cbO?',5lgƠQ@y_sֵz> Qv.hً7K|F>A}}@ֿ:V48G>b9a55We;@9:Y,X:QI rǺ0bĩ;6xs*wg Ν9HDnI}pZ}LXޏVQtX[ ikԗ yqCQA+&2;սǯԩ~;sr&vG[sqsE[).X-K@Ly8;/zz_6XM&zQ9=ͮT5^nm3ږlp&.ґҪjn˙H1Ս-a5gZk9AH܋88M'ZJ(g8:V5Vx_m>df,Y@FWKkf}" + ٗSDʝwLj Skpde=%AuV~e뫵,'Mg#G?]=\ԁz x=4iqo6 ;DE-gv!,1v,:6K:NdKiW*%.f0e3_LȌm>ԲcMdr5_Q~ DޯS +/ןWĪb|ّ*=f*Gԙ&V<7P{߬<4 j@ipkGU=enVHAyS;>c6~/JNEA1'@^~@ҝ^Z/;|m]Q˶'7WvIbK}7rY&>os~N<]>Gwr6I3k_ ҁiU) sCmz  |`^Ԝݪ4Swb744Wf m#EكKq1 +1P`ivWm\މe[ +2X50Eq*;h-vgaC Z(x[5YQ 1i 6RT`?8t{o1 Dgxqlc4חnn6| IE-/f_l`B%>yϖ𽷗fz"v﬿*Z9o;d3(ȯN%ޯcPyn7rZչ 5675k' .6^N'^P:);`E5gC-kŖ/`Mp]+c՟d$uUMNH4$~uɈ\&'o +noq|4eO!]l/tW^ӂtLMey2j-HF +n!AiDKVۍF%kU.mXB(u%7F`::n{>,/ Ԏt J!#@]7 /{n"̪d4n梿I!\)|$E%?/ K[E^q]szo\#Y?[X{o%f~D{ƻ/ +h_KtUmV)3=h}i5̻ +kM}A  sd=;5(42>,80Sy<`B-a裡6Xd8Y@Ym-c 81(؛o*c|&k殚L콌HUDc[Ug sA}m{kՊqޙu]+<ˍ҂ܛC ҭhFqJKw1ӫornbܼkteqjh+ԍnsz:ErXxc}kہSok%М̄,>o~vO:t7KbcR%acOB\n=<n\jy<Э$f\vË1? XxАߟsV)Qմ#PnSaGpvV)Hۼ +^D*9@m$%0o~2;U=W2[pbpAܕnPE!̮:_\BɒZw'5d͹Y>?ːwp B ~2ybj jJpg l)ov'A]ՠpc(,/$@e6OJ +yHTrY3$u@h5sAI` ?fH֪魒ϏEbċ⒊Oke!]1qp及wVסHAA4aE#br?b~}a)GYp^*)9褢r\͍9Ty0HeK'~ۡW; B("5˵`ݎ_]7y*3q zCU5 .-9Gh#PXe6'Zh)V']I9{]ygc +5W/j@BqC2[H';Go9~ ΗOvDx>j:* PfȀ(bwgp̏X|x~Oe鍖{KM ^MCeuo~^)|qjTe<:cW5c9O{g6pʍIPa=rtm(> h.=8jQG8_ckξ0u/-:gSVrKK{Gܢ+&r$Mm.'U bcE/a3&bpAPce^8Lw$/p&9fo>O迪@-Er%p`]g+Pq6t>bҊ-|wmtiݰe, jMgBc38CV> /fF,?HΧ)KaMS菂q0lrM H ^. +%S_Ntoр&!9DGRBHvo].3{Ĕ{MR*4$;ט[,M|Gi-B0VD6<8LXO_@~T={{4 %Q Tإ*eM_'|5KDg4֚t`7(se=CoVEHG=gN*Eghƭi}?Q9I=S^ҸU{A! +jbcY7y s 8cBK轈N7b"Bn;4[ ;jQ,XF*L=jrzu Sp.Qd!rmӈs v3 Y5^%?PLS>7RtgR-%!$6ƧمW5+WS/B4x5$78ΠpZ X_j[oi (r1x '&&e RȎpOVAnazW|ޣ:IzT~:N.ADc0Ѯppw1X0Z>z6~/8ݨWt^'I?E1??ea/U?Gx]3!;ArK~iOuDR9jnm/ f{ʹR 0C^ %kGS7@vWX2gak͞ ɶU`);Vuz5wmЦymWo!x~`l!~ /c>B%~4bdN5vBr?i?o?sB/`bzݟQenaTնkvX= iyr c>},<ۮ0*e'.o~9:RW^lγt |PoPVN=oI`O3뺪m;J'utܾqsKU;)F#vqibYmAM~2jpW,354ٟ}4Z1K5eoс>QBOO8biI6rU7ɛ[7S8NfW*6 A/D͆"*bGwbT.svqKF+ lNQN%HIYZ9;Pzּ-\ezzlCȨCIY1odݺ:$]\:i^! yт8K'tky'umelmX_WҊ;jn]uS##ozd* :usTũukH7tyeɹJ33ܟ.wEI?3{xiަ75x.x^ +y.LqHU&zKy=Gmi {਼x|v}a?GV.C|55"24 ڽNh=Iiw`=﵌W݇DÒG'_É&eoxV<2W=pb{ցo[ꁞ]?:;ƽlW5/\}lٺ-+w/UVkQ>&nD#f0bۇܮdRh~ͧ3g +ψ(+Ν~>68}E j,U;4ȭҬ~>;Qb.~xFke0Y@cT5:CnzF{.,_ʼi*81mIy-@*fު?z议O4vJ ah\wdS$ moXܩ^n픱m7?)WQxeqw6'X/!a[3S6uKW;eA1%b/bNn!uMnF7irXGk1 +X0zjAB&q RK mXJ>t\w_uI6O-:<4!!6Pt2s~WT]˧6K!MlD帗g{d ՕL12=:n=ݓݕ\`;.įHNZAd#%O)z <=oݔ6eИ-F` 'cnKj ISs}>|v~5w\r[:RO3u !ik0Mm/}d9,-A*<ۓO|Z|xL1BrwqdLގ N'Waag*9R3Ϟk^wz{6m2^]ݍCԧ oȩi/kϏ&Pc ƃX$`V3 -OBiUƸմA=XkK~-} 9da1}IgWRlڴXo#+2N5OLnJt;~Xe%м/>ϖp&vjf,JufxW|` +W}f[_a3>f&Ԗ\TIʦd.3)N]Ooט8oW(>>`[AL+H:v1%=ЮySnd G)ܡ=oLw+'`<70och< ⟾iB+v= ިԫn^\3B_h 9풑<'q |l>=cy3+QX!ɭEK^7f/F&qP&?}dlay:dVpP +zw☋=8 +&Y.)H .:G`ׯOmo??l/-Ă:foj\#:N|ԉU;ҳmvs\&yQg{%t'=ۭuE>u.QZr<=ez^빳D`-7&,r,KϖCΝJ=,v۝&aaUx&GWXnu2Ty/'JZkt\и7aXʚ{ /C_`kQPh{M6[ۯЁ1<:h/<V9^20q&UggBCE>qwocޠǚEE4!~KK^#z2Ώ9|׶"}p@U 専8w"&J\C'Fp/D`3M/U5t3n6 +kOٔ