1
+ # bluemira is an integrated inter-disciplinary design tool for future fusion
2
+ # reactors. It incorporates several modules, some of which rely on other
3
+ # codes, to carry out a range of typical conceptual fusion reactor design
4
+ # activities.
5
+ #
6
+ # Copyright (C) 2021 M. Coleman, J. Cook, F. Franza, I.A. Maione, S. McIntosh,
7
+ # J. Morris, D. Short
8
+ #
9
+ # bluemira is free software; you can redistribute it and/or
10
+ # modify it under the terms of the GNU Lesser General Public
11
+ # License as published by the Free Software Foundation; either
12
+ # version 2.1 of the License, or (at your option) any later version.
13
+ #
14
+ # bluemira is distributed in the hope that it will be useful,
15
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17
+ # Lesser General Public License for more details.
18
+ #
19
+ # You should have received a copy of the GNU Lesser General Public
20
+ # License along with bluemira; if not, see <https://www.gnu.org/licenses/>.
21
+ """
22
+ Geometry Optimisation
23
+
24
+ Example taken from: bluemira/examples/optimisation/geometry_optimisation.ex.py
25
+
26
+ In this example we will go through how to set up a simple geometry
27
+ optimisation, including a geometric constraint.
28
+
29
+ The problem to solve is, minimise the length of our wall boundary,
30
+ in the xz-plane, whilst keeping it a minimum distance from our plasma.
31
+
32
+ We will greatly simplify this problem by working with a circular
33
+ plasma, we will use a PrincetonD for the wall shape,
34
+ and set the minimum distance to half a meter.
35
+ """
36
+
37
+ import numpy as np
38
+ import os
39
+ import sys
40
+ from bluemira .display import plot_2d
41
+ from bluemira .display .plotter import PlotOptions
42
+ from bluemira .geometry .optimisation import optimise_geometry
43
+ from bluemira .geometry .parameterisations import GeometryParameterisation , PrincetonD
44
+ from bluemira .geometry .tools import distance_to , make_circle
45
+ from bluemira .geometry .wire import BluemiraWire
46
+
47
+ import simvue
48
+
49
+ def f_objective (geom : GeometryParameterisation ) -> float :
50
+ """Objective function to minimise a shape's length."""
51
+ return geom .create_shape ().length
52
+
53
+
54
+ def distance_constraint (
55
+ geom : GeometryParameterisation , boundary : BluemiraWire , min_distance : float , run : simvue .Run
56
+ ) -> float :
57
+ """
58
+ A constraint to keep a minimum distance between two shapes.
59
+
60
+ The constraint must be in the form f(x) <= 0, i.e., constraint
61
+ is satisfied if f(x) <= 0.
62
+
63
+ Since what we want is 'min_distance <= distance(A, B)', we rewrite
64
+ this in the form 'min_distance - distance(A, B) <= 0', and return
65
+ the left-hand side from this function.
66
+ """
67
+ shape = geom .create_shape ()
68
+ # Log all variables as metrics after each iteration, giving human readable names:
69
+ run .log_metrics (
70
+ {
71
+ "inboard_limb_radius" : float (geom .variables ["x1" ].value ),
72
+ "outboard_limb_radius" : float (geom .variables ["x2" ].value ),
73
+ "vertical_offset" : float (geom .variables ["dz" ].value ),
74
+ "length_of_wall" : float (shape .length ),
75
+ "distance_to_plasma" : float (distance_to (shape , boundary )[0 ])
76
+ }
77
+ )
78
+ return min_distance - distance_to (shape , boundary )[0 ]
79
+
80
+ # The original example prints stuff to the console to track progress
81
+ # Instead of changing these lines to log events (since we probably want both),
82
+ # We can make a class which intercepts stdout and also sends messages to Simvue
83
+ class StdoutToSimvue ():
84
+ def __init__ (self , run : simvue .Run ):
85
+ self .run = run
86
+
87
+ def write (self , message : str ):
88
+ # Log the message as an event (so long as it isnt a blank line)
89
+ if message .strip ():
90
+ run .log_event (message )
91
+ # And print to console as normal
92
+ sys .__stdout__ .write (message )
93
+
94
+ def flush (self ):
95
+ sys .__stdout__ .flush ()
96
+
97
+ # Here we will start doing our optimisation. First create a Simvue run,
98
+ # using the Run class as a context manager:
99
+ with simvue .Run () as run :
100
+ # Initialise our run:
101
+ run .init (
102
+ name = "bluemira_geometry_optimisation" ,
103
+ folder = "/simvue_client_demos" ,
104
+ visibility = "tenant" if os .environ .get ("CI" ) else None ,
105
+ tags = ["bluemira" , "simvue_client_examples" ],
106
+ description = "Minimise the length of a parameterised geometry using gradient-based optimisation algorithm." ,
107
+ )
108
+
109
+ # Redirect stdout so that print statements also get logged as events:
110
+ stdout_sender = StdoutToSimvue (run )
111
+ sys .stdout = stdout_sender
112
+
113
+ # Next define the shape of our plasma, and the minimum distance we want between
114
+ # our wall boundary and our plasma:
115
+ min_distance = 0.5
116
+ plasma = make_circle (radius = 2 , center = (8 , 0 , 0.25 ), axis = (0 , 1 , 0 ))
117
+
118
+ # As with any optimisation, it's important to pick a reasonable initial
119
+ # parameterisation.
120
+ wall_boundary = PrincetonD ({
121
+ "x1" : {"value" : 4 , "upper_bound" : 6 },
122
+ "x2" : {"value" : 12 , "lower_bound" : 10 },
123
+ })
124
+
125
+ print ("Initial parameterisation:" )
126
+ print (wall_boundary .variables )
127
+ print (f"Length of wall : { wall_boundary .create_shape ().length } " )
128
+ print (f"Distance to plasma: { distance_to (wall_boundary .create_shape (), plasma )[0 ]} " )
129
+
130
+ # Create metadata for our original parameters:
131
+ _metadata = {
132
+ var : {
133
+ "initial" : wall_boundary .variables [var ].value ,
134
+ "lower_bound" : wall_boundary .variables [var ].lower_bound ,
135
+ "upper_bound" : wall_boundary .variables [var ].upper_bound
136
+ }
137
+ for var in ["x1" , "x2" , "dz" ]
138
+ }
139
+ run .update_metadata ({"bluemira_parameters" : _metadata })
140
+
141
+ # Create and upload an image of the initial design to Simvue
142
+ _plot = plot_2d ([wall_boundary .create_shape (), plasma ])
143
+ _fig = _plot .get_figure ()
144
+ run .save_object (_fig , category = "input" , name = "initial_shape" )
145
+
146
+ # Optimise our geometry using a gradient descent method
147
+ result = optimise_geometry (
148
+ wall_boundary ,
149
+ algorithm = "SLSQP" ,
150
+ f_objective = f_objective ,
151
+ opt_conditions = {"ftol_abs" : 1e-6 },
152
+ keep_history = True ,
153
+ ineq_constraints = [
154
+ {
155
+ "f_constraint" : lambda g : distance_constraint (g , plasma , min_distance , run ),
156
+ "tolerance" : np .array ([1e-8 ]),
157
+ },
158
+ ],
159
+ )
160
+
161
+ # Print final results after optimisation
162
+ print ("Optimised parameterisation:" )
163
+ print (result .geom .variables )
164
+
165
+ boundary = result .geom .create_shape ()
166
+ print (f"Length of wall : { boundary .length } " )
167
+ print (f"Distance to plasma: { distance_to (boundary , plasma )[0 ]} " )
168
+
169
+ # Update metadata with final optimised values
170
+ _metadata = {
171
+ var : {
172
+ "final" : result .geom .variables [var ].value ,
173
+ }
174
+ for var in ["x1" , "x2" , "dz" ]
175
+ }
176
+ run .update_metadata ({"bluemira_parameters" : _metadata })
177
+
178
+ # Create and upload an image of the optimised design to Simvue
179
+ _plot = plot_2d ([boundary , plasma ])
180
+ _fig = _plot .get_figure ()
181
+ run .save_object (_fig , category = "output" , name = "final_shape" )
182
+
183
+ # Use the history to create and upload an image of the design iterations
184
+ geom = PrincetonD ()
185
+ ax = plot_2d (plasma , show = False )
186
+ for i , (x , _ ) in enumerate (result .history ):
187
+ geom .variables .set_values_from_norm (x )
188
+ wire = geom .create_shape ()
189
+ wire_options = {
190
+ "alpha" : 0.5 + ((i + 1 ) / len (result .history )) / 2 ,
191
+ "color" : "red" ,
192
+ "linewidth" : 0.1 ,
193
+ }
194
+ ax = plot_2d (wire , options = PlotOptions (wire_options = wire_options ), ax = ax , show = False )
195
+ _plot = plot_2d (boundary , ax = ax , show = True )
196
+ _fig = _plot .get_figure ()
197
+ run .save_object (_fig , category = "output" , name = "design_iterations" )
0 commit comments