Skip to content

Commit 73e12b9

Browse files
Merge pull request #5312 from NREL/python-plugin-search-paths
Wrap PythonPlugin:SearchPaths
2 parents 2d4cbea + 187352f commit 73e12b9

20 files changed

+954
-2
lines changed

resources/model/OpenStudio.idd

+47
Original file line numberDiff line numberDiff line change
@@ -38660,6 +38660,53 @@ OS:EnergyManagementSystem:ConstructionIndexVariable,
3866038660

3866138661
\group Python Plugin System
3866238662

38663+
OS:PythonPlugin:SearchPaths,
38664+
\memo Add directories to the search path for Python plugin modules
38665+
\memo The directory containing the EnergyPlus executable file is
38666+
\memo automatically added so that the Python interpreter can find the
38667+
\memo packaged up pyenergyplus Python package.
38668+
\memo By default, the current working directory and input file directory
38669+
\memo are also added to the search path. However, this object allows
38670+
\memo modifying this behavior. With this object, searching these directories
38671+
\memo can be disabled, and users can add supplemental search paths
38672+
\memo that point to libraries of plugin scripts.
38673+
\unique-object
38674+
\min-fields 3
38675+
\extensible:1
38676+
A1, \field Handle
38677+
\type handle
38678+
\required-field
38679+
A2, \field Add Current Working Directory to Search Path
38680+
\note Adding the current working directory allows Python to find
38681+
\note plugin scripts in the current directory.
38682+
\note required-field disabled as it has a default
38683+
\type choice
38684+
\key Yes
38685+
\key No
38686+
\required-field
38687+
A3, \field Add Input File Directory to Search Path
38688+
\note Enabling this will allow Python to find plugin scripts in the
38689+
\note same directory as the running input file, even if that is not
38690+
\note the current working directory.
38691+
\note required-field disabled as it has a default
38692+
\type choice
38693+
\key Yes
38694+
\key No
38695+
\required-field
38696+
A4, \field Add epin Environment Variable to Search Path
38697+
\note The "epin" environment variable is set by some EnergyPlus interfaces
38698+
\note in order to let EnergyPlus find external files in special locations.
38699+
\note If this is enabled, and that variable is set, the value of the variable
38700+
\note will be added to the Python search path.
38701+
\type choice
38702+
\key Yes
38703+
\key No
38704+
\required-field
38705+
A5; \field Search Path 1
38706+
\type alpha
38707+
\begin-extensible
38708+
\retaincase
38709+
3866338710
OS:PythonPlugin:Instance,
3866438711
\memo A single plugin to be executed during the simulation, which can contain multiple calling points
3866538712
\memo for the same class instance by overriding multiple calling point methods.

src/energyplus/CMakeLists.txt

+2
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,7 @@ set(${target_name}_src
302302
ForwardTranslator/ForwardTranslatePythonPluginVariable.cpp
303303
ForwardTranslator/ForwardTranslatePythonPluginTrendVariable.cpp
304304
ForwardTranslator/ForwardTranslatePythonPluginOutputVariable.cpp
305+
ForwardTranslator/ForwardTranslatePythonPluginSearchPaths.cpp
305306
ForwardTranslator/ForwardTranslateRefractionExtinctionGlazing.cpp
306307
ForwardTranslator/ForwardTranslateRefrigerationAirChiller.cpp
307308
ForwardTranslator/ForwardTranslateRefrigerationCase.cpp
@@ -791,6 +792,7 @@ set(${target_name}_test_src
791792
Test/PythonPluginVariable_GTest.cpp
792793
Test/PythonPluginOutputVariable_GTest.cpp
793794
Test/PythonPluginTrendVariable_GTest.cpp
795+
Test/PythonPluginSearchPaths_GTest.cpp
794796
Test/RunPeriod_GTest.cpp
795797
Test/RunPeriodControlDaylightSavingTime_GTest.cpp
796798
Test/RunPeriodControlSpecialDays_GTest.cpp

src/energyplus/ForwardTranslator.cpp

+7
Original file line numberDiff line numberDiff line change
@@ -2433,6 +2433,11 @@ namespace energyplus {
24332433
retVal = translatePythonPluginOutputVariable(obj);
24342434
break;
24352435
}
2436+
case openstudio::IddObjectType::OS_PythonPlugin_SearchPaths: {
2437+
auto obj = modelObject.cast<PythonPluginSearchPaths>();
2438+
retVal = translatePythonPluginSearchPaths(obj);
2439+
break;
2440+
}
24362441
case openstudio::IddObjectType::OS_RadianceParameters: {
24372442
// no-op
24382443
break;
@@ -3602,6 +3607,8 @@ namespace energyplus {
36023607
IddObjectType::OS_ExternalInterface_FunctionalMockupUnitImport_To_Schedule,
36033608
IddObjectType::OS_ExternalInterface_FunctionalMockupUnitImport_To_Variable,
36043609

3610+
IddObjectType::
3611+
OS_PythonPlugin_SearchPaths, // this FT intentionally happens before PythonPlugin_Instance so that we can't end up with two PythonPlugin_SearchPaths objects
36053612
IddObjectType::OS_PythonPlugin_Instance,
36063613
IddObjectType::OS_PythonPlugin_Variable,
36073614
IddObjectType::OS_PythonPlugin_TrendVariable,

src/energyplus/ForwardTranslator.hpp

+3
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ namespace model {
337337
class PythonPluginVariable;
338338
class PythonPluginTrendVariable;
339339
class PythonPluginOutputVariable;
340+
class PythonPluginSearchPaths;
340341
class RefractionExtinctionGlazing;
341342
class RefrigerationAirChiller;
342343
class RefrigerationCase;
@@ -1256,6 +1257,8 @@ namespace energyplus {
12561257

12571258
boost::optional<IdfObject> translatePythonPluginOutputVariable(model::PythonPluginOutputVariable& modelObject);
12581259

1260+
boost::optional<IdfObject> translatePythonPluginSearchPaths(model::PythonPluginSearchPaths& modelObject);
1261+
12591262
boost::optional<IdfObject> translateRefractionExtinctionGlazing(model::RefractionExtinctionGlazing& modelObject);
12601263

12611264
boost::optional<IdfObject> translateRefrigerationAirChiller(model::RefrigerationAirChiller& modelObject);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/***********************************************************************************************************************
2+
* OpenStudio(R), Copyright (c) 2008-2023, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved.
3+
*
4+
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
5+
* following conditions are met:
6+
*
7+
* (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following
8+
* disclaimer.
9+
*
10+
* (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
11+
* disclaimer in the documentation and/or other materials provided with the distribution.
12+
*
13+
* (3) Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote products
14+
* derived from this software without specific prior written permission from the respective party.
15+
*
16+
* (4) Other than as required in clauses (1) and (2), distributions in any form of modifications or other derivative works
17+
* may not use the "OpenStudio" trademark, "OS", "os", or any other confusingly similar designation without specific prior
18+
* written permission from Alliance for Sustainable Energy, LLC.
19+
*
20+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
21+
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE UNITED STATES GOVERNMENT, OR THE UNITED
23+
* STATES DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
24+
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
25+
* USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
26+
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
27+
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28+
***********************************************************************************************************************/
29+
30+
#include "../ForwardTranslator.hpp"
31+
#include "../../model/Model.hpp"
32+
33+
#include "../../model/PythonPluginSearchPaths.hpp"
34+
35+
#include "../../utilities/idf/IdfExtensibleGroup.hpp"
36+
37+
#include <utilities/idd/PythonPlugin_SearchPaths_FieldEnums.hxx>
38+
#include <utilities/idd/IddEnums.hxx>
39+
40+
constexpr static auto pythonSearchPathsName = "Python Plugin Search Paths";
41+
42+
using namespace openstudio::model;
43+
44+
namespace openstudio {
45+
46+
namespace energyplus {
47+
48+
boost::optional<IdfObject> ForwardTranslator::translatePythonPluginSearchPaths(model::PythonPluginSearchPaths& modelObject) {
49+
50+
auto searchPaths = modelObject.searchPaths();
51+
if (searchPaths.empty()) {
52+
return boost::none;
53+
}
54+
55+
IdfObject idfObject = createAndRegisterIdfObject(openstudio::IddObjectType::PythonPlugin_SearchPaths, modelObject);
56+
57+
idfObject.setName(pythonSearchPathsName);
58+
59+
// Add Current Working Directory to Search Path: Optional Boolean
60+
if (modelObject.addCurrentWorkingDirectorytoSearchPath()) {
61+
idfObject.setString(PythonPlugin_SearchPathsFields::AddCurrentWorkingDirectorytoSearchPath, "Yes");
62+
} else {
63+
idfObject.setString(PythonPlugin_SearchPathsFields::AddCurrentWorkingDirectorytoSearchPath, "No");
64+
}
65+
66+
// Add Input File Directory to Search Path: Optional Boolean
67+
if (modelObject.addInputFileDirectorytoSearchPath()) {
68+
idfObject.setString(PythonPlugin_SearchPathsFields::AddInputFileDirectorytoSearchPath, "Yes");
69+
} else {
70+
idfObject.setString(PythonPlugin_SearchPathsFields::AddInputFileDirectorytoSearchPath, "No");
71+
}
72+
73+
// Add epin Environment Variable to Search Path: Optional Boolean
74+
if (modelObject.addepinEnvironmentVariabletoSearchPath()) {
75+
idfObject.setString(PythonPlugin_SearchPathsFields::AddepinEnvironmentVariabletoSearchPath, "Yes");
76+
} else {
77+
idfObject.setString(PythonPlugin_SearchPathsFields::AddepinEnvironmentVariabletoSearchPath, "No");
78+
}
79+
80+
// Search Path
81+
for (const openstudio::path& searchPath : modelObject.searchPaths()) {
82+
IdfExtensibleGroup eg = idfObject.pushExtensibleGroup();
83+
eg.setString(PythonPlugin_SearchPathsExtensibleFields::SearchPath, searchPath.generic_string());
84+
}
85+
86+
return idfObject;
87+
} // End of translate function
88+
89+
} // end namespace energyplus
90+
} // end namespace openstudio
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/***********************************************************************************************************************
2+
* OpenStudio(R), Copyright (c) 2008-2023, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved.
3+
*
4+
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
5+
* following conditions are met:
6+
*
7+
* (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following
8+
* disclaimer.
9+
*
10+
* (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
11+
* disclaimer in the documentation and/or other materials provided with the distribution.
12+
*
13+
* (3) Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote products
14+
* derived from this software without specific prior written permission from the respective party.
15+
*
16+
* (4) Other than as required in clauses (1) and (2), distributions in any form of modifications or other derivative works
17+
* may not use the "OpenStudio" trademark, "OS", "os", or any other confusingly similar designation without specific prior
18+
* written permission from Alliance for Sustainable Energy, LLC.
19+
*
20+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
21+
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE UNITED STATES GOVERNMENT, OR THE UNITED
23+
* STATES DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
24+
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
25+
* USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
26+
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
27+
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28+
***********************************************************************************************************************/
29+
30+
#include <gtest/gtest.h>
31+
#include "EnergyPlusFixture.hpp"
32+
33+
#include "../ForwardTranslator.hpp"
34+
35+
#include "../../model/Model.hpp"
36+
#include "../../model/PythonPluginSearchPaths.hpp"
37+
#include "../../model/PythonPluginSearchPaths_Impl.hpp"
38+
#include "../../model/ExternalFile.hpp"
39+
#include "../../model/ExternalFile_Impl.hpp"
40+
#include "../../model/PythonPluginInstance.hpp"
41+
#include "../../model/PythonPluginInstance_Impl.hpp"
42+
43+
#include "../../utilities/idf/Workspace.hpp"
44+
#include "../../utilities/idf/IdfObject.hpp"
45+
#include "../../utilities/idf/WorkspaceObject.hpp"
46+
#include "../../utilities/idf/IdfExtensibleGroup.hpp"
47+
#include "../../utilities/idf/WorkspaceExtensibleGroup.hpp"
48+
#include "../../utilities/core/PathHelpers.hpp"
49+
// E+ FieldEnums
50+
#include <utilities/idd/IddEnums.hxx>
51+
#include <utilities/idd/IddFactory.hxx>
52+
#include <utilities/idd/PythonPlugin_SearchPaths_FieldEnums.hxx>
53+
54+
#include <resources.hxx>
55+
56+
using namespace openstudio::energyplus;
57+
using namespace openstudio::model;
58+
using namespace openstudio;
59+
60+
TEST_F(EnergyPlusFixture, ForwardTranslator_PythonPluginSearchPaths) {
61+
62+
ForwardTranslator ft;
63+
64+
Model m;
65+
PythonPluginSearchPaths pythonPluginSearchPaths = m.getUniqueModelObject<PythonPluginSearchPaths>();
66+
67+
pythonPluginSearchPaths.setName("My PythonPluginSearchPaths");
68+
69+
EXPECT_TRUE(pythonPluginSearchPaths.searchPaths().empty());
70+
// No search paths; not translated
71+
{
72+
EXPECT_TRUE(pythonPluginSearchPaths.setAddCurrentWorkingDirectorytoSearchPath(false)); // Opposite from IDD default
73+
EXPECT_TRUE(pythonPluginSearchPaths.setAddInputFileDirectorytoSearchPath(false)); // Opposite from IDD default
74+
EXPECT_TRUE(pythonPluginSearchPaths.setAddepinEnvironmentVariabletoSearchPath(false)); // Opposite from IDD default
75+
76+
const Workspace w = ft.translateModel(m);
77+
78+
const auto idfObjs = w.getObjectsByType(IddObjectType::PythonPlugin_SearchPaths);
79+
ASSERT_EQ(0u, idfObjs.size());
80+
}
81+
82+
{
83+
EXPECT_TRUE(pythonPluginSearchPaths.setAddCurrentWorkingDirectorytoSearchPath(true));
84+
EXPECT_TRUE(pythonPluginSearchPaths.setAddInputFileDirectorytoSearchPath(true));
85+
EXPECT_TRUE(pythonPluginSearchPaths.setAddepinEnvironmentVariabletoSearchPath(true));
86+
87+
std::vector<std::string> searchPaths({"/path/to/lib1", "/path/to/lib2"});
88+
EXPECT_TRUE(pythonPluginSearchPaths.setSearchPaths(searchPaths));
89+
EXPECT_EQ(2u, pythonPluginSearchPaths.searchPaths().size());
90+
91+
const Workspace w = ft.translateModel(m);
92+
93+
const auto idfObjs = w.getObjectsByType(IddObjectType::PythonPlugin_SearchPaths);
94+
ASSERT_EQ(1u, idfObjs.size());
95+
96+
const auto& idfObject = idfObjs.front();
97+
EXPECT_EQ("Yes", idfObject.getString(PythonPlugin_SearchPathsFields::AddCurrentWorkingDirectorytoSearchPath).get());
98+
EXPECT_EQ("Yes", idfObject.getString(PythonPlugin_SearchPathsFields::AddInputFileDirectorytoSearchPath).get());
99+
EXPECT_EQ("Yes", idfObject.getString(PythonPlugin_SearchPathsFields::AddepinEnvironmentVariabletoSearchPath).get());
100+
101+
ASSERT_EQ(2u, idfObject.extensibleGroups().size());
102+
for (int i = 0; i < 2; ++i) {
103+
EXPECT_EQ(searchPaths[i], idfObject.extensibleGroups()[i].getString(PythonPlugin_SearchPathsExtensibleFields::SearchPath).get());
104+
}
105+
}
106+
107+
{
108+
EXPECT_TRUE(pythonPluginSearchPaths.setAddCurrentWorkingDirectorytoSearchPath(false)); // Opposite from IDD default
109+
EXPECT_TRUE(pythonPluginSearchPaths.setAddInputFileDirectorytoSearchPath(false)); // Opposite from IDD default
110+
EXPECT_TRUE(pythonPluginSearchPaths.setAddepinEnvironmentVariabletoSearchPath(false)); // Opposite from IDD default
111+
112+
std::vector<std::string> searchPaths({"/path/to/lib1", "/path/to/lib2"});
113+
EXPECT_TRUE(pythonPluginSearchPaths.setSearchPaths(searchPaths));
114+
EXPECT_EQ(2u, pythonPluginSearchPaths.searchPaths().size());
115+
116+
const Workspace w = ft.translateModel(m);
117+
118+
const auto idfObjs = w.getObjectsByType(IddObjectType::PythonPlugin_SearchPaths);
119+
ASSERT_EQ(1u, idfObjs.size());
120+
121+
const auto& idfObject = idfObjs.front();
122+
EXPECT_EQ("No", idfObject.getString(PythonPlugin_SearchPathsFields::AddCurrentWorkingDirectorytoSearchPath).get());
123+
EXPECT_EQ("No", idfObject.getString(PythonPlugin_SearchPathsFields::AddInputFileDirectorytoSearchPath).get());
124+
EXPECT_EQ("No", idfObject.getString(PythonPlugin_SearchPathsFields::AddepinEnvironmentVariabletoSearchPath).get());
125+
126+
ASSERT_EQ(2u, idfObject.extensibleGroups().size());
127+
for (int i = 0; i < 2; ++i) {
128+
EXPECT_EQ(searchPaths[i], idfObject.extensibleGroups()[i].getString(PythonPlugin_SearchPathsExtensibleFields::SearchPath).get());
129+
}
130+
}
131+
132+
{
133+
EXPECT_TRUE(pythonPluginSearchPaths.setAddCurrentWorkingDirectorytoSearchPath(false)); // Opposite from IDD default
134+
EXPECT_TRUE(pythonPluginSearchPaths.setAddInputFileDirectorytoSearchPath(false)); // Opposite from IDD default
135+
EXPECT_TRUE(pythonPluginSearchPaths.setAddepinEnvironmentVariabletoSearchPath(false)); // Opposite from IDD default
136+
137+
openstudio::path p = resourcesPath() / toPath("model/PythonPluginThermochromicWindow.py");
138+
ASSERT_TRUE(exists(p));
139+
140+
boost::optional<ExternalFile> externalfile = ExternalFile::getExternalFile(m, openstudio::toString(p));
141+
ASSERT_TRUE(externalfile) << "Path doesn't exist: '" << p << "'";
142+
143+
PythonPluginInstance pythonPluginInstance(*externalfile, "ZN_1_wall_south_Window_1_Control");
144+
145+
std::vector<std::string> searchPaths({"/path/to/lib1", "/path/to/lib2"});
146+
EXPECT_TRUE(pythonPluginSearchPaths.setSearchPaths(searchPaths));
147+
EXPECT_EQ(2u, pythonPluginSearchPaths.searchPaths().size());
148+
149+
const Workspace w = ft.translateModel(m);
150+
151+
const auto idfObjs = w.getObjectsByType(IddObjectType::PythonPlugin_SearchPaths);
152+
ASSERT_EQ(1u, idfObjs.size());
153+
154+
const auto& idfObject = idfObjs.front();
155+
EXPECT_EQ("No", idfObject.getString(PythonPlugin_SearchPathsFields::AddCurrentWorkingDirectorytoSearchPath).get());
156+
EXPECT_EQ("No", idfObject.getString(PythonPlugin_SearchPathsFields::AddInputFileDirectorytoSearchPath).get());
157+
EXPECT_EQ("No", idfObject.getString(PythonPlugin_SearchPathsFields::AddepinEnvironmentVariabletoSearchPath).get());
158+
159+
ASSERT_EQ(3u, idfObject.extensibleGroups().size());
160+
for (int i = 0; i < 2; ++i) {
161+
EXPECT_EQ(searchPaths[i], idfObject.extensibleGroups()[i].getString(PythonPlugin_SearchPathsExtensibleFields::SearchPath).get());
162+
}
163+
// since PythonPlugin_Instance is translated after PythonPlugin_SearchPaths, this search path is appended to the existing search paths
164+
EXPECT_EQ(openstudio::toString(externalfile->filePath().parent_path()),
165+
idfObject.extensibleGroups()[2].getString(PythonPlugin_SearchPathsExtensibleFields::SearchPath).get());
166+
}
167+
}

src/model/CMakeLists.txt

+4
Original file line numberDiff line numberDiff line change
@@ -1228,6 +1228,9 @@ set(${target_name}_src
12281228
PythonPluginOutputVariable.hpp
12291229
PythonPluginOutputVariable_Impl.hpp
12301230
PythonPluginOutputVariable.cpp
1231+
PythonPluginSearchPaths.hpp
1232+
PythonPluginSearchPaths_Impl.hpp
1233+
PythonPluginSearchPaths.cpp
12311234
RadianceParameters.hpp
12321235
RadianceParameters_Impl.hpp
12331236
RadianceParameters.cpp
@@ -2221,6 +2224,7 @@ set(${target_name}_test_src
22212224
test/PythonPluginVariable_GTest.cpp
22222225
test/PythonPluginTrendVariable_GTest.cpp
22232226
test/PythonPluginOutputVariable_GTest.cpp
2227+
test/PythonPluginSearchPaths_GTest.cpp
22242228
test/RadianceParameters_GTest.cpp
22252229
test/RefractionExtinctionGlazing_GTest.cpp
22262230
test/RefrigerationAirChiller_GTest.cpp

0 commit comments

Comments
 (0)